mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-24 21:55:52 +02:00
Archive commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
ng-template(#selectFrame)
|
||||
.frame-select-container
|
||||
a.frame-select(*ngFor="let s of sprites; index as i" [class.active]="frame === i"
|
||||
(mousedown)="select(i)" (mouseenter)="enter(i)" (mouseleave)="leave()")
|
||||
sprite-box([sprite]="s" [fill]="pony.coatFill" [outline]="pony.coatOutline" [size]="70" [x]="x" [y]="y"
|
||||
[circle]="circle" [reverseExtra]="reverseExtra")
|
||||
.frame-select-number {{i}}
|
||||
|
||||
a.frame-select([popover]="selectFrame" [placement]="placement" [isOpen]="popoverIsOpen" triggers=""
|
||||
(click)="togglePopover()" (mousedown)="$event.stopPropagation()" containerClass="popover-frames")
|
||||
sprite-box([sprite]="sprite" [fill]="pony.coatFill" [outline]="pony.coatOutline" [size]="70" [x]="x" [y]="y"
|
||||
[circle]="circle" [reverseExtra]="reverseExtra")
|
||||
.frame-select-number {{frame}}
|
||||
@@ -0,0 +1,16 @@
|
||||
.frame-select {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
&.active .sprite-box {
|
||||
background: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.frame-select-number {
|
||||
color: #999;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 1px;
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core';
|
||||
import { Sprite, PonyInfo } from '../../../../common/interfaces';
|
||||
|
||||
let openedPopover: ToolsFrame;
|
||||
|
||||
@Component({
|
||||
selector: 'tools-frame',
|
||||
templateUrl: 'tools-frame.pug',
|
||||
styleUrls: ['tools-frame.scss'],
|
||||
})
|
||||
export class ToolsFrame {
|
||||
@Input() x = 0;
|
||||
@Input() y = 0;
|
||||
@Input() sprites!: Sprite[];
|
||||
@Input() frame!: number;
|
||||
@Input() pony!: PonyInfo;
|
||||
@Input() reverseExtra = false;
|
||||
@Input() circle?: string;
|
||||
@Output() frameChange = new EventEmitter<number>();
|
||||
popoverIsOpen = false;
|
||||
placement = 'right';
|
||||
private savedFrame = 0;
|
||||
private selected = false;
|
||||
constructor(private element: ElementRef) {
|
||||
}
|
||||
get sprite() {
|
||||
return this.sprites[this.frame];
|
||||
}
|
||||
closePopover = () => {
|
||||
if (this.popoverIsOpen) {
|
||||
this.togglePopover();
|
||||
}
|
||||
}
|
||||
togglePopover() {
|
||||
const rect = (this.element.nativeElement as HTMLElement).getBoundingClientRect();
|
||||
this.placement = (rect.left < (window.innerWidth / 2)) ? 'right' : 'left';
|
||||
|
||||
if (!this.popoverIsOpen) {
|
||||
if (openedPopover) {
|
||||
openedPopover.popoverIsOpen = false;
|
||||
}
|
||||
|
||||
openedPopover = this;
|
||||
}
|
||||
|
||||
this.popoverIsOpen = !this.popoverIsOpen;
|
||||
|
||||
if (this.popoverIsOpen) {
|
||||
this.selected = false;
|
||||
window.addEventListener('mousedown', this.closePopover);
|
||||
} else {
|
||||
window.removeEventListener('mousedown', this.closePopover);
|
||||
}
|
||||
|
||||
setTimeout(() => { }, 10);
|
||||
}
|
||||
select(index: number) {
|
||||
this.selected = true;
|
||||
this.frame = index;
|
||||
this.togglePopover();
|
||||
this.frameChange.emit(this.frame);
|
||||
}
|
||||
enter(index: number) {
|
||||
if (!this.selected) {
|
||||
this.savedFrame = this.frame;
|
||||
this.frame = index;
|
||||
this.frameChange.emit(this.frame);
|
||||
}
|
||||
}
|
||||
leave() {
|
||||
if (!this.selected) {
|
||||
this.frame = this.savedFrame;
|
||||
this.frameChange.emit(this.frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.tools-offset
|
||||
.text-center
|
||||
button.btn.btn-xs.btn-default((click)="moveY(-1)")
|
||||
fa-icon([icon]="upIcon" [fixedWidth]="true")
|
||||
.text-center
|
||||
button.btn.btn-xs.btn-default((click)="moveX(-1)")
|
||||
fa-icon([icon]="leftIcon" [fixedWidth]="true")
|
||||
small.text-center.tools-offset-value
|
||||
| {{offset?.x}} / {{offset?.y}}
|
||||
button.btn.btn-xs.btn-default((click)="moveX(1)")
|
||||
fa-icon([icon]="rightIcon" [fixedWidth]="true")
|
||||
.text-center
|
||||
button.btn.btn-xs.btn-default((click)="moveY(1)")
|
||||
fa-icon([icon]="downIcon" [fixedWidth]="true")
|
||||
@@ -0,0 +1,17 @@
|
||||
.tools-offset {
|
||||
width: 110px;
|
||||
margin: auto;
|
||||
|
||||
.btn {
|
||||
position: relative;
|
||||
box-shadow: 0 0 5px black;
|
||||
}
|
||||
}
|
||||
|
||||
.tools-offset-value {
|
||||
width: 40px;
|
||||
padding: 5px;
|
||||
background: #222;
|
||||
color: #eee;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Point } from '../../../../common/interfaces';
|
||||
import { faChevronRight, faChevronLeft, faChevronDown, faChevronUp } from '../../../../client/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'tools-offset',
|
||||
templateUrl: 'tools-offset.pug',
|
||||
styleUrls: ['tools-offset.scss'],
|
||||
})
|
||||
export class ToolsOffset {
|
||||
readonly rightIcon = faChevronRight;
|
||||
readonly leftIcon = faChevronLeft;
|
||||
readonly upIcon = faChevronUp;
|
||||
readonly downIcon = faChevronDown;
|
||||
@Input() offset?: Point;
|
||||
@Output() change = new EventEmitter<void>();
|
||||
moveX(value: number) {
|
||||
if (this.offset) {
|
||||
this.offset.x += value;
|
||||
this.change.emit();
|
||||
}
|
||||
}
|
||||
moveY(value: number) {
|
||||
if (this.offset) {
|
||||
this.offset.y += value;
|
||||
this.change.emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.tools-range.input-group([class.input-group-sm]="small")
|
||||
.input-group-prepend
|
||||
button.btn.btn-default((click)="decrement()")
|
||||
fa-icon([icon]="vertical ? upIcon : leftIcon" [fixedWidth]="true")
|
||||
input.form-control([(ngModel)]="value" type="number" step=1 [min]="min" [max]="max" [placeholder]="placeholder")
|
||||
.input-group-append
|
||||
button.btn.btn-default((click)="increment()")
|
||||
fa-icon([icon]="vertical ? downIcon : rightIcon" [fixedWidth]="true")
|
||||
@@ -0,0 +1,7 @@
|
||||
.tools-range {
|
||||
width: 160px;
|
||||
|
||||
&.input-group-sm {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Component, forwardRef, Input, ChangeDetectionStrategy, EventEmitter, Output } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { faChevronRight, faChevronLeft, faChevronUp, faChevronDown } from '../../../../client/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'tools-range',
|
||||
templateUrl: 'tools-range.pug',
|
||||
styleUrls: ['tools-range.scss'],
|
||||
providers: [
|
||||
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToolsRange), multi: true },
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ToolsRange implements ControlValueAccessor {
|
||||
readonly rightIcon = faChevronRight;
|
||||
readonly leftIcon = faChevronLeft;
|
||||
readonly upIcon = faChevronUp;
|
||||
readonly downIcon = faChevronDown;
|
||||
@Input() min = 0;
|
||||
@Input() max = 100;
|
||||
@Input() vertical = false;
|
||||
@Input() small = false;
|
||||
@Input() placeholder?: string;
|
||||
@Output() change = new EventEmitter<number>();
|
||||
private _value = 0;
|
||||
private propagateChange: any = () => { };
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
set value(value: number) {
|
||||
this._value = value;
|
||||
this.propagateChange(value);
|
||||
this.change.emit();
|
||||
}
|
||||
decrement() {
|
||||
if (this.value > this.min) {
|
||||
this.value = this.value - 1;
|
||||
}
|
||||
}
|
||||
increment() {
|
||||
if (this.value < this.max) {
|
||||
this.value = this.value + 1;
|
||||
}
|
||||
}
|
||||
writeValue(value: number | undefined) {
|
||||
if (value !== undefined) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
registerOnChange(callback: any) {
|
||||
this.propagateChange = callback;
|
||||
}
|
||||
registerOnTouched() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.tools-xy.btn-group
|
||||
button.btn.btn-xs.btn-default((click)="changeY(y - 1)")
|
||||
fa-icon([icon]="upIcon" [fixedWidth]="true")
|
||||
button.btn.btn-xs.btn-default((click)="changeX(x - 1)")
|
||||
fa-icon([icon]="leftIcon" [fixedWidth]="true")
|
||||
button.btn.btn-xs.btn-default((click)="changeX(x + 1)")
|
||||
fa-icon([icon]="rightIcon" [fixedWidth]="true")
|
||||
button.btn.btn-xs.btn-default((click)="changeY(y + 1)")
|
||||
fa-icon([icon]="downIcon" [fixedWidth]="true")
|
||||
@@ -0,0 +1,9 @@
|
||||
.tools-xy {
|
||||
width: 80px;
|
||||
|
||||
.btn {
|
||||
padding: 0;
|
||||
font-size: 10px;
|
||||
width: 19px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, EventEmitter, Output } from '@angular/core';
|
||||
import { faChevronRight, faChevronLeft, faChevronUp, faChevronDown } from '../../../../client/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'tools-xy',
|
||||
templateUrl: 'tools-xy.pug',
|
||||
styleUrls: ['tools-xy.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ToolsXY {
|
||||
readonly rightIcon = faChevronRight;
|
||||
readonly leftIcon = faChevronLeft;
|
||||
readonly upIcon = faChevronUp;
|
||||
readonly downIcon = faChevronDown;
|
||||
@Input() min = 0;
|
||||
@Input() max = 100;
|
||||
@Input() x = 0;
|
||||
@Input() y = 0;
|
||||
@Output() xChange = new EventEmitter<number>();
|
||||
@Output() yChange = new EventEmitter<number>();
|
||||
@Output() change = new EventEmitter<void>();
|
||||
changeX(value: number) {
|
||||
this.x = value;
|
||||
this.xChange.emit(value);
|
||||
this.change.emit();
|
||||
}
|
||||
changeY(value: number) {
|
||||
this.y = value;
|
||||
this.yChange.emit(value);
|
||||
this.change.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import { max, compact } from 'lodash';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { Psd, writePsd, Layer } from 'ag-psd';
|
||||
import { SpriteSet, PonyInfoNumber, PaletteSpriteSet, NoDraw } from '../../common/interfaces';
|
||||
import { times, cloneDeep, setFlag, includes, toInt } from '../../common/utils';
|
||||
import { createDefaultPony, syncLockedPonyInfoNumber, toPaletteNumber, mockPaletteManager } from '../../common/ponyInfo';
|
||||
import { Sets } from '../../client/ponyUtils';
|
||||
import { defaultDrawPonyOptions, defaultPonyState } from '../../client/ponyHelpers';
|
||||
import { createCanvas, disableImageSmoothing } from '../../client/canvasUtils';
|
||||
import { ContextSpriteBatch } from '../../graphics/contextSpriteBatch';
|
||||
import { BLACK, BLUE, CYAN, WHITE, RED, GREEN, YELLOW, MAGENTA, TRANSPARENT } from '../../common/colors';
|
||||
import { colorToCSS } from '../../common/color';
|
||||
import { decompressPony, compressPonyString } from '../../common/compressPony';
|
||||
import { drawPony } from '../../client/ponyDraw';
|
||||
import * as sprites from '../../generated/sprites';
|
||||
import { drawPixelTextOnCanvas, fillRect } from '../../graphics/graphicsUtils';
|
||||
import { Sheet, SheetLayer, ignoreSet, DEFAULT_COLOR } from '../../common/sheets';
|
||||
import { createHeadAnimation } from '../../client/ponyAnimations';
|
||||
|
||||
const PONY_X = 30;
|
||||
const PONY_Y = 50;
|
||||
const patternColors = [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, BLACK];
|
||||
const whiteColors = [WHITE, WHITE, WHITE, WHITE, WHITE, WHITE, WHITE];
|
||||
|
||||
function maxPatterns(sprites: Sets): number {
|
||||
return max(sprites.map(s => s && s.length ? max(s.map(x => x ? x.length : 0)) : 0))!;
|
||||
}
|
||||
|
||||
const backupSprites: any = {
|
||||
head: [
|
||||
undefined,
|
||||
[
|
||||
[
|
||||
{}
|
||||
],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
function getSets(sheet: Sheet, key: string, override?: string): Sets | undefined {
|
||||
const setsKey = override || key || '';
|
||||
const sets = backupSprites[setsKey] || (sprites as any)[setsKey];
|
||||
|
||||
if (sheet.duplicateFirstFrame !== undefined) {
|
||||
return times(sheet.duplicateFirstFrame, () => sets[0]);
|
||||
} else {
|
||||
return sheet.single ? [sets] : sets;
|
||||
}
|
||||
}
|
||||
|
||||
function getSetsForFirstKey(sheet: Sheet) {
|
||||
const layer = sheet.layers.find(l => !!l.set);
|
||||
return layer && getSets(sheet, layer.set!);
|
||||
}
|
||||
|
||||
export function getCols(sheet: Sheet) {
|
||||
return sheet.state!.animation.frames.length;
|
||||
}
|
||||
|
||||
export function getRows(sheet: Sheet) {
|
||||
if (sheet.rows !== undefined) {
|
||||
return sheet.rows;
|
||||
} else {
|
||||
const sets = getSetsForFirstKey(sheet);
|
||||
const maxFrames = sets && max(sets.map(f => f ? f.length : 0));
|
||||
return (maxFrames || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function savePsd(psd: Psd, name: string) {
|
||||
saveAs(new Blob([writePsd(psd, { generateThumbnail: true })], { type: 'application/octet-stream' }), name);
|
||||
}
|
||||
|
||||
export function createPsd(sheet: Sheet, rows: number, cols: number): Psd {
|
||||
const width = canvasWidth(sheet, rows, cols);
|
||||
const height = canvasHeight(sheet, rows, cols);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
children: compact([
|
||||
{ name: '<bg>', canvas: createBackground(rows, cols, width, height, sheet), transparencyProtected: true },
|
||||
...sheet.layers!.map(layer => createPsdLayer(sheet, rows, cols, layer)),
|
||||
{ name: '<refs>', canvas: createRefsCanvas(width, height, sheet.paletteOffsetY) },
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
function canvasWidth(sheet: Sheet, _rows: number, cols: number) {
|
||||
if (sheet.wrap) {
|
||||
cols = sheet.wrap;
|
||||
}
|
||||
|
||||
return (sheet.offset * (cols - 1)) + sheet.width;
|
||||
}
|
||||
|
||||
function canvasHeight(sheet: Sheet, rows: number, _cols: number) {
|
||||
if (sheet.wrap) {
|
||||
rows = Math.ceil(rows / sheet.wrap);
|
||||
}
|
||||
|
||||
return sheet.height * rows;
|
||||
}
|
||||
|
||||
function drawPsdLayer(
|
||||
sheet: Sheet, rows: number, cols: number, layer: SheetLayer, pattern = -1, extra = false
|
||||
): HTMLCanvasElement {
|
||||
const { width, height, offset, offsetY = 0, wrap } = sheet;
|
||||
const pony = createPony();
|
||||
const baseState = { ...defaultPonyState(), ...sheet.state, blushColor: BLACK };
|
||||
const options = { ...defaultDrawPonyOptions(), ...layer.options };
|
||||
const ignoreColor = (layer.drawBlack === undefined ? !!layer.head : layer.drawBlack) ? TRANSPARENT : BLACK;
|
||||
const fieldName = layer.fieldName || sheet.fieldName;
|
||||
|
||||
if (!layer.head) {
|
||||
baseState.headAnimation = createHeadAnimation('', 1, false, [[]]);
|
||||
pony.head = ignoreSet();
|
||||
pony.nose = ignoreSet();
|
||||
pony.ears = ignoreSet();
|
||||
} else if (layer.noFace) {
|
||||
baseState.headAnimation = createHeadAnimation('', 1, false, [[]]);
|
||||
pony.head = ignoreSet();
|
||||
pony.nose = ignoreSet();
|
||||
}
|
||||
|
||||
if (!layer.body) {
|
||||
options.no = setFlag(options.no, NoDraw.BodyOnly, true);
|
||||
}
|
||||
|
||||
if (!layer.frontLeg) {
|
||||
options.no = setFlag(options.no, NoDraw.FrontLeg, true);
|
||||
}
|
||||
|
||||
if (!layer.backLeg) {
|
||||
options.no = setFlag(options.no, NoDraw.BackLeg, true);
|
||||
}
|
||||
|
||||
if (!layer.frontFarLeg) {
|
||||
options.no = setFlag(options.no, NoDraw.FrontFarLeg, true);
|
||||
}
|
||||
|
||||
if (!layer.backFarLeg) {
|
||||
options.no = setFlag(options.no, NoDraw.BackFarLeg, true);
|
||||
}
|
||||
|
||||
layer.setup && layer.setup(pony, baseState);
|
||||
|
||||
syncLockedPonyInfoNumber(pony);
|
||||
|
||||
const actualRows = wrap ? Math.ceil(rows / wrap) : rows;
|
||||
const actualCols = wrap ? wrap : cols;
|
||||
const empties = sheet.empties && includes(sheet.setsWithEmpties, layer.set) ? sheet.empties : [];
|
||||
|
||||
return drawFrames(actualRows, actualCols, width, height, offset, (batch, x, y) => {
|
||||
const xIndexBase = (wrap ? actualCols * y + x : x);
|
||||
const xIndexOffset = empties.filter(i => i <= xIndexBase).length;
|
||||
const xIndex = includes(empties, xIndexBase) ? 0 : (xIndexBase - xIndexOffset);
|
||||
const yIndex = wrap ? 0 : y;
|
||||
|
||||
const state = cloneDeep(baseState);
|
||||
|
||||
sheet.frame && sheet.frame(pony, state, options, xIndex, yIndex, pattern);
|
||||
layer.frame && layer.frame(pony, state, options, xIndex, yIndex, pattern);
|
||||
|
||||
state.animationFrame = xIndex;
|
||||
|
||||
if (layer.set && fieldName) {
|
||||
const sets = getSets(sheet, layer.set, layer.setOverride);
|
||||
|
||||
if (!sets) {
|
||||
throw new Error(`Missing sets for (${layer.set})`);
|
||||
}
|
||||
|
||||
const frameIndex = sheet.single ? 0 : xIndex;
|
||||
const typeIndex = sheet.single ? xIndex : yIndex;
|
||||
const aframe = sets[frameIndex];
|
||||
const type = (aframe && typeIndex < aframe.length) ? typeIndex : -1;
|
||||
const set: SpriteSet<number> = { type };
|
||||
|
||||
if (pattern !== -1) {
|
||||
set.fills = patternColors;
|
||||
set.outlines = patternColors;
|
||||
|
||||
if (aframe && typeIndex < aframe.length && aframe[typeIndex] && pattern < aframe[typeIndex]!.length) {
|
||||
set.pattern = pattern;
|
||||
} else {
|
||||
set.type = -1;
|
||||
}
|
||||
} else {
|
||||
set.fills = whiteColors;
|
||||
set.outlines = whiteColors;
|
||||
|
||||
if (!(aframe && typeIndex < aframe.length && aframe[typeIndex])) {
|
||||
set.type = -1;
|
||||
}
|
||||
}
|
||||
|
||||
layer.frameSet && layer.frameSet(set, xIndex, yIndex, pattern);
|
||||
|
||||
(pony as any)[fieldName] = set.type === -1 ? ignoreSet() : set;
|
||||
}
|
||||
|
||||
batch.disableShading = pattern !== -1;
|
||||
batch.ignoreColor = ignoreColor;
|
||||
|
||||
const pal = toPaletteNumber(pony);
|
||||
|
||||
if (layer.extra !== undefined) {
|
||||
const set = pal[layer.extra] as PaletteSpriteSet;
|
||||
|
||||
if (extra) {
|
||||
set.palette = mockPaletteManager.add([0, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK]);
|
||||
} else {
|
||||
set.extraPalette = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
drawPony(batch, pal, state, PONY_X, PONY_Y + offsetY + toInt(layer.shiftY), options);
|
||||
});
|
||||
}
|
||||
|
||||
function createPsdPatternLayers(sheet: Sheet, rows: number, cols: number, layer: SheetLayer): Layer[] {
|
||||
const sets = getSets(sheet, layer.set!, layer.setOverride);
|
||||
const patterns = layer.patterns || (sets ? maxPatterns(sets) : 0) || 6;
|
||||
|
||||
return compact([
|
||||
layer.extra && {
|
||||
name: 'extra',
|
||||
canvas: drawPsdLayer(sheet, rows, cols, layer, -1, true),
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
canvas: drawPsdLayer(sheet, rows, cols, layer),
|
||||
},
|
||||
...times(patterns, i => ({
|
||||
name: `pattern ${i}`,
|
||||
canvas: drawPsdLayer(sheet, rows, cols, layer, i),
|
||||
hidden: true,
|
||||
clipping: true,
|
||||
blendMode: 'multiply',
|
||||
})),
|
||||
]);
|
||||
}
|
||||
|
||||
function createPsdLayer(sheet: Sheet, rows: number, cols: number, layer: SheetLayer): Layer {
|
||||
const name = layer.name;
|
||||
|
||||
if (layer.set) {
|
||||
return { name, children: createPsdPatternLayers(sheet, rows, cols, layer) };
|
||||
} else {
|
||||
return { name, canvas: drawPsdLayer(sheet, rows, cols, layer) };
|
||||
}
|
||||
}
|
||||
|
||||
function drawFrames(
|
||||
rows: number, cols: number, w: number, h: number, offset: number,
|
||||
draw: (batch: ContextSpriteBatch, x: number, y: number) => void
|
||||
): HTMLCanvasElement {
|
||||
const canvas = createCanvas((offset * (cols - 1)) + w, h * rows);
|
||||
const buffer = createCanvas(w, h);
|
||||
const batch = new ContextSpriteBatch(buffer);
|
||||
const viewContext = canvas.getContext('2d')!;
|
||||
viewContext.save();
|
||||
disableImageSmoothing(viewContext);
|
||||
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
batch.start(sprites.paletteSpriteSheet, 0);
|
||||
draw(batch, x, y);
|
||||
batch.end();
|
||||
viewContext.drawImage(buffer, x * offset, y * h);
|
||||
}
|
||||
}
|
||||
|
||||
viewContext.restore();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function createBackground(rows: number, cols: number, width: number, height: number, sheet: Sheet) {
|
||||
const canvas = createCanvas(width, height);
|
||||
const context = canvas.getContext('2d')!;
|
||||
|
||||
if (sheet.wrap) {
|
||||
cols = sheet.wrap;
|
||||
rows = Math.ceil(rows / sheet.wrap);
|
||||
}
|
||||
|
||||
fillRect(context, 'lightgreen', 0, 0, canvas.width, canvas.height);
|
||||
|
||||
context.globalAlpha = 0.1;
|
||||
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
const color = (x + (y % 2)) % 2 ? 'green' : 'blue';
|
||||
fillRect(context, color, x * sheet.offset, y * sheet.height, sheet.width, sheet.height);
|
||||
}
|
||||
}
|
||||
|
||||
context.globalAlpha = 1;
|
||||
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
const gap = sheet.width - sheet.offset;
|
||||
const index = sheet.wrap ? (cols * y + x) : x;
|
||||
drawPixelTextOnCanvas(context, x * sheet.offset + gap + 2, y * sheet.height + 2, 0x76c189ff, index.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function createRefsCanvas(width: number, height: number, offsetY = 0) {
|
||||
const canvas = createCanvas(width, height);
|
||||
const context = canvas.getContext('2d')!;
|
||||
const h = 2;
|
||||
|
||||
patternColors.forEach((c, i) => fillRect(context, colorToCSS(c), 5, 5 + h * i + offsetY, 10, h));
|
||||
|
||||
fillRect(context, '#888888', 25, 10 + offsetY, 8, 10);
|
||||
fillRect(context, '#d9d9d9', 27, 12 + offsetY, 4, 4);
|
||||
fillRect(context, '#afafaf', 27, 16 + offsetY, 4, 2);
|
||||
fillRect(context, '#9f9f9f', 20, 5 + offsetY, 8, 10);
|
||||
fillRect(context, '#ffffff', 22, 7 + offsetY, 4, 4);
|
||||
fillRect(context, '#cdcdcd', 22, 11 + offsetY, 4, 2);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function createPony(): PonyInfoNumber {
|
||||
const pony = decompressPony(compressPonyString(createDefaultPony()));
|
||||
pony.mane!.type = 0;
|
||||
pony.backMane!.type = 0;
|
||||
pony.tail!.type = 0;
|
||||
pony.coatFill = DEFAULT_COLOR;
|
||||
pony.lockCoatOutline = true;
|
||||
pony.lockBackLegAccessory = false;
|
||||
return syncLockedPonyInfoNumber(pony);
|
||||
}
|
||||
|
||||
export function drawPsd(psd: Psd, scale: number, canvas?: HTMLCanvasElement): HTMLCanvasElement {
|
||||
const buffer = canvas || createCanvas(100, 100);
|
||||
buffer.width = psd.width * scale;
|
||||
buffer.height = psd.height * scale;
|
||||
const context = buffer.getContext('2d')!;
|
||||
context.save();
|
||||
context.scale(scale, scale);
|
||||
disableImageSmoothing(context);
|
||||
drawLayer(psd, context);
|
||||
context.restore();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function drawLayer(layer: Layer, context: CanvasRenderingContext2D) {
|
||||
if (!layer.hidden) {
|
||||
layer.canvas && context.drawImage(layer.canvas, 0, 0);
|
||||
layer.children && layer.children.forEach(c => drawLayer(c, context));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
ng-template(#shareLinkPopover)
|
||||
div
|
||||
span.close.ml-2((click)="shareLinkOpen = false") ×
|
||||
| {{shareLink}}
|
||||
|
||||
.tools-animation.mb-5
|
||||
.d-flex(*ngIf="loaded")
|
||||
character-preview([pony]="info" [state]="state" [scale]="scale" style="height: 350px")
|
||||
|
||||
.flex-grow-1.ml-2
|
||||
.form-inline
|
||||
.form-group
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon" [fixedWidth]="true")
|
||||
.form-group
|
||||
label.ml-2 pony:
|
||||
.btn-group.dropdown.ml-1(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle)
|
||||
| {{pony.name}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item(*ngFor="let p of ponies" (click)="setPony(p)") {{p.name}}
|
||||
button.btn.btn-default.ml-1((click)="reloadPonies()" title="Reload ponies")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
.form-group
|
||||
label.ml-2 scale:
|
||||
scale-picker.ml-1([(scale)]="scale")
|
||||
//-.form-group
|
||||
button.btn.ml-1(btnCheckbox [(ngModel)]="flip" (change)="update()" [btnHighlight]="flip")
|
||||
fa-icon([icon]="rightIcon" [fixedWidth]="true")
|
||||
.form-group
|
||||
label.ml-2 mode:
|
||||
.btn-group.dropdown.ml-1(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle)
|
||||
| {{mode}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item((click)="setMode('body')") body
|
||||
a.dropdown-item((click)="setMode('head')") head
|
||||
|
||||
hr
|
||||
|
||||
.form-inline
|
||||
.from-group.d-flex
|
||||
button.btn.btn-default((click)="playing = false; frame = 0;" title="Stop")
|
||||
fa-icon([icon]="stopIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.ml-1((click)="playing = !playing" [title]="playing ? 'Pause' : 'Play'")
|
||||
fa-icon([icon]="playing ? pauseIcon : playIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.ml-1((click)="replay()" [disabled]="!playing" title="Replay")
|
||||
fa-icon([icon]="replayIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.ml-1((mousedown)="prevFrame()" [disabled]="playing" title="Prev frame")
|
||||
fa-icon([icon]="prevIcon" [fixedWidth]="true")
|
||||
input.form-control.ml-1(
|
||||
[(ngModel)]="frame" [disabled]="playing" type="number" placeholder="frame" style="width: 100px;"
|
||||
min="0" step="1")
|
||||
button.btn.btn-default.ml-1((click)="nextFrame()" [disabled]="playing" title="Next frame")
|
||||
fa-icon([icon]="nextIcon" [fixedWidth]="true")
|
||||
|
||||
.input-group.ml-1(style="width: 250px;")
|
||||
input.form-control([(ngModel)]="animation.name" (change)="update(); sortAnimations()" [disabled]="animation.builtin")
|
||||
.input-group-append.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle)
|
||||
.dropdown-menu.dropdown-menu-right(*dropdownMenu)
|
||||
a.dropdown-item(*ngFor="let a of animations" (click)="selectAnimation(a)")
|
||||
| {{a.name}}
|
||||
|
||||
button.btn.ml-1(btnCheckbox [(ngModel)]="switch" (change)="update()" [btnHighlight]="switch"
|
||||
title="Switch close and far legs")
|
||||
fa-icon([icon]="switchIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.ml-1((click)="newAnimation()" title="New Animation")
|
||||
fa-icon([icon]="fileIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.ml-1((click)="duplicateAnimation()" title="Duplicate Animation")
|
||||
fa-icon([icon]="copyIcon" [fixedWidth]="true")
|
||||
button.btn.btn-danger.ml-1((click)="removeAnimation()" title="Delete Animation")
|
||||
fa-icon([icon]="trashIcon" [fixedWidth]="true")
|
||||
|
||||
.form-inline
|
||||
.from-group
|
||||
button.btn.btn-default(
|
||||
(click)="share()" title="Share" [popover]="shareLinkPopover" [isOpen]="shareLinkOpen"
|
||||
popover-enable="shareLink" popover-class="popover-wide")
|
||||
fa-icon([icon]="shareIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.ml-1((click)="export()" title="Export")
|
||||
fa-icon([icon]="codeIcon" [fixedWidth]="true")
|
||||
.btn-group.dropdown.ml-1(dropdown)
|
||||
button.btn.btn-default((click)="png()" title="Save as PNG")
|
||||
| PNG
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle)
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item((click)="png(2)") ×2
|
||||
a.dropdown-item((click)="png(3)") ×3
|
||||
a.dropdown-item((click)="png(4)") ×4
|
||||
.btn-group.ml-1(dropdown)
|
||||
button.btn.btn-default((click)="gif()" title="Save as GIF")
|
||||
| GIF
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle)
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item((click)="gif(2)") ×2
|
||||
a.dropdown-item((click)="gif(3)") ×3
|
||||
a.dropdown-item((click)="gif(4)") ×4
|
||||
|
||||
.btn.btn-warning.ml-1(*ngIf="animation.builtin" disabled)
|
||||
| Changes to builtin animations aren't saved, duplicate it, to save the changes
|
||||
|
||||
hr
|
||||
|
||||
.form-inline
|
||||
.form-group
|
||||
label duration:
|
||||
tools-range.ml-2([(ngModel)]="activeFrame.duration" (change)="update()" min="1" max="100" small="true")
|
||||
.form-group
|
||||
label.ml-2 fps:
|
||||
tools-range.ml-2([(ngModel)]="animation.fps" (change)="update()" min="1" max="100" small="true")
|
||||
.form-group
|
||||
button.btn.btn-sm.ml-1([(ngModel)]="animation.loop" btnCheckbox [btnHighlight]="animation.loop")
|
||||
| loop
|
||||
.btn-group.ml-1(dropdown)
|
||||
button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle title="Before animation")
|
||||
| before: {{beforeAnimation?.name || 'none'}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item((click)="selectBeforeAnimation(undefined)")
|
||||
| none
|
||||
a.dropdown-item(*ngFor="let a of bodyAnimations" (click)="selectBeforeAnimation(a)")
|
||||
| {{a.name}}
|
||||
.btn-group.ml-1(dropdown)
|
||||
button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle title="After animation")
|
||||
| after: {{afterAnimation?.name || 'none'}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item((click)="selectAfterAnimation(undefined)")
|
||||
| none
|
||||
a.dropdown-item(*ngFor="let a of bodyAnimations" (click)="selectAfterAnimation(a)")
|
||||
| {{a.name}}
|
||||
|
||||
.form-inline
|
||||
.form-group
|
||||
label head offset:
|
||||
tools-range.ml-2(
|
||||
[(ngModel)]="activeFrame.headX" (change)="update()" [min]="-20" [max]="20" [small]="true")
|
||||
.btn-group.ml-1
|
||||
button.btn.btn-default.btn-sm((click)="moveAllHead(-1, 0)" title="All frames left")
|
||||
fa-icon([icon]="doubleLeftIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.btn-sm((click)="moveAllHead(1, 0)" title="All frames right")
|
||||
fa-icon([icon]="doubleRightIcon" [fixedWidth]="true")
|
||||
tools-range.ml-2(
|
||||
[(ngModel)]="activeFrame.headY" (change)="update()" [min]="-20" [max]="20" [small]="true" [vertical]="true")
|
||||
.btn-group.ml-1
|
||||
button.btn.btn-default.btn-sm((click)="moveAllHead(0, -1)" title="All frames up")
|
||||
fa-icon([icon]="doubleUpIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.btn-sm((click)="moveAllHead(0, 1)" title="All frames down")
|
||||
fa-icon([icon]="doubleDownIcon" [fixedWidth]="true")
|
||||
|
||||
.form-inline
|
||||
.form-group(*ngIf="mode === 'body'")
|
||||
label body offset:
|
||||
tools-range.ml-2(
|
||||
[(ngModel)]="activeFrame.bodyX" (change)="update()" [min]="-50" [max]="50" [small]="true")
|
||||
.btn-group.ml-1
|
||||
button.btn.btn-default.btn-sm((click)="moveAllBody(-1, 0)" title="All frames left")
|
||||
fa-icon([icon]="doubleLeftIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.btn-sm((click)="moveAllBody(1, 0)" title="All frames right")
|
||||
fa-icon([icon]="doubleRightIcon" [fixedWidth]="true")
|
||||
tools-range.ml-2(
|
||||
[(ngModel)]="activeFrame.bodyY" (change)="update()" [min]="-50" [max]="50" [small]="true" [vertical]="true")
|
||||
.btn-group.ml-1
|
||||
button.btn.btn-default.btn-sm((click)="moveAllBody(0, -1)" title="All frames up")
|
||||
fa-icon([icon]="doubleUpIcon" [fixedWidth]="true")
|
||||
button.btn.btn-default.btn-sm((click)="moveAllBody(0, 1)" title="All frames down")
|
||||
fa-icon([icon]="doubleDownIcon" [fixedWidth]="true")
|
||||
|
||||
.form-inline
|
||||
.form-group(*ngIf="mode === 'body'")
|
||||
label shadow (x, size):
|
||||
tools-range.ml-2(
|
||||
[(ngModel)]="activeFrame.shadowOffset" (change)="update()" [min]="-50" [max]="50" [small]="true")
|
||||
tools-range.ml-2(
|
||||
[(ngModel)]="activeFrame.shadowFrame" (change)="update()" [min]="-50" [max]="50" [small]="true")
|
||||
|
||||
hr
|
||||
|
||||
.form-inline
|
||||
.from-group.d-flex
|
||||
label frame:
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="addFrame()" title="Add new frame after current one")
|
||||
fa-icon([icon]="plusIcon" [fixedWidth]="true")
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="duplicateFrame()" title="Duplicate frame and add after current one")
|
||||
fa-icon([icon]="cloneIcon" [fixedWidth]="true")
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="moveFrameLeft()" [disabled]="frame === 0" title="Move current frame left")
|
||||
fa-icon([icon]="leftIcon" [fixedWidth]="true")
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="moveFrameRight()" [disabled]="frame === (totalFrames - 1)" title="Move current frame right")
|
||||
fa-icon([icon]="rightIcon" [fixedWidth]="true")
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="removeFrame()" [disabled]="totalFrames < 2" title="Delete current frame")
|
||||
fa-icon([icon]="trashIcon" [fixedWidth]="true")
|
||||
|
||||
.tools-animation-timeline(*ngIf="loaded && mode === 'body'")
|
||||
.float-left
|
||||
table.table.table-bordered.table-frames-header.table-sm
|
||||
thead
|
||||
tr: th Frame (duration)
|
||||
tbody
|
||||
tr: td Head offset (x/y)
|
||||
tr: td Body offset (x/y)
|
||||
tr: td Shadow (x/size)
|
||||
tr: td.frame-cell Front close Leg
|
||||
tr
|
||||
td.frame-cell
|
||||
check-box.float-left([(checked)]="bodyAnimation.lockFrontLegs" (checkedChange)="update()" [icon]="lockIcon")
|
||||
| Front far Leg
|
||||
tr: td.frame-cell Back close Leg
|
||||
tr
|
||||
td.frame-cell
|
||||
check-box.float-left([(checked)]="bodyAnimation.lockBackLegs" (checkedChange)="update()" [icon]="lockIcon")
|
||||
| Back far Leg
|
||||
tr: td.frame-cell Body
|
||||
tr: td.frame-cell Wing
|
||||
tr: td.frame-cell Tail
|
||||
.frames-view
|
||||
table.table.table-bordered.table-frames.table-sm(*ngIf="mode === 'body'")
|
||||
thead
|
||||
tr
|
||||
th(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)")
|
||||
| {{i + 1}}
|
||||
span.text-muted.ml-1 ({{f.duration || 1}})
|
||||
tbody
|
||||
tr
|
||||
td(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)")
|
||||
| {{f.headX}} / {{f.headY}}
|
||||
tr
|
||||
td(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)")
|
||||
| {{f.bodyX}} / {{f.bodyY}}
|
||||
tr
|
||||
td(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)")
|
||||
| {{f.shadowOffset}} / {{f.shadowFrame}}
|
||||
tr
|
||||
td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.frontLeg" [sprites]="frontLegs" [pony]="info" [x]="-15" [y]="-40"
|
||||
(frameChange)="update()" (click)="selectFrame(i)")
|
||||
tools-xy.frame-cell-xy([(x)]="f.frontLegX" [(y)]="f.frontLegY" (change)="update()")
|
||||
.frame-cell-offset {{f.frontLegX}} / {{f.frontLegY}}
|
||||
tr([class.frames-locked]="bodyAnimation.lockFrontLegs")
|
||||
td.frame-cell.far-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.frontFarLeg" [sprites]="frontLegs" [pony]="info" [x]="-15" [y]="-40"
|
||||
(frameChange)="update()" (click)="selectFrame(i)")
|
||||
tools-xy.frame-cell-xy([(x)]="f.frontFarLegX" [(y)]="f.frontFarLegY" (change)="update()")
|
||||
.frame-cell-offset {{f.frontFarLegX}} / {{f.frontFarLegY}}
|
||||
tr
|
||||
td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.backLeg" [sprites]="backLegs" [pony]="info" [x]="-30" [y]="-40"
|
||||
(frameChange)="update()" (click)="selectFrame(i)")
|
||||
tools-xy.frame-cell-xy([(x)]="f.backLegX" [(y)]="f.backLegY" (change)="update()")
|
||||
.frame-cell-offset {{f.backLegX}} / {{f.backLegY}}
|
||||
tr([class.frames-locked]="bodyAnimation.lockBackLegs")
|
||||
td.frame-cell.far-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.backFarLeg" [sprites]="backLegs" [pony]="info" [x]="-30" [y]="-40"
|
||||
(frameChange)="update()" (click)="selectFrame(i)")
|
||||
tools-xy.frame-cell-xy([(x)]="f.backFarLegX" [(y)]="f.backFarLegY" (change)="update()")
|
||||
.frame-cell-offset {{f.backFarLegX}} / {{f.backFarLegY}}
|
||||
tr
|
||||
td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.body" [sprites]="body" [pony]="info" [x]="-30" [y]="-40"
|
||||
(frameChange)="update()" (click)="selectFrame(i)")
|
||||
tr
|
||||
td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.wing" [sprites]="wing" [pony]="info" [x]="-30" [y]="-40"
|
||||
(frameChange)="update()" (click)="selectFrame(i)")
|
||||
tr
|
||||
td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.tail" [sprites]="tail" [pony]="info" [x]="-30" [y]="-40"
|
||||
(frameChange)="update()" (click)="selectFrame(i)")
|
||||
|
||||
.tools-animation-timeline(*ngIf="loaded && mode === 'head'")
|
||||
.float-left
|
||||
table.table.table-bordered.table-frames-header.table-sm
|
||||
thead
|
||||
tr: th Frame (duration)
|
||||
tbody
|
||||
tr: td Head offset (x/y)
|
||||
tr: td.frame-cell Right eye
|
||||
tr
|
||||
td.frame-cell
|
||||
check-box.float-left([(checked)]="headAnimation.lockEyes" (checkedChange)="update()" [icon]="lockIcon")
|
||||
| Left eye
|
||||
tr: td.frame-cell Mouth
|
||||
.frames-view
|
||||
table.table.table-bordered.table-frames.table-sm
|
||||
thead
|
||||
tr
|
||||
th(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)")
|
||||
| {{i + 1}}
|
||||
span.text-muted.ml-1 ({{f.duration || 1}})
|
||||
tbody
|
||||
tr
|
||||
td(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)")
|
||||
| {{f.headX}} / {{f.headY}}
|
||||
tr
|
||||
td.frame-cell(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.right" [sprites]="rightEyes" [pony]="info" [x]="-15" [y]="-20" [circle]="coat"
|
||||
(frameChange)="update()" [reverseExtra]="true" (click)="selectFrame(i)")
|
||||
tr([class.frames-locked]="headAnimation.lockEyes")
|
||||
td.frame-cell(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.left" [sprites]="leftEyes" [pony]="info" [x]="-15" [y]="-20" [circle]="coat"
|
||||
(frameChange)="update()" [reverseExtra]="true" (click)="selectFrame(i)")
|
||||
tr
|
||||
td.frame-cell(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)")
|
||||
tools-frame(
|
||||
[(frame)]="f.mouth" [sprites]="mouths" [pony]="info" [x]="-15" [y]="-20" [circle]="coat"
|
||||
(frameChange)="update()" [reverseExtra]="true" (click)="selectFrame(i)")
|
||||
@@ -0,0 +1,99 @@
|
||||
@import '../../../../styles/partials/variables';
|
||||
|
||||
$frame-size: 79px;
|
||||
|
||||
.tools-animation {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tools-animation-timeline {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.frame-cell {
|
||||
height: $frame-size;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
td, th {
|
||||
&.active {
|
||||
background-color: #aef5ae !important;
|
||||
}
|
||||
}
|
||||
|
||||
.table-frames-header {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
color: $text-muted;
|
||||
|
||||
td, th {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.frames-view {
|
||||
background: #eee;
|
||||
color: #222;
|
||||
overflow-x: scroll;
|
||||
border: solid 1px #ddd;
|
||||
}
|
||||
|
||||
.frames-locked {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.from-group {
|
||||
> .input-group, > .form-control {
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-frames {
|
||||
table-layout: fixed;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
border: none;
|
||||
color: $inverse-color;
|
||||
|
||||
td, th {
|
||||
width: $frame-size;
|
||||
text-align: center;
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.frame-cell-xy {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0.2;
|
||||
|
||||
:hover > & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.frame-cell-offset {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,804 @@
|
||||
import { Component, HostListener, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { flatMap, dropRightWhile, compact } from 'lodash';
|
||||
import {
|
||||
BodyAnimation as IBodyAnimation,
|
||||
BodyAnimationFrame as IBodyAnimationFrame,
|
||||
HeadAnimation as IHeadAnimation,
|
||||
HeadAnimationFrame as IHeadAnimationFrame,
|
||||
ColorExtraSet, PonyInfo, PonyObject, BodyShadow, PonyEye
|
||||
} from '../../../common/interfaces';
|
||||
import { removeItem, repeat, isKeyEventInvalid, cloneDeep, array } from '../../../common/utils';
|
||||
import { toPalette, createDefaultPony, syncLockedPonyInfo } from '../../../common/ponyInfo';
|
||||
import { Key } from '../../../client/input/input';
|
||||
import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers';
|
||||
import {
|
||||
headAnimations, animations, createBodyFrame, createHeadFrame, stand, sit, mergeAnimations,
|
||||
sitDown, lieDown, lie, sitUp, standUp
|
||||
} from '../../../client/ponyAnimations';
|
||||
import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch';
|
||||
import * as sprites from '../../../generated/sprites';
|
||||
import { createCanvas, disableImageSmoothing, saveCanvas } from '../../../client/canvasUtils';
|
||||
import { loadAndInitSpriteSheets, createEyeSprite } from '../../../client/spriteUtils';
|
||||
import { drawPony } from '../../../client/ponyDraw';
|
||||
import {
|
||||
faLock, faHome, faArrowRight, faArrowLeft, faPause, faPlay, faChevronRight, faChevronLeft, faRetweet,
|
||||
faClone, faPlus, faAngleDoubleDown, faAngleDoubleUp, faAngleDoubleRight, faAngleDoubleLeft,
|
||||
faCode, faShare, faTrash, faCopy, faFile, faStop, faRedo, faSync
|
||||
} from '../../../client/icons';
|
||||
import { FrameService, FrameLoop } from '../../services/frameService';
|
||||
import { StorageService } from '../../services/storageService';
|
||||
import { decompressPonyString } from '../../../common/compressPony';
|
||||
|
||||
const ponyWidth = 80;
|
||||
const ponyHeight = 80;
|
||||
|
||||
type AnimationMode = 'body' | 'head';
|
||||
|
||||
interface BaseAnimationFrame {
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface BodyAnimationFrame extends IBodyAnimationFrame, BaseAnimationFrame {
|
||||
shadowOffset: number;
|
||||
shadowFrame: number;
|
||||
}
|
||||
|
||||
interface HeadAnimationFrame extends IHeadAnimationFrame, BaseAnimationFrame { }
|
||||
|
||||
interface BaseAnimation {
|
||||
name: string;
|
||||
fps: number;
|
||||
loop: boolean;
|
||||
builtin?: boolean;
|
||||
}
|
||||
|
||||
interface BodyAnimation extends BaseAnimation {
|
||||
lockFrontLegs?: boolean;
|
||||
lockBackLegs?: boolean;
|
||||
frames: BodyAnimationFrame[];
|
||||
}
|
||||
|
||||
interface HeadAnimation extends BaseAnimation {
|
||||
lockEyes?: boolean;
|
||||
frames: HeadAnimationFrame[];
|
||||
}
|
||||
|
||||
interface AnimationsData {
|
||||
active?: string;
|
||||
animations?: BodyAnimation[];
|
||||
headActive?: string;
|
||||
headAnimations?: HeadAnimation[];
|
||||
}
|
||||
|
||||
interface PonyItem {
|
||||
name: string;
|
||||
info: PonyInfo;
|
||||
}
|
||||
|
||||
const testPony = { name: 'test pony', info: createDefaultPony() };
|
||||
|
||||
function eyeSprite(e: PonyEye | undefined) {
|
||||
return createEyeSprite(e, 0, sprites.defaultPalette);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tools-animation',
|
||||
templateUrl: 'tools-animation.pug',
|
||||
styleUrls: ['tools-animation.scss'],
|
||||
})
|
||||
export class ToolsAnimation implements OnInit, OnDestroy {
|
||||
readonly lockIcon = faLock;
|
||||
readonly homeIcon = faHome;
|
||||
readonly rightIcon = faArrowRight;
|
||||
readonly leftIcon = faArrowLeft;
|
||||
readonly stopIcon = faStop;
|
||||
readonly pauseIcon = faPause;
|
||||
readonly playIcon = faPlay;
|
||||
readonly replayIcon = faRedo;
|
||||
readonly prevIcon = faChevronLeft;
|
||||
readonly nextIcon = faChevronRight;
|
||||
readonly switchIcon = faRetweet;
|
||||
readonly fileIcon = faFile;
|
||||
readonly copyIcon = faCopy;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly shareIcon = faShare;
|
||||
readonly codeIcon = faCode;
|
||||
readonly doubleLeftIcon = faAngleDoubleLeft;
|
||||
readonly doubleRightIcon = faAngleDoubleRight;
|
||||
readonly doubleUpIcon = faAngleDoubleUp;
|
||||
readonly doubleDownIcon = faAngleDoubleDown;
|
||||
readonly plusIcon = faPlus;
|
||||
readonly cloneIcon = faClone;
|
||||
readonly syncIcon = faSync;
|
||||
loaded = false;
|
||||
pony: PonyItem;
|
||||
ponies: PonyItem[] = [testPony];
|
||||
scale = 3;
|
||||
shareLink?: string;
|
||||
shareLinkOpen = false;
|
||||
state = defaultPonyState();
|
||||
bodyAnimations: BodyAnimation[] = animations.map(fromBodyAnimation);
|
||||
bodyAnimation: BodyAnimation;
|
||||
headAnimations: HeadAnimation[] = headAnimations.map(fromHeadAnimation);
|
||||
headAnimation: HeadAnimation;
|
||||
body = sprites.body.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 }));
|
||||
wing = sprites.wings.map(types => types![3]![0]!);
|
||||
tail = sprites.tails.map(types => types![17]![0]!);
|
||||
frontLegs: ColorExtraSet = sprites.frontLegs.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 }));
|
||||
backLegs: ColorExtraSet = sprites.backLegs.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 }));
|
||||
leftEyes: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite);
|
||||
rightEyes: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite);
|
||||
mouths: ColorExtraSet = sprites.noses
|
||||
.map(m => m[0][0])
|
||||
.map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palette: sprites.defaultPalette }));
|
||||
flip = false;
|
||||
switch = false;
|
||||
mode: AnimationMode;
|
||||
beforeAnimation: BodyAnimation | undefined;
|
||||
afterAnimation: BodyAnimation | undefined;
|
||||
private _playing = false;
|
||||
private _frame = 0;
|
||||
private loop: FrameLoop;
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private route: ActivatedRoute,
|
||||
private storage: StorageService,
|
||||
frameService: FrameService
|
||||
) {
|
||||
this.mode = storage.getItem('tools-animation-mode') as AnimationMode || 'body';
|
||||
this.loop = frameService.create(delta => this.tick(delta));
|
||||
|
||||
const data = this.loadAnimations();
|
||||
|
||||
const extraAnimations: IBodyAnimation[] = [
|
||||
mergeAnimations('sit-lie-sit', 24, false, [...repeat(12, sit), lieDown, ...repeat(12, lie), sitUp, sit]),
|
||||
mergeAnimations('stand-sit-stand', 24, false, [...repeat(12, stand), sitDown, ...repeat(12, sit), standUp, stand]),
|
||||
mergeAnimations('stand-to-sit', 24, false, [stand, sitDown, sit]),
|
||||
mergeAnimations('sit-to-lie', 24, false, [sit, lieDown, lie]),
|
||||
{ ...stand, loop: false, name: 'standing (1s)', frames: array(stand.fps, stand.frames[0]) },
|
||||
{ ...sit, loop: false, name: 'sitting (1s)', frames: array(sit.fps, sit.frames[0]) },
|
||||
{ ...lie, loop: false, name: 'lying (1s)', frames: array(lie.fps, lie.frames[0]) },
|
||||
];
|
||||
|
||||
this.bodyAnimations.push(...extraAnimations.map((a, i) => fromBodyAnimation(a, 90 + i)));
|
||||
this.bodyAnimations.push(...(data.animations || []).map(fixBodyAnimation));
|
||||
this.headAnimations.push(...(data.headAnimations || []).map(fixHeadAnimation));
|
||||
this.bodyAnimation = this.bodyAnimations[0];
|
||||
this.headAnimation = this.headAnimations[0];
|
||||
|
||||
this.sortAnimations();
|
||||
this.selectBodyAnimation(this.bodyAnimations[parseInt(data.active || '0', 10) | 0] || this.bodyAnimation);
|
||||
this.selectHeadAnimation(this.headAnimations[parseInt(data.headActive || '0', 10) | 0] || this.headAnimation);
|
||||
|
||||
this.pony = this.ponies[0];
|
||||
this.pony.info.coatFill = '#9f7e7e';
|
||||
this.pony.info.mane!.type = 9;
|
||||
this.pony.info.mane!.fills![0] = '#e2cf67';
|
||||
this.pony.info.lockEyes = false;
|
||||
this.pony.info.cm = [
|
||||
'orange', 'orange', 'orange', 'orange', 'orange',
|
||||
'orange', '', '', '', 'orange',
|
||||
'orange', '', '', '', 'orange',
|
||||
'orange', '', '', '', 'orange',
|
||||
'orange', 'orange', 'orange', 'orange', 'orange',
|
||||
];
|
||||
|
||||
syncLockedPonyInfo(this.pony.info);
|
||||
this.reloadPonies();
|
||||
}
|
||||
get info() {
|
||||
return this.pony.info;
|
||||
}
|
||||
get frame() {
|
||||
return this._frame;
|
||||
}
|
||||
set frame(value: number) {
|
||||
if (this._frame !== value) {
|
||||
this._frame = value % this.frames.length;
|
||||
|
||||
if (!this.playing) {
|
||||
if (this.mode === 'body') {
|
||||
this.state.animationFrame = this.frame;
|
||||
} else {
|
||||
this.state.headAnimationFrame = this.frame;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get activeFrame(): BaseAnimationFrame {
|
||||
return this.frames[this.frame] || ({} as any);
|
||||
}
|
||||
get totalFrames() {
|
||||
return this.frames.length;
|
||||
}
|
||||
get playing() {
|
||||
return this._playing;
|
||||
}
|
||||
set playing(value: boolean) {
|
||||
if (this._playing !== value) {
|
||||
this._playing = value;
|
||||
this.time = 0;
|
||||
this.update();
|
||||
|
||||
if (!value) {
|
||||
this.frame = this.mode === 'body' ? this.state.animationFrame : this.state.headAnimationFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
get bodyFrames() {
|
||||
return this.bodyAnimation.frames;
|
||||
}
|
||||
get headFrames() {
|
||||
return this.headAnimation.frames;
|
||||
}
|
||||
ngOnInit() {
|
||||
this.route.params.subscribe(({ id }) => id && this.fetchAnimation(id));
|
||||
|
||||
return loadAndInitSpriteSheets().then(() => {
|
||||
this.loaded = true;
|
||||
this.update();
|
||||
this.loop.init();
|
||||
});
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.loop.destroy();
|
||||
}
|
||||
reloadPonies() {
|
||||
this.http.get<PonyObject[]>('/api-tools/ponies')
|
||||
.subscribe(data => {
|
||||
this.ponies = [
|
||||
testPony,
|
||||
...data
|
||||
.map(p => ({ name: p.name, info: decompressPonyString(p.info) }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
];
|
||||
|
||||
const ponyName = this.storage.getItem('tools-animation-pony');
|
||||
|
||||
if (ponyName) {
|
||||
this.pony = this.ponies.find(p => p.name === ponyName) || this.pony;
|
||||
}
|
||||
});
|
||||
}
|
||||
setPony(pony: PonyItem) {
|
||||
this.pony = pony;
|
||||
this.update();
|
||||
this.storage.setItem('tools-animation-pony', pony.name);
|
||||
}
|
||||
selectBodyAnimation(animation: BodyAnimation) {
|
||||
this.bodyAnimation = animation;
|
||||
this.update();
|
||||
}
|
||||
selectHeadAnimation(animation: HeadAnimation) {
|
||||
this.headAnimation = animation;
|
||||
this.update();
|
||||
}
|
||||
setMode(mode: AnimationMode) {
|
||||
this.mode = mode;
|
||||
this.storage.setItem('tools-animation-mode', mode);
|
||||
}
|
||||
replay() {
|
||||
this.bodyAnimationPlaying = 0;
|
||||
this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying];
|
||||
this.time = 0;
|
||||
}
|
||||
private fetchAnimation(id: string) {
|
||||
return this.http.get<{ type: string, animation: any; }>(`/api-tools/animation/${id}`)
|
||||
.subscribe(({ type, animation }) => {
|
||||
if (type === 'body') {
|
||||
this.bodyAnimations.push(animation);
|
||||
} else {
|
||||
this.headAnimations.push(animation);
|
||||
}
|
||||
|
||||
this.sortAnimations();
|
||||
this.selectAnimation(animation);
|
||||
});
|
||||
}
|
||||
private createAnimation(): BodyAnimation | HeadAnimation {
|
||||
if (this.mode === 'body') {
|
||||
return { name: 'new animation', loop: true, fps: 24, frames: [createDefaultBodyFrame()] };
|
||||
} else {
|
||||
return { name: 'new animation', loop: true, fps: 24, frames: [createDefaultHeadFrame()] };
|
||||
}
|
||||
}
|
||||
newAnimation() {
|
||||
const animation = this.createAnimation();
|
||||
const animations = this.animations as any[];
|
||||
animations.push(animation);
|
||||
this.selectAnimation(animation);
|
||||
}
|
||||
duplicateAnimation() {
|
||||
const animation = cloneDeep(this.animation);
|
||||
const animations = this.animations as any[];
|
||||
animation.name = animation.name.replace(/# builtin \d+ #/, '').trim() + ' (clone)';
|
||||
delete animation.builtin;
|
||||
animations.push(animation);
|
||||
this.selectAnimation(animation);
|
||||
}
|
||||
removeAnimation() {
|
||||
const animations = this.animations;
|
||||
|
||||
if (animations.length && confirm('are you sure ?')) {
|
||||
removeItem(animations, this.animation);
|
||||
this.selectAnimation(animations[0]);
|
||||
}
|
||||
}
|
||||
selectAnimation(animation: BodyAnimation | HeadAnimation) {
|
||||
if (this.mode === 'body') {
|
||||
this.selectBodyAnimation(animation as BodyAnimation);
|
||||
} else {
|
||||
this.selectHeadAnimation(animation as HeadAnimation);
|
||||
}
|
||||
}
|
||||
selectBeforeAnimation(animation: BodyAnimation | undefined) {
|
||||
this.beforeAnimation = animation;
|
||||
this.update();
|
||||
}
|
||||
selectAfterAnimation(animation: BodyAnimation | undefined) {
|
||||
this.afterAnimation = animation;
|
||||
this.update();
|
||||
}
|
||||
selectFrame(index: number) {
|
||||
this.frame = index;
|
||||
}
|
||||
prevFrame() {
|
||||
this.frame = this.frame === 0 ? (this.frames.length - 1) : (this.frame - 1);
|
||||
}
|
||||
nextFrame() {
|
||||
this.frame = this.frame + 1;
|
||||
}
|
||||
addFrame() {
|
||||
const frames = this.frames as any[];
|
||||
const frame = this.mode === 'body' ? createDefaultBodyFrame() : createDefaultHeadFrame();
|
||||
frames.splice(this.frame + 1, 0, frame);
|
||||
this.update();
|
||||
this.frame++;
|
||||
}
|
||||
duplicateFrame() {
|
||||
const frames = this.frames as any[];
|
||||
frames.splice(this.frame + 1, 0, cloneDeep(frames[this.frame]));
|
||||
this.update();
|
||||
this.frame++;
|
||||
}
|
||||
removeFrame() {
|
||||
const frames = this.frames;
|
||||
|
||||
if (frames.length && confirm('are you sure ?')) {
|
||||
frames.splice(this.frame, 1);
|
||||
this.update();
|
||||
this.frame = Math.min(this.frame, frames.length - 1);
|
||||
}
|
||||
}
|
||||
moveFrameLeft() {
|
||||
if (this.frame > 0) {
|
||||
swap(this.frames, this.frame, this.frame - 1);
|
||||
this.update();
|
||||
this.frame--;
|
||||
}
|
||||
}
|
||||
moveFrameRight() {
|
||||
const frames = this.frames;
|
||||
|
||||
if (this.frame < (frames.length - 1)) {
|
||||
swap(frames, this.frame, this.frame + 1);
|
||||
this.update();
|
||||
this.frame++;
|
||||
}
|
||||
}
|
||||
isActive(index: number) {
|
||||
return this.frame === index;
|
||||
}
|
||||
get animations() {
|
||||
return this.mode === 'body' ? this.bodyAnimations : this.headAnimations;
|
||||
}
|
||||
get animation() {
|
||||
return this.mode === 'body' ? this.bodyAnimation : this.headAnimation;
|
||||
}
|
||||
get frames() {
|
||||
return this.animation.frames;
|
||||
}
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
keydown(e: KeyboardEvent) {
|
||||
if (!isKeyEventInvalid(e) && this.handleKey(e.keyCode)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
moveAllHead(x: number, y: number) {
|
||||
(this.frames as HeadAnimationFrame[]).forEach(f => {
|
||||
f.headX += x;
|
||||
f.headY += y;
|
||||
});
|
||||
this.update();
|
||||
}
|
||||
moveAllBody(x: number, y: number) {
|
||||
(this.frames as BodyAnimationFrame[]).forEach(f => {
|
||||
f.bodyX += x;
|
||||
f.bodyY += y;
|
||||
});
|
||||
this.update();
|
||||
}
|
||||
handleKey(keyCode: number) {
|
||||
if (keyCode === Key.OPEN_BRACKET || keyCode === Key.LEFT || keyCode === Key.COMMA) {
|
||||
this.prevFrame();
|
||||
} else if (keyCode === Key.CLOSE_BRACKET || keyCode === Key.RIGHT || keyCode === Key.PERIOD) {
|
||||
this.nextFrame();
|
||||
} else if (keyCode === Key.ENTER) {
|
||||
this.playing = !this.playing;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
share() {
|
||||
const wasOpened = this.shareLinkOpen;
|
||||
this.shareLink = undefined;
|
||||
this.shareLinkOpen = false;
|
||||
|
||||
if (!wasOpened) {
|
||||
const animation = { type: this.mode, animation: this.animation };
|
||||
this.http.post<{ name: string; }>('/api-tools/animation', { animation })
|
||||
.subscribe(({ name }) => {
|
||||
this.shareLink = `${location.protocol}//${location.host}/tools/animation/${name}`;
|
||||
this.shareLinkOpen = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
export() {
|
||||
if (this.mode === 'body') {
|
||||
const frames = this.bodyAnimation.frames
|
||||
.map(f => [f.duration, '[' + compressBodyFrame(f).join(', ') + ']'])
|
||||
.map(([repeat, frame]) => repeat > 1 ? `...repeat(${repeat}, ${frame})` : frame);
|
||||
console.log(`frames: [\n${frames.map(x => `\t${x}`).join(',\n')}\n]`);
|
||||
|
||||
if (this.bodyAnimation.frames.some(f => !!f.shadowFrame || !!f.shadowOffset)) {
|
||||
const shadow = this.bodyAnimation.frames.map(f => [f.shadowFrame, f.shadowOffset]);
|
||||
console.log(`shadow: [${shadow.map(x => `[${x.join(', ')}]`).join(', ')}]`);
|
||||
}
|
||||
} else {
|
||||
const animation = toHeadAnimation(this.headAnimation, true);
|
||||
const compressed = animation.frames.map(compressHeadFrame);
|
||||
console.log(JSON.stringify(compressed));
|
||||
}
|
||||
}
|
||||
png(scale = 1) {
|
||||
const { canvas } = this.createAnimationSprites(scale);
|
||||
saveCanvas(canvas, `${this.animation.name}.png`);
|
||||
}
|
||||
gif(scale = 1) {
|
||||
const { canvas, empty } = this.createAnimationSprites(scale);
|
||||
const wnd = window.open('')!;
|
||||
const width = scale * ponyWidth;
|
||||
const height = scale * ponyHeight;
|
||||
const fps = this.animation.fps || 24;
|
||||
const image = canvas.toDataURL();
|
||||
|
||||
this.http.post<{ name: string; }>('/api-tools/animation-gif', { image, width, height, fps, remove: empty })
|
||||
.subscribe(({ name }) => wnd.location.href = `/api-tools/animation/${name}.gif`);
|
||||
}
|
||||
sortAnimations() {
|
||||
this.bodyAnimations.sort(compareAnimations);
|
||||
this.headAnimations.sort(compareAnimations);
|
||||
}
|
||||
private update() {
|
||||
if (this.headAnimation && this.headAnimation.lockEyes) {
|
||||
this.headAnimation.frames.forEach(f => f.left = f.right);
|
||||
}
|
||||
|
||||
if (this.bodyAnimation) {
|
||||
if (this.bodyAnimation.lockFrontLegs) {
|
||||
this.bodyAnimation.frames.forEach(f => {
|
||||
f.frontFarLeg = f.frontLeg;
|
||||
f.frontFarLegX = f.frontLegX;
|
||||
f.frontFarLegY = f.frontLegY;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.bodyAnimation.lockBackLegs) {
|
||||
this.bodyAnimation.frames.forEach(f => {
|
||||
f.backFarLeg = f.backLeg;
|
||||
f.backFarLegX = f.backLegX;
|
||||
f.backFarLegY = f.backLegY;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.bodyAnimationsToPlay = compact([
|
||||
this.playing && this.beforeAnimation && { ...toBodyAnimation(this.beforeAnimation, true, false), loop: false },
|
||||
toBodyAnimation(this.bodyAnimation, this.playing, this.switch),
|
||||
this.playing && this.afterAnimation && { ...toBodyAnimation(this.afterAnimation, true, false), loop: true },
|
||||
]);
|
||||
|
||||
this.bodyAnimationPlaying = 0;
|
||||
|
||||
this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying];
|
||||
this.state.headAnimation = toHeadAnimation(this.headAnimation, this.playing);
|
||||
|
||||
if (this.playing) {
|
||||
this.state.animationFrame = 0;
|
||||
this.state.headAnimationFrame = 0;
|
||||
}
|
||||
|
||||
this.saveAnimations();
|
||||
}
|
||||
private bodyAnimationsToPlay: IBodyAnimation[] = [];
|
||||
private bodyAnimationPlaying = 0;
|
||||
private time = 0;
|
||||
private tick(delta: number) {
|
||||
if (this.playing) {
|
||||
this.time += delta;
|
||||
|
||||
if (this.mode === 'body') {
|
||||
if (this.state.animation) {
|
||||
const frame = this.time * this.state.animation.fps;
|
||||
|
||||
if (frame > this.state.animation.frames.length && !this.state.animation.loop) {
|
||||
this.bodyAnimationPlaying = (this.bodyAnimationPlaying + 1) % this.bodyAnimationsToPlay.length;
|
||||
this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying];
|
||||
this.state.animationFrame = 0;
|
||||
this.time = 0;
|
||||
} else {
|
||||
this.state.animationFrame = Math.floor(frame) % this.state.animation.frames.length;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.state.headAnimation) {
|
||||
const frame = Math.floor(this.time * this.state.headAnimation.fps);
|
||||
this.state.headAnimationFrame = frame % this.state.headAnimation.frames.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private saveAnimations() {
|
||||
this.storage.setJSON('tools-animations', <AnimationsData>{
|
||||
active: this.bodyAnimations.indexOf(this.bodyAnimation).toString(),
|
||||
animations: this.bodyAnimations.filter(a => !a.builtin),
|
||||
headActive: this.headAnimations.indexOf(this.headAnimation).toString(),
|
||||
headAnimations: this.headAnimations.filter(a => !a.builtin),
|
||||
});
|
||||
}
|
||||
private loadAnimations(): AnimationsData {
|
||||
return this.storage.getJSON<AnimationsData>('tools-animations', {});
|
||||
}
|
||||
private createAnimationSprites(scale: number) {
|
||||
const animation = toBodyAnimation(this.bodyAnimation, true, this.switch);
|
||||
const headAnimation = toHeadAnimation(this.headAnimation, true);
|
||||
const frames = this.mode === 'body' ? animation.frames.length : headAnimation.frames.length;
|
||||
const buffer = createCanvas(ponyWidth, ponyHeight);
|
||||
const batch = new ContextSpriteBatch(buffer);
|
||||
const info = toPalette(this.pony.info);
|
||||
const cols = Math.ceil(Math.sqrt(frames));
|
||||
const canvas = createCanvas(ponyWidth * cols * scale, ponyHeight * Math.ceil(frames / cols) * scale);
|
||||
const context = canvas.getContext('2d')!;
|
||||
const empty = (cols * Math.ceil(frames / cols)) - frames;
|
||||
const options = defaultDrawPonyOptions();
|
||||
disableImageSmoothing(context);
|
||||
context.scale(scale, scale);
|
||||
|
||||
for (let i = 0; i < frames; i++) {
|
||||
const x = i % cols;
|
||||
const y = Math.floor(i / cols);
|
||||
|
||||
batch.start(sprites.paletteSpriteSheet, 0);
|
||||
|
||||
drawPony(batch, info, {
|
||||
...defaultPonyState(),
|
||||
animation,
|
||||
animationFrame: this.mode === 'body' ? i : 0,
|
||||
headAnimation: this.mode === 'head' ? headAnimation : undefined,
|
||||
headAnimationFrame: this.mode === 'head' ? i : 0,
|
||||
blinkFrame: 1,
|
||||
}, ponyWidth / 2, ponyHeight - 10, options);
|
||||
|
||||
batch.end();
|
||||
context.drawImage(buffer, x * ponyWidth, y * ponyHeight);
|
||||
}
|
||||
|
||||
return { canvas, empty };
|
||||
}
|
||||
}
|
||||
|
||||
// helper methods
|
||||
|
||||
function compareAnimations<T extends { name: string; }>(a: T, b: T): number {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
function swap(array: any[], a: number, b: number) {
|
||||
const temp = array[a];
|
||||
array[a] = array[b];
|
||||
array[b] = temp;
|
||||
}
|
||||
|
||||
function fromBodyAnimation({ name, frames, fps, loop, shadow }: IBodyAnimation, index: number): BodyAnimation {
|
||||
const fs: BodyAnimationFrame[] = [];
|
||||
|
||||
frames.forEach((f, i) => {
|
||||
const l = fs[fs.length - 1];
|
||||
const s = shadow && shadow[i];
|
||||
|
||||
if (
|
||||
l && l.headX === f.headX && l.headY === f.headY && l.bodyX === f.bodyX && l.bodyY === f.bodyY
|
||||
&& l.body === f.body && l.frontLeg === f.frontLeg && l.backLeg === f.backLeg
|
||||
&& l.frontFarLeg === f.frontFarLeg && l.backFarLeg === f.backFarLeg
|
||||
&& l.frontLegX === f.frontLegX && l.frontLegY === f.frontLegY
|
||||
&& l.frontFarLegX === f.frontFarLegX && l.frontFarLegY === f.frontFarLegY
|
||||
&& l.backLegX === f.backLegX && l.backLegY === f.backLegY
|
||||
&& l.backFarLegX === f.backFarLegX && l.backFarLegY === f.backFarLegY
|
||||
&& l.wing === f.wing
|
||||
) {
|
||||
l.duration++;
|
||||
} else {
|
||||
fs.push({
|
||||
duration: 1,
|
||||
...f,
|
||||
shadowOffset: s && s.offset || 0,
|
||||
shadowFrame: s && s.frame || 0
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
builtin: true,
|
||||
loop,
|
||||
fps,
|
||||
name: `# ${index.toString().padStart(2, '0')}-${name}`,
|
||||
frames: fs,
|
||||
};
|
||||
}
|
||||
|
||||
function toBodyAnimation({ name, loop, fps, frames }: BodyAnimation, full: boolean, switchFarClose: boolean): IBodyAnimation {
|
||||
let shadow: BodyShadow[] | undefined = undefined;
|
||||
|
||||
if (frames.some(f => !!f.shadowFrame || !!f.shadowOffset)) {
|
||||
shadow = flatMap(frames, f => repeat(full ? f.duration : 1, { frame: f.shadowFrame, offset: f.shadowOffset }));
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
loop,
|
||||
fps,
|
||||
shadow,
|
||||
frames: flatMap(frames, f => repeat(full ? f.duration : 1, {
|
||||
body: f.body,
|
||||
head: f.head,
|
||||
wing: f.wing,
|
||||
tail: f.tail,
|
||||
frontLeg: switchFarClose ? f.frontFarLeg : f.frontLeg,
|
||||
frontFarLeg: switchFarClose ? f.frontLeg : f.frontFarLeg,
|
||||
backLeg: switchFarClose ? f.backFarLeg : f.backLeg,
|
||||
backFarLeg: switchFarClose ? f.backLeg : f.backFarLeg,
|
||||
bodyX: f.bodyX,
|
||||
bodyY: f.bodyY,
|
||||
headX: f.headX,
|
||||
headY: f.headY,
|
||||
frontLegX: switchFarClose ? f.frontFarLegX : f.frontLegX,
|
||||
frontLegY: switchFarClose ? f.frontFarLegY : f.frontLegY,
|
||||
frontFarLegX: switchFarClose ? f.frontLegX : f.frontFarLegX,
|
||||
frontFarLegY: switchFarClose ? f.frontLegY : f.frontFarLegY,
|
||||
backLegX: switchFarClose ? f.backFarLegX : f.backLegX,
|
||||
backLegY: switchFarClose ? f.backFarLegY : f.backLegY,
|
||||
backFarLegX: switchFarClose ? f.backLegX : f.backFarLegX,
|
||||
backFarLegY: switchFarClose ? f.backLegY : f.backFarLegY,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function compressBodyFrame(f: BodyAnimationFrame): number[] {
|
||||
return dropRightWhile([
|
||||
f.body, f.head, f.wing, f.tail, f.frontLeg, f.frontFarLeg, f.backLeg, f.backFarLeg,
|
||||
f.bodyX, f.bodyY, f.headX, f.headY,
|
||||
f.frontLegX, f.frontLegY, f.frontFarLegX, f.frontFarLegY,
|
||||
f.backLegX, f.backLegY, f.backFarLegX, f.backFarLegY,
|
||||
], x => !x);
|
||||
}
|
||||
|
||||
function fromHeadAnimation({ name, fps, loop, frames }: IHeadAnimation, index: number): HeadAnimation {
|
||||
const fs: HeadAnimationFrame[] = [];
|
||||
|
||||
frames.forEach(f => {
|
||||
const l = fs[fs.length - 1];
|
||||
|
||||
if (l && l.headX === f.headX && l.headY === f.headY && l.left === f.left && l.right === f.right && l.mouth === f.mouth) {
|
||||
l.duration++;
|
||||
} else {
|
||||
fs.push({ duration: 1, ...f });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
builtin: true,
|
||||
fps,
|
||||
loop,
|
||||
name: `# builtin ${index.toString().padStart(2, '0')} # ${name}`,
|
||||
frames: fs,
|
||||
};
|
||||
}
|
||||
|
||||
function toHeadAnimation({ name, frames, fps, loop }: HeadAnimation, full: boolean): IHeadAnimation {
|
||||
const fs = (full && !loop) ? repeat(fps, createDefaultHeadFrame()).concat(frames) : frames;
|
||||
|
||||
return {
|
||||
name,
|
||||
fps,
|
||||
loop,
|
||||
frames: flatMap(fs, f => repeat(full ? f.duration : 1, f)),
|
||||
};
|
||||
}
|
||||
|
||||
function compressHeadFrame({ headX, headY, left, right, mouth }: IHeadAnimationFrame) {
|
||||
return [headX, headY, left, right, mouth];
|
||||
}
|
||||
|
||||
function createDefaultBodyFrame(): BodyAnimationFrame {
|
||||
return { duration: 1, ...createBodyFrame([1, 1, 0, 0, 1, 1, 1, 1]), shadowFrame: 0, shadowOffset: 0 };
|
||||
}
|
||||
|
||||
function createDefaultHeadFrame(): HeadAnimationFrame {
|
||||
return { duration: 1, ...createHeadFrame([0, 0, 1, 1, 0]) };
|
||||
}
|
||||
|
||||
// fixing helpers
|
||||
|
||||
function fixBodyAnimation(a: BodyAnimation): BodyAnimation {
|
||||
return {
|
||||
name: a.name || '',
|
||||
fps: a.fps || 24,
|
||||
loop: a.loop || false,
|
||||
lockFrontLegs: a.lockFrontLegs || false,
|
||||
lockBackLegs: a.lockBackLegs || false,
|
||||
frames: (a.frames || []).map(fixBodyFrame),
|
||||
};
|
||||
}
|
||||
|
||||
function fixBodyFrame(f: BodyAnimationFrame): BodyAnimationFrame {
|
||||
return {
|
||||
duration: f.duration || 1,
|
||||
body: f.body || 0,
|
||||
head: f.head || 0,
|
||||
wing: f.wing || 0,
|
||||
tail: f.tail || 0,
|
||||
frontLeg: f.frontLeg || 0,
|
||||
frontFarLeg: f.frontFarLeg || 0,
|
||||
backLeg: f.backLeg || 0,
|
||||
backFarLeg: f.backFarLeg || 0,
|
||||
bodyX: f.bodyX || 0,
|
||||
bodyY: f.bodyY || 0,
|
||||
headX: f.headX || 0,
|
||||
headY: f.headY || 0,
|
||||
frontLegX: f.frontLegX || 0,
|
||||
frontLegY: f.frontLegY || 0,
|
||||
frontFarLegX: f.frontFarLegX || 0,
|
||||
frontFarLegY: f.frontFarLegY || 0,
|
||||
backLegX: f.backLegX || 0,
|
||||
backLegY: f.backLegY || 0,
|
||||
backFarLegX: f.backFarLegX || 0,
|
||||
backFarLegY: f.backFarLegY || 0,
|
||||
shadowFrame: f.shadowFrame || 0,
|
||||
shadowOffset: f.shadowOffset || 0,
|
||||
};
|
||||
}
|
||||
|
||||
function fixHeadAnimation(a: HeadAnimation): HeadAnimation {
|
||||
return {
|
||||
name: a.name || '',
|
||||
fps: a.fps || 24,
|
||||
loop: a.loop || false,
|
||||
lockEyes: a.lockEyes || false,
|
||||
frames: (a.frames || []).map(fixHeadFrame),
|
||||
};
|
||||
}
|
||||
|
||||
function fixHeadFrame(f: HeadAnimationFrame): HeadAnimationFrame {
|
||||
return {
|
||||
duration: f.duration || 1,
|
||||
headX: f.headX || 0,
|
||||
headY: f.headY || 0,
|
||||
left: f.left || 0,
|
||||
right: f.right || 0,
|
||||
mouth: f.mouth || 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.d-flex.m-2
|
||||
canvas.pixelart(#canvas width="1000" height="900")
|
||||
.ml-2
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
button.btn.btn-default.ml-1((click)="toggleBg()")
|
||||
| Toggle BG
|
||||
.mt-2
|
||||
fa-icon.supporter-1([icon]="starIcon" size="lg")
|
||||
fa-icon.supporter-2.ml-1([icon]="starIcon" size="lg")
|
||||
fa-icon.supporter-3.ml-1([icon]="starIcon" size="lg")
|
||||
@@ -0,0 +1,191 @@
|
||||
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
||||
import {
|
||||
drawSpeechBaloon, drawNamePlate, createCommonPalettes, drawBaloon, DrawNameFlags
|
||||
} from '../../../graphics/graphicsUtils';
|
||||
import { drawCanvas } from '../../../graphics/contextSpriteBatch';
|
||||
import {
|
||||
GRASS_COLOR, getMessageColor, OUTLINE_COLOR, MOD_COLOR, ADMIN_COLOR, PATREON_COLOR, ANNOUNCEMENT_COLOR,
|
||||
WHITE, PARTY_COLOR, RED, ORANGE, PURPLE, GREEN, YELLOW, BLUE, BLACK, CYAN, TRANSPARENT, WHISPER_COLOR
|
||||
} from '../../../common/colors';
|
||||
import { loadAndInitSpriteSheets } from '../../../client/spriteUtils';
|
||||
import { MessageType, FontPalettes, Palette } from '../../../common/interfaces';
|
||||
import { faHome, faStar } from '../../../client/icons';
|
||||
import * as sprites from '../../../generated/sprites';
|
||||
import { disableImageSmoothing } from '../../../client/canvasUtils';
|
||||
import { mockPaletteManager } from '../../../common/ponyInfo';
|
||||
import { fontPal, fontSmallPal } from '../../../client/fonts';
|
||||
import { measureText, drawText, drawOutlinedText, lineBreak, drawTextAligned, HAlign } from '../../../graphics/spriteFont';
|
||||
import { rect } from '../../../common/rect';
|
||||
import { colorToCSS } from '../../../common/color';
|
||||
|
||||
interface Message {
|
||||
label: string;
|
||||
color: number;
|
||||
palette?: (palettes: FontPalettes) => Palette | undefined;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tools-chat',
|
||||
templateUrl: 'tools-chat.pug',
|
||||
})
|
||||
export class ToolsChat implements AfterViewInit {
|
||||
readonly homeIcon = faHome;
|
||||
readonly starIcon = faStar;
|
||||
@ViewChild('canvas', { static: true }) element!: ElementRef;
|
||||
messages: Message[] = [
|
||||
{ label: 'Chat message', color: getMessageColor(MessageType.Chat) },
|
||||
{ label: 'System message', color: getMessageColor(MessageType.System) },
|
||||
{ label: 'Admin message', color: getMessageColor(MessageType.Admin) },
|
||||
{ label: 'Mod message', color: getMessageColor(MessageType.Mod) },
|
||||
{ label: 'Announcement message', color: getMessageColor(MessageType.Announcement) },
|
||||
{ label: 'Party message', color: getMessageColor(MessageType.Party) },
|
||||
{ label: 'Thinking message', color: getMessageColor(MessageType.Thinking) },
|
||||
{ label: 'PartyThinking message', color: getMessageColor(MessageType.PartyThinking) },
|
||||
{ label: 'PartyAnnouncement message', color: getMessageColor(MessageType.PartyAnnouncement) },
|
||||
// {
|
||||
// label: 'PartyAnnouncement msg ⚧', color: WHITE,
|
||||
// palette: () => mockPaletteManager.add([
|
||||
// TRANSPARENT,
|
||||
// ANNOUNCEMENT_COLOR,
|
||||
// PARTY_COLOR, ANNOUNCEMENT_COLOR,
|
||||
// PARTY_COLOR, ANNOUNCEMENT_COLOR,
|
||||
// PARTY_COLOR, ANNOUNCEMENT_COLOR,
|
||||
// PARTY_COLOR, ANNOUNCEMENT_COLOR,
|
||||
// PARTY_COLOR, ANNOUNCEMENT_COLOR,
|
||||
// ]),
|
||||
// },
|
||||
// {
|
||||
// label: 'PartyAnnouncement msg ⚧', color: WHITE,
|
||||
// palette: () => mockPaletteManager.add([
|
||||
// TRANSPARENT,
|
||||
// ANNOUNCEMENT_COLOR,
|
||||
// ANNOUNCEMENT_COLOR, ANNOUNCEMENT_COLOR,
|
||||
// ANNOUNCEMENT_COLOR, ANNOUNCEMENT_COLOR,
|
||||
// ANNOUNCEMENT_COLOR, PARTY_COLOR,
|
||||
// PARTY_COLOR, PARTY_COLOR,
|
||||
// PARTY_COLOR, PARTY_COLOR,
|
||||
// ]),
|
||||
// },
|
||||
// { label: 'Red message', color: getMessageColor(MessageType.Red) },
|
||||
// { label: 'Green message', color: getMessageColor(MessageType.Green) },
|
||||
// { label: 'Blue message', color: getMessageColor(MessageType.Blue) },
|
||||
{ label: 'Supporter message 1', color: getMessageColor(MessageType.Supporter1) },
|
||||
{ label: 'Supporter message 2', color: WHITE, palette: p => p.supporter2 },
|
||||
{ label: 'Supporter message 3', color: WHITE, palette: p => p.supporter3 },
|
||||
{ label: 'Whisper message', color: WHISPER_COLOR },
|
||||
// { label: 'Supporter message 2', color: 0xffd45aff },
|
||||
];
|
||||
names = [
|
||||
{ label: 'Regular name', color: WHITE, font: () => fontPal },
|
||||
{ label: 'Party name', color: PARTY_COLOR, font: () => fontPal },
|
||||
{ label: 'MODERATOR tag', color: MOD_COLOR, font: () => fontSmallPal },
|
||||
{ label: 'DEVELOPER tag', color: ADMIN_COLOR, font: () => fontSmallPal },
|
||||
{ label: 'SUPPORTER tag', color: PATREON_COLOR, font: () => fontSmallPal },
|
||||
{ label: 'HIDDEN tag', color: ANNOUNCEMENT_COLOR, font: () => fontSmallPal },
|
||||
];
|
||||
private bg = GRASS_COLOR;
|
||||
private initialized = false;
|
||||
ngAfterViewInit() {
|
||||
loadAndInitSpriteSheets()
|
||||
.then(() => this.initialized = true)
|
||||
.then(() => this.redraw());
|
||||
}
|
||||
toggleBg() {
|
||||
this.bg = this.bg === GRASS_COLOR ? 0x172e14ff : GRASS_COLOR;
|
||||
this.redraw();
|
||||
}
|
||||
redraw() {
|
||||
if (!this.initialized)
|
||||
return;
|
||||
|
||||
const canvas1 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => {
|
||||
const palettes = createCommonPalettes(mockPaletteManager);
|
||||
|
||||
this.messages.forEach(({ label, color, palette }, index) => {
|
||||
const size = measureText(label, fontPal);
|
||||
const options = { palette: palette ? palette(palettes.mainFont) : palettes.mainFont.white };
|
||||
drawSpeechBaloon(batch, label, color, options, 10 + size.w / 2, 20 + 20 * index, size.w, size.h, 1, 5);
|
||||
});
|
||||
|
||||
this.names.forEach(({ label, color, font }, index) => {
|
||||
const palette = font() === fontSmallPal ? palettes.smallFont.white : palettes.mainFont.white;
|
||||
drawOutlinedText(batch, label, font(), color, OUTLINE_COLOR, 190, 10 + 15 * index, { palette });
|
||||
});
|
||||
|
||||
// ---
|
||||
|
||||
drawOutlinedText(
|
||||
batch, '<SUPPORTER>', fontSmallPal, PATREON_COLOR, OUTLINE_COLOR, 190, 120, { palette: palettes.smallFont.white });
|
||||
|
||||
// names
|
||||
|
||||
drawNamePlate(batch, 'Some name 1', 220, 150, DrawNameFlags.None, palettes, 'sup1');
|
||||
drawNamePlate(batch, 'Some name 2', 220, 175, DrawNameFlags.None, palettes, 'sup2');
|
||||
drawNamePlate(batch, 'Some name 3', 220, 200, DrawNameFlags.None, palettes, 'sup3');
|
||||
|
||||
// speech baloons
|
||||
|
||||
drawBaloon(
|
||||
batch, { message: 'regular baloon', type: MessageType.Chat, created: 0 },
|
||||
50, 300, rect(0, 0, 1000, 1000), palettes);
|
||||
drawBaloon(
|
||||
batch, { message: 'thinking baloon', type: MessageType.Thinking, created: 0 },
|
||||
50, 330, rect(0, 0, 1000, 1000), palettes);
|
||||
drawBaloon(
|
||||
batch, { message: 'whisper baloon', type: MessageType.Whisper, created: 0 },
|
||||
50, 360, rect(0, 0, 1000, 1000), palettes);
|
||||
});
|
||||
|
||||
const canvas2 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => {
|
||||
const emojiPalette = mockPaletteManager.addArray(sprites.emojiPalette);
|
||||
|
||||
for (let i = 0; i < sprites.emojiPal.length; i++) {
|
||||
const x = i % 10;
|
||||
const y = Math.floor(i / 10);
|
||||
batch.drawSprite(sprites.emojiPal[i].sprite, WHITE, emojiPalette, 10 + x * 12, 10 + y * 12);
|
||||
}
|
||||
|
||||
const palette = mockPaletteManager.addArray(sprites.fontSupporter2Palette);
|
||||
drawText(batch, 'Lorem 🍎 ipsum', fontPal, WHITE, 10, 150, { palette, emojiPalette });
|
||||
|
||||
const palette1 = mockPaletteManager.addArray(sprites.fontSupporter1Palette);
|
||||
drawText(batch, '<SUPPÓĄRTER>', fontSmallPal, WHITE, 20, 180, { palette: palette1 });
|
||||
|
||||
const palette2 = mockPaletteManager.addArray(sprites.fontSupporter2Palette);
|
||||
drawText(batch, '<SUPPÓĄRTER>', fontSmallPal, WHITE, 20, 195, { palette: palette2 });
|
||||
|
||||
let palette3 = mockPaletteManager.add([TRANSPARENT, RED, BLUE, ORANGE, WHITE, PURPLE, BLACK, GREEN, CYAN, YELLOW]);
|
||||
palette3 = mockPaletteManager.addArray(sprites.fontSupporter3Palette);
|
||||
drawText(batch, '<SUPPÓĄ⚧RTER>', fontSmallPal, WHITE, 20, 210, { palette: palette3 });
|
||||
});
|
||||
|
||||
const canvas3 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => {
|
||||
/* tslint:disable */
|
||||
const loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce scelerisque interdum scelerisque. Suspendisse malesuada, enim in viverra ornare, dui ex laoreet ipsum, at mollis orci felis vitae ipsum. In faucibus venenatis augue, ac ornare libero. Etiam vitae aliquet neque.';
|
||||
const text = lineBreak(loremIpsum, fontPal, 200);
|
||||
|
||||
batch.drawRect(WHITE, 10, 10, 200, 100);
|
||||
drawText(batch, text, fontPal, BLACK, 10, 10);
|
||||
|
||||
const bounds = rect(10 + 200 + 20, 10, 200, 100);
|
||||
batch.drawRect(WHITE, bounds.x, bounds.y, bounds.w, bounds.h);
|
||||
drawTextAligned(batch, text, fontPal, BLACK, bounds, HAlign.Right);
|
||||
|
||||
const bounds2 = rect(10 + 200 + 20, 10 + 120, 200, 20);
|
||||
batch.drawRect(WHITE, bounds2.x, bounds2.y, bounds2.w, bounds2.h);
|
||||
drawTextAligned(batch, 'test text', fontPal, BLACK, bounds2, HAlign.Right);
|
||||
});
|
||||
|
||||
const canvas = this.element.nativeElement as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d')!;
|
||||
context.fillStyle = colorToCSS(this.bg);
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
disableImageSmoothing(context);
|
||||
context.save();
|
||||
context.scale(2, 2);
|
||||
context.drawImage(canvas1, 0, 0);
|
||||
context.drawImage(canvas2, 300, 0);
|
||||
context.drawImage(canvas3, 0, 400);
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.collision-area(
|
||||
(agDrag)="drag($event)" agDragRelative="self" (contextmenu)="false"
|
||||
style="position: relative; width: 1200px; height: 900px; margin: 20px; background: #222;")
|
||||
canvas(#canvas width="1200" height="900")
|
||||
.text-danger.ml-4(*ngIf="collided") collided
|
||||
@@ -0,0 +1,781 @@
|
||||
import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';
|
||||
import { AgDragEvent } from '../../shared/directives/agDrag';
|
||||
import { Rect, Point } from '../../../common/interfaces';
|
||||
import { roundPosition } from '../../../common/positionUtils';
|
||||
import { point, distanceSquaredXY, clamp } from '../../../common/utils';
|
||||
import { createCanvas, disableImageSmoothing } from '../../../client/canvasUtils';
|
||||
|
||||
const pixelSize = 10;
|
||||
const tileWidth = 32 * pixelSize;
|
||||
const tileHeight = 24 * pixelSize;
|
||||
|
||||
interface ExPoint extends Point {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
function toWorldX(x: number) {
|
||||
return x / tileWidth;
|
||||
}
|
||||
|
||||
function toWorldY(y: number) {
|
||||
return y / tileHeight;
|
||||
}
|
||||
|
||||
function toWorld(pt: Point) {
|
||||
return { x: toWorldX(pt.x), y: toWorldY(pt.y) };
|
||||
}
|
||||
|
||||
function toScreenX(x: number) {
|
||||
return x * tileWidth;
|
||||
}
|
||||
|
||||
function toScreenY(y: number) {
|
||||
return y * tileHeight;
|
||||
}
|
||||
|
||||
function toScreen(pt: Point) {
|
||||
return { x: toScreenX(pt.x), y: toScreenY(pt.y) };
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tools-collisions',
|
||||
templateUrl: 'tools-collisions.pug',
|
||||
})
|
||||
export class ToolsCollisions implements OnInit {
|
||||
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
||||
start: Point = { x: 0, y: 0 };
|
||||
target: Point = { x: 0, y: 0 };
|
||||
steps: Point[] = [];
|
||||
deflection: Point = { x: 0, y: 0 };
|
||||
collided = false;
|
||||
rects: Rect[] = [
|
||||
{ x: 1, y: 1, w: 1, h: 1 },
|
||||
{ x: 3, y: 1.5, w: 0.5, h: 1 },
|
||||
{ x: 3.5, y: 1.5, w: 0.5, h: 1.5 },
|
||||
{ x: 2, y: 4, w: 2, h: 1 },
|
||||
{ x: 3.5, y: 3.5, w: 1, h: 1 },
|
||||
];
|
||||
collider = new Uint8Array(6 * 32 * 6 * 24);
|
||||
colliderCanvas!: HTMLCanvasElement;
|
||||
ngOnInit() {
|
||||
const line = 6 * 32;
|
||||
|
||||
for (const r of this.rects) {
|
||||
const x0 = Math.floor(r.x * 32);
|
||||
const y0 = Math.floor(r.y * 24);
|
||||
const x1 = x0 + Math.floor(r.w * 32);
|
||||
const y1 = y0 + Math.floor(r.h * 24);
|
||||
|
||||
for (let y = y0; y < y1; y++) {
|
||||
for (let x = x0; x < x1; x++) {
|
||||
this.collider[x + y * line] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 60, x0 = 40; y < 70; y++ , x0++) {
|
||||
for (let x = x0; x < 60; x++) {
|
||||
this.collider[x + y * line] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 70, x0 = 80; y < 80; y++ , x0--) {
|
||||
for (let x = x0; x < (x0 + 20); x++) {
|
||||
this.collider[x + y * line] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 10, x0 = 80; y < 20; y++ , x0 -= 2) {
|
||||
for (let x = x0; x < (x0 + 20); x++) {
|
||||
this.collider[x + y * line] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.colliderCanvas = createCanvas(6 * 32, 6 * 24);
|
||||
const context = this.colliderCanvas.getContext('2d')!;
|
||||
const data = context.getImageData(0, 0, this.colliderCanvas.width, this.colliderCanvas.height);
|
||||
|
||||
for (let i = 0; i < this.collider.length; i++) {
|
||||
if (this.collider[i] !== 0) {
|
||||
data.data[i * 4 + 0] = 255;
|
||||
data.data[i * 4 + 1] = 255;
|
||||
data.data[i * 4 + 2] = 0;
|
||||
data.data[i * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
context.putImageData(data, 0, 0);
|
||||
|
||||
this.draw();
|
||||
}
|
||||
drag({ type, x, y, dx, dy, event: { shiftKey } }: AgDragEvent) {
|
||||
if (type === 'start') {
|
||||
this.start = toWorld({ x, y });
|
||||
}
|
||||
|
||||
if (shiftKey) {
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
this.target = toWorld({ x, y: toScreenY(this.start.y) });
|
||||
} else {
|
||||
this.target = toWorld({ x: toScreenX(this.start.x), y });
|
||||
}
|
||||
} else {
|
||||
this.target = toWorld({ x, y });
|
||||
}
|
||||
|
||||
this.draw();
|
||||
}
|
||||
draw() {
|
||||
if (true) {
|
||||
// roundPosition(this.start);
|
||||
// roundPosition(this.target);
|
||||
|
||||
this.steps = [];
|
||||
this.collided = false;
|
||||
|
||||
let steps = 100;
|
||||
const current = point(this.start.x, this.start.y);
|
||||
|
||||
while (--steps > 0) {
|
||||
const collision = point(0, 0);
|
||||
|
||||
if (isColliding(current.x, current.y, this.target.x, this.target.y, this.rects, collision)) {
|
||||
this.collided = true;
|
||||
this.steps.push(collision);
|
||||
current.x = collision.x;
|
||||
current.y = collision.y;
|
||||
} else {
|
||||
this.steps.push(point(this.target.x, this.target.y));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (steps <= 0) {
|
||||
console.error('Failed');
|
||||
}
|
||||
} else {
|
||||
const collision = getClosestCollisionOld(this.start, this.target, this.rects);
|
||||
|
||||
if (equal(this.target, collision)) {
|
||||
this.deflection = { ...this.target };
|
||||
} else {
|
||||
const coll = getClosestCollisionOld(collision, this.target, this.rects);
|
||||
|
||||
if (equal(collision, coll)) {
|
||||
const horizontal = getClosestCollisionOld(collision, { x: this.target.x, y: collision.y }, this.rects);
|
||||
const vertical = getClosestCollisionOld(collision, { x: collision.x, y: this.target.y }, this.rects);
|
||||
this.deflection = equal(collision, horizontal) ? vertical : horizontal;
|
||||
} else {
|
||||
console.log('not', collision, coll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkedTiles = new Set<string>();
|
||||
|
||||
// plot
|
||||
|
||||
{
|
||||
let srcX = this.start.x;
|
||||
let srcY = this.start.y;
|
||||
let dstX = this.target.x;
|
||||
let dstY = this.target.y;
|
||||
|
||||
if (srcX > dstX) {
|
||||
const tx = srcX;
|
||||
const ty = srcY;
|
||||
srcX = dstX;
|
||||
srcY = dstY;
|
||||
dstX = tx;
|
||||
dstY = ty;
|
||||
}
|
||||
|
||||
const x0 = Math.floor(srcX);
|
||||
const y0 = Math.floor(srcY);
|
||||
const x1 = Math.floor(dstX);
|
||||
const y1 = Math.floor(dstY);
|
||||
|
||||
let steps = 100;
|
||||
let x = x0;
|
||||
let y = y0;
|
||||
|
||||
const DYbyDX = (dstY - srcY) / (dstX - srcX);
|
||||
|
||||
checkedTiles.add(`${x}-${y}`);
|
||||
|
||||
if (srcY < dstY) {
|
||||
while (--steps && (x !== x1 || y !== y1)) {
|
||||
const dx = (x + 1) - srcX;
|
||||
const dy = dx * DYbyDX;
|
||||
const ay = srcY + dy;
|
||||
|
||||
if (ay < (y + 1)) {
|
||||
x++;
|
||||
} else {
|
||||
y++;
|
||||
}
|
||||
|
||||
checkedTiles.add(`${x}-${y}`);
|
||||
}
|
||||
} else {
|
||||
while (--steps && (x !== x1 || y !== y1)) {
|
||||
const dx = (x + 1) - srcX;
|
||||
const dy = dx * DYbyDX;
|
||||
const ay = srcY + dy;
|
||||
|
||||
if (ay >= y) {
|
||||
x++;
|
||||
} else {
|
||||
y--;
|
||||
}
|
||||
|
||||
checkedTiles.add(`${x}-${y}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// end
|
||||
|
||||
const result = checkInLine(
|
||||
this.start.x * 32, this.start.y * 24,
|
||||
this.target.x * 32, this.target.y * 24,
|
||||
this.collider);
|
||||
|
||||
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d')!;
|
||||
|
||||
context.fillStyle = '#444';
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const xs = Math.ceil(canvas.width / tileWidth);
|
||||
const ys = Math.ceil(canvas.height / tileHeight);
|
||||
|
||||
context.save();
|
||||
context.fillStyle = '#533';
|
||||
context.globalAlpha = 0.5;
|
||||
|
||||
for (let y = 0; y < ys; y++) {
|
||||
for (let x = 0; x < xs; x++) {
|
||||
if (checkedTiles.has(`${x}-${y}`)) {
|
||||
context.fillRect(x * tileWidth, y * tileHeight, tileWidth - 1, tileHeight - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.restore();
|
||||
|
||||
context.save();
|
||||
context.scale(pixelSize, pixelSize);
|
||||
context.globalAlpha = 0.2;
|
||||
disableImageSmoothing(context);
|
||||
context.drawImage(this.colliderCanvas, 0, 0);
|
||||
context.restore();
|
||||
|
||||
context.save();
|
||||
context.strokeStyle = 'white';
|
||||
context.globalAlpha = 0.05;
|
||||
context.beginPath();
|
||||
|
||||
for (let y = 0; y < canvas.height; y += pixelSize) {
|
||||
context.moveTo(0, round5(y));
|
||||
context.lineTo(canvas.width, round5(y));
|
||||
}
|
||||
|
||||
for (let x = 0; x < canvas.width; x += pixelSize) {
|
||||
context.moveTo(round5(x), 0);
|
||||
context.lineTo(round5(x), canvas.height);
|
||||
}
|
||||
|
||||
context.stroke();
|
||||
context.restore();
|
||||
|
||||
context.save();
|
||||
context.strokeStyle = 'white';
|
||||
context.globalAlpha = 0.15;
|
||||
context.beginPath();
|
||||
|
||||
for (let y = 0; y < canvas.height; y += tileHeight) {
|
||||
context.moveTo(0, round5(y));
|
||||
context.lineTo(canvas.width, round5(y));
|
||||
}
|
||||
|
||||
for (let x = 0; x < canvas.width; x += tileWidth) {
|
||||
context.moveTo(round5(x), 0);
|
||||
context.lineTo(round5(x), canvas.height);
|
||||
}
|
||||
|
||||
context.stroke();
|
||||
context.restore();
|
||||
|
||||
context.save();
|
||||
context.fillStyle = 'lime';
|
||||
context.globalAlpha = 0.2;
|
||||
|
||||
let collided = false;
|
||||
|
||||
for (const pt of result.checks) {
|
||||
const colliding = isCollidingWithRect(pt.x, pt.y, this.rects);
|
||||
context.fillStyle = pt.type === 'break' ? 'blue' : 'lime'; // colliding ? 'red' : (collided ? 'orange' : 'lime');
|
||||
collided = collided || colliding;
|
||||
context.fillRect(pt.x * pixelSize, pt.y * pixelSize, pixelSize, pixelSize);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
|
||||
// context.save();
|
||||
// context.strokeStyle = 'orange';
|
||||
// context.setLineDash([3, 3]);
|
||||
|
||||
// for (const rect of this.rects) {
|
||||
// context.strokeRect(
|
||||
// round5(toScreenX(rect.x)), round5(toScreenY(rect.y)),
|
||||
// Math.round(toScreenX(rect.w)), Math.round(toScreenY(rect.h)));
|
||||
// }
|
||||
|
||||
// context.restore();
|
||||
|
||||
drawLine(context, toScreen(this.start), toScreen(this.target), 'gray', true);
|
||||
|
||||
let last = this.start;
|
||||
|
||||
this.steps = [point(result.result.x / 32, result.result.y / 24)];
|
||||
|
||||
for (const c of this.steps) {
|
||||
drawLine(context, toScreen(last), toScreen(c), 'lime', true);
|
||||
last = c;
|
||||
}
|
||||
|
||||
drawPoint(context, toScreen(this.start), 'greenyellow');
|
||||
drawPoint(context, toScreen(this.target), 'red');
|
||||
|
||||
for (const c of this.steps) {
|
||||
const colliding = isColliding(c.x, c.y, c.x, c.y, this.rects, point(0, 0));
|
||||
drawPoint(context, toScreen(c), colliding ? 'red' : 'yellow');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawLine(context: CanvasRenderingContext2D, a: Point, b: Point, color: string, arrow = false) {
|
||||
context.save();
|
||||
context.strokeStyle = color;
|
||||
// context.lineWidth = 2;
|
||||
context.beginPath();
|
||||
context.moveTo(a.x, a.y);
|
||||
context.lineTo(b.x, b.y);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
|
||||
if (arrow && (a.x !== b.x || a.y !== b.y)) {
|
||||
const scale = 0.5;
|
||||
context.save();
|
||||
context.fillStyle = color;
|
||||
context.translate(b.x, b.y);
|
||||
context.rotate(Math.atan2(b.y - a.y, b.x - a.x));
|
||||
context.beginPath();
|
||||
context.moveTo(0, 0);
|
||||
context.lineTo(-12 * scale, -6 * scale);
|
||||
context.lineTo(-12 * scale, 6 * scale);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function drawPoint(context: CanvasRenderingContext2D, { x, y }: Point, color: string) {
|
||||
context.save();
|
||||
context.shadowColor = 'black';
|
||||
context.shadowBlur = 3;
|
||||
context.fillStyle = color;
|
||||
context.globalAlpha = 0.5;
|
||||
context.beginPath();
|
||||
context.arc(x, y, 2, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function round5(x: number) {
|
||||
return Math.ceil(x) - 0.5;
|
||||
}
|
||||
|
||||
function equal(a: Point, b: Point) {
|
||||
return a.x === b.x && a.y === b.y;
|
||||
}
|
||||
|
||||
function isColliding(srcX: number, srcY: number, dstX: number, dstY: number, rects: Rect[], collision: Point): boolean {
|
||||
const temp = point(0, 0);
|
||||
let collided = false;
|
||||
collision.x = dstX;
|
||||
collision.y = dstY;
|
||||
|
||||
for (const r of rects) {
|
||||
if (getCollision(srcX, srcY, dstX, dstY, r.x, r.y, r.x + r.w, r.y + r.h, temp)) {
|
||||
if (!collided || (distanceSquaredXY(srcX, srcY, temp.x, temp.y) < distanceSquaredXY(srcX, srcY, collision.x, collision.y))) {
|
||||
collision.x = temp.x;
|
||||
collision.y = temp.y;
|
||||
collided = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
roundPosition(collision);
|
||||
return collided;
|
||||
}
|
||||
|
||||
function getClosestCollisionOld(a: Point, b: Point, rects: Rect[]) {
|
||||
return rects.reduce((pt, r) => getCollisionTest(a, pt, r) || pt, { ...b });
|
||||
}
|
||||
|
||||
function getCollisionTest({ x, y }: Point, b: Point, r: Rect): Point | undefined {
|
||||
const vx = b.x - x;
|
||||
const vy = b.y - y;
|
||||
const p = [-vx, vx, -vy, vy];
|
||||
const q = [x - r.x, r.x + r.w - x, y - r.y, r.y + r.h - y];
|
||||
let u1 = -999999;
|
||||
let u2 = 999999;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (p[i] === 0) {
|
||||
if (q[i] < 0) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
const t = q[i] / p[i];
|
||||
|
||||
if (p[i] < 0 && u1 < t) {
|
||||
u1 = t;
|
||||
} else if (p[i] > 0 && u2 > t) {
|
||||
u2 = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (u1 > u2 || u1 > 1 || u1 < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
x: x + u1 * vx,
|
||||
y: y + u1 * vy,
|
||||
};
|
||||
}
|
||||
|
||||
function isCollidingWithRect(x: number, y: number, rects: Rect[]) {
|
||||
for (const r of rects) {
|
||||
const x0 = Math.floor(r.x * 32) | 0;
|
||||
const y0 = Math.floor(r.y * 24) | 0;
|
||||
const x1 = Math.ceil((r.x + r.w) * 32) | 0;
|
||||
const y1 = Math.ceil((r.y + r.h) * 24) | 0;
|
||||
|
||||
if (x >= x0 && x < x1 && y >= y0 && y < y1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkInLine(srcX: number, srcY: number, dstX: number, dstY: number, collider: Uint8Array) {
|
||||
function isColliding(x: number, y: number) {
|
||||
return x < 0 || y < 0 || x >= (6 * 32) || y >= (6 * 32) || collider[x + y * (6 * 32)] !== 0;
|
||||
}
|
||||
|
||||
const checks: ExPoint[] = [];
|
||||
const result = point(srcX, srcY);
|
||||
|
||||
const x0 = Math.floor(srcX) | 0;
|
||||
const y0 = Math.floor(srcY) | 0;
|
||||
const x1 = Math.floor(dstX) | 0;
|
||||
const y1 = Math.floor(dstY) | 0;
|
||||
|
||||
let minX = Math.min(x0, x1) | 0;
|
||||
let maxX = Math.max(x0, x1) | 0;
|
||||
let minY = Math.min(y0, y1) | 0;
|
||||
let maxY = Math.max(y0, y1) | 0;
|
||||
|
||||
let x = x0 | 0;
|
||||
let y = y0 | 0;
|
||||
|
||||
checks.push({ x, y });
|
||||
|
||||
let actualX = x | 0;
|
||||
let actualY = y | 0;
|
||||
|
||||
const a = (dstY - srcY) / (dstX - srcX);
|
||||
const b = srcY - a * srcX;
|
||||
const useGt = srcY < dstY;
|
||||
|
||||
let stepXT = 0 | 0, stepYT = 0 | 0;
|
||||
let stepXF = 0 | 0, stepYF = 0 | 0;
|
||||
let ox = 0, oy = 0;
|
||||
|
||||
const shiftRight = srcX <= dstX;
|
||||
const shiftLeft = srcX >= dstX;
|
||||
const shiftUp = srcY >= dstY;
|
||||
const shiftDown = srcY <= dstY;
|
||||
const horizontalOrVertical = srcX === dstX || srcY === dstY;
|
||||
|
||||
if (srcX < dstX) {
|
||||
if (srcY < dstY) {
|
||||
ox = 1;
|
||||
oy = 1;
|
||||
stepYT = 1 | 0;
|
||||
stepXF = 1 | 0;
|
||||
} else {
|
||||
ox = 1;
|
||||
stepYT = -1 | 0;
|
||||
stepXF = 1 | 0;
|
||||
}
|
||||
} else if (srcX > dstX) {
|
||||
if (srcY < dstY) {
|
||||
oy = 1;
|
||||
stepYT = 1 | 0;
|
||||
stepXF = -1 | 0;
|
||||
} else {
|
||||
stepYT = -1 | 0;
|
||||
stepXF = -1 | 0;
|
||||
}
|
||||
} else {
|
||||
if (srcY < dstY) {
|
||||
stepYF = stepYT = 1 | 0;
|
||||
} else {
|
||||
stepYF = stepYT = -1 | 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (let steps = 1000; steps; steps--) {
|
||||
const fx = a * (x + ox) + b;
|
||||
const fy = y + oy;
|
||||
|
||||
let tx = 0 | 0;
|
||||
let ty = 0 | 0;
|
||||
|
||||
if (useGt ? (fx > fy) : (fx < fy)) {
|
||||
tx = (tx + stepXT) | 0;
|
||||
ty = (ty + stepYT) | 0;
|
||||
} else {
|
||||
tx = (tx + stepXF) | 0;
|
||||
ty = (ty + stepYF) | 0;
|
||||
}
|
||||
|
||||
x = (x + tx) | 0;
|
||||
y = (y + ty) | 0;
|
||||
|
||||
if (x < minX || x > maxX || y < minY || y > maxY) {
|
||||
break;
|
||||
}
|
||||
|
||||
let actualNX = (actualX + tx) | 0;
|
||||
let actualNY = (actualY + ty) | 0;
|
||||
|
||||
let collides = isColliding(actualNX, actualNY);
|
||||
let canMove = false;
|
||||
|
||||
if (collides) {
|
||||
if (tx !== 0) {
|
||||
let canShiftUp = false;
|
||||
let canShiftDown = false;
|
||||
|
||||
if (shiftUp && (canShiftUp = !isColliding(actualX, actualY - 1)) && !isColliding(actualNX, actualY - 1)) {
|
||||
actualNX = actualX;
|
||||
actualNY -= 1;
|
||||
dstY -= 1;
|
||||
collides = false;
|
||||
} else if (shiftDown && (canShiftDown = !isColliding(actualX, actualY + 1)) && !isColliding(actualNX, actualY + 1)) {
|
||||
actualNX = actualX;
|
||||
actualNY += 1;
|
||||
dstY += 1;
|
||||
collides = false;
|
||||
} else if (shiftUp && canShiftUp && !isColliding(actualNX, actualY - 2)) {
|
||||
actualNX = actualX;
|
||||
actualNY -= 1;
|
||||
dstY -= 1;
|
||||
collides = false;
|
||||
} else if (shiftDown && canShiftDown && !isColliding(actualNX, actualY + 2)) {
|
||||
actualNX = actualX;
|
||||
actualNY += 1;
|
||||
dstY += 1;
|
||||
collides = false;
|
||||
}
|
||||
|
||||
canMove = canShiftUp || canShiftDown;
|
||||
} else {
|
||||
let canShiftLeft = false;
|
||||
let canShiftRight = false;
|
||||
|
||||
if (shiftLeft && (canShiftLeft = !isColliding(actualX - 1, actualY)) && !isColliding(actualX - 1, actualNY)) {
|
||||
actualNX -= 1;
|
||||
actualNY = actualY;
|
||||
dstX -= 1;
|
||||
collides = false;
|
||||
} else if (shiftRight && (canShiftRight = !isColliding(actualX + 1, actualY)) && !isColliding(actualX + 1, actualNY)) {
|
||||
actualNX += 1;
|
||||
actualNY = actualY;
|
||||
dstX += 1;
|
||||
collides = false;
|
||||
} else if (shiftLeft && canShiftLeft && !isColliding(actualX - 2, actualNY)) {
|
||||
actualNX -= 1;
|
||||
actualNY = actualY;
|
||||
dstX -= 1;
|
||||
collides = false;
|
||||
} else if (shiftRight && canShiftRight && !isColliding(actualX + 2, actualNY)) {
|
||||
actualNX += 1;
|
||||
actualNY = actualY;
|
||||
dstX += 1;
|
||||
collides = false;
|
||||
}
|
||||
|
||||
canMove = canShiftLeft || canShiftRight;
|
||||
}
|
||||
}
|
||||
|
||||
if (!collides) {
|
||||
actualX = actualNX;
|
||||
actualY = actualNY;
|
||||
checks.push({ x: actualX, y: actualY });
|
||||
} else if (!canMove || horizontalOrVertical) {
|
||||
checks.push({ x: actualX, y: actualY, type: 'break' });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const epsilon = 1 / 1024;
|
||||
const left = Math.min(x0, actualX);
|
||||
const right = Math.max(x0 + 1, actualX + 1) - epsilon;
|
||||
const top = Math.min(y0, actualY);
|
||||
const bottom = Math.max(y0 + 1, actualY + 1) - epsilon;
|
||||
|
||||
result.x = clamp(dstX, left, right);
|
||||
result.y = clamp(dstY, top, bottom);
|
||||
|
||||
return { checks, result };
|
||||
}
|
||||
|
||||
// if (srcX < dstX) {
|
||||
// if (srcY < dstY) {
|
||||
// if ((a * (x + 1) + b) > (y + 1)) {
|
||||
// ty++;
|
||||
// } else {
|
||||
// tx++;
|
||||
// }
|
||||
// } else {
|
||||
// if ((a * (x + 1) + b) < y) {
|
||||
// ty--;
|
||||
// } else {
|
||||
// tx++;
|
||||
// }
|
||||
// }
|
||||
// } else if (srcX > dstX) {
|
||||
// if (srcY < dstY) {
|
||||
// if ((a * x + b) > (y + 1)) {
|
||||
// ty++;
|
||||
// } else {
|
||||
// tx--;
|
||||
// }
|
||||
// } else {
|
||||
// if ((a * x + b) < y) {
|
||||
// ty--;
|
||||
// } else {
|
||||
// tx--;
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// if (srcY < dstY) {
|
||||
// ty++;
|
||||
// } else {
|
||||
// ty--;
|
||||
// }
|
||||
// }
|
||||
|
||||
function getCollision(
|
||||
srcX: number, srcY: number, dstX: number, dstY: number, x0: number, y0: number, x1: number, y1: number, out: Point
|
||||
): boolean {
|
||||
const vx = dstX - srcX;
|
||||
const vy = dstY - srcY;
|
||||
let u1 = -999999;
|
||||
let u2 = 999999;
|
||||
|
||||
{
|
||||
const p = -vx;
|
||||
const q = srcX - x0;
|
||||
|
||||
if (p === 0) {
|
||||
if (q < 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const t = q / p;
|
||||
|
||||
if (p < 0 && u1 < t) {
|
||||
u1 = t;
|
||||
} else if (p > 0 && u2 > t) {
|
||||
u2 = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const p = vx;
|
||||
const q = x1 - srcX;
|
||||
|
||||
if (p === 0) {
|
||||
if (q < 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const t = q / p;
|
||||
|
||||
if (p < 0 && u1 < t) {
|
||||
u1 = t;
|
||||
} else if (p > 0 && u2 > t) {
|
||||
u2 = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const p = -vy;
|
||||
const q = srcY - y0;
|
||||
|
||||
if (p === 0) {
|
||||
if (q < 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const t = q / p;
|
||||
|
||||
if (p < 0 && u1 < t) {
|
||||
u1 = t;
|
||||
} else if (p > 0 && u2 > t) {
|
||||
u2 = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const p = vy;
|
||||
const q = y1 - srcY;
|
||||
|
||||
if (p === 0) {
|
||||
if (q < 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const t = q / p;
|
||||
|
||||
if (p < 0 && u1 < t) {
|
||||
u1 = t;
|
||||
} else if (p > 0 && u2 > t) {
|
||||
u2 = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (u1 > u2 || u1 > 1 || u1 < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out.x = srcX + u1 * vx;
|
||||
out.y = srcY + u1 * vy;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
.container-fluid.p-1
|
||||
.form-inline
|
||||
.form-group
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
.form-group
|
||||
scale-picker.ml-1([(scale)]="scale" (scaleChange)="redraw()")
|
||||
.form-group
|
||||
.input-group.ml-1(style="width: 200px;")
|
||||
input.form-control([(ngModel)]="name" placeholder="name")
|
||||
.input-group-append.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle)
|
||||
.dropdown-menu.dropdown-menu-right(*dropdownMenu)
|
||||
a.dropdown-item(*ngFor="let e of entities" (click)="setEntity(e)")
|
||||
| {{e.name}}
|
||||
button.btn.btn-default.ml-1((click)="saveEntity()" [disabled]="!name")
|
||||
fa-icon([icon]="saveIcon" [fixedWidth]="true")
|
||||
button.btn.btn-danger.ml-1((click)="setEntity(null)")
|
||||
fa-icon([icon]="eraserIcon" [fixedWidth]="true")
|
||||
button.btn.btn-danger.ml-1((click)="removeEntity()")
|
||||
fa-icon([icon]="trashIcon" [fixedWidth]="true")
|
||||
|
||||
.float-left.mt-1.mr-2
|
||||
canvas(#canvas width=512 height=512 (mousedown)="mousedown($event)" (agDrag)="drag($event)" agDragRelative="self")
|
||||
|
||||
.form-group.d-flex
|
||||
label.control-label.text-muted draw:
|
||||
custom-checkbox.ml-2([(checked)]="drawCenter" (checkedChange)="changed()") center
|
||||
custom-checkbox.ml-2([(checked)]="drawSelection" (checkedChange)="changed()") selection
|
||||
custom-checkbox.ml-2([(checked)]="drawHold" (checkedChange)="changed()") as held
|
||||
|
||||
div
|
||||
.form-group(*ngFor="let p of parts")
|
||||
.form-inline(*ngIf="p.type === 'sprite'")
|
||||
label.text-muted sprite:
|
||||
input.form-control.ml-1(
|
||||
[(ngModel)]="p.sprite" (input)="changed()" style="width: 200px;" [typeahead]="sprites" (typeaheadOnSelect)="changed()")
|
||||
label.ml-1 x:
|
||||
input.form-control.input-sm.ml-1(type="number" [ngModel]="-p.x" (ngModelChange)="p.x = -$event" (input)="changed()" min="-1000" max="1000")
|
||||
label.ml-1 y:
|
||||
input.form-control.input-sm.ml-1(type="number" [ngModel]="-p.y" (ngModelChange)="p.y = -$event" (input)="changed()" min="-1000" max="1000")
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="centerPart(p)" title="Center sprite")
|
||||
fa-icon([icon]="crosshairsIcon" [fixedWidth]="true")
|
||||
button.btn.btn-sm.btn-danger.ml-1((click)="removePart(p)" title="Remove part")
|
||||
fa-icon([icon]="trashIcon" [fixedWidth]="true")
|
||||
|
||||
.form-inline(*ngIf="p.type === 'cover' || p.type === 'collider' || p.type === 'pickable'")
|
||||
label.text-muted {{p.type}}:
|
||||
label.ml-1 x:
|
||||
input.form-control.input-sm.ml-1(type="number" [(ngModel)]="p.x" (input)="changed()" min="-1000" max="1000")
|
||||
label.ml-1 y:
|
||||
input.form-control.input-sm.ml-1(type="number" [(ngModel)]="p.y" (input)="changed()" min="-1000" max="1000")
|
||||
.form-inline(*ngIf="p.type === 'cover' || p.type === 'collider'")
|
||||
label.ml-1 w:
|
||||
input.form-control.input-sm.ml-1(type="number" [(ngModel)]="p.w" (input)="changed()" min="1" max="1000")
|
||||
label.ml-1 h:
|
||||
input.form-control.input-sm.ml-1(type="number" [(ngModel)]="p.h" (input)="changed()" min="1" max="1000")
|
||||
button.btn.btn-sm.btn-danger.ml-1((click)="removePart(p)" title="Remove part")
|
||||
fa-icon([icon]="trashIcon" [fixedWidth]="true")
|
||||
|
||||
hr
|
||||
|
||||
.form-group.form-inline
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="addPart('sprite')")
|
||||
fa-icon.mr-1([icon]="plusIcon" [fixedWidth]="true")
|
||||
| Sprite
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="addPart('cover')")
|
||||
fa-icon.mr-1([icon]="plusIcon" [fixedWidth]="true")
|
||||
| Cover
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="addPart('collider')")
|
||||
fa-icon.mr-1([icon]="plusIcon" [fixedWidth]="true")
|
||||
| Collider
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="addPart('pickable')")
|
||||
fa-icon.mr-1([icon]="plusIcon" [fixedWidth]="true")
|
||||
| Pickable
|
||||
@@ -0,0 +1,423 @@
|
||||
import { Component, OnInit, HostListener, ElementRef, ViewChild } from '@angular/core';
|
||||
import { findLastIndex, compact } from 'lodash';
|
||||
import { removeItem, containsPoint, isKeyEventInvalid, cloneDeep, att } from '../../../common/utils';
|
||||
import * as sprites from '../../../generated/sprites';
|
||||
import { setPaletteManager, pickable, getRenderableBounds, createPalette } from '../../../common/mixins';
|
||||
import { parseColor, withAlphaFloat } from '../../../common/color';
|
||||
import { PaletteManager, releasePalette } from '../../../graphics/paletteManager';
|
||||
import { PaletteSpriteBatch, Rect, PaletteRenderable, Dict, Entity, EntityPart, DrawOptions } from '../../../common/interfaces';
|
||||
import { SHADOW_COLOR, WHITE, BLACK, TRANSPARENT, RED, ORANGE, PURPLE } from '../../../common/colors';
|
||||
import { Key } from '../../../client/input/input';
|
||||
import { drawOutline } from '../../../graphics/graphicsUtils';
|
||||
import { drawCanvas, ContextSpriteBatch } from '../../../graphics/contextSpriteBatch';
|
||||
import { loadAndInitSpriteSheets } from '../../../client/spriteUtils';
|
||||
import { AgDragEvent } from '../../shared/directives/agDrag';
|
||||
import { faHome, faSave, faEraser, faTrash, faPlus, faCrosshairs } from '../../../client/icons';
|
||||
import { StorageService } from '../../services/storageService';
|
||||
import { mockPaletteManager, toPalette } from '../../../common/ponyInfo';
|
||||
import { OFFLINE_PONY } from '../../../common/constants';
|
||||
import { drawPony } from '../../../client/ponyDraw';
|
||||
import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers';
|
||||
import { createBaseEntity } from '../../../common/entities';
|
||||
import { decompressPonyString } from '../../../common/compressPony';
|
||||
import { disableImageSmoothing } from '../../../client/canvasUtils';
|
||||
import { toScreenX, toScreenYWithZ } from '../../../common/positionUtils';
|
||||
|
||||
const COVER = parseColor('DeepSkyBlue');
|
||||
const COLLIDER = ORANGE;
|
||||
const PICKABLE = PURPLE;
|
||||
const BG = parseColor('lightgreen');
|
||||
const LINES = withAlphaFloat(BLACK, 0.1);
|
||||
const SELECTION = withAlphaFloat(WHITE, 0.5);
|
||||
const DEFAULT_PALETTE = [TRANSPARENT, WHITE];
|
||||
const paletteManager = new PaletteManager();
|
||||
const defaultPalette = paletteManager.add(DEFAULT_PALETTE);
|
||||
const X = 128;
|
||||
const Y = 190;
|
||||
|
||||
const colors: Dict<number> = {
|
||||
cover: COVER,
|
||||
collider: COLLIDER,
|
||||
pickable: PICKABLE,
|
||||
};
|
||||
|
||||
interface BasePart {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface BoundsPart extends BasePart {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
interface SpritePart extends BasePart {
|
||||
type: 'sprite';
|
||||
sprite: string;
|
||||
}
|
||||
|
||||
interface CoverPart extends BoundsPart {
|
||||
type: 'cover';
|
||||
}
|
||||
|
||||
interface ColliderPart extends BoundsPart {
|
||||
type: 'collider';
|
||||
}
|
||||
|
||||
interface PickablePart extends BasePart {
|
||||
type: 'pickable';
|
||||
}
|
||||
|
||||
type Part = SpritePart | CoverPart | ColliderPart | PickablePart;
|
||||
|
||||
interface PartEntity {
|
||||
name: string;
|
||||
parts: Part[];
|
||||
}
|
||||
|
||||
interface EntityData {
|
||||
parts?: Part[];
|
||||
entities?: PartEntity[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tools-entity',
|
||||
templateUrl: 'tools-entity.pug',
|
||||
})
|
||||
export class ToolsEntity implements OnInit {
|
||||
readonly homeIcon = faHome;
|
||||
readonly saveIcon = faSave;
|
||||
readonly eraserIcon = faEraser;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly crosshairsIcon = faCrosshairs;
|
||||
readonly plusIcon = faPlus;
|
||||
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
||||
scale = 2;
|
||||
name = '';
|
||||
drawCenter = true;
|
||||
drawSelection = true;
|
||||
drawHold = false;
|
||||
sprites = Object.keys(sprites).filter(key => {
|
||||
const s = (sprites as any)[key] as any;
|
||||
return !!(s && s.color);
|
||||
});
|
||||
selectedPart = -1;
|
||||
entities: PartEntity[] = [];
|
||||
parts: Part[] = [];
|
||||
pony = toPalette(decompressPonyString(OFFLINE_PONY), mockPaletteManager);
|
||||
private startX = 0;
|
||||
private startY = 0;
|
||||
constructor(private storage: StorageService) {
|
||||
}
|
||||
ngOnInit() {
|
||||
setPaletteManager(paletteManager);
|
||||
loadAndInitSpriteSheets().then(() => this.changed());
|
||||
|
||||
const data = this.load();
|
||||
this.parts = compact(data.parts || [this.createSpritePart('apple')]);
|
||||
this.entities = compact(data.entities || []);
|
||||
}
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
keydown(e: KeyboardEvent) {
|
||||
if (!isKeyEventInvalid(e) && this.handleKey(e.keyCode)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
handleKey(keyCode: number) {
|
||||
if (keyCode === Key.UP) {
|
||||
this.movePart(0, -1);
|
||||
} else if (keyCode === Key.DOWN) {
|
||||
this.movePart(0, 1);
|
||||
} else if (keyCode === Key.LEFT) {
|
||||
this.movePart(-1, 0);
|
||||
} else if (keyCode === Key.RIGHT) {
|
||||
this.movePart(1, 0);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
mousedown(e: MouseEvent) {
|
||||
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
|
||||
const { left, top } = canvas.getBoundingClientRect();
|
||||
const x = (e.pageX - left) / this.scale - X;
|
||||
const y = (e.pageY - top) / this.scale - Y;
|
||||
|
||||
this.selectedPart = findLastIndex(this.parts, p => {
|
||||
if (this.drawHold) {
|
||||
return p.type === 'pickable';
|
||||
} else {
|
||||
const bounds = getBounds(p);
|
||||
return !!bounds && containsPoint(0, 0, bounds, x, y);
|
||||
}
|
||||
});
|
||||
|
||||
this.changed();
|
||||
}
|
||||
drag({ dx, dy, type }: AgDragEvent) {
|
||||
const part = this.parts[this.selectedPart];
|
||||
|
||||
if (part) {
|
||||
if (type === 'start') {
|
||||
this.startX = part.x;
|
||||
this.startY = part.y;
|
||||
}
|
||||
|
||||
part.x = Math.round(this.startX + dx / this.scale);
|
||||
part.y = Math.round(this.startY + dy / this.scale);
|
||||
}
|
||||
|
||||
this.changed();
|
||||
}
|
||||
setEntity(entity: PartEntity | null) {
|
||||
if (entity) {
|
||||
this.name = entity.name;
|
||||
this.parts = entity.parts;
|
||||
} else {
|
||||
this.name = '';
|
||||
this.parts = [];
|
||||
}
|
||||
|
||||
this.changed();
|
||||
}
|
||||
saveEntity() {
|
||||
if (this.name) {
|
||||
const existing = this.entities.find(e => e.name === this.name);
|
||||
|
||||
if (existing) {
|
||||
existing.parts = cloneDeep(this.parts);
|
||||
} else {
|
||||
this.entities.push({
|
||||
name: this.name,
|
||||
parts: cloneDeep(this.parts),
|
||||
});
|
||||
}
|
||||
|
||||
this.changed();
|
||||
}
|
||||
}
|
||||
removeEntity() {
|
||||
removeItem(this.entities, this.entities.find(e => e.name === this.name));
|
||||
this.changed();
|
||||
}
|
||||
movePart(dx: number, dy: number) {
|
||||
const part = this.parts[this.selectedPart];
|
||||
|
||||
if (part) {
|
||||
part.x += dx;
|
||||
part.y += dy;
|
||||
}
|
||||
|
||||
this.changed();
|
||||
}
|
||||
changed() {
|
||||
requestAnimationFrame(() => this.redraw());
|
||||
}
|
||||
redraw() {
|
||||
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
|
||||
const scale = this.scale;
|
||||
const width = Math.ceil(canvas.width / scale);
|
||||
const height = Math.ceil(canvas.height / scale);
|
||||
|
||||
const draw = (batch: ContextSpriteBatch) => {
|
||||
if (this.drawCenter) {
|
||||
batch.drawRect(LINES, 0, Y, width, 1);
|
||||
batch.drawRect(LINES, X, 0, 1, height);
|
||||
}
|
||||
|
||||
this.parts.forEach(p => drawPart(batch, p, X, Y));
|
||||
|
||||
const part = this.parts[this.selectedPart] as Part | undefined;
|
||||
|
||||
if (part) {
|
||||
const bounds = getBounds(part);
|
||||
|
||||
if (this.drawSelection && bounds) {
|
||||
const sx = X + bounds.x;
|
||||
const sy = Y + bounds.y;
|
||||
drawOutline(batch, SELECTION, sx - 1, sy - 1, bounds.w + 2, bounds.h + 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.drawCenter) {
|
||||
batch.drawRect(RED, X, Y, 1, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const buffer = drawCanvas(width, height, sprites.paletteSpriteSheet, BG, batch => {
|
||||
if (this.drawHold) {
|
||||
const spritePart = this.parts.find(p => p.type === 'sprite') as SpritePart | undefined;
|
||||
const pickablePart = this.parts.find(p => p.type === 'pickable') as PickablePart | undefined;
|
||||
|
||||
const holding: Entity | undefined = spritePart && pickablePart && getSprite(spritePart.sprite) ? {
|
||||
...createBaseEntity(0, 0, 0, 0),
|
||||
...drawMixin(getSprite(spritePart.sprite), -spritePart.x, -spritePart.y),
|
||||
...pickable(pickablePart.x, pickablePart.y),
|
||||
} : undefined;
|
||||
|
||||
const state = { ...defaultPonyState(), holding };
|
||||
drawPony(batch, this.pony, state, X, Y, defaultDrawPonyOptions());
|
||||
} else {
|
||||
draw(batch);
|
||||
}
|
||||
});
|
||||
|
||||
drawBufferScaled(canvas, buffer, scale);
|
||||
|
||||
this.save();
|
||||
}
|
||||
createSpritePart(sprite: string): SpritePart {
|
||||
return {
|
||||
type: 'sprite',
|
||||
sprite,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
createBoundsPart(type: any): CoverPart {
|
||||
return {
|
||||
type,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 10,
|
||||
h: 10,
|
||||
};
|
||||
}
|
||||
createPart(type: string): Part {
|
||||
switch (type) {
|
||||
case 'sprite':
|
||||
return this.createSpritePart('apple');
|
||||
case 'cover':
|
||||
case 'collider':
|
||||
return this.createBoundsPart(type);
|
||||
case 'pickable':
|
||||
return { type, x: 0, y: 0 };
|
||||
default:
|
||||
throw new Error(`Invalid type (${type})`);
|
||||
}
|
||||
}
|
||||
addPart(type: string) {
|
||||
this.parts.push(this.createPart(type));
|
||||
this.changed();
|
||||
}
|
||||
removePart(part: Part) {
|
||||
removeItem(this.parts, part);
|
||||
this.changed();
|
||||
}
|
||||
centerPart(part: Part) {
|
||||
if (part.type === 'sprite') {
|
||||
const sprite = getSprite(part.sprite);
|
||||
const color = sprite && sprite.color;
|
||||
|
||||
if (color) {
|
||||
part.x = Math.round(-color.ox - color.w / 2);
|
||||
part.y = Math.round(-color.oy - color.h / 2);
|
||||
}
|
||||
}
|
||||
|
||||
this.changed();
|
||||
}
|
||||
private save() {
|
||||
this.storage.setJSON('tools-entity', <EntityData>{
|
||||
parts: this.parts,
|
||||
entities: this.entities,
|
||||
});
|
||||
}
|
||||
private load() {
|
||||
return this.storage.getJSON<EntityData>('tools-entity', {});
|
||||
}
|
||||
}
|
||||
|
||||
function getBounds(part: Part): Rect | undefined {
|
||||
if (part.type === 'sprite') {
|
||||
const sprite = getSprite(part.sprite);
|
||||
const color = sprite && sprite.color;
|
||||
|
||||
if (color) {
|
||||
return {
|
||||
x: part.x + color.ox,
|
||||
y: part.y + color.oy,
|
||||
w: color.w,
|
||||
h: color.h,
|
||||
};
|
||||
}
|
||||
} else if (part.type === 'cover' || part.type === 'collider') {
|
||||
return part;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getSprite(name: string): PaletteRenderable {
|
||||
return (sprites as any)[name];
|
||||
}
|
||||
|
||||
function drawSpritePart(batch: PaletteSpriteBatch, part: SpritePart, px: number, py: number) {
|
||||
const sprite = getSprite(part.sprite);
|
||||
|
||||
if (!sprite)
|
||||
return;
|
||||
|
||||
const x = px + part.x;
|
||||
const y = py + part.y;
|
||||
const palette = paletteManager.addArray(sprite.palettes![0]);
|
||||
|
||||
sprite.shadow && batch.drawSprite(sprite.shadow, SHADOW_COLOR, defaultPalette, x, y);
|
||||
sprite.color && batch.drawSprite(sprite.color, WHITE, palette, x, y);
|
||||
|
||||
releasePalette(palette);
|
||||
}
|
||||
|
||||
function drawPart(batch: PaletteSpriteBatch, part: Part, x: number, y: number) {
|
||||
if (part.type === 'sprite') {
|
||||
return drawSpritePart(batch, part, x, y);
|
||||
} else if (part.type === 'cover' || part.type === 'collider') {
|
||||
return drawOutline(batch, colors[part.type], part.x + x, part.y + y, part.w, part.h);
|
||||
} else if (part.type === 'pickable') {
|
||||
return drawOutline(batch, colors[part.type], part.x + x, part.y + y, 1, 1);
|
||||
} else {
|
||||
throw new Error(`Invalid part type (${(part as any).type})`);
|
||||
}
|
||||
}
|
||||
|
||||
function drawBufferScaled(canvas: HTMLCanvasElement, buffer: HTMLCanvasElement, scale: number) {
|
||||
const context = canvas.getContext('2d')!;
|
||||
context.save();
|
||||
disableImageSmoothing(context);
|
||||
context.scale(scale, scale);
|
||||
context.drawImage(buffer, 0, 0);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawMixin(sprite: PaletteRenderable, dx: number, dy: number, paletteIndex = 0): EntityPart {
|
||||
const bounds = getRenderableBounds(sprite, dx, dy);
|
||||
|
||||
if (SERVER && !TESTS)
|
||||
return { bounds };
|
||||
|
||||
const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette);
|
||||
const palette = createPalette(att(sprite.palettes, paletteIndex));
|
||||
|
||||
return {
|
||||
bounds,
|
||||
draw(this: Entity, batch: PaletteSpriteBatch, options: DrawOptions) {
|
||||
const x = toScreenX(this.x + (this.ox || 0)) - dx;
|
||||
const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy;
|
||||
const opacity = 1 - 0.6 * (this.coverLifting || 0);
|
||||
|
||||
if (sprite.shadow !== undefined) {
|
||||
batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y);
|
||||
}
|
||||
|
||||
batch.globalAlpha = opacity;
|
||||
|
||||
if (sprite.color !== undefined) {
|
||||
batch.drawSprite(sprite.color, WHITE, palette, x, y);
|
||||
}
|
||||
|
||||
batch.globalAlpha = 1;
|
||||
},
|
||||
palettes: compact([defaultPalette, palette]),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.form-inline.p-1
|
||||
.form-group
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
|
||||
.form-group
|
||||
label.ml-2
|
||||
scale-picker([(scale)]="scale" (scaleChange)="redraw()")
|
||||
|
||||
.form-group
|
||||
label.control-label.ml-2 columns:
|
||||
input.form-control.ml-1(type="number" [(ngModel)]="columns" (input)="redraw()" style="width: 80px;")
|
||||
|
||||
.form-group
|
||||
button.btn.btn-default.ml-2((click)="png()") PNG
|
||||
|
||||
canvas(#canvas)
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||
import { PonyInfo, PonyState } from '../../../common/interfaces';
|
||||
import { toPalette, createDefaultPony, syncLockedPonyInfo } from '../../../common/ponyInfo';
|
||||
import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers';
|
||||
import { expressions } from '../../../common/expressions';
|
||||
import { createCanvas, disableImageSmoothing, saveCanvas } from '../../../client/canvasUtils';
|
||||
import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch';
|
||||
import { RED } from '../../../common/colors';
|
||||
import { loadAndInitSpriteSheets } from '../../../client/spriteUtils';
|
||||
import { createBodyAnimation } from '../../../client/ponyAnimations';
|
||||
import { drawPony } from '../../../client/ponyDraw';
|
||||
import { faHome } from '../../../client/icons';
|
||||
import { paletteSpriteSheet } from '../../../generated/sprites';
|
||||
|
||||
@Component({
|
||||
selector: 'tools-expressions',
|
||||
templateUrl: 'tools-expressions.pug',
|
||||
})
|
||||
export class ToolsExpressions implements OnInit {
|
||||
readonly homeIcon = faHome;
|
||||
scale = 2;
|
||||
columns = 12;
|
||||
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
||||
constructor() {
|
||||
}
|
||||
ngOnInit() {
|
||||
loadAndInitSpriteSheets()
|
||||
.then(() => this.redraw());
|
||||
}
|
||||
redraw() {
|
||||
this.draw();
|
||||
}
|
||||
png() {
|
||||
this.draw();
|
||||
saveCanvas(this.canvas.nativeElement, 'expressions.png');
|
||||
}
|
||||
private draw() {
|
||||
drawSheet(this.canvas.nativeElement, this.scale, this.columns);
|
||||
}
|
||||
}
|
||||
|
||||
function drawSheet(canvas: HTMLCanvasElement, scale: number, columns: number, bg = 'lightgreen'): HTMLCanvasElement {
|
||||
const frameWidth = 55;
|
||||
const frameOffset = 50;
|
||||
const frameHeight = 30;
|
||||
const buffer = createCanvas(frameWidth, frameHeight);
|
||||
const batch = new ContextSpriteBatch(buffer);
|
||||
const pony = createPony();
|
||||
const state = createState();
|
||||
const info = toPalette(pony);
|
||||
const filteredExpressions = expressions.filter(([, expr]) => !!expr).slice(2);
|
||||
const rows = Math.ceil(filteredExpressions.length / columns);
|
||||
const options = defaultDrawPonyOptions();
|
||||
|
||||
canvas.width = ((frameOffset * (columns - 1)) + frameWidth) * scale;
|
||||
canvas.height = (frameHeight * rows) * scale;
|
||||
|
||||
const viewContext = canvas.getContext('2d')!;
|
||||
viewContext.save();
|
||||
disableImageSmoothing(viewContext);
|
||||
viewContext.scale(scale, scale);
|
||||
|
||||
if (bg) {
|
||||
viewContext.fillStyle = bg;
|
||||
viewContext.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
viewContext.font = 'normal 6px monospace';
|
||||
viewContext.textAlign = 'right';
|
||||
viewContext.fillStyle = 'black';
|
||||
|
||||
filteredExpressions.forEach(([name, [right, left, muzzle, rightIris = 0, leftIris = 0, extra = 0]]: any, i) => {
|
||||
state.expression = { right, left, muzzle, rightIris, leftIris, extra };
|
||||
|
||||
batch.start(paletteSpriteSheet, 0);
|
||||
drawPony(batch, info, state, 35, 50, options);
|
||||
batch.end();
|
||||
|
||||
const x = (i % columns) * frameOffset;
|
||||
const y = Math.floor(i / columns) * frameHeight;
|
||||
|
||||
viewContext.drawImage(buffer, x, y);
|
||||
viewContext.fillText(name, x + 18, y + 20);
|
||||
});
|
||||
|
||||
viewContext.restore();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function createState(): PonyState {
|
||||
const state = defaultPonyState();
|
||||
state.blushColor = RED;
|
||||
state.animation = createBodyAnimation('', 24, false, [[0, 1]]);
|
||||
return state;
|
||||
}
|
||||
|
||||
function createPony(): PonyInfo {
|
||||
const pony = createDefaultPony();
|
||||
pony.mane!.type = 0;
|
||||
pony.backMane!.type = 0;
|
||||
pony.tail!.type = 0;
|
||||
pony.coatFill = 'dec078';
|
||||
pony.lockCoatOutline = true;
|
||||
pony.lockBackLegAccessory = false;
|
||||
pony.eyeColorRight = 'cornflowerblue';
|
||||
return syncLockedPonyInfo(pony);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.m-4
|
||||
ul.nav.nav-pills
|
||||
a.nav-link(routerLink="/variants") Variant explorer
|
||||
a.nav-link(routerLink="/animation") Animator
|
||||
a.nav-link(routerLink="/sheet") Sheet generator
|
||||
a.nav-link(routerLink="/expressions") Expressions
|
||||
a.nav-link(routerLink="/entity") Entity
|
||||
a.nav-link(routerLink="/palette") Palette
|
||||
a.nav-link(routerLink="/ui") UI
|
||||
a.nav-link(routerLink="/chat") Chat
|
||||
a.nav-link(routerLink="/webgl") WebGL
|
||||
a.nav-link(routerLink="/perf") Perf
|
||||
a.nav-link(routerLink="/states") States
|
||||
a.nav-link(routerLink="/regions") Regions
|
||||
a.nav-link(routerLink="/collisions") Collisions
|
||||
a.nav-link(routerLink="/map") Map
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'tools-index',
|
||||
templateUrl: 'tools-index.pug',
|
||||
})
|
||||
export class ToolsIndex {
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.form-inline.p-1
|
||||
.form-group
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
|
||||
.form-group
|
||||
label.ml-2 map
|
||||
.btn-group.dropdown.ml-1(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle) {{selectedMap || 'main'}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
button.dropdown-item(*ngFor="let m of maps" (click)="selectMap(m)") {{m || 'main'}}
|
||||
|
||||
.form-group
|
||||
label.ml-2 type
|
||||
.btn-group.dropdown.ml-1(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle) {{type}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
button.dropdown-item((click)="setType('regular')") regular
|
||||
button.dropdown-item((click)="setType('minimap')") minimap
|
||||
|
||||
.form-group
|
||||
label.ml-2 scale
|
||||
scale-picker.ml-1([(scale)]="scale" (scaleChange)="redraw()" [maxScale]="8")
|
||||
|
||||
.form-group
|
||||
button.btn.btn-default.ml-2((click)="fetch()") Reload
|
||||
|
||||
.form-group
|
||||
button.btn.btn-default.ml-2((click)="png()") PNG
|
||||
|
||||
.form-group
|
||||
custom-checkbox.ml-2([(checked)]="grid" (checkedChange)="redraw()") Grid
|
||||
|
||||
canvas(#canvas)
|
||||
@@ -0,0 +1,210 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { saveCanvas, disableImageSmoothing, createCanvas } from '../../../client/canvasUtils';
|
||||
import { loadAndInitSpriteSheets } from '../../../client/spriteUtils';
|
||||
import { tileHeight, tileWidth, REGION_SIZE } from '../../../common/constants';
|
||||
import { faHome } from '../../../client/icons';
|
||||
import { updateMap, getTile, createWorldMap, setRegion, setTile } from '../../../common/worldMap';
|
||||
import {
|
||||
Season, DrawOptions, defaultDrawOptions, EntityFlags, Entity, WorldMap, defaultWorldState, MapType, MapFlags
|
||||
} from '../../../common/interfaces';
|
||||
import { drawCanvas } from '../../../graphics/contextSpriteBatch';
|
||||
import { paletteSpriteSheet } from '../../../generated/sprites';
|
||||
import { createRegion } from '../../../common/region';
|
||||
import { deserializeTiles } from '../../../common/compress';
|
||||
import { createTileSets } from '../../../client/tileUtils';
|
||||
import { createCamera } from '../../../common/camera';
|
||||
import { mockPaletteManager } from '../../../common/ponyInfo';
|
||||
import { isCritter } from '../../../common/entityUtils';
|
||||
import {
|
||||
createAnEntity, cloud, pony, apple, apple2, orange, orange2, candy, gift1, gift2, appleGreen, appleGreen2
|
||||
} from '../../../common/entities';
|
||||
import { drawMap } from '../../../client/draw';
|
||||
import { includes, observableToPromise, hasFlag } from '../../../common/utils';
|
||||
import { getShadowColor, HOUR_LENGTH, createLightData } from '../../../common/timeUtils';
|
||||
import { StorageService } from '../../services/storageService';
|
||||
import { getTileColor } from '../../../common/colors';
|
||||
import { colorToCSS } from '../../../common/color';
|
||||
|
||||
export interface ToolsMapOtherInfo {
|
||||
season: Season;
|
||||
entities: { type: number; x: number; y: number; order: number; id: number; }[];
|
||||
}
|
||||
|
||||
export interface ToolsMapInfo {
|
||||
width: number;
|
||||
height: number;
|
||||
defaultTile: number;
|
||||
tiles?: string;
|
||||
type: MapType;
|
||||
info: ToolsMapOtherInfo;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tools-map',
|
||||
templateUrl: 'tools-map.pug',
|
||||
})
|
||||
export class ToolsMap implements OnInit {
|
||||
readonly homeIcon = faHome;
|
||||
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
||||
maps: string[] = [];
|
||||
selectedMap = '';
|
||||
grid = false;
|
||||
private map?: WorldMap;
|
||||
private info?: ToolsMapOtherInfo;
|
||||
constructor(private http: HttpClient, private storage: StorageService) {
|
||||
}
|
||||
get scale() {
|
||||
return this.storage.getInt('tools-map-scale') || 1;
|
||||
}
|
||||
set scale(value) {
|
||||
this.storage.setInt('tools-map-scale', value);
|
||||
}
|
||||
get type() {
|
||||
return this.storage.getItem('tools-map-type') || 'regular';
|
||||
}
|
||||
set type(value) {
|
||||
this.storage.setItem('tools-map-type', value);
|
||||
}
|
||||
async ngOnInit() {
|
||||
await loadAndInitSpriteSheets();
|
||||
await this.fetchList();
|
||||
await this.fetch();
|
||||
}
|
||||
setType(type: string) {
|
||||
this.type = type;
|
||||
this.redraw();
|
||||
}
|
||||
async fetchList() {
|
||||
this.maps = await observableToPromise(this.http.get<string[]>('/api-tools/maps'));
|
||||
}
|
||||
fetch() {
|
||||
this.http.get<ToolsMapInfo>('/api-tools/map', { params: { map: this.selectedMap } }).subscribe(map => {
|
||||
this.info = map.info;
|
||||
|
||||
const regionsX = map.width / REGION_SIZE;
|
||||
const regionsY = map.height / REGION_SIZE;
|
||||
const { type, defaultTile } = map;
|
||||
|
||||
this.map = createWorldMap({ type, flags: MapFlags.None, defaultTile, regionsX, regionsY });
|
||||
const tiles = deserializeTiles(map.tiles!);
|
||||
|
||||
for (let y = 0, i = 0; y < regionsX; y++) {
|
||||
for (let x = 0; x < regionsY; x++ , i++) {
|
||||
setRegion(this.map, x, y, createRegion(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0, i = 0; y < map.height; y++) {
|
||||
for (let x = 0; x < map.width; x++ , i++) {
|
||||
setTile(this.map, x, y, tiles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
});
|
||||
}
|
||||
selectMap(map: string) {
|
||||
this.selectedMap = map;
|
||||
this.fetch();
|
||||
}
|
||||
redraw() {
|
||||
this.draw();
|
||||
}
|
||||
png() {
|
||||
saveCanvas(this.canvas.nativeElement, 'map.png');
|
||||
}
|
||||
private draw() {
|
||||
if (this.map && this.info) {
|
||||
if (this.type === 'regular') {
|
||||
drawTheMap(this.canvas.nativeElement, this.map, this.info, this.scale, this.grid);
|
||||
} else if (this.type === 'minimap') {
|
||||
drawMinimap(this.canvas.nativeElement, this.map, this.info, this.scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawTheMap(canvas: HTMLCanvasElement, map: WorldMap, info: ToolsMapOtherInfo, scale: number, grid: boolean) {
|
||||
const mapCanvas = drawCanvas(map.width * tileWidth, map.height * tileHeight, paletteSpriteSheet, 0x222222ff, batch => {
|
||||
const camera = createCamera();
|
||||
camera.w = map.width * tileWidth;
|
||||
camera.h = map.height * tileHeight;
|
||||
|
||||
const tileSets = createTileSets(mockPaletteManager, info.season, map.type);
|
||||
const lightData = createLightData(info.season);
|
||||
|
||||
const drawOptions: DrawOptions = {
|
||||
...defaultDrawOptions,
|
||||
tileGrid: grid,
|
||||
shadowColor: getShadowColor(lightData, HOUR_LENGTH * 12),
|
||||
};
|
||||
|
||||
const ignoreTypes = [
|
||||
cloud, pony, apple, apple2, appleGreen, appleGreen2, orange, orange2, candy, gift1, gift2
|
||||
].map(e => e.type);
|
||||
|
||||
const shouldDraw = (e: Entity) => {
|
||||
return !hasFlag(e.flags, EntityFlags.Debug) && !isCritter(e) && !includes(ignoreTypes, e.type);
|
||||
};
|
||||
|
||||
map.entitiesDrawable = info.entities
|
||||
.map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager, defaultWorldState))
|
||||
.filter(shouldDraw);
|
||||
|
||||
updateMap(map, 0);
|
||||
drawMap(batch, map, camera, {} as any, drawOptions, tileSets, []);
|
||||
});
|
||||
|
||||
canvas.width = Math.floor(mapCanvas.width / scale);
|
||||
canvas.height = Math.floor(mapCanvas.height / scale);
|
||||
const context = canvas.getContext('2d')!;
|
||||
// disableImageSmoothing(context);
|
||||
context.scale(1 / scale, 1 / scale);
|
||||
context.drawImage(mapCanvas, 0, 0);
|
||||
}
|
||||
|
||||
function drawMinimap(canvas: HTMLCanvasElement, map: WorldMap, info: ToolsMapOtherInfo, scale: number) {
|
||||
const tileWidth = 1;
|
||||
const tileHeight = 1;
|
||||
|
||||
const mapCanvas = createCanvas(map.width * tileWidth, map.height * tileHeight);
|
||||
const mapContext = mapCanvas.getContext('2d')!;
|
||||
|
||||
updateMap(map, 0);
|
||||
|
||||
for (let x = 0; x < map.width; x++) {
|
||||
for (let y = 0; y < map.height; y++) {
|
||||
const tile = getTile(map, x, y);
|
||||
const color = getTileColor(tile, info.season);
|
||||
mapContext.fillStyle = colorToCSS(color);
|
||||
mapContext.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
map.entities = info.entities
|
||||
.map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager, defaultWorldState));
|
||||
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
for (const e of map.entities) {
|
||||
if (e.minimap && e.minimap.order === i) {
|
||||
const { color, rect } = e.minimap;
|
||||
mapContext.fillStyle = colorToCSS(color);
|
||||
mapContext.fillRect(Math.round(e.x + rect.x), Math.round(e.y + rect.y), rect.w, rect.h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = mapCanvas.width * scale;
|
||||
canvas.height = mapCanvas.height * scale;
|
||||
const context = canvas.getContext('2d')!;
|
||||
context.save();
|
||||
|
||||
if (scale >= 1) {
|
||||
disableImageSmoothing(context);
|
||||
}
|
||||
|
||||
context.scale(scale, scale);
|
||||
context.drawImage(mapCanvas, 0, 0);
|
||||
context.restore();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
.container-fluid
|
||||
.form-inline.py-2
|
||||
.form-group
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
.form-group
|
||||
scale-picker.ml-1([(scale)]="scale" (scaleChange)="redraw()")
|
||||
|
||||
.float-left.mr-2
|
||||
canvas(#canvas width=512 height=512)
|
||||
|
||||
.float-left
|
||||
.form-group.form-inline
|
||||
label.text-muted sprite:
|
||||
input.form-control.ml-1([(ngModel)]="spriteName" (change)="spriteChanged()" style="width: 200px;"
|
||||
[typeahead]="sprites" (typeaheadOnSelect)="spriteChanged()")
|
||||
button.btn.btn-default.ml-1((click)="loadPalette()")
|
||||
| load palette
|
||||
|
||||
div
|
||||
.form-group(*ngFor="let c of palette")
|
||||
.badge([style.background]="c.original" style="border: solid 1px #666") ........
|
||||
|
|
||||
color-picker([(color)]="c.current" (colorChange)="redraw()" style="display: inline-block;")
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||
import * as sprites from '../../../generated/sprites';
|
||||
import { setPaletteManager } from '../../../common/mixins';
|
||||
import { parseColor, colorToCSS } from '../../../common/color';
|
||||
import { PaletteManager, releasePalette } from '../../../graphics/paletteManager';
|
||||
import { drawCanvas } from '../../../graphics/contextSpriteBatch';
|
||||
import { disableImageSmoothing } from '../../../client/canvasUtils';
|
||||
import { SHADOW_COLOR, WHITE } from '../../../common/colors';
|
||||
import { PaletteRenderable } from '../../../common/interfaces';
|
||||
import { loadAndInitSpriteSheets } from '../../../client/spriteUtils';
|
||||
import { faHome } from '../../../client/icons';
|
||||
|
||||
const BG = parseColor('lightgreen');
|
||||
const DEFAULT_PALETTE = [0, 0xffffffff];
|
||||
const paletteManager = new PaletteManager();
|
||||
const defaultPalette = paletteManager.add(DEFAULT_PALETTE);
|
||||
|
||||
@Component({
|
||||
selector: 'tools-palette',
|
||||
templateUrl: 'tools-palette.pug',
|
||||
})
|
||||
export class ToolsPalette implements OnInit {
|
||||
readonly homeIcon = faHome;
|
||||
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
||||
scale = 3;
|
||||
sprites = Object.keys(sprites).filter(key => {
|
||||
const s = (sprites as any)[key] as any;
|
||||
return !!(s && s.color);
|
||||
});
|
||||
spriteName = '';
|
||||
palette = ['red', 'blue', 'orange', 'violet'].map(x => ({ original: x, current: x }));
|
||||
ngOnInit() {
|
||||
setPaletteManager(paletteManager);
|
||||
loadAndInitSpriteSheets().then(() => this.redraw());
|
||||
}
|
||||
spriteChanged() {
|
||||
this.redraw();
|
||||
}
|
||||
loadPalette() {
|
||||
const sprite = (sprites as any)[this.spriteName] as PaletteRenderable;
|
||||
|
||||
if (sprite) {
|
||||
this.palette = Array.from(sprite.palettes![0]).map(colorToCSS).map(c => ({ original: c, current: c }));
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
}
|
||||
redraw() {
|
||||
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
|
||||
const width = Math.ceil(canvas.width / this.scale);
|
||||
const height = Math.ceil(canvas.height / this.scale);
|
||||
|
||||
const buffer = drawCanvas(width, height, sprites.paletteSpriteSheet, BG, batch => {
|
||||
const sprite = (sprites as any)[this.spriteName] as PaletteRenderable;
|
||||
|
||||
if (sprite) {
|
||||
const palette = paletteManager.add(this.palette.map(x => parseColor(x.current)));
|
||||
|
||||
const x = (width - (sprite.color!.w + sprite.color!.ox)) / 2;
|
||||
const y = (height - (sprite.color!.h + sprite.color!.oy)) / 2;
|
||||
|
||||
console.log(x, y, width, sprite.color!.w, sprite.color!.ox);
|
||||
|
||||
batch.drawSprite(sprite.shadow, SHADOW_COLOR, defaultPalette, x, y);
|
||||
batch.drawSprite(sprite.color, WHITE, palette, x, y);
|
||||
|
||||
releasePalette(palette);
|
||||
}
|
||||
});
|
||||
|
||||
const viewContext = canvas.getContext('2d')!;
|
||||
viewContext.save();
|
||||
disableImageSmoothing(viewContext);
|
||||
viewContext.scale(this.scale, this.scale);
|
||||
viewContext.drawImage(buffer, 0, 0);
|
||||
viewContext.restore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// import { TextEncoder } from 'util';
|
||||
|
||||
function forEachCharacter(value: string, callback: (code: number) => void) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const code = value.charCodeAt(i);
|
||||
|
||||
// high surrogate
|
||||
if (code >= 0xd800 && code <= 0xdbff) {
|
||||
if ((i + 1) < value.length) {
|
||||
const extra = value.charCodeAt(i + 1);
|
||||
|
||||
// low surrogate
|
||||
if ((extra & 0xfc00) === 0xdc00) {
|
||||
i++;
|
||||
callback(((code & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
callback(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function charLengthInBytes(code: number): number {
|
||||
if ((code & 0xffffff80) === 0) {
|
||||
return 1;
|
||||
} else if ((code & 0xfffff800) === 0) {
|
||||
return 2;
|
||||
} else if ((code & 0xffff0000) === 0) {
|
||||
return 3;
|
||||
} else {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
function stringLengthInBytes(value: string): number {
|
||||
let result = 0;
|
||||
forEachCharacter(value, code => result += charLengthInBytes(code));
|
||||
return result;
|
||||
}
|
||||
|
||||
function encodeStringTo(buffer: Uint8Array | Buffer, offset: number, value: string): number {
|
||||
forEachCharacter(value, code => {
|
||||
const length = charLengthInBytes(code);
|
||||
|
||||
if (length === 1) {
|
||||
buffer[offset++] = code;
|
||||
} else {
|
||||
if (length === 2) {
|
||||
buffer[offset++] = ((code >> 6) & 0x1f) | 0xc0;
|
||||
} else if (length === 3) {
|
||||
buffer[offset++] = ((code >> 12) & 0x0f) | 0xe0;
|
||||
buffer[offset++] = ((code >> 6) & 0x3f) | 0x80;
|
||||
} else {
|
||||
buffer[offset++] = ((code >> 18) & 0x07) | 0xf0;
|
||||
buffer[offset++] = ((code >> 12) & 0x3f) | 0x80;
|
||||
buffer[offset++] = ((code >> 6) & 0x3f) | 0x80;
|
||||
}
|
||||
|
||||
buffer[offset++] = (code & 0x3f) | 0x80;
|
||||
}
|
||||
});
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
export function encodeString(value: string | null): Uint8Array | null {
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
const buffer = new Uint8Array(stringLengthInBytes(value));
|
||||
encodeStringTo(buffer, 0, value);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function encodeStringNew(value: string | null): Uint8Array | null {
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
const buffer = new Uint8Array(stringLengthInBytes2(value));
|
||||
encodeStringTo2(buffer, 0, value);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// new methods
|
||||
|
||||
function charLengthInBytes2(code: number): number {
|
||||
if ((code & 0xffffff80) === 0) {
|
||||
return 1;
|
||||
} else if ((code & 0xfffff800) === 0) {
|
||||
return 2;
|
||||
} else if ((code & 0xffff0000) === 0) {
|
||||
return 3;
|
||||
} else {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
export function stringLengthInBytes2(value: string): number {
|
||||
let result = 0;
|
||||
forEachCharacter2(value, code => result = (result + charLengthInBytes2(code)) | 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function encodeStringTo2(buffer: Uint8Array, offset: number, value: string): number {
|
||||
forEachCharacter2(value, code => {
|
||||
const length = charLengthInBytes2(code) | 0;
|
||||
|
||||
if (length === 1) {
|
||||
buffer[offset++] = code;
|
||||
} else {
|
||||
if (length === 2) {
|
||||
buffer[offset++] = ((code >> 6) & 0x1f) | 0xc0;
|
||||
} else if (length === 3) {
|
||||
buffer[offset++] = ((code >> 12) & 0x0f) | 0xe0;
|
||||
buffer[offset++] = ((code >> 6) & 0x3f) | 0x80;
|
||||
} else {
|
||||
buffer[offset++] = ((code >> 18) & 0x07) | 0xf0;
|
||||
buffer[offset++] = ((code >> 12) & 0x3f) | 0x80;
|
||||
buffer[offset++] = ((code >> 6) & 0x3f) | 0x80;
|
||||
}
|
||||
|
||||
buffer[offset++] = (code & 0x3f) | 0x80;
|
||||
}
|
||||
});
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
function forEachCharacter2(value: string, callback: (code: number) => void) {
|
||||
const length = value.length | 0;
|
||||
const lengthMinusOne = Math.max(0, length - 1) | 0;
|
||||
|
||||
for (let i = 0; i < length; i = (i + 1) | 0) {
|
||||
let code = value.charCodeAt(i) | 0;
|
||||
|
||||
// high surrogate
|
||||
if (code >= 0xd800 && code <= 0xdbff) {
|
||||
if (i < lengthMinusOne) {
|
||||
const extra = value.charCodeAt(i + 1) | 0;
|
||||
|
||||
// low surrogate
|
||||
if ((extra & 0xfc00) === 0xdc00) {
|
||||
i = (i + 1) | 0;
|
||||
code = (((((code & 0x3ff) << 10) + (extra & 0x3ff)) | 0) + 0x10000) | 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
callback(code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.m-2
|
||||
.mb-2
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
button.btn.btn-default.ml-1((click)="run()") run test
|
||||
button.btn.btn-default.ml-1((click)="stats()") run stats
|
||||
button.btn.btn-default.ml-1(*ngFor="let test of tests" (click)="test.func()") {{test.name}}
|
||||
pre.rounded.p-3(style="color: white; background: #222;")
|
||||
| {{output}}
|
||||
@@ -0,0 +1,404 @@
|
||||
import { Component, NgZone } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { isEqual, range, random, times } from 'lodash';
|
||||
import { parseColorWithAlpha, parseColorFast, colorToHexRGB } from '../../../common/color';
|
||||
import { fillToOutline } from '../../../common/colors';
|
||||
import { toColorListNumber } from '../../../common/ponyInfo';
|
||||
import { decodePonyInfo, createPostDecompressPony } from '../../../common/compressPony';
|
||||
import { faHome } from '../../../client/icons';
|
||||
import { PaletteManager } from '../../../graphics/paletteManager';
|
||||
import { filterBadWords, createMatchEntries } from '../../../common/swears';
|
||||
import { bitWriter } from '../../../common/bitUtils';
|
||||
import { includes as utilsIncludes } from '../../../common/utils';
|
||||
import { encodeString, encodeStringNew } from './methods';
|
||||
|
||||
@Component({
|
||||
selector: 'tools-perf',
|
||||
templateUrl: 'tools-perf.pug',
|
||||
})
|
||||
export class ToolsPerf {
|
||||
readonly homeIcon = faHome;
|
||||
output = '';
|
||||
ponies: string[] = [];
|
||||
messages: string[] = [];
|
||||
tests: { name: string; func: () => void; }[] = [];
|
||||
constructor(http: HttpClient, private zone: NgZone) {
|
||||
http.get<string[]>('/tests/ponies.json').subscribe(data => this.ponies = data);
|
||||
http.get<string[]>('/tests/messages.json').subscribe(data => this.messages = data);
|
||||
this.output = createPostDecompressPony().toString();
|
||||
this.tests.push({ name: 'arrays', func: () => this.runTest(arrayTest) });
|
||||
this.tests.push({ name: 'compress colors', func: () => this.runTest(compressColorsTest) });
|
||||
this.tests.push({ name: 'parse color', func: () => this.runTest(parseColorTest) });
|
||||
this.tests.push({ name: 'compare arrays', func: () => this.runTest(compareArrays) });
|
||||
this.tests.push({ name: 'utf', func: () => this.runTest(() => utfTest(this.messages)) });
|
||||
this.tests.push({ name: 'includes', func: () => this.runTest(includeTest) });
|
||||
}
|
||||
run() {
|
||||
this.runTest(() => utfTest(this.messages));
|
||||
}
|
||||
stats() {
|
||||
swearEntryTest(this.messages, x => this.output = x);
|
||||
}
|
||||
private runTest(test: () => void) {
|
||||
this.zone.runOutsideAngular(() => setTimeout(test, 20));
|
||||
}
|
||||
}
|
||||
|
||||
function measure(name: string, iterations: number, func: (i: number) => void) {
|
||||
if (!iterations)
|
||||
return;
|
||||
|
||||
const start = performance.now();
|
||||
let v: any;
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
v = func(i);
|
||||
}
|
||||
|
||||
const end = performance.now();
|
||||
const diff = end - start;
|
||||
|
||||
console.log(`${name}: ${diff.toFixed(0)}ms, ${(diff / iterations).toFixed(3)}ms per iteration // ${!!v}`);
|
||||
}
|
||||
|
||||
export function compressColorsTest() {
|
||||
const colors = [
|
||||
3553475327, 2592097791, 3662487807, 545184511, 2744818431, 1658462463, 16764927, 7864319,
|
||||
4278253055, 2492366335, 10033407, 11763711, 6730751, 3003165951, 2997456127, 1447512063,
|
||||
1936281087, 874586623, 3713423103, 2099652351, 4293220607, 512819199, 852308735, 3664828159,
|
||||
3692313855, 2147472639, 2861699071, 5944319, 42992383, 2570622463, 2583699455, 1051954175,
|
||||
3013286911, 2762969343, 1400052223, 1991223551, 1049483775, 611346431, 272724223, 1392443647,
|
||||
1589395455, 3201321215, 2679322623, 3233857791, 1280068863, 2560137471, 437918463, 3908210943,
|
||||
3602601215, 4001558271, 2806294527, 2508550143, 1732985855, 1330597887, 1381126911, 1750746879,
|
||||
1211049983, 926365695, 960051711, 2659530751, 3597364223, 777334527, 2201321727, 2120247039,
|
||||
2441106175, 2354205439, 1844342271, 3613774847, 1967148031, 4289003775, 3600494335, 3164100095,
|
||||
2845275903, 3098517247, 1463486719, 1699355647, 1834628607, 1936084991, 2307095039, 857933311,
|
||||
740297471, 807801599, 740629247, 656811007, 3587560959, 2998055679, 2425393407, 1869574143,
|
||||
4294967295, 4294238719, 4293575679, 4292051711, 4291190527
|
||||
].map(x => x >>> 0);
|
||||
|
||||
const oldMethod = bitWriter(write => colors.forEach(x => write(x >> 8, 24)));
|
||||
|
||||
const truncated = colors.map(c => (c >>> 8) & 0xffffff);
|
||||
truncated.sort((a, b) => a > b ? 1 : (a < b ? -1 : 0));
|
||||
|
||||
console.log(truncated);
|
||||
console.log(truncated.slice(1).map((c, i) => (c - truncated[i]).toString(16)));
|
||||
|
||||
const newMethod = bitWriter(write => truncated.forEach(x => write(x, 24)));
|
||||
|
||||
console.log('old', oldMethod.byteLength);
|
||||
console.log('new', newMethod.byteLength);
|
||||
}
|
||||
|
||||
export const results: any[] = [];
|
||||
|
||||
export function parseColorTest() {
|
||||
const iterations = 100000;
|
||||
|
||||
function parseColorExperimental(value: string) {
|
||||
return (parseInt(value, 16) << 8) | 0xff;
|
||||
}
|
||||
|
||||
measure('COLOR parseColorWithAlpha', iterations, () => {
|
||||
return parseColorWithAlpha('ff4354', 1);
|
||||
});
|
||||
|
||||
measure('COLOR parseColorFast', iterations, () => {
|
||||
return parseColorFast('ff4354');
|
||||
});
|
||||
|
||||
measure('COLOR parseColorExperimental', iterations, () => {
|
||||
return parseColorExperimental('ff4354');
|
||||
});
|
||||
}
|
||||
|
||||
export function compareArrays() {
|
||||
const iterations = 100000;
|
||||
const a = [2423, 534534, 546124, 23412, 54364, 67756, 234234];
|
||||
const b = [564, 867867, 65645, 567567, 34534, 32453, 867867];
|
||||
const c = [2423, 534534, 546124, 23412, 54364, 67756, 234234];
|
||||
const d = [2423, 534534, 546124, 23412, 54364, 67756];
|
||||
let t: any;
|
||||
|
||||
function compareArrays(a: number[], b: number[]) {
|
||||
if (a.length !== b.length)
|
||||
return false;
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
measure('ARRAY _.isEqual', iterations, () => {
|
||||
t = isEqual(a, b);
|
||||
t = isEqual(a, c) || t;
|
||||
t = isEqual(a, d) || t;
|
||||
results.push(t);
|
||||
});
|
||||
|
||||
measure('ARRAY compareArrays', iterations, () => {
|
||||
t = compareArrays(a, b);
|
||||
t = compareArrays(a, c) || t;
|
||||
t = compareArrays(a, d) || t;
|
||||
results.push(t);
|
||||
});
|
||||
}
|
||||
|
||||
export function decode(data: string[]) {
|
||||
let manager = new PaletteManager();
|
||||
// manager = ({ addArray: (x: any) => x } as any);
|
||||
|
||||
measure('DECODE 1', 10000, i => {
|
||||
results.push(decodePonyInfo(data[i % data.length], manager));
|
||||
});
|
||||
|
||||
// console.log(((manager as any).palettes as any[]).map(x => x.length).join(', '));
|
||||
}
|
||||
|
||||
export function utfTest(messages: string[]) {
|
||||
const iterations = 300000;
|
||||
|
||||
measure('old', iterations, i => {
|
||||
results.push(encodeString(messages[i % messages.length]));
|
||||
});
|
||||
|
||||
measure('new', iterations, i => {
|
||||
results.push(encodeStringNew(messages[i % messages.length]));
|
||||
});
|
||||
|
||||
const encoder = new (window as any).TextEncoder('utf8');
|
||||
|
||||
measure('native', iterations, i => {
|
||||
results.push(encoder.encode(messages[i % messages.length]));
|
||||
});
|
||||
}
|
||||
|
||||
export function arrayTest() {
|
||||
const iterations = 10000;
|
||||
const length = 100;
|
||||
const size = 24;
|
||||
const typed = new Int32Array(length * size);
|
||||
const typed2 = new Uint16Array(length * size);
|
||||
|
||||
for (let i = 0; i < typed.length; i++) {
|
||||
typed2[i] = typed[i] = Math.random() * 0xffff;
|
||||
}
|
||||
|
||||
const objects = times(length, i => ({
|
||||
a: typed[i * size + 0],
|
||||
b: typed[i * size + 1],
|
||||
c: typed[i * size + 2],
|
||||
d: typed[i * size + 3],
|
||||
e: typed[i * size + 4],
|
||||
f: typed[i * size + 5],
|
||||
g: typed[i * size + 6],
|
||||
h: typed[i * size + 7],
|
||||
i: typed[i * size + 8],
|
||||
j: typed[i * size + 9],
|
||||
k: typed[i * size + 10],
|
||||
l: typed[i * size + 11],
|
||||
m: typed[i * size + 12],
|
||||
n: typed[i * size + 13],
|
||||
o: typed[i * size + 14],
|
||||
p: typed[i * size + 15],
|
||||
q: typed[i * size + 16],
|
||||
r: typed[i * size + 17],
|
||||
s: typed[i * size + 18],
|
||||
t: typed[i * size + 19],
|
||||
u: typed[i * size + 20],
|
||||
v: typed[i * size + 21],
|
||||
w: typed[i * size + 22],
|
||||
x: typed[i * size + 23],
|
||||
}));
|
||||
|
||||
const indexes = times(length, () => (Math.random() * length) | 0);
|
||||
|
||||
measure('typed', iterations, index => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const offset = (indexes[(i + index) % length]) * 24;
|
||||
|
||||
for (let j = 0; j < size; j++) {
|
||||
sum += typed[offset + j];
|
||||
}
|
||||
}
|
||||
results.push(sum);
|
||||
});
|
||||
|
||||
measure('typed2', iterations, index => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const offset = (indexes[(i + index) % length]) * 24;
|
||||
|
||||
for (let j = 0; j < size; j++) {
|
||||
sum += typed2[offset + j];
|
||||
}
|
||||
}
|
||||
results.push(sum);
|
||||
});
|
||||
|
||||
measure('objects', iterations, index => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const offset = indexes[(i + index) % length];
|
||||
const o = objects[offset];
|
||||
sum += o.a + o.b + o.c + o.d + o.e + o.f + o.g + o.h + o.i + o.j + o.k + o.l +
|
||||
o.m + o.n + o.o + o.p + o.q + o.r + o.s + o.t + o.u + o.v + o.w + o.x;
|
||||
}
|
||||
results.push(sum);
|
||||
});
|
||||
}
|
||||
|
||||
export function fillToOutlineTest() {
|
||||
const iterations = 100000;
|
||||
|
||||
function fillToOutlineFast(color: string) {
|
||||
return colorToHexRGB(parseColorFast(color));
|
||||
}
|
||||
|
||||
measure('FILL-TO-OUTLINE fillToOutline', iterations, () => {
|
||||
fillToOutline('32cd32');
|
||||
});
|
||||
|
||||
measure('FILL-TO-OUTLINE fillToOutlineFast', iterations, () => {
|
||||
fillToOutlineFast('32cd32');
|
||||
});
|
||||
}
|
||||
|
||||
export function includeTest() {
|
||||
const iterations = 100000;
|
||||
const array = range(1000).map(() => random(0, 1000));
|
||||
let t = 0;
|
||||
|
||||
// measure('INCLUDE _.includes', iterations, () => {
|
||||
// t += includes(array, array[random(0, 1000)]) as any | 0;
|
||||
// t += includes(array, array[random(0, 1000)]) as any | 0;
|
||||
// t += includes(array, array[random(0, 1000)]) as any | 0;
|
||||
// });
|
||||
|
||||
measure('INCLUDE includes', iterations, () => {
|
||||
t += utilsIncludes(array, array[random(0, 1000)]) as any | 0;
|
||||
t += utilsIncludes(array, array[random(0, 1000)]) as any | 0;
|
||||
t += utilsIncludes(array, array[random(0, 1000)]) as any | 0;
|
||||
});
|
||||
|
||||
measure('INCLUDE indexOf !== -1', iterations, () => {
|
||||
t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0;
|
||||
t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0;
|
||||
t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0;
|
||||
});
|
||||
|
||||
results.push(t);
|
||||
}
|
||||
|
||||
export function toColorListTest() {
|
||||
function toColorList2Old(colors: number[]) {
|
||||
return [0, ...colors.map(c => c || 0xff)];
|
||||
}
|
||||
|
||||
const iterations = 100000;
|
||||
let t: any[] = [];
|
||||
|
||||
measure('TOCOLORLIST2 old', iterations, () => {
|
||||
t.push(toColorList2Old([1, 2, Date.now(), Date.now(), 5, 6]));
|
||||
});
|
||||
|
||||
measure('TOCOLORLIST2 new', iterations, () => {
|
||||
t.push(toColorListNumber([1, 2, Date.now(), Date.now(), 5, 6]));
|
||||
});
|
||||
|
||||
results.push(t);
|
||||
}
|
||||
|
||||
export function copyTest() {
|
||||
const iterations = 10000;
|
||||
let t: any[] = [];
|
||||
const src = new Uint32Array(1000);
|
||||
const dst = new Uint32Array(10000);
|
||||
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
src[i] = Math.random() * 10000;
|
||||
}
|
||||
|
||||
for (let i = 0; i < dst.length; i++) {
|
||||
dst[i] = Math.random() * 10000;
|
||||
}
|
||||
|
||||
measure('ONE_BY_ONE', iterations, iteration => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let j = 0; j < src.length; j++) {
|
||||
dst[1000 * i + j] = src[j];
|
||||
}
|
||||
}
|
||||
|
||||
t.push(dst, iteration);
|
||||
});
|
||||
|
||||
measure('COPY_BUFFER', iterations, iteration => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
dst.set(src, 1000 * i);
|
||||
}
|
||||
|
||||
t.push(dst, iteration);
|
||||
});
|
||||
|
||||
results.push(t);
|
||||
}
|
||||
|
||||
export function regexTest(testStrings: string[]) {
|
||||
const iterations = 1000000;
|
||||
const t: any[] = [];
|
||||
|
||||
measure('WITH_GROUPS', iterations, iteration => {
|
||||
const test = testStrings[iteration % testStrings.length];
|
||||
t.push(test.replace(/(a|b)(.)(.)(.)(.)(.)/, 'X'));
|
||||
});
|
||||
|
||||
measure('WITHOUT_GROUPS', iterations, iteration => {
|
||||
const test = testStrings[iteration % testStrings.length];
|
||||
t.push(test.replace(/(?:a|b)(?:.)(?:.)(?:.)(?:.)(?:.)/, 'X'));
|
||||
});
|
||||
|
||||
results.push(t);
|
||||
}
|
||||
|
||||
export function swearTest(testStrings: string[]) {
|
||||
const iterations = 10000;
|
||||
const t: any[] = [];
|
||||
|
||||
measure('TEST', iterations, iteration => {
|
||||
const test = testStrings[iteration % testStrings.length];
|
||||
t.push(filterBadWords(test));
|
||||
});
|
||||
|
||||
results.push(t);
|
||||
}
|
||||
|
||||
export function swearEntryTest(testStrings: string[], onResult: (output: string) => void) {
|
||||
const iterations = 2000;
|
||||
const output: any[] = [];
|
||||
const entries = createMatchEntries();
|
||||
|
||||
for (const e of entries) {
|
||||
const start = performance.now();
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const test = testStrings[i % testStrings.length];
|
||||
results.push(test.replace(e.regex, '*****'));
|
||||
}
|
||||
|
||||
const diff = performance.now() - start;
|
||||
output.push({ e, diff });
|
||||
}
|
||||
|
||||
onResult(output
|
||||
.sort((a, b) => b.diff - a.diff)
|
||||
.map(x => `${x.diff.toFixed(2).padStart(6)} "${x.e.line}"`)
|
||||
.join('\n'));
|
||||
}
|
||||
|
||||
(window as any).__results = results;
|
||||
@@ -0,0 +1,26 @@
|
||||
.m-4(style="position: relative;" (agDrag)="dragRegion($event)" agDragRelative="self")
|
||||
table.table-fixed.pointer-none
|
||||
tbody
|
||||
tr(*ngFor="let row of regions")
|
||||
td(*ngFor="let cell of row"
|
||||
style="border: solid 1px #666;"
|
||||
[style.background]="cell"
|
||||
[style.width.px]="regionSize * tileWidth * scale"
|
||||
[style.height.px]="regionSize * tileHeight * scale")
|
||||
div(style="position: absolute; border: dashed 1px red; top: 0; left: 0;"
|
||||
[style.width.px]="currentMapSize * tileWidth * scale"
|
||||
[style.height.px]="currentMapSize * tileHeight * scale")
|
||||
div(style="position: absolute; border: solid 1px lime; display: flex; justify-content: center; align-items: center;"
|
||||
[style.width.px]="camera.w * scale"
|
||||
[style.height.px]="camera.h * scale"
|
||||
[style.left.px]="camera.x * scale"
|
||||
[style.top.px]="camera.y * scale")
|
||||
div(style="width: 30%; height: 30%; border: dashed 1px lime;")
|
||||
div(style="position: absolute; border: dashed 1px orange;"
|
||||
[style.width.px]="approxCamera.w * scale"
|
||||
[style.height.px]="approxCamera.h * scale"
|
||||
[style.left.px]="approxCamera.x * scale"
|
||||
[style.top.px]="approxCamera.y * scale")
|
||||
div(style="position: absolute; width: 4px; height: 4px; border-radius: 100%; background: lime;"
|
||||
[style.left.px]="player.x * tileWidth * scale - 2"
|
||||
[style.top.px]="player.y * tileHeight * scale - 2")
|
||||
@@ -0,0 +1,172 @@
|
||||
import { times, fill, clamp } from 'lodash';
|
||||
import { Component, OnInit, HostListener, OnDestroy } from '@angular/core';
|
||||
import { rect } from '../../../common/rect';
|
||||
import { AgDragEvent } from '../../shared/directives/agDrag';
|
||||
import { updateCamera, centerCameraOn, isAreaVisible, createCamera } from '../../../common/camera';
|
||||
import { tileWidth, tileHeight, PONY_SPEED_TROT } from '../../../common/constants';
|
||||
import { Key } from '../../../client/input/input';
|
||||
import { Rect, Point } from '../../../common/interfaces';
|
||||
import { toWorldX, toWorldY } from '../../../common/positionUtils';
|
||||
|
||||
export function getRegionsBounds(client: any, region: any) {
|
||||
const screenSize = client.screenSize;
|
||||
const width = Math.ceil(((1.3 * screenSize.width) / region.size) / 2) * 2 + 2;
|
||||
const height = Math.ceil(((1.3 * screenSize.height) / region.size) / 2) * 2 + 2;
|
||||
return rect(region.x - Math.ceil(width / 2), region.y - Math.ceil(height / 2), width, height);
|
||||
}
|
||||
|
||||
export function getRegionsBoundsCameraBased(_entity: Point, camera: Rect, regionSize: number) {
|
||||
const left = Math.floor(camera.x / regionSize - 0.5);
|
||||
const top = Math.floor(camera.y / regionSize - 0.5);
|
||||
const right = Math.floor((camera.x + camera.w) / regionSize + 0.5);
|
||||
const bottom = Math.floor((camera.y + camera.h) / regionSize + 0.5);
|
||||
return rect(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tools-regions',
|
||||
templateUrl: 'tools-regions.pug',
|
||||
})
|
||||
export class ToolsRegions implements OnInit, OnDestroy {
|
||||
currentMapSize = 80;
|
||||
tileWidth = tileWidth;
|
||||
tileHeight = tileHeight;
|
||||
screen = { width: 390, height: 580 };
|
||||
// screen = { width: 1920, height: 1080 };
|
||||
regionsX = 18;
|
||||
regionsY = 16;
|
||||
regionSize = 8;
|
||||
scale = 0.25;
|
||||
zoom = 2;
|
||||
regions: string[][];
|
||||
camera = createCamera();
|
||||
approxCamera = createCamera();
|
||||
player = { x: 0, y: 0 };
|
||||
frame = 0;
|
||||
lastFrame = 0;
|
||||
constructor() {
|
||||
this.regions = times(this.regionsY, () => times(this.regionsX, () => ''));
|
||||
}
|
||||
ngOnInit() {
|
||||
this.update();
|
||||
this.frame = requestAnimationFrame(this.tick);
|
||||
}
|
||||
ngOnDestroy() {
|
||||
cancelAnimationFrame(this.frame);
|
||||
}
|
||||
update() {
|
||||
const regionWidth = this.regionSize * tileWidth;
|
||||
const regionHeight = this.regionSize * tileHeight;
|
||||
const map = { width: this.regionsX * this.regionSize, height: this.regionsY * this.regionSize } as any;
|
||||
|
||||
this.camera.w = Math.ceil(this.screen.width / this.zoom);
|
||||
this.camera.h = Math.ceil(this.screen.height / this.zoom);
|
||||
updateCamera(this.camera, this.player, map);
|
||||
|
||||
this.approxCamera.w = this.camera.w * 1.3;
|
||||
this.approxCamera.h = this.camera.h * 1.3;
|
||||
centerCameraOn(this.approxCamera, this.player);
|
||||
updateCamera(this.approxCamera, this.player, map);
|
||||
|
||||
this.regions.forEach(x => fill(x, ''));
|
||||
|
||||
const rx = clamp(Math.floor(this.player.x / this.regionSize), 0, this.regionsX - 1);
|
||||
const ry = clamp(Math.floor(this.player.y / this.regionSize), 0, this.regionsY - 1);
|
||||
|
||||
const bounds1 = getRegionsBounds(
|
||||
{
|
||||
screenSize: {
|
||||
width: Math.ceil(this.camera.w / tileWidth),
|
||||
height: Math.ceil(this.camera.h / tileHeight)
|
||||
}
|
||||
},
|
||||
{ size: this.regionSize, x: rx, y: ry });
|
||||
|
||||
const bounds2 = getRegionsBoundsCameraBased(
|
||||
this.player,
|
||||
rect(toWorldX(this.camera.x), toWorldY(this.camera.y), toWorldX(this.camera.w), toWorldY(this.camera.h)),
|
||||
this.regionSize);
|
||||
|
||||
const bounds = [bounds1, bounds2][1];
|
||||
|
||||
for (let ix = 0; ix <= bounds.w; ix++) {
|
||||
for (let iy = 0; iy <= bounds.h; iy++) {
|
||||
const yy = bounds.y + iy;
|
||||
const xx = bounds.x + ix;
|
||||
|
||||
if (xx >= 0 && xx < this.regionsX && yy >= 0 && yy < this.regionsY) {
|
||||
this.regions[yy][xx] = 'Sienna';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let x = 0; x < this.regionsX; x++) {
|
||||
for (let y = 0; y < this.regionsY; y++) {
|
||||
if (isAreaVisible(this.camera, x * regionWidth, y * regionHeight, regionWidth, regionHeight)) {
|
||||
if (this.regions[y][x] === 'Sienna') {
|
||||
this.regions[y][x] = 'SeaGreen';
|
||||
} else {
|
||||
this.regions[y][x] = 'Crimson';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.regions[ry][rx] = 'MediumSeaGreen';
|
||||
}
|
||||
dragRegion({ x, y }: AgDragEvent) {
|
||||
this.player.x = x / (this.tileWidth * this.scale);
|
||||
this.player.y = y / (this.tileHeight * this.scale);
|
||||
this.update();
|
||||
}
|
||||
private right = false;
|
||||
private left = false;
|
||||
private up = false;
|
||||
private down = false;
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
keydown(e: KeyboardEvent) {
|
||||
if (e.keyCode === Key.KEY_P) {
|
||||
this.zoom = this.zoom === 4 ? 1 : (this.zoom + 1);
|
||||
this.update();
|
||||
} else if (e.keyCode === Key.RIGHT) {
|
||||
this.right = true;
|
||||
} else if (e.keyCode === Key.LEFT) {
|
||||
this.left = true;
|
||||
} else if (e.keyCode === Key.UP) {
|
||||
this.up = true;
|
||||
} else if (e.keyCode === Key.DOWN) {
|
||||
this.down = true;
|
||||
}
|
||||
}
|
||||
@HostListener('window:keyup', ['$event'])
|
||||
keyup(e: KeyboardEvent) {
|
||||
if (e.keyCode === Key.RIGHT) {
|
||||
this.right = false;
|
||||
} else if (e.keyCode === Key.LEFT) {
|
||||
this.left = false;
|
||||
} else if (e.keyCode === Key.UP) {
|
||||
this.up = false;
|
||||
} else if (e.keyCode === Key.DOWN) {
|
||||
this.down = false;
|
||||
}
|
||||
}
|
||||
tick = (now: number) => {
|
||||
this.frame = requestAnimationFrame(this.tick);
|
||||
const delta = (now - this.lastFrame) / 1000;
|
||||
this.lastFrame = now;
|
||||
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
|
||||
if (this.right) dx += 1;
|
||||
if (this.left) dx -= 1;
|
||||
if (this.up) dy -= 1;
|
||||
if (this.down) dy += 1;
|
||||
|
||||
if (dx || dy) {
|
||||
this.player.x += dx * delta * PONY_SPEED_TROT;
|
||||
this.player.y += dy * delta * PONY_SPEED_TROT;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
.form-inline
|
||||
.form-group
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
|
||||
.form-group
|
||||
label.ml-2 sheets:
|
||||
.btn-group.dropdown.ml-1(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle style="width: 200px;")
|
||||
| {{sheet.name}}
|
||||
fa-icon.ml-1(*ngIf="sheet.file" [icon]="imageIcon")
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
div(*ngFor="let s of sheets")
|
||||
.dropdown-divider(*ngIf="s.spacer")
|
||||
a.dropdown-item((click)="setSheet(s)" *ngIf="!s.spacer")
|
||||
| {{s.name}}
|
||||
fa-icon.ml-1(*ngIf="s.file" [icon]="imageIcon")
|
||||
|
||||
.form-group
|
||||
label.ml-1
|
||||
scale-picker([(scale)]="scale" (scaleChange)="redraw()")
|
||||
|
||||
.form-group
|
||||
button.btn.btn-default.ml-1((click)="redraw()")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.form-group
|
||||
label.ml-2 rows:
|
||||
input.form-control.ml-1(type="number" [(ngModel)]="rows" (input)="redraw()" style="width: 70px;")
|
||||
|
||||
.form-group
|
||||
label.ml-2 cols:
|
||||
input.form-control.ml-1(type="number" [(ngModel)]="cols" (input)="redraw()" style="width: 70px;")
|
||||
|
||||
.form-group
|
||||
label.ml-2 pattern:
|
||||
input.form-control.ml-1(type="number" [(ngModel)]="pattern" (input)="redraw()" style="width: 70px;")
|
||||
|
||||
.form-group
|
||||
button.btn.btn-default.ml-2((click)="png()") PNG
|
||||
button.btn.btn-default.ml-1((click)="psd()") PSD
|
||||
button.btn.btn-default.ml-1((click)="allPSDs()") All PSDs
|
||||
button.btn.btn-danger.ml-1(*ngIf="sheet.alert" disabled) {{sheet.alert}}
|
||||
|
||||
.tools-sheet-offsets(#offsetsDiv [hidden]="!sheet.offsets")
|
||||
tools-offset(
|
||||
*ngFor="let o of sheet.offsets" [offset]="o" (change)="redraw()" [style.width.px]="sheet.offset * scale")
|
||||
|
||||
.tools-sheet-scroll([class.with-offsets]="sheet.offsets" (scroll)="offsetsDiv.scrollLeft = $event.target.scrollLeft")
|
||||
canvas(#canvas)
|
||||
@@ -0,0 +1,30 @@
|
||||
.tools-sheet-offsets {
|
||||
position: relative;
|
||||
height: 70px;
|
||||
overflow: hidden;
|
||||
padding-right: 100px;
|
||||
white-space: nowrap;
|
||||
|
||||
> tools-offset {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tools-sheet-scroll {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 45px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
&.with-offsets {
|
||||
top: 45px + 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
padding: 5px;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||
import { compact } from 'lodash';
|
||||
import { getCols, getRows, createPsd, savePsd, drawPsd } from '../sheetExport';
|
||||
import { loadAndInitSpriteSheets } from '../../../client/spriteUtils';
|
||||
import { saveCanvas } from '../../../client/canvasUtils';
|
||||
import { faHome, faSync, faFileImage } from '../../../client/icons';
|
||||
import { StorageService } from '../../services/storageService';
|
||||
import { at } from '../../../common/utils';
|
||||
import { sheets, Sheet } from '../../../common/sheets';
|
||||
|
||||
@Component({
|
||||
selector: 'tools-sheet',
|
||||
templateUrl: 'tools-sheet.pug',
|
||||
styleUrls: ['tools-sheet.scss'],
|
||||
})
|
||||
export class ToolsSheet implements OnInit {
|
||||
readonly homeIcon = faHome;
|
||||
readonly syncIcon = faSync;
|
||||
readonly imageIcon = faFileImage;
|
||||
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
||||
sheets = sheets;
|
||||
sheet = sheets[0] as Sheet;
|
||||
scale = 2;
|
||||
rows = 1;
|
||||
cols = 1;
|
||||
pattern = -1;
|
||||
constructor(private storage: StorageService) {
|
||||
const sheet = at(this.sheets, storage.getInt('tools-sheet-sheet'))!;
|
||||
|
||||
if ('name' in sheet) {
|
||||
this.setSheet(sheet);
|
||||
}
|
||||
}
|
||||
ngOnInit() {
|
||||
loadAndInitSpriteSheets()
|
||||
.then(() => this.redraw());
|
||||
}
|
||||
setSheet(sheet: Sheet) {
|
||||
sheet = sheet.spacer ? this.sheets[0] as Sheet : sheet;
|
||||
this.sheet = sheet;
|
||||
this.cols = getCols(sheet);
|
||||
this.rows = getRows(sheet);
|
||||
this.redraw();
|
||||
this.storage.setInt('tools-sheet-sheet', this.sheets.indexOf(sheet));
|
||||
}
|
||||
png() {
|
||||
this.redraw();
|
||||
saveCanvas(this.canvas.nativeElement, 'sheet.png');
|
||||
}
|
||||
psd() {
|
||||
const psd = createPsd(this.sheet, this.rows, this.cols);
|
||||
psd.canvas = drawPsd(psd, 1);
|
||||
savePsd(psd, `${this.sheet.file}.psd`);
|
||||
}
|
||||
allPSDs() {
|
||||
this.sheets
|
||||
.filter(x => 'name' in x && !!x.file)
|
||||
.map(x => x as Sheet)
|
||||
.forEach(sheet => {
|
||||
const rows = getRows(sheet);
|
||||
const cols = getCols(sheet);
|
||||
const psd = createPsd(sheet, rows, cols);
|
||||
psd.canvas = drawPsd(psd, 1);
|
||||
savePsd(psd, `${sheet.file}.psd`);
|
||||
});
|
||||
}
|
||||
redraw() {
|
||||
if (this.canvas) {
|
||||
const psd = createPsd(this.sheet, this.rows, this.cols);
|
||||
const layers = compact(psd.children!.map(c => c.children));
|
||||
const patterns = compact(layers.map(xs => xs.find(x => x.name === `pattern ${this.pattern}`)));
|
||||
patterns.forEach(x => x.hidden = false);
|
||||
drawPsd(psd, this.scale, this.canvas.nativeElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
.p-1(style="position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow: hidden;")
|
||||
.mb-3
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
button.btn.btn-default.ml-1((click)="logPositions()")
|
||||
| log positions
|
||||
|
||||
.state-item-container
|
||||
.state-item(*ngFor="let s of states" [style.left.px]="s.x" [style.top.px]="s.y" (agDrag)="drag(s, $event)")
|
||||
div([style.background]="s.color")
|
||||
div
|
||||
div {{s.name}}
|
||||
div(*ngIf="s.variants") (+{{s.variants}})
|
||||
|
||||
svg.pointer-none(width="1500" height="1500")
|
||||
defs
|
||||
marker(*ngFor="let c of arrowColors" [attr.id]="'head-' + c" orient="auto" markerWidth="5" markerHeight="6"
|
||||
refX="0.1" refY="3")
|
||||
path(d="M0,0 V6 L5,3 Z" [attr.fill]="c")
|
||||
path(*ngFor="let a of arrows" [attr.marker-end]="'url(#head-' + a.color + ')'" stroke-width="2"
|
||||
[attr.stroke]="a.color" fill="none" [attr.d]="a.path")
|
||||
|
||||
.state-item-time(*ngFor="let t of times" [style.left.px]="t.x" [style.top.px]="t.y" [title]="t.title || ''")
|
||||
div([style.background]="t.color")
|
||||
span {{t.text}}
|
||||
@@ -0,0 +1,57 @@
|
||||
$state-size: 100px;
|
||||
$time-size: 20px;
|
||||
|
||||
.state-item-container {
|
||||
position: relative;
|
||||
|
||||
> svg {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.state-item {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
cursor: pointer;
|
||||
|
||||
> div {
|
||||
position: absolute;
|
||||
left: -$state-size / 2;
|
||||
top: -$state-size / 2;
|
||||
width: $state-size;
|
||||
height: $state-size;
|
||||
background: gray;
|
||||
border-radius: 100%;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 5px #000;
|
||||
}
|
||||
}
|
||||
|
||||
.state-item-time{
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
z-index: 2;
|
||||
|
||||
> div {
|
||||
position: absolute;
|
||||
left: -$time-size / 2;
|
||||
top: -$time-size / 2;
|
||||
width: $time-size;
|
||||
height: $time-size;
|
||||
background: gray;
|
||||
border-radius: 100%;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { fromPairs } from 'lodash';
|
||||
import { faHome } from '../../../client/icons';
|
||||
import { ponyStates, } from '../../../client/ponyStates';
|
||||
import { AgDragEvent } from '../../shared/directives/agDrag';
|
||||
import { AnimatorState } from '../../../common/animator';
|
||||
import { distance, flatten } from '../../../common/utils';
|
||||
|
||||
function getPos(key: string, type: 'x' | 'y', defaultValue = 100) {
|
||||
const item = localStorage.getItem(`tools-stats-${key}-${type}`);
|
||||
return item ? parseInt(item, 10) : defaultValue;
|
||||
}
|
||||
|
||||
function setPos(key: string, type: 'x' | 'y', value: number) {
|
||||
localStorage.setItem(`tools-stats-${key}-${type}`, value.toString());
|
||||
}
|
||||
|
||||
const defaultPositions: any = {
|
||||
'any': { 'x': 917, 'y': 43 },
|
||||
'standing': { 'x': 317, 'y': 300 },
|
||||
'trotting': { 'x': 660, 'y': 304 },
|
||||
'swimming': { 'x': 988, 'y': 435 },
|
||||
'swimming-to-trotting': { 'x': 747, 'y': 548 },
|
||||
'trotting-to-swimming': { 'x': 805, 'y': 413 },
|
||||
'booping': { 'x': 106, 'y': 301 },
|
||||
'booping-sitting': { 'x': 111, 'y': 569 },
|
||||
'booping-lying': { 'x': 127, 'y': 821 },
|
||||
'booping-flying': { 'x': 110, 'y': 64 },
|
||||
'sitting': { 'x': 312, 'y': 569 },
|
||||
'sitting-down': { 'x': 186, 'y': 437 },
|
||||
'standing-up': { 'x': 313, 'y': 436 },
|
||||
'sitting-to-trotting': { 'x': 517, 'y': 500 },
|
||||
'lying': { 'x': 326, 'y': 818 },
|
||||
'lying-down': { 'x': 254, 'y': 692 },
|
||||
'sitting-up': { 'x': 385, 'y': 682 },
|
||||
'lying-to-trotting': { 'x': 541, 'y': 731 },
|
||||
'hovering': { 'x': 334, 'y': 21 },
|
||||
'flying': { 'x': 641, 'y': 29 },
|
||||
'flying-up': { 'x': 378, 'y': 182 },
|
||||
'flying-down': { 'x': 247, 'y': 177 },
|
||||
'trotting-to-flying': { 'x': 601, 'y': 188 },
|
||||
'flying-to-trotting': { 'x': 805, 'y': 270 },
|
||||
'swinging': { 'x': 484, 'y': 238 },
|
||||
'swimming-to-flying': { 'x': 1063, 'y': 182 },
|
||||
'flying-to-swimming': { 'x': 938, 'y': 232 },
|
||||
'booping-swimming': { 'x': 1144, 'y': 432 }
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'tools-states',
|
||||
templateUrl: 'tools-states.pug',
|
||||
styleUrls: ['tools-states.scss'],
|
||||
})
|
||||
export class ToolsStates {
|
||||
readonly homeIcon = faHome;
|
||||
private startX = 0;
|
||||
private startY = 0;
|
||||
arrowColors = ['orange', 'red', 'lime'];
|
||||
states = ponyStates.map(state => {
|
||||
const def = defaultPositions[state.name] || { x: 0, y: 0 };
|
||||
|
||||
return {
|
||||
color: state.name === 'any' ? 'orange' : (state.animation.loop ? 'LightSeaGreen' : 'cornflowerblue'),
|
||||
name: state.name,
|
||||
variants: Object.keys(state.variants || {}).join(', '),
|
||||
state,
|
||||
x: getPos(state.name, 'x', def.x),
|
||||
y: getPos(state.name, 'y', def.y),
|
||||
};
|
||||
});
|
||||
arrows: { path: string; color: string; }[] = [];
|
||||
times: { x: number; y: number; color: string; text: string; title?: string; }[] = [];
|
||||
constructor() {
|
||||
this.updateArrows();
|
||||
}
|
||||
drag(state: any, { dx, dy, type }: AgDragEvent) {
|
||||
if (type === 'start') {
|
||||
this.startX = state.x;
|
||||
this.startY = state.y;
|
||||
}
|
||||
|
||||
setPos(state.name, 'x', state.x = this.startX + dx);
|
||||
setPos(state.name, 'y', state.y = this.startY + dy);
|
||||
this.updateArrows();
|
||||
}
|
||||
logPositions() {
|
||||
const positions = fromPairs(this.states.map(({ name, x, y }) => [name, { x, y }]));
|
||||
console.log(JSON.stringify(positions).replace(/"/g, `'`).replace(/},/g, '},\n'));
|
||||
}
|
||||
private updateArrows() {
|
||||
this.times = [];
|
||||
this.arrows = flatten(this.states.map(s => s.state.from.map(f => ({
|
||||
to: s,
|
||||
from: this.findState(f.state),
|
||||
color: f.exitAfter === 0 ? (f.keepTime ? 'orange' : 'red') : 'lime',
|
||||
exitAfter: f.exitAfter,
|
||||
enterTime: f.enterTime,
|
||||
onlyDirectTo: f.onlyDirectTo,
|
||||
}))))
|
||||
.filter(({ from, to }) => from && to)
|
||||
.map(({ from, to, color, exitAfter, enterTime, onlyDirectTo }) => {
|
||||
const length = distance(from, to) || 1;
|
||||
const r1 = 50;
|
||||
const nx1 = ((to.x - from.x) / length) * r1;
|
||||
const ny1 = ((to.y - from.y) / length) * r1;
|
||||
const r2 = 60;
|
||||
const nx2 = ((to.x - from.x) / length) * r2;
|
||||
const ny2 = ((to.y - from.y) / length) * r2;
|
||||
const r3 = 75;
|
||||
const nx3 = ((to.x - from.x) / length) * r3;
|
||||
const ny3 = ((to.y - from.y) / length) * r3;
|
||||
const r4 = 80;
|
||||
const nx4 = ((to.x - from.x) / length) * r4;
|
||||
const ny4 = ((to.y - from.y) / length) * r4;
|
||||
|
||||
const fromX = from.x + nx1;
|
||||
const fromY = from.y + ny1;
|
||||
|
||||
if (exitAfter) {
|
||||
this.times.push({ x: fromX, y: fromY, color, text: exitAfter.toFixed(1) });
|
||||
}
|
||||
|
||||
if (enterTime) {
|
||||
this.times.push({ x: to.x - nx3, y: to.y - ny3, color, text: enterTime.toFixed(1) });
|
||||
}
|
||||
|
||||
if (onlyDirectTo) {
|
||||
this.times.push({
|
||||
x: from.x + nx4, y: from.y + ny4, color, text: '?', title: `only directly to: ${onlyDirectTo.name}`
|
||||
});
|
||||
}
|
||||
|
||||
return { path: `M ${fromX} ${fromY} L ${to.x - nx2} ${to.y - ny2}`, color };
|
||||
});
|
||||
}
|
||||
private findState(state: AnimatorState<any>) {
|
||||
return this.states.find(s => s.state === state)!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
ng-template(#modal)
|
||||
settings-modal((close)="modalRef.hide()")
|
||||
|
||||
.container-fluid.mt-3(*ngIf="initialized")
|
||||
tabset(type="pills" saveActiveTab="tools-ui-main-tab")
|
||||
tab(title="General")
|
||||
ng-template(tabContent)
|
||||
.mt-2.d-flex
|
||||
div(style="width: 300px;")
|
||||
.mb-3
|
||||
.form-group
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
|
||||
h3 Color picker
|
||||
.mb-3
|
||||
.form-group
|
||||
color-picker([(color)]="color")
|
||||
.form-group
|
||||
color-picker([(color)]="color")
|
||||
|
||||
h3 Checkbox
|
||||
.mb-3
|
||||
.form-group.mb-2.d-flex.align-items-center
|
||||
check-box([(checked)]="customOutlines")
|
||||
check-box.ml-2([(checked)]="checked" [icon]="heartIcon")
|
||||
check-box.ml-2([(checked)]="checked" [icon]="lockIcon" [disabled]="true")
|
||||
label.ml-2
|
||||
| check box
|
||||
span.badge.badge-success.ml-2(*ngIf="checked") checked
|
||||
.form-group
|
||||
button.btn.btn-default(btnCheckbox [(ngModel)]="checked") btnCheckbox
|
||||
.btn-group.ml-2(btnRadioGroup [(ngModel)]="radio")
|
||||
button.btn.btn-default(btnRadio="a") a
|
||||
button.btn.btn-default(btnRadio="b") b
|
||||
button.btn.btn-default(btnRadio="c") c
|
||||
span.ml-2 btnRadio: {{radio}}
|
||||
|
||||
h3 Slider
|
||||
.mb-3
|
||||
.form-group
|
||||
label
|
||||
span#slider-label music volume:
|
||||
span.text-muted.ml-1 {{slider}}
|
||||
.form-group
|
||||
slider-bar([(value)]="slider" [step]="1" labelledBy="slider-label")
|
||||
.form-group
|
||||
slider-bar.w-75([(value)]="slider" [step]="1" [disabled]="true" label="another slider")
|
||||
|
||||
h3 Emotes
|
||||
.mb-3
|
||||
.form-group
|
||||
p.
|
||||
I #[emote-box(emote="blue_heart")] you. #[emote-box(emote="rock")] #[emote-box(emote="🍎")]
|
||||
#[emote-box(emote="imp")] #[emote-box(emote="face")]
|
||||
|
||||
h3 Custom checkbox
|
||||
.mb-3
|
||||
custom-checkbox([(checked)]="customChecked") Label here
|
||||
| checked: {{customChecked}}
|
||||
|
||||
h3 Supporter stars
|
||||
.mb-3
|
||||
.form-group
|
||||
fa-icon.supporter-1([icon]="starIcon" size="lg")
|
||||
fa-icon.supporter-2.ml-1([icon]="starIcon" size="lg")
|
||||
fa-icon.supporter-3.ml-1([icon]="starIcon" size="lg")
|
||||
|
||||
h3 External links
|
||||
.mb-3
|
||||
.form-group
|
||||
a(href="http://google.com/") should open in new tab
|
||||
|
||||
.ml-5(style="width: 400px;")
|
||||
h3 Character preview
|
||||
.mb-3
|
||||
character-preview([pony]="pony" name="Offline Pony" [tag]="selected.tag")
|
||||
|
||||
h3 Sprite box
|
||||
.mb-3
|
||||
.form-group.d-flex
|
||||
sprite-box([class.active]="spriteActive" (click)="spriteActive = !spriteActive" [sprite]="sprite" fill="red" outline="maroon" circle="gray")
|
||||
sprite-box([class.active]="spriteActive" [sprite]="sprite" [fill]="fills" [outline]="outlines")
|
||||
.form-group
|
||||
color-picker([(color)]="fills[0]")
|
||||
|
||||
h3 Dropdown
|
||||
.mb-3.d-flex
|
||||
.dropdown(dropdown [autoClose]="autoCloseDropdown")
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle)
|
||||
| toggle me
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
.dropdown-item aaa
|
||||
.dropdown-item bbb
|
||||
.dropdown-item ccc
|
||||
.dropdown.ml-1(dropdown [autoClose]="autoCloseDropdown")
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle)
|
||||
| 2nd one
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
.dropdown-item 111
|
||||
.dropdown-item 222
|
||||
.dropdown-item 333
|
||||
.btn-group.ml-1(btnRadioGroup [(ngModel)]="autoCloseDropdown")
|
||||
button.btn([btnRadio]="true" [btnHighlight]="autoCloseDropdown === true") true
|
||||
button.btn([btnRadio]="false" [btnHighlight]="autoCloseDropdown === false") false
|
||||
button.btn([btnRadio]="'outsideClick'" [btnHighlight]="autoCloseDropdown === 'outsideClick'") outsideClick
|
||||
|
||||
h3 Tabset
|
||||
.mb-3
|
||||
tabset(type="pills" saveActiveTab="tools-ui-tab-active-index")
|
||||
tab(title="A")
|
||||
ng-template(tabContent) aaa
|
||||
tab(title="B")
|
||||
ng-template(tabContent) bbb
|
||||
tab(title="C")
|
||||
ng-template(tabContent) ccc
|
||||
tab(title="D" [disabled]="true")
|
||||
ng-template(tabContent) ddd
|
||||
|
||||
.ml-5(style="width: 400px;")
|
||||
h3 Bitmap box
|
||||
.mb-3
|
||||
bitmap-box([bitmap]="pony.cm" tool="brush" [color]="color" [width]="cmSize" [height]="cmSize")
|
||||
|
||||
h3 Sprite set selection
|
||||
.mb-3
|
||||
.form-group
|
||||
set-selection(
|
||||
label="Tail" [base]="baseHairColor" [set]="pony.tail" [sets]="tails"
|
||||
[outlineHidden]="!customOutlines" (change)="changed()")
|
||||
|
||||
tab(title="Game")
|
||||
ng-template(tabContent)
|
||||
.mt-3.d-flex
|
||||
div(style="width: 300px")
|
||||
h3 Modal
|
||||
.mb-3
|
||||
.form-group
|
||||
button.btn.btn-default((click)="showModal(modal)") show modal
|
||||
|
||||
h3 Supporter stars
|
||||
.mb-3
|
||||
.form-group
|
||||
custom-checkbox([(checked)]="isPartyLeader") is party leader
|
||||
.form-group
|
||||
party-list(style="position: absolute;")
|
||||
|
||||
.ml-5(style="width: 400px")
|
||||
h3 Pony box
|
||||
.mb-3
|
||||
.form-group.py-4
|
||||
pony-box([pony]="selected")
|
||||
.form-group.pt-3
|
||||
.btn-group.btn-group-sm
|
||||
button.btn([btnHighlight]="isIgnored(selected)" (click)="toggleIgnored(selected)") ignore
|
||||
button.btn([btnHighlight]="selected.modInfo.note" (click)="selected.modInfo.note = selected.modInfo.note ? '' : 'test'") note
|
||||
button.btn([btnHighlight]="isHidden(selected)" (click)="toggleHidden(selected)") hide
|
||||
.btn-group.btn-group-sm.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle) mute
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item((click)="selected.modInfo.mute=''") none
|
||||
a.dropdown-item((click)="selected.modInfo.mute='perma'") perma
|
||||
a.dropdown-item((click)="selected.modInfo.mute='5 hours'") timed
|
||||
.btn-group.btn-group-sm.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle) shadow
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item((click)="selected.modInfo.shadow=''") none
|
||||
a.dropdown-item((click)="selected.modInfo.shadow='perma'") perma
|
||||
a.dropdown-item((click)="selected.modInfo.shadow='5 hours'") timed
|
||||
.btn-group.btn-group-sm.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle) tag: {{selected.tag || 'none'}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item(*ngFor="let tag of tags" (click)="selected.tag = tag") {{tag || 'none'}}
|
||||
.form-group
|
||||
custom-checkbox([(checked)]="isFriend") is friend
|
||||
|
||||
h3 Portrait
|
||||
.mb-3
|
||||
.form-group.d-flex
|
||||
portrait-box([pony]="pal")
|
||||
portrait-box.ml-2([pony]="pal" size="medium")
|
||||
portrait-box.ml-2([pony]="pal" size="small")
|
||||
portrait-box.ml-2([pony]="pal" size="small" [noBorder]="true" [flip]="true")
|
||||
|
||||
.ml-5(style="width: 400px")
|
||||
h3 Settings box
|
||||
.mb-3
|
||||
div(style="position: relative")
|
||||
settings-box
|
||||
canvas#canvas(width="300" height="100" style="background: #111; width: 300px; height: 100px; position: static;")
|
||||
|
||||
h3 Page loader
|
||||
.mb-3
|
||||
.form-group
|
||||
page-loader
|
||||
|
||||
tab(title="Chatlog")
|
||||
ng-template(tabContent)
|
||||
.mt-2
|
||||
.bg-grass(style="width: 800px; height: 600px; position: relative;")
|
||||
chat-log(#chatlog style="position: absolute; left: 5px; bottom: 5px; right: 5px;")
|
||||
.mt-2
|
||||
button.btn.btn-default((click)="addMessage(chatlog, 'some message')")
|
||||
| Add message
|
||||
button.btn.btn-default.ml-1((click)="addMessage(chatlog, 'some 🍎 message')")
|
||||
| Add emote message
|
||||
button.btn.btn-default.ml-1((click)="addWhisper(chatlog, 'some whisper message')")
|
||||
| Add whisper
|
||||
button.btn.ml-1((click)="spamChat(chatlog)" [btnHighlight]="spamChatInterval")
|
||||
| Spam chat
|
||||
slider-bar([(value)]="chatlogOpacity" style="width: 300px")
|
||||
span {{chatlogOpacity / 100 | percent}}
|
||||
|
||||
tab(title="Settings")
|
||||
ng-template(tabContent)
|
||||
.modal.show.d-block(style="margin-top: 55px")
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
settings-modal([focusTrap]="focusTrap")
|
||||
button.btn.btn-default((click)="focusTrap = !focusTrap" style="position: fixed; top: 10px; right: 10px;")
|
||||
| Toggle focus trap
|
||||
|
||||
tab(title="Invites")
|
||||
ng-template(tabContent)
|
||||
.modal.show.d-block(style="margin-top: 55px")
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
invites-modal([focusTrap]="true")
|
||||
|
||||
tab(title="Actions")
|
||||
ng-template(tabContent)
|
||||
.modal.show.d-block(style="margin-top: 55px; height: calc(100% - 140px)")
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
actions-modal((close)="saveActions()")
|
||||
action-bar([editable]="actionBarEditable" style="position: fixed; left: 50px; bottom: 5px;")
|
||||
custom-checkbox([(checked)]="actionBarEditable" style="position: fixed; left: 10px; bottom: 5px;" title="editable")
|
||||
color-picker([(color)]="expressionActionsColor" style="position: fixed; top: 60px; left: 10px; width: 250px; z-index: 10000;")
|
||||
|
||||
tab(title="Camera angle")
|
||||
ng-template(tabContent)
|
||||
.mt-3.d-flex
|
||||
div(style="width: 400px")
|
||||
h3 Camera angle
|
||||
.mb-3
|
||||
.form-group.d-flex.align-items-center
|
||||
label.mb-0 angle:
|
||||
slider-bar([(value)]="angle" [max]="90")
|
||||
.text-muted(style="width: 100px") {{angle | number:'0.0-0'}} deg
|
||||
.form-group.d-flex.align-items-center
|
||||
label.mb-0 horizontal tile height:
|
||||
.text-muted.ml-2 {{horizontalTileHeight}} px
|
||||
.form-group.d-flex.align-items-center
|
||||
label.mb-0 vertical tile height:
|
||||
.text-muted.ml-2 {{verticalTileHeight}} px
|
||||
|
||||
tab(title="Virtual list")
|
||||
ng-template(tabContent)
|
||||
style.
|
||||
.list {
|
||||
background: #eee; color: #222; width: 250px; height: 500px;
|
||||
}
|
||||
.list2 {
|
||||
background: #eee; color: #222; width: 250px; max-height: 500px;
|
||||
}
|
||||
.item {
|
||||
height: 42px; padding: 10px; border-bottom: solid 1px #ddd;
|
||||
}
|
||||
.d-flex
|
||||
div(style="margin: 50px;")
|
||||
virtual-list.list([itemSize]="42")
|
||||
.item(*virtualFor="let item of virtualItems; index as i; count as c")
|
||||
div {{item.name}} [{{i}}/{{c}}]
|
||||
|
||||
div(style="margin: 50px;")
|
||||
virtual-list.list2([itemSize]="42")
|
||||
.item(*virtualFor="let item of virtualItems2; index as i; count as c")
|
||||
div {{item.name}} [{{i}}/{{c}}]
|
||||
|
||||
div(style="margin: 50px;")
|
||||
button.btn.btn-default((click)="virtualItems2.push({ name: 'An item ' + virtualItems2.length })") Add
|
||||
button.btn.btn-default.ml-1((click)="virtualItems2.pop()") Remove
|
||||
button.btn.btn-default.ml-1((click)="virtualItems2.push(virtualItems2.shift())") Move
|
||||
|
||||
#range-indicator
|
||||
@@ -0,0 +1,243 @@
|
||||
import { Component, OnInit, NgZone, TemplateRef, OnDestroy } from '@angular/core';
|
||||
import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
|
||||
import { random } from 'lodash';
|
||||
import { OFFLINE_PONY, CM_SIZE, SUPPORTER_PONY, DEFAULT_CHATLOG_OPACITY } from '../../../common/constants';
|
||||
import { toPalette, mockPaletteManager, getBaseFill, syncLockedPonyInfo } from '../../../common/ponyInfo';
|
||||
import * as sprites from '../../../generated/sprites';
|
||||
import { PonyTownGame, redrawActionButtons } from '../../../client/game';
|
||||
import { fromNow, setFlag, times } from '../../../common/utils';
|
||||
import { ChatLog } from '../../shared/chat-log/chat-log';
|
||||
import { randomString } from '../../../common/stringUtils';
|
||||
import { MessageType, Entity, EntityPlayerState } from '../../../common/interfaces';
|
||||
import { loadAndInitSpriteSheets } from '../../../client/spriteUtils';
|
||||
import { SettingsService } from '../../services/settingsService';
|
||||
import { faHome, faStar, faLock, faHeart } from '../../../client/icons';
|
||||
import { decompressPonyString } from '../../../common/compressPony';
|
||||
import { getAllTags } from '../../../common/tags';
|
||||
import { Model } from '../../services/model';
|
||||
import { isPartyLeader } from '../../../client/partyUtils';
|
||||
import { createPony } from '../../../common/pony';
|
||||
import { serializeActions, deserializeActions } from '../../../client/buttonActions';
|
||||
import { initializeToys } from '../../../client/ponyDraw';
|
||||
import { ACTION_EXPRESSION_BG, updateActionColor } from '../../../common/colors';
|
||||
import { parseColor, colorToCSS, colorNames } from '../../../common/color';
|
||||
import { isHidden, isIgnored, isFriend } from '../../../common/entityUtils';
|
||||
import { initFeatureFlags } from '../../../client/clientUtils';
|
||||
|
||||
const offlinePonyInfo = decompressPonyString(OFFLINE_PONY, true);
|
||||
const offlinePonyPal = toPalette(offlinePonyInfo);
|
||||
const defaultPalette = mockPaletteManager.addArray(sprites.defaultPalette);
|
||||
|
||||
const offlinePony = createPony(1, 0, OFFLINE_PONY, defaultPalette, mockPaletteManager);
|
||||
offlinePony.name = 'Offline pony';
|
||||
|
||||
const supporterPony = createPony(2, 0, SUPPORTER_PONY, defaultPalette, mockPaletteManager);
|
||||
supporterPony.name = 'Supporter pony';
|
||||
|
||||
const pendingPony = createPony(3, 0, SUPPORTER_PONY, defaultPalette, mockPaletteManager);
|
||||
pendingPony.name = 'Pending pony';
|
||||
|
||||
const tails = sprites.tails[0]!.slice();
|
||||
const labels = ['none', 'Long tail', 'Short tail', 'Short smooth tail', 'Long puffy tail', 'Long wavy tail'];
|
||||
tails.forEach((t, i) => t ? t[0].label = labels[i] : undefined);
|
||||
const colors = Object.values(colorNames);
|
||||
|
||||
@Component({
|
||||
selector: 'tools-ui',
|
||||
templateUrl: 'tools-ui.pug',
|
||||
})
|
||||
export class ToolsUI implements OnInit, OnDestroy {
|
||||
readonly homeIcon = faHome;
|
||||
readonly starIcon = faStar;
|
||||
readonly heartIcon = faHeart;
|
||||
readonly lockIcon = faLock;
|
||||
isHidden = isHidden;
|
||||
isIgnored = isIgnored;
|
||||
focusTrap = true;
|
||||
tails = tails;
|
||||
cmSize = CM_SIZE;
|
||||
pony = offlinePonyInfo;
|
||||
customOutlines = false;
|
||||
pal = offlinePonyPal;
|
||||
color = 'cornflowerblue';
|
||||
checked = true;
|
||||
radio = 'a';
|
||||
slider = 50;
|
||||
sprite = sprites.tails[0]![1]![0];
|
||||
fills = ['ff0000', '00ff00'];
|
||||
outlines = ['990000', '009900'];
|
||||
spriteActive = false;
|
||||
selected = offlinePony;
|
||||
timeout = fromNow(1000 * 3600 * 10).toISOString();
|
||||
autoCloseDropdown: any = true;
|
||||
spamChatInterval: any;
|
||||
initialized = false;
|
||||
customChecked = false;
|
||||
actionBarEditable = true;
|
||||
tags = ['', ...getAllTags().map(t => t.id)];
|
||||
animationFrame: any;
|
||||
virtualItems = times(1000, i => ({ value: i, name: `This is item ${i}`, color: colors[i % colors.length] }));
|
||||
virtualItems2 = [{ name: 'An item 0' }];
|
||||
constructor(
|
||||
private game: PonyTownGame,
|
||||
private zone: NgZone,
|
||||
public settings: SettingsService,
|
||||
private modalService: BsModalService,
|
||||
private model: Model,
|
||||
) {
|
||||
this.selected.name = 'Offline Pony';
|
||||
this.selected.site = {
|
||||
id: '',
|
||||
name: 'Offline Pony (official)',
|
||||
provider: 'twitter',
|
||||
url: 'https://twitter.com/offlinepony',
|
||||
};
|
||||
this.selected.tag = 'dev';
|
||||
this.selected.modInfo = {
|
||||
account: 'offline-pony [abc]',
|
||||
country: 'PL',
|
||||
counters: { swears: 5 },
|
||||
age: 12,
|
||||
};
|
||||
|
||||
game.player = {
|
||||
id: 123,
|
||||
name: 'Player pony',
|
||||
} as any;
|
||||
game.party = {
|
||||
leaderId: 0,
|
||||
members: [
|
||||
{ id: 1, leader: true, offline: false, pending: false, pony: offlinePony, self: false },
|
||||
{ id: 2, leader: false, offline: true, pending: false, pony: supporterPony, self: false },
|
||||
{ id: 3, leader: false, offline: false, pending: true, pony: pendingPony, self: false },
|
||||
],
|
||||
};
|
||||
game.onClock.next('00:00');
|
||||
game.failedFBO = true;
|
||||
game.send = <T>(action: (server: any) => T) => action({
|
||||
action() { },
|
||||
select() { },
|
||||
say() { },
|
||||
expression() { },
|
||||
getInvites: () => Promise.resolve([
|
||||
{ id: 'a', info: OFFLINE_PONY, name: 'Offline Pony', active: true },
|
||||
{ id: 'b', info: OFFLINE_PONY, name: 'Fuzzy', active: true },
|
||||
{ id: 'c', info: OFFLINE_PONY, name: 'Meno', active: true },
|
||||
{ id: 'd', info: OFFLINE_PONY, name: 'Offline Pony', active: true },
|
||||
{ id: 'e', info: OFFLINE_PONY, name: 'Fuzzy', active: true },
|
||||
{ id: 'f', info: OFFLINE_PONY, name: 'Meno', active: false },
|
||||
{ id: 'g', info: OFFLINE_PONY, name: 'Offline Pony', active: false },
|
||||
{ id: 'h', info: OFFLINE_PONY, name: 'Fuzzy', active: false },
|
||||
{ id: 'i', info: OFFLINE_PONY, name: 'Meno', active: false },
|
||||
{ id: 'j', info: OFFLINE_PONY, name: 'Meno', active: false },
|
||||
]),
|
||||
} as any);
|
||||
}
|
||||
ngOnInit() {
|
||||
initFeatureFlags({});
|
||||
|
||||
return loadAndInitSpriteSheets()
|
||||
.then(() => {
|
||||
initializeToys(mockPaletteManager);
|
||||
this.initialized = true;
|
||||
this.model.loading = true;
|
||||
this.zone.runOutsideAngular(() => this.update());
|
||||
});
|
||||
}
|
||||
ngOnDestroy() {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
}
|
||||
get baseHairColor() {
|
||||
return getBaseFill(this.pony.mane);
|
||||
}
|
||||
get isFriend() {
|
||||
return isFriend(this.selected);
|
||||
}
|
||||
set isFriend(value) {
|
||||
this.selected.playerState = setFlag(this.selected.playerState, EntityPlayerState.Friend, value);
|
||||
}
|
||||
update() {
|
||||
this.animationFrame = requestAnimationFrame(() => this.update());
|
||||
redrawActionButtons(this.game.actionsChanged);
|
||||
this.game.actionsChanged = false;
|
||||
this.game.onFrame.next();
|
||||
}
|
||||
changed() {
|
||||
syncLockedPonyInfo(this.pony);
|
||||
}
|
||||
toggleIgnored(entity: Entity) {
|
||||
entity.playerState = setFlag(entity.playerState, EntityPlayerState.Ignored, !isIgnored(entity));
|
||||
}
|
||||
toggleHidden(entity: Entity) {
|
||||
entity.playerState = setFlag(entity.playerState, EntityPlayerState.Hidden, !isHidden(entity));
|
||||
}
|
||||
spamChat(chatlog: ChatLog) {
|
||||
if (this.spamChatInterval) {
|
||||
clearInterval(this.spamChatInterval);
|
||||
this.spamChatInterval = 0;
|
||||
} else {
|
||||
this.spamChatInterval = 1;
|
||||
this.zone.runOutsideAngular(() => this.spamChatInterval = setInterval(() => {
|
||||
chatlog.addMessage({
|
||||
id: 0,
|
||||
crc: undefined,
|
||||
name: randomString(random(1, 20)),
|
||||
message: randomString(random(1, 40)),
|
||||
type: MessageType.Chat
|
||||
});
|
||||
}, 50));
|
||||
}
|
||||
}
|
||||
get isPartyLeader() {
|
||||
return isPartyLeader(this.game);
|
||||
}
|
||||
set isPartyLeader(value: boolean) {
|
||||
if (value) {
|
||||
this.game.party!.leaderId = this.game.player!.id;
|
||||
} else {
|
||||
this.game.party!.leaderId = 1;
|
||||
}
|
||||
}
|
||||
get chatlogOpacity() {
|
||||
return this.settings.account.chatlogOpacity || DEFAULT_CHATLOG_OPACITY;
|
||||
}
|
||||
set chatlogOpacity(value: number) {
|
||||
this.settings.account.chatlogOpacity = value;
|
||||
}
|
||||
addMessage(chatlog: ChatLog, message: string) {
|
||||
chatlog.addMessage({ name: 'test name', id: 123, crc: undefined, message, type: MessageType.Chat });
|
||||
}
|
||||
addWhisper(chatlog: ChatLog, message: string) {
|
||||
chatlog.addMessage({ name: 'test name', id: 123, crc: undefined, message, type: MessageType.Whisper });
|
||||
}
|
||||
angle = 45;
|
||||
get angleInRad() {
|
||||
return (this.angle / 180) * Math.PI;
|
||||
}
|
||||
get horizontalTileHeight() {
|
||||
return 32 * Math.sin(this.angleInRad);
|
||||
}
|
||||
get verticalTileHeight() {
|
||||
return 32 * Math.cos(this.angleInRad);
|
||||
}
|
||||
// angle = Math.asin(expectedHorizontalTileHeight / 32) // 0.848062078981481
|
||||
modalRef?: BsModalRef;
|
||||
showModal(template: TemplateRef<any>) {
|
||||
this.modalRef = this.modalService.show(template, {});
|
||||
}
|
||||
saveActions() {
|
||||
if (DEVELOPMENT) {
|
||||
const serialized = serializeActions(this.game.actions);
|
||||
this.game.actions = deserializeActions(serialized);
|
||||
console.log(serialized);
|
||||
}
|
||||
}
|
||||
// actions
|
||||
get expressionActionsColor() {
|
||||
return ACTION_EXPRESSION_BG;
|
||||
}
|
||||
set expressionActionsColor(value) {
|
||||
updateActionColor(colorToCSS(parseColor(value)));
|
||||
this.game.actionsChanged = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
.form-inline.p-1
|
||||
.form-group
|
||||
a.btn.btn-default(routerLink="/")
|
||||
fa-icon([icon]="homeIcon")
|
||||
.form-group
|
||||
label.control-label.ml-2 horizontal:
|
||||
.btn-group.dropdown.ml-1(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle style="width: 140px;")
|
||||
| {{horizontal}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item(*ngFor="let f of fields" (click)="horizontal = f; redraw();") {{f}}
|
||||
|
||||
.form-group
|
||||
label.control-label.ml-2 vertical:
|
||||
.btn-group.dropdown.ml-1(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle style="width: 140px;")
|
||||
| {{vertical}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item(*ngFor="let f of fields" (click)="vertical = f; redraw();") {{f}}
|
||||
|
||||
.form-group
|
||||
label.control-label.ml-2 coat:
|
||||
color-picker.color-picker-inline.ml-1([(color)]="coat" (colorChange)="redraw()")
|
||||
|
||||
.form-group
|
||||
label.control-label.ml-2 hair:
|
||||
color-picker.color-picker-inline.ml-1([(color)]="hair" (colorChange)="redraw()")
|
||||
|
||||
.form-group
|
||||
label.control-label.ml-2 just head
|
||||
check-box.check-box-inline.ml-1([(checked)]="justHead" (checkedChange)="redraw()")
|
||||
|
||||
.form-group
|
||||
label.control-label.ml-2
|
||||
.btn-group.dropdown.ml-1(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(dropdownToggle)
|
||||
| ×{{scale}}
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item((click)="scale = 1; redraw()") ×1
|
||||
a.dropdown-item((click)="scale = 2; redraw()") ×2
|
||||
a.dropdown-item((click)="scale = 3; redraw()") ×3
|
||||
a.dropdown-item((click)="scale = 4; redraw()") ×4
|
||||
|
||||
canvas(#canvas)
|
||||
@@ -0,0 +1,10 @@
|
||||
.color-picker-inline {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.check-box-inline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-top: 2px;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||
import { PonyInfo, PonyState } from '../../../common/interfaces';
|
||||
import { createCanvas, disableImageSmoothing } from '../../../client/canvasUtils';
|
||||
import { toPalette, createDefaultPony, syncLockedPonyInfo } from '../../../common/ponyInfo';
|
||||
import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers';
|
||||
import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch';
|
||||
import { loadAndInitSpriteSheets } from '../../../client/spriteUtils';
|
||||
import { compressPonyString, decompressPony } from '../../../common/compressPony';
|
||||
import { drawPony } from '../../../client/ponyDraw';
|
||||
import { faHome } from '../../../client/icons';
|
||||
import { paletteSpriteSheet } from '../../../generated/sprites';
|
||||
|
||||
@Component({
|
||||
selector: 'tools-variants',
|
||||
templateUrl: 'tools-variants.pug',
|
||||
styleUrls: ['tools-variants.scss'],
|
||||
})
|
||||
export class ToolsVariants implements OnInit {
|
||||
readonly homeIcon = faHome;
|
||||
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
||||
fields: string[];
|
||||
vertical: keyof PonyInfo = 'backMane';
|
||||
horizontal: keyof PonyInfo = 'mane';
|
||||
coat = 'red';
|
||||
hair = 'gold';
|
||||
justHead = false;
|
||||
scale = 2;
|
||||
private pony: PonyInfo = createDefaultPony();
|
||||
private state: PonyState = defaultPonyState();
|
||||
constructor() {
|
||||
this.fields = Object.keys(this.pony)
|
||||
.filter(key => {
|
||||
const value = (this.pony as any)[key];
|
||||
return value && value.type !== undefined;
|
||||
});
|
||||
}
|
||||
ngOnInit() {
|
||||
loadAndInitSpriteSheets()
|
||||
.then(() => this.redraw());
|
||||
}
|
||||
redraw() {
|
||||
this.draw();
|
||||
}
|
||||
private draw() {
|
||||
this.pony.coatFill = this.coat;
|
||||
this.pony.mane!.fills![0] = this.hair;
|
||||
syncLockedPonyInfo(this.pony);
|
||||
|
||||
this.fields.forEach(f => (this.pony as any)[f].type = 0);
|
||||
(this.pony as any)[this.vertical].type = 999;
|
||||
(this.pony as any)[this.horizontal].type = 999;
|
||||
|
||||
const fixed: any = decompressPony(compressPonyString(this.pony));
|
||||
const maxX = fixed[this.horizontal].type;
|
||||
const maxY = fixed[this.vertical].type;
|
||||
|
||||
const scale = this.scale;
|
||||
const info = toPalette(this.pony);
|
||||
const buffer = createCanvas(80, 80);
|
||||
const batch = new ContextSpriteBatch(buffer);
|
||||
const options = defaultDrawPonyOptions();
|
||||
|
||||
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
|
||||
canvas.width = ((maxX + 1) * (this.justHead ? 45 : 60) + 10) * scale;
|
||||
canvas.height = ((maxY + 1) * (this.justHead ? 45 : 60) + 10) * scale;
|
||||
|
||||
const viewContext = canvas.getContext('2d')!;
|
||||
viewContext.save();
|
||||
disableImageSmoothing(viewContext);
|
||||
viewContext.scale(scale, scale);
|
||||
|
||||
viewContext.fillStyle = 'LightGreen';
|
||||
viewContext.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let y = 0; y <= maxY; y++) {
|
||||
(info as any)[this.vertical].type = y;
|
||||
|
||||
for (let x = 0; x <= maxX; x++) {
|
||||
batch.start(paletteSpriteSheet, 0);
|
||||
|
||||
(info as any)[this.horizontal].type = x;
|
||||
|
||||
drawPony(batch, info, this.state, 40, 60, options);
|
||||
|
||||
batch.end();
|
||||
|
||||
if (this.justHead) {
|
||||
viewContext.drawImage(buffer, 0, 0, 55, 45, x * 45 - 10, y * 45, 55, 45);
|
||||
} else {
|
||||
viewContext.drawImage(buffer, x * 60 - 10, y * 60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewContext.restore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
style.
|
||||
.canvas-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#ref {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
box-shadow: 0 0 5px #000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#fps {
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
font-family: monospace;
|
||||
padding: 2px 7px;
|
||||
background: black;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.canvas-container
|
||||
canvas#canvas.pixelart(#canvas width="800" height="600")
|
||||
canvas#ref(#canvas2)
|
||||
#fps 0
|
||||
@@ -0,0 +1,928 @@
|
||||
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||
// import { range, sample } from 'lodash';
|
||||
// import { PonyState, Palette, Sprite } from '../../../common/interfaces';
|
||||
// import { startGameLoop } from '../../../client/gameLoop';
|
||||
// import { defaultPonyState } from '../../../client/ponyHelpers';
|
||||
// import { SpriteBatch } from '../../../graphics/spriteBatch';
|
||||
// import { loadSpriteSheets, loadAndInitSpriteSheets } from '../../../client/spriteUtils';
|
||||
// import { Key } from '../../../client/input/input';
|
||||
// import { trot } from '../../../client/ponyAnimations';
|
||||
// import { createTexturesForSpriteSheets } from '../../../graphics/spriteSheetUtils';
|
||||
// import { PaletteManager, releasePalette } from '../../../graphics/paletteManager';
|
||||
// import { getWebGLContext, createViewMatrix2 } from '../../../graphics/webgl/webglUtils';
|
||||
// import * as sprites from '../../../generated/sprites';
|
||||
// import { SpriteBatch2 } from '../../../graphics/spriteBatch2';
|
||||
// import { loadImage, createCanvas } from '../../../client/canvasUtils';
|
||||
// import { WHITE, ORANGE, RED, BLUE } from '../../../common/colors';
|
||||
// import { PaletteSpriteBatch } from '../../../graphics/paletteSpriteBatch';
|
||||
// import { PaletteSpriteBatch2 } from '../../../graphics/paletteSpriteBatch2';
|
||||
// import { PaletteSpriteBatch3 } from '../../../graphics/paletteSpriteBatch3';
|
||||
// import { createFrameBuffer, bindFrameBuffer } from '../../../graphics/webgl/glFbo';
|
||||
// import { createTexture, Texture2D } from '../../../graphics/webgl/texture2d';
|
||||
// import { times } from '../../../common/utils';
|
||||
// import { createShader } from '../../../graphics/webgl/shader-new';
|
||||
// import {
|
||||
// lightShader, spriteShader, sprite2Shader, paletteDepthShader, paletteLayersShader, paletteLayersInstancedShader
|
||||
// } from '../../../generated/shaders';
|
||||
|
||||
@Component({
|
||||
selector: 'tools-webgl',
|
||||
templateUrl: 'tools-webgl.pug',
|
||||
})
|
||||
export class ToolsWebgl implements OnInit {
|
||||
@ViewChild('canvas', { static: true }) canvasElement!: ElementRef;
|
||||
@ViewChild('canvas2', { static: true }) canvasElement2!: ElementRef;
|
||||
ngOnInit() {
|
||||
// testSpriteBatch(this.canvasElement.nativeElement);
|
||||
}
|
||||
}
|
||||
|
||||
// export function testLightsShader(canvas: HTMLCanvasElement) {
|
||||
// const gl = getWebGLContext(canvas);
|
||||
// const shader = createShader(gl, lightShader);
|
||||
// const batch = new SpriteBatch(gl);
|
||||
// const fps = document.getElementById('fps')!;
|
||||
|
||||
// const handle = startGameLoop({
|
||||
// fps: 0,
|
||||
// load() {
|
||||
// return loadAndInitSpriteSheets();
|
||||
// },
|
||||
// init() {
|
||||
// createTexturesForSpriteSheets(gl, sprites.spriteSheets);
|
||||
// },
|
||||
// update(_delta: number) {
|
||||
// },
|
||||
// draw() {
|
||||
// const scale = 2;
|
||||
// const width = Math.ceil(window.innerWidth / scale);
|
||||
// const height = Math.ceil(window.innerHeight / scale);
|
||||
|
||||
// gl.canvas.width = width;
|
||||
// gl.canvas.height = height;
|
||||
// gl.canvas.style.width = (width * scale) + 'px';
|
||||
// gl.canvas.style.height = (height * scale) + 'px';
|
||||
|
||||
// gl.clearColor(0.13, 0.5, 0.5, 1.0);
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
// gl.disable(gl.DEPTH_TEST);
|
||||
// gl.depthFunc(gl.LEQUAL);
|
||||
// gl.enable(gl.BLEND);
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// const transformMatrix = mat4.ortho(mat4.create(), 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 0, 1000);
|
||||
|
||||
// batch.begin(shader, {} as any);
|
||||
|
||||
// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix);
|
||||
// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1);
|
||||
|
||||
// batch.drawImage(ORANGE, -1, -1, 2, 2, 100, 100, 300, 200);
|
||||
|
||||
// batch.end();
|
||||
// },
|
||||
// });
|
||||
|
||||
// window.addEventListener('keyup', e => {
|
||||
// if (e.keyCode === Key.KEY_Q) {
|
||||
// handle.cancel();
|
||||
// fps.textContent = `off`;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function testSpriteBatch(canvas: HTMLCanvasElement) {
|
||||
// const gl = getWebGLContext(canvas);
|
||||
// const shader = createShader(gl, paletteLayersShader);
|
||||
// const batch = new PaletteSpriteBatch3(gl);
|
||||
// const paletteManager = new PaletteManager();
|
||||
// const defaultPalette = paletteManager.addArray(sprites.defaultPalette);
|
||||
// // const applePalette = paletteManager.addArray(sprites.apple_1.palettes![0]);
|
||||
// const fps = document.getElementById('fps')!;
|
||||
|
||||
// const handle = startGameLoop({
|
||||
// fps: 0,
|
||||
// load() {
|
||||
// return loadAndInitSpriteSheets();
|
||||
// },
|
||||
// init() {
|
||||
// createTexturesForSpriteSheets(gl, sprites.spriteSheets);
|
||||
// },
|
||||
// update(_delta: number) {
|
||||
// },
|
||||
// draw() {
|
||||
// const scale = 2;
|
||||
// const width = Math.ceil(window.innerWidth / scale);
|
||||
// const height = Math.ceil(window.innerHeight / scale);
|
||||
|
||||
// gl.canvas.width = width;
|
||||
// gl.canvas.height = height;
|
||||
// gl.canvas.style.width = (width * scale) + 'px';
|
||||
// gl.canvas.style.height = (height * scale) + 'px';
|
||||
|
||||
// paletteManager.commit(gl);
|
||||
|
||||
// batch.palette = paletteManager.texture;
|
||||
// batch.rectSprite = sprites.pixel2;
|
||||
// batch.defaultPalette = defaultPalette;
|
||||
|
||||
// gl.clearColor(0.13, 0.5, 0.5, 1.0);
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
// gl.disable(gl.DEPTH_TEST);
|
||||
// gl.depthFunc(gl.LEQUAL);
|
||||
// gl.enable(gl.BLEND);
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// const transformMatrix = mat4.ortho(mat4.create(), 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 0, 1000);
|
||||
|
||||
// batch.begin(shader, {} as any);
|
||||
|
||||
// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix);
|
||||
// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1);
|
||||
// gl.uniform1f(shader.uniforms.pixelSize, paletteManager.pixelSize);
|
||||
|
||||
// // batch.drawSprite(sprites.apple_1.color, WHITE, applePalette, 100, 100);
|
||||
// batch.drawRect(ORANGE, 200, 200, 300, 150);
|
||||
|
||||
// batch.end();
|
||||
// },
|
||||
// });
|
||||
|
||||
// window.addEventListener('keyup', e => {
|
||||
// if (e.keyCode === Key.KEY_Q) {
|
||||
// handle.cancel();
|
||||
// fps.textContent = `off`;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function testInstancedSprites(canvas: HTMLCanvasElement) {
|
||||
// const gl = getWebGLContext(canvas);
|
||||
// const shader = createShader(gl, paletteLayersShader);
|
||||
// const shader2 = createShader(gl, paletteLayersInstancedShader);
|
||||
// const batch = new PaletteSpriteBatch3(gl);
|
||||
// const batch2 = new InstancedPaletteSpriteBatch(gl);
|
||||
// const paletteManager = new PaletteManager();
|
||||
// const defaultPalette = paletteManager.addArray(sprites.defaultPalette);
|
||||
// const fps = document.getElementById('fps')!;
|
||||
// const randomSprites = [
|
||||
// sprites.pumpkin_default,
|
||||
// sprites.box_lanterns,
|
||||
// sprites.flower_patch5,
|
||||
// sprites.leafpile_medium,
|
||||
// ];
|
||||
|
||||
// const width = 600;
|
||||
// const height = 500;
|
||||
// const itemCount = 100000;
|
||||
// const items = times(itemCount, () => {
|
||||
// const sprite = sample(randomSprites)!;
|
||||
|
||||
// return {
|
||||
// sprite: sprite.color,
|
||||
// palette: paletteManager.addArray(sprite.palettes![0]),
|
||||
// x: width * Math.random(),
|
||||
// y: height * Math.random(),
|
||||
// };
|
||||
// });
|
||||
|
||||
// let mode = 'instanced';
|
||||
|
||||
// const handle = startGameLoop({
|
||||
// fps: 0,
|
||||
// load() {
|
||||
// return loadAndInitSpriteSheets();
|
||||
// },
|
||||
// init() {
|
||||
// createTexturesForSpriteSheets(gl, sprites.spriteSheets);
|
||||
// },
|
||||
// update(_delta: number) {
|
||||
// },
|
||||
// draw() {
|
||||
// const scale = 2;
|
||||
// const width = Math.ceil(window.innerWidth / scale);
|
||||
// const height = Math.ceil(window.innerHeight / scale);
|
||||
|
||||
// gl.canvas.width = width;
|
||||
// gl.canvas.height = height;
|
||||
// gl.canvas.style.width = (width * scale) + 'px';
|
||||
// gl.canvas.style.height = (height * scale) + 'px';
|
||||
|
||||
// paletteManager.commit(gl);
|
||||
|
||||
// batch.palette = paletteManager.texture;
|
||||
// batch.rectSprite = sprites.pixelRect2;
|
||||
// batch.defaultPalette = defaultPalette;
|
||||
|
||||
// batch2.palette = paletteManager.texture;
|
||||
// batch2.rectSprite = sprites.pixelRect2;
|
||||
// batch2.defaultPalette = defaultPalette;
|
||||
|
||||
// gl.clearColor(0.13, 0.5, 0.5, 1.0);
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
// gl.disable(gl.DEPTH_TEST);
|
||||
// gl.depthFunc(gl.LEQUAL);
|
||||
// gl.enable(gl.BLEND);
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// const transformMatrix = mat4.ortho(mat4.create(), 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 0, 1000);
|
||||
|
||||
// if (mode === 'regular') {
|
||||
// batch.begin(shader, {} as any);
|
||||
// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix);
|
||||
// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1);
|
||||
// gl.uniform1f(shader.uniforms.pixelSize, paletteManager.pixelSize);
|
||||
|
||||
// for (const item of items) {
|
||||
// batch.drawSprite(item.sprite, WHITE, item.palette, item.x, item.y);
|
||||
// }
|
||||
|
||||
// batch.end();
|
||||
// } else if (mode === 'instanced') {
|
||||
// batch2.begin(shader2);
|
||||
// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix);
|
||||
// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1);
|
||||
// gl.uniform1f(shader.uniforms.pixelSize, paletteManager.pixelSize);
|
||||
|
||||
// for (const item of items) {
|
||||
// batch2.drawSprite(item.sprite, WHITE, item.palette, item.x, item.y);
|
||||
// }
|
||||
|
||||
// batch2.end();
|
||||
// }
|
||||
|
||||
// fps.textContent = `${this.fps.toFixed(0)} fps (${mode})`;
|
||||
// },
|
||||
// });
|
||||
|
||||
// window.addEventListener('keyup', e => {
|
||||
// if (e.keyCode === Key.KEY_Q) {
|
||||
// handle.cancel();
|
||||
// fps.textContent = `off`;
|
||||
// }
|
||||
|
||||
// if (e.keyCode === Key.KEY_M) {
|
||||
// mode = mode === 'regular' ? 'instanced' : 'regular';
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function testLayeredSprites(canvas: HTMLCanvasElement, canvas2: HTMLCanvasElement) {
|
||||
// function transferPixels(src: HTMLCanvasElement, dst: HTMLCanvasElement, srcChannel: number, dstChannel: number) {
|
||||
// const srcContext = src.getContext('2d')!;
|
||||
// const dstContext = dst.getContext('2d')!;
|
||||
// const srcData = srcContext.getImageData(0, 0, src.width, src.height);
|
||||
// const dstData = dstContext.getImageData(0, 0, dst.width, dst.height);
|
||||
|
||||
// for (let y = 0; y < dstData.height; y++) {
|
||||
// for (let x = 0; x < dstData.width; x++) {
|
||||
// const offset = (x + y * dstData.width) * 4;
|
||||
// dstData.data[offset + dstChannel] = srcData.data[offset + srcChannel];
|
||||
// }
|
||||
// }
|
||||
|
||||
// dstContext.putImageData(dstData, 0, 0);
|
||||
// }
|
||||
|
||||
// function transferPixels2(src: HTMLCanvasElement, dstData: ImageData, srcChannel: number, dstChannel: number) {
|
||||
// const srcContext = src.getContext('2d')!;
|
||||
// const srcData = srcContext.getImageData(0, 0, src.width, src.height);
|
||||
|
||||
// for (let y = 0; y < srcData.height; y++) {
|
||||
// for (let x = 0; x < srcData.width; x++) {
|
||||
// const offset = (x + y * srcData.width) * 4;
|
||||
// dstData.data[offset + dstChannel] = srcData.data[offset + srcChannel];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const gl = getWebGLContext(canvas);
|
||||
// const shader = createShader(gl, paletteLayersShader);
|
||||
// const batch = new PaletteSpriteBatch3(gl);
|
||||
// const paletteManager = new PaletteManager();
|
||||
// const defaultPalette = paletteManager.addArray(sprites.defaultPalette);
|
||||
// const palettes = [
|
||||
// paletteManager.addArray(sprites.pumpkin_default.palettes![0]),
|
||||
// paletteManager.addArray(sprites.rock.palettes![0]),
|
||||
// paletteManager.addArray(sprites.sign_1.palettes![0]),
|
||||
// paletteManager.addArray(sprites.box_lanterns.palettes![0]),
|
||||
// ];
|
||||
|
||||
// const theSprites: Sprite[] = [];
|
||||
|
||||
// const handle = startGameLoop({
|
||||
// fps: 0,
|
||||
// load() {
|
||||
// return loadAndInitSpriteSheets();
|
||||
// },
|
||||
// init() {
|
||||
// const canvas = canvas2;
|
||||
// canvas2.style.display = 'block';
|
||||
// canvas.width = 256;
|
||||
// canvas.height = 256;
|
||||
// const context = canvas.getContext('2d')!;
|
||||
// context.fillStyle = 'white';
|
||||
// context.globalAlpha = 200 / 0xff;
|
||||
// context.fillRect(0, 0, 256, 256);
|
||||
// context.globalAlpha = 1;
|
||||
// const sheetData = (sprites.spriteSheets[1] as any).data;
|
||||
// const sheetImage = createCanvas(sheetData.width, sheetData.height);
|
||||
// sheetImage.getContext('2d')!.putImageData(sheetData, 0, 0);
|
||||
// const sheetCanvas = createCanvas(256, 256);
|
||||
// const sheetContext = sheetCanvas.getContext('2d')!;
|
||||
|
||||
// const pixels = sheetContext.createImageData(256, 256); // new Uint8Array(256 * 256 * 4);
|
||||
|
||||
// for (let i = 0; i < pixels.width * pixels.height * 4; i++) {
|
||||
// pixels.data[i] = 255;
|
||||
// }
|
||||
|
||||
// const pumpkin = sprites.pumpkin_default.color;
|
||||
// sheetContext.clearRect(0, 0, 256, 256);
|
||||
// sheetContext.drawImage(sheetImage, pumpkin.x, pumpkin.y, pumpkin.w, pumpkin.h, 0, 0, pumpkin.w, pumpkin.h);
|
||||
// transferPixels(sheetCanvas, canvas, 0, 0);
|
||||
// transferPixels2(sheetCanvas, pixels, 0, 0);
|
||||
|
||||
// const rock = sprites.rock.color;
|
||||
// sheetContext.clearRect(0, 0, 256, 256);
|
||||
// sheetContext.drawImage(sheetImage, rock.x, rock.y, rock.w, rock.h, 0, 0, rock.w, rock.h);
|
||||
// transferPixels(sheetCanvas, canvas, 0, 1);
|
||||
// transferPixels2(sheetCanvas, pixels, 0, 1);
|
||||
|
||||
// const sign = sprites.sign_1.color;
|
||||
// sheetContext.clearRect(0, 0, 256, 256);
|
||||
// sheetContext.drawImage(sheetImage, sign.x, sign.y, sign.w, sign.h, 0, 0, sign.w, sign.h);
|
||||
// transferPixels(sheetCanvas, canvas, 0, 2);
|
||||
// transferPixels2(sheetCanvas, pixels, 0, 2);
|
||||
|
||||
// const box = sprites.box_lanterns.color;
|
||||
// sheetContext.clearRect(0, 0, 256, 256);
|
||||
// sheetContext.drawImage(sheetImage, box.x, box.y, box.w, box.h, 0, 0, box.w, box.h);
|
||||
// // transferPixels(sheetCanvas, canvas, 0, 3);
|
||||
// transferPixels2(sheetCanvas, pixels, 0, 3);
|
||||
|
||||
// const texture = createTexture(gl, pixels);
|
||||
// // texture.bind(0);
|
||||
// // gl.texImage2D(gl.TEXTURE_2D, 0, texture.format, 256, 256, 0, texture.format, texture.type, pixels);
|
||||
|
||||
// const spr = {
|
||||
// x: 0,
|
||||
// y: 0,
|
||||
// w: 50,
|
||||
// h: 50,
|
||||
// ox: 0,
|
||||
// oy: 0,
|
||||
// tex: texture,
|
||||
// };
|
||||
|
||||
// theSprites.push({ ...spr, type: 3 });
|
||||
// theSprites.push({ ...spr, type: 4 });
|
||||
// theSprites.push({ ...spr, type: 5 });
|
||||
// theSprites.push({ ...spr, type: 6 });
|
||||
|
||||
// createTexturesForSpriteSheets(gl, sprites.spriteSheets);
|
||||
// },
|
||||
// update(_delta: number) {
|
||||
// },
|
||||
// draw() {
|
||||
// const scale = 4;
|
||||
// const width = Math.ceil(window.innerWidth / scale);
|
||||
// const height = Math.ceil(window.innerHeight / scale);
|
||||
|
||||
// gl.canvas.width = width;
|
||||
// gl.canvas.height = height;
|
||||
// gl.canvas.style.width = (width * scale) + 'px';
|
||||
// gl.canvas.style.height = (height * scale) + 'px';
|
||||
|
||||
// paletteManager.commit(gl);
|
||||
// batch.palette = paletteManager.texture;
|
||||
// batch.rectSprite = sprites.pixelRect2;
|
||||
// batch.defaultPalette = defaultPalette;
|
||||
|
||||
// gl.clearColor(0.13, 0.5, 0.5, 1.0);
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||
// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
// gl.enable(gl.DEPTH_TEST);
|
||||
// gl.depthFunc(gl.LEQUAL);
|
||||
// gl.enable(gl.BLEND);
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// const transformMatrix = mat4.ortho(mat4.create(), 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 0, 1000);
|
||||
|
||||
// batch.begin(shader, {} as any);
|
||||
// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix);
|
||||
// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1);
|
||||
// gl.uniform1f(shader.uniforms.pixelSize, paletteManager.pixelSize);
|
||||
// batch.drawSprite(sprites.pumpkin_default.color, WHITE, palettes[0], 50, 50);
|
||||
// batch.drawSprite(theSprites[0], WHITE, palettes[0], 50, 120);
|
||||
// batch.drawSprite(theSprites[1], WHITE, palettes[1], 100, 120);
|
||||
// batch.drawSprite(theSprites[2], WHITE, palettes[2], 150, 120);
|
||||
// batch.drawSprite(theSprites[3], WHITE, palettes[3], 200, 120);
|
||||
// batch.end();
|
||||
// },
|
||||
// });
|
||||
|
||||
// window.addEventListener('keyup', e => {
|
||||
// if (e.keyCode === Key.KEY_Q) {
|
||||
// handle.cancel();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function test3DMap(canvas: HTMLCanvasElement) {
|
||||
// const gl = getWebGLContext(canvas);
|
||||
// const paletteShader = createShader(gl, paletteDepthShader);
|
||||
// const paletteSpriteBatch = new PaletteSpriteBatch2(gl);
|
||||
// const paletteManager = new PaletteManager();
|
||||
// // const treePalette = paletteManager.add(sprites.tree_6Crown0_0.palettes![0]);
|
||||
// const grassPalette = paletteManager.addArray(sprites.grass_tile.palettes![0]);
|
||||
// // const wallPalette = paletteManager.add(sprites.stone_wall_6.palettes![0]);
|
||||
// const defaultPalette = paletteManager.addArray(sprites.defaultPalette);
|
||||
|
||||
// const camera = {
|
||||
// x: -2,
|
||||
// y: -2,
|
||||
// w: 100,
|
||||
// h: 100,
|
||||
// };
|
||||
|
||||
// let redZ = 2;
|
||||
// let blueZ = -2;
|
||||
|
||||
// const handle = startGameLoop({
|
||||
// fps: 0,
|
||||
// load() {
|
||||
// return loadAndInitSpriteSheets();
|
||||
// },
|
||||
// init() {
|
||||
// createTexturesForSpriteSheets(gl, sprites.spriteSheets);
|
||||
// },
|
||||
// update(_delta: number) {
|
||||
// },
|
||||
// draw() {
|
||||
// const scale = 4;
|
||||
// const width = Math.ceil(window.innerWidth / scale);
|
||||
// const height = Math.ceil(window.innerHeight / scale);
|
||||
|
||||
// camera.w = width / 32;
|
||||
// camera.h = height / 32;
|
||||
|
||||
// gl.canvas.width = width;
|
||||
// gl.canvas.height = height;
|
||||
// gl.canvas.style.width = (width * scale) + 'px';
|
||||
// gl.canvas.style.height = (height * scale) + 'px';
|
||||
|
||||
// paletteManager.commit(gl);
|
||||
// paletteSpriteBatch.palette = paletteManager.texture;
|
||||
// paletteSpriteBatch.rectSprite = sprites.pixelRect2;
|
||||
// paletteSpriteBatch.defaultPalette = defaultPalette;
|
||||
|
||||
// gl.clearColor(0.13, 0.5, 0.5, 1.0);
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||
|
||||
// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
|
||||
// gl.enable(gl.DEPTH_TEST);
|
||||
// gl.depthFunc(gl.LEQUAL);
|
||||
|
||||
// gl.enable(gl.BLEND);
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// const viewMatrix = mat4.lookAt(mat4.create(), [0, 1, 1], [0, 0, 0], [0, 0, -1]);
|
||||
// const projMatrix = mat4.ortho(mat4.create(), camera.x, camera.x + camera.w, camera.y + camera.h, camera.y, -1000, 1000);
|
||||
// const transformMatrix = mat4.mul(mat4.create(), projMatrix, viewMatrix);
|
||||
|
||||
// paletteSpriteBatch.begin(paletteShader, sprites.paletteSpriteSheet);
|
||||
// gl.uniformMatrix4fv(paletteShader.uniforms.transfrom, false, transformMatrix);
|
||||
// gl.uniform4f(paletteShader.uniforms.lighting, 1, 1, 1, 1);
|
||||
// gl.uniform1f(paletteShader.uniforms.pixelSize, paletteManager.pixelSize);
|
||||
|
||||
// // for (let y = 0; y < 15; y++) {
|
||||
// // for (let x = 0; x < 10; x++) {
|
||||
// // paletteSpriteBatch.drawSprite(sprites.grass_tile.color, WHITE, grassPalette, x * 32, y * 24);
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // paletteSpriteBatch.drawSprite(sprites.stone_wall_6.color!, WHITE, wallPalette, 60, 40);
|
||||
// // paletteSpriteBatch.drawSprite(sprites.tree_6Crown0_0.color!, WHITE, treePalette, 120, 10);
|
||||
|
||||
// const sprite = sprites.grass_tile.color;
|
||||
|
||||
// function drawHTileAt(x: number, y: number, z: number, color: number) {
|
||||
// paletteSpriteBatch.drawQuad(color, grassPalette,
|
||||
// x + 0, y + 0, z,
|
||||
// x + 1, y + 0, z,
|
||||
// x + 1, y + 1, z,
|
||||
// x + 0, y + 1, z,
|
||||
// sprite.x, sprite.y, sprite.w, sprite.h);
|
||||
// }
|
||||
|
||||
// drawHTileAt(0.5, 0, redZ, RED);
|
||||
// drawHTileAt(0, 0, 0, WHITE);
|
||||
// drawHTileAt(0.5, 0, 0, ORANGE);
|
||||
|
||||
// paletteSpriteBatch.drawQuad(BLUE, grassPalette,
|
||||
// 0, 0, blueZ,
|
||||
// 1, 0, blueZ,
|
||||
// 1, 1, blueZ - 1,
|
||||
// 0, 1, blueZ - 1,
|
||||
// sprite.x, sprite.y, sprite.w, sprite.h);
|
||||
|
||||
// paletteSpriteBatch.end();
|
||||
// },
|
||||
// });
|
||||
|
||||
// window.addEventListener('keyup', e => {
|
||||
// if (e.keyCode === Key.KEY_Q) {
|
||||
// handle.cancel();
|
||||
// }
|
||||
// });
|
||||
|
||||
// window.addEventListener('keydown', e => {
|
||||
// if (e.keyCode === Key.LEFT) {
|
||||
// camera.x -= 1;
|
||||
// } else if (e.keyCode === Key.RIGHT) {
|
||||
// camera.x += 1;
|
||||
// } else if (e.keyCode === Key.UP) {
|
||||
// camera.y -= 1;
|
||||
// } else if (e.keyCode === Key.DOWN) {
|
||||
// camera.y += 1;
|
||||
// }
|
||||
|
||||
// if (e.keyCode === Key.KEY_A) {
|
||||
// camera.x -= 1;
|
||||
// } else if (e.keyCode === Key.KEY_D) {
|
||||
// camera.x += 1;
|
||||
// } else if (e.keyCode === Key.KEY_W) {
|
||||
// camera.y -= 1;
|
||||
// } else if (e.keyCode === Key.KEY_S) {
|
||||
// camera.y += 1;
|
||||
// }
|
||||
|
||||
// if (e.keyCode === Key.KEY_R) {
|
||||
// redZ -= 0.1;
|
||||
// } else if (e.keyCode === Key.KEY_T) {
|
||||
// redZ += 0.1;
|
||||
// }
|
||||
|
||||
// if (e.keyCode === Key.KEY_F) {
|
||||
// blueZ -= 0.1;
|
||||
// } else if (e.keyCode === Key.KEY_G) {
|
||||
// blueZ += 0.1;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function oldTests(canvas: HTMLCanvasElement) {
|
||||
// let skewTime = 0;
|
||||
// let animationTime = 0;
|
||||
// let lightTexture: Texture2D;
|
||||
// // let tilesTexture: Texture2D;
|
||||
|
||||
// const gl = getWebGLContext(canvas);
|
||||
// const spriteShaderInstance = createShader(gl, spriteShader);
|
||||
// const paletteShader = createShader(gl, paletteDepthShader);
|
||||
// const lightShader = createShader(gl, sprite2Shader);
|
||||
// const spriteBatch = new SpriteBatch(gl);
|
||||
// const spriteBatch2 = new SpriteBatch2(gl);
|
||||
// const paletteSpriteBatch2 = new PaletteSpriteBatch2(gl);
|
||||
// const paletteSpriteBatch = new PaletteSpriteBatch(gl);
|
||||
// const paletteManager = new PaletteManager();
|
||||
// const palettes: Palette[] = [];
|
||||
// const fbo = createFrameBuffer(gl, 1024, 1024, { depth: false });
|
||||
// //const light = createFBO(gl, 1024, 1024, { depth: true });
|
||||
|
||||
// // const INFO = createDefaultPony();
|
||||
|
||||
// const STATE: PonyState = {
|
||||
// ...defaultPonyState(),
|
||||
// animation: trot,
|
||||
// animationFrame: 0,
|
||||
// headAnimation: undefined,
|
||||
// headAnimationFrame: 0,
|
||||
// blinkFrame: 2,
|
||||
// };
|
||||
|
||||
// const scale = 4;
|
||||
// // const ponyInfo = toPalette(INFO, paletteManager);
|
||||
// const treePalette = paletteManager.addArray(sprites.tree_6Crown0_0.palettes![0]);
|
||||
// const rockPalette = paletteManager.addArray(sprites.rock.palettes![0]);
|
||||
// const grassPalette = paletteManager.addArray(sprites.grass_2.palettes![0]);
|
||||
|
||||
// let mouseX = 0, mouseY = 0;
|
||||
|
||||
// // font.lineSpacing = 5;
|
||||
|
||||
// canvas.addEventListener('mousemove', e => {
|
||||
// mouseX = e.pageX / scale;
|
||||
// mouseY = e.pageY / scale;
|
||||
// });
|
||||
|
||||
// const handle = startGameLoop({
|
||||
// load() {
|
||||
// return Promise.all([
|
||||
// //loadImage('/assets/images/pony2.png').then(img => pony2 = createTexture(gl, img, gl.RGBA, gl.UNSIGNED_BYTE)),
|
||||
// // loadImage('/assets/images/tiles.png').then(img => tilesTexture = createTexture(gl, img)),
|
||||
// loadImage('/images/light2.png').then(img => lightTexture = createTexture(gl, img)),
|
||||
// loadSpriteSheets(sprites.spriteSheets, loadImage),
|
||||
// ]).then(() => {
|
||||
// createTexturesForSpriteSheets(gl, sprites.spriteSheets);
|
||||
// });
|
||||
// },
|
||||
// init() {
|
||||
// },
|
||||
// update(delta: number) {
|
||||
// skewTime += delta * 2; // * 0.5;
|
||||
|
||||
// while (skewTime > 1) {
|
||||
// skewTime -= 1;
|
||||
// }
|
||||
|
||||
// STATE.animationFrame = Math.floor(animationTime * 24) % STATE.animation!.frames.length;
|
||||
// },
|
||||
// draw() {
|
||||
// gl.canvas.width = document.body.clientWidth;
|
||||
// gl.canvas.height = document.body.clientHeight;
|
||||
// gl.canvas.style.width = document.body.clientWidth + 'px';
|
||||
// gl.canvas.style.height = document.body.clientHeight + 'px';
|
||||
|
||||
// paletteManager.commit(gl);
|
||||
// paletteSpriteBatch.palette = paletteManager.texture;
|
||||
|
||||
// const width = Math.ceil(gl.canvas.width / scale);
|
||||
// const height = Math.ceil(gl.canvas.height / scale);
|
||||
|
||||
// // render color
|
||||
|
||||
// bindFrameBuffer(fbo);
|
||||
|
||||
// gl.viewport(0, 0, width, height);
|
||||
// gl.clearColor(0.13, 0.5, 0.5, 1.0);
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
// gl.disable(gl.DEPTH_TEST);
|
||||
// gl.enable(gl.BLEND);
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// const viewMatrix = mat4.ortho(mat4.create(), 0, width, height, 0, 0, 1000);
|
||||
|
||||
// paletteSpriteBatch.begin(paletteShader, {} as any);
|
||||
|
||||
// gl.uniformMatrix4fv(paletteShader.uniforms.transfrom, false, viewMatrix);
|
||||
// gl.uniform4f(paletteShader.uniforms.lighting, 1, 1, 1, 1);
|
||||
// gl.uniform1f(paletteShader.uniforms.pixelSize, paletteManager.pixelSize);
|
||||
|
||||
// for (let y = 0; y < 15; y++) {
|
||||
// for (let x = 0; x < 10; x++) {
|
||||
// paletteSpriteBatch.drawSprite(sprites.grass_2.color, WHITE, grassPalette, x * 32, y * 24);
|
||||
// }
|
||||
// }
|
||||
|
||||
// paletteSpriteBatch.drawSprite(sprites.tree_6Crown0_0.color!, WHITE, treePalette, 120, 10);
|
||||
// paletteSpriteBatch.end();
|
||||
|
||||
// // render frame buffer (color)
|
||||
|
||||
// gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
// gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
// gl.clearColor(0.13, 0.5, 0.5, 1.0);
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
// gl.disable(gl.DEPTH_TEST);
|
||||
// gl.disable(gl.BLEND);
|
||||
|
||||
// const fboMatrix = mat4.ortho(mat4.create(), 0, width, height, 0, 0, 1000);
|
||||
|
||||
// spriteBatch.begin(spriteShaderInstance, { texture: fbo.depth } as any);
|
||||
|
||||
// gl.uniformMatrix4fv(spriteShaderInstance.uniforms.transfrom, false, fboMatrix);
|
||||
// gl.uniform4f(spriteShaderInstance.uniforms.lighting, 1, 1, 1, 1);
|
||||
// spriteBatch.drawImage(WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
// spriteBatch.end();
|
||||
// },
|
||||
// draw2() {
|
||||
// gl.canvas.width = document.body.clientWidth;
|
||||
// gl.canvas.height = document.body.clientHeight;
|
||||
// gl.canvas.style.width = document.body.clientWidth + 'px';
|
||||
// gl.canvas.style.height = document.body.clientHeight + 'px';
|
||||
|
||||
// paletteManager.commit(gl);
|
||||
// paletteSpriteBatch2.palette = paletteManager.texture;
|
||||
|
||||
// const width = Math.ceil(gl.canvas.width / scale);
|
||||
// const height = Math.ceil(gl.canvas.height / scale);
|
||||
|
||||
// // render color
|
||||
|
||||
// bindFrameBuffer(fbo);
|
||||
|
||||
// gl.viewport(0, 0, width, height);
|
||||
// gl.clearColor(0.13, 0.5, 0.5, 1.0);
|
||||
// gl.clearDepth(1000); //gl.DEPTH_CLEAR_VALUE);
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||
|
||||
// gl.enable(gl.DEPTH_TEST);
|
||||
// //gl.depthMask(true);
|
||||
// gl.depthFunc(gl.LEQUAL);
|
||||
// //gl.depthRange(0, 1000);
|
||||
|
||||
// gl.enable(gl.BLEND);
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// //const mat = createViewMatrix(mat4.create(), width, height, 1);
|
||||
|
||||
// const tst = mat4.create();
|
||||
// mat4.identity(tst);
|
||||
// mat4.translate(tst, tst, vec3.fromValues(-1, 1, 0));
|
||||
// mat4.scale(tst, tst, vec3.fromValues(2 / width, -2 / height, 1));
|
||||
// mat4.scale(tst, tst, vec3.fromValues(1, 1, 0.005));
|
||||
// //mat4.translate(tst, tst, vec3.fromValues(0, 0, 0));
|
||||
|
||||
// const tst2 = mat4.ortho(mat4.create(), 0, width, height, 0, 0, 100);
|
||||
|
||||
// //const viewMatrix = mat4.ortho(this.viewMatrix, camera.x, camera.x + camera.w, camera.y, camera.y + camera.h, 0, 100);
|
||||
// //const fboMatrix = mat4.ortho(
|
||||
// // this.fboMatrix, 0, this.canvas.width / actualScale, this.canvas.height / actualScale, 0, 0, 100);
|
||||
|
||||
// //console.log(mat4.str(tst));
|
||||
// //console.log(mat4.str(tst2));
|
||||
// //handle.cancel();
|
||||
|
||||
// //spriteBatch.begin(spriteShader);
|
||||
// //spriteShader.uniforms.transform = mat;
|
||||
// //spriteShader.uniforms.lighting = [1, 1, s1, 1];
|
||||
// //spriteBatch.drawRect(GRAY, 10, 140, SIZE, SIZE);
|
||||
// //spriteBatch.end();
|
||||
|
||||
// paletteSpriteBatch2.begin(paletteShader, {} as any);
|
||||
|
||||
// gl.uniformMatrix4fv(paletteShader.uniforms.transfrom, false, tst2);
|
||||
// gl.uniform4f(paletteShader.uniforms.lighting, 1, 1, 1, 1);
|
||||
// gl.uniform1f(paletteShader.uniforms.pixelSize, paletteManager.pixelSize);
|
||||
|
||||
// paletteSpriteBatch2.depth = -100;
|
||||
|
||||
// for (let y = 0; y < 15; y++) {
|
||||
// for (let x = 0; x < 10; x++) {
|
||||
// paletteSpriteBatch2.drawSprite(sprites.grass_2.color, WHITE, grassPalette, x * 32, y * 24);
|
||||
// }
|
||||
// }
|
||||
|
||||
// paletteSpriteBatch2.depth = -90;
|
||||
// paletteSpriteBatch2.drawSprite(sprites.rock.color, WHITE, rockPalette, 65, 70);
|
||||
|
||||
// paletteSpriteBatch2.depth = -80;
|
||||
// // drawPony(paletteSpriteBatch2, ponyInfo, STATE, 100, 100, defaultDrawPonyOptions());
|
||||
|
||||
// //const tail = mat2d.create();
|
||||
// //mat2d.identity(tail);
|
||||
// //mat2d.translate(tail, tail, vec2.fromValues(10, 60));
|
||||
// //skewY(tail, tail, (skewTime > 0.5 ? (1 - skewTime) : skewTime) * 0.4);
|
||||
// //mat2d.translate(tail, tail, vec2.fromValues(-46, -44));
|
||||
// //
|
||||
// //paletteSpriteBatch.transform = tail;
|
||||
// //paletteSpriteBatch.drawSprite(sprites.ponTails[1][1].color, null, ponyInfo.tail.palette, 0, 0);
|
||||
// //paletteSpriteBatch.transform = null;
|
||||
|
||||
// paletteSpriteBatch2.depth = -60;
|
||||
// paletteSpriteBatch2.drawSprite(sprites.tree_6Crown0_0.color, WHITE, treePalette, 120, 10);
|
||||
|
||||
// //gl.depthMask(false);
|
||||
|
||||
// paletteSpriteBatch2.end();
|
||||
|
||||
// //spriteBatch.begin(spriteShader);
|
||||
// //spriteShader.uniforms.transform = mat;
|
||||
// //spriteShader.uniforms.lighting = [1, 1, 1, 1];
|
||||
// //spriteBatch.drawRect(GRAY, 10, 140, paletteManager.textureSize, paletteManager.textureSize);
|
||||
// //spriteBatch.drawImage(
|
||||
// // paletteManager.texture, null, 0, 0, paletteManager.textureSize, paletteManager.textureSize,
|
||||
// // 10, 140, paletteManager.textureSize, paletteManager.textureSize);
|
||||
// //spriteBatch.drawImage(lightTexture, null, 0, 0, 64, 64, 50, 50, 64, 64);
|
||||
// //spriteBatch.end();
|
||||
|
||||
// // render lighting test
|
||||
|
||||
// // gl.blendEquation(gl.FUNC_ADD);
|
||||
// // gl.blendFunc(gl.ONE, gl.ONE);
|
||||
// //
|
||||
// // spriteBatch2.begin(lightShader);
|
||||
// // lightShader.uniforms.transform = tst;
|
||||
// // spriteBatch2.defaultDepth = 70;
|
||||
// // spriteBatch2.drawImage(lightTexture, null, 0, 0, 128, 128, mouseX - 64, mouseY - 64, 128, 128);
|
||||
// // spriteBatch2.end();
|
||||
|
||||
// // render lighting
|
||||
|
||||
// // light.bind();
|
||||
// //
|
||||
// // gl.viewport(0, 0, width, height);
|
||||
// // gl.clearColor(0.2, 0, 0, 1.0);
|
||||
// // gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
// //
|
||||
// // gl.disable(gl.DEPTH_TEST);
|
||||
// // gl.enable(gl.BLEND);
|
||||
// // gl.blendEquation(gl.FUNC_ADD);
|
||||
// // gl.blendFunc(gl.ONE, gl.ONE);
|
||||
// //
|
||||
// // spriteBatch.begin(spriteShader);
|
||||
// // spriteShader.uniforms.transform = mat;
|
||||
// // spriteShader.uniforms.lighting = [1, 1, 1, 1];
|
||||
// // spriteBatch.drawImage(lightTexture, null, 0, 0, 128, 128, mouseX - 64, mouseY - 64, 128, 128);
|
||||
// // spriteBatch.end();
|
||||
|
||||
// // render frame buffer (color)
|
||||
|
||||
// gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
// gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
// gl.clearColor(1, 1, 1, 1);
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
// gl.disable(gl.DEPTH_TEST);
|
||||
// gl.enable(gl.BLEND);
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.DST_COLOR, gl.ZERO);
|
||||
|
||||
// spriteBatch.begin(spriteShaderInstance, { texture: fbo.depth } as any);
|
||||
|
||||
// const viewMatrix = createViewMatrix2(mat4.create(), canvas.width, canvas.height, scale);
|
||||
// gl.uniformMatrix4fv(spriteShaderInstance.uniforms.transfrom, false, viewMatrix);
|
||||
// gl.uniform4f(spriteShaderInstance.uniforms.lighting, 1, 1, 1, 1);
|
||||
// spriteBatch.drawImage(WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
// //spriteBatch.drawImage(fbo.color[0], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
// //spriteBatch.drawImage(fbo.color[1], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
// //spriteBatch.drawImage(light.color[0], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
// spriteBatch.end();
|
||||
|
||||
// // render light
|
||||
|
||||
// bindFrameBuffer(fbo);
|
||||
|
||||
// gl.viewport(0, 0, width, height);
|
||||
// gl.clearColor(0.13, 0.5, 0.5, 1.0); // ambient light color here
|
||||
// gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
// gl.enable(gl.DEPTH_TEST);
|
||||
// gl.depthFunc(gl.LESS);
|
||||
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.ONE, gl.ONE);
|
||||
|
||||
// spriteBatch2.begin(lightShader, { texture: lightTexture } as any);
|
||||
// gl.uniformMatrix4fv(lightShader.uniforms.transfrom, false, tst);
|
||||
// spriteBatch2.depth = 70;
|
||||
// spriteBatch2.drawImage(WHITE, 0, 0, 256, 200, mouseX - 128, mouseY - 100, 256, 200);
|
||||
// spriteBatch2.end();
|
||||
|
||||
// // render frame buffer (light)
|
||||
|
||||
// gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
// gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// gl.disable(gl.DEPTH_TEST);
|
||||
// gl.enable(gl.BLEND);
|
||||
// gl.blendEquation(gl.FUNC_ADD);
|
||||
// gl.blendFunc(gl.DST_COLOR, gl.ZERO);
|
||||
|
||||
// spriteBatch.begin(spriteShaderInstance, { texture: fbo.color[0] } as any);
|
||||
// const mat = createViewMatrix2(mat4.create(), canvas.width, canvas.height, scale);
|
||||
// gl.uniformMatrix4fv(spriteShaderInstance.uniforms.transfrom, false, mat);
|
||||
// gl.uniform4f(spriteShaderInstance.uniforms.lighting, 1, 1, 1, 1);
|
||||
// //spriteBatch.drawImage(fbo.depth, WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
// spriteBatch.drawImage(WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
// //spriteBatch.drawImage(fbo.color[1], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
// //spriteBatch.drawImage(light.color[0], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
// spriteBatch.end();
|
||||
// },
|
||||
// } as any);
|
||||
|
||||
// window.addEventListener('keyup', e => {
|
||||
// if (e.keyCode === Key.KEY_Q) {
|
||||
// handle.cancel();
|
||||
// }
|
||||
// });
|
||||
|
||||
// window.addEventListener('keydown', e => {
|
||||
// if (e.keyCode === Key.KEY_E) {
|
||||
// animationTime -= 1 / 24;
|
||||
// } else if (e.keyCode === Key.KEY_R) {
|
||||
// animationTime += 1 / 24;
|
||||
// } else if (e.keyCode === Key.KEY_T) {
|
||||
// for (let i = 0; i < 5; i++) {
|
||||
// palettes.push(paletteManager.add(range(0, 40 * Math.random()).map(() => Math.random() * 0xffffffff)));
|
||||
// }
|
||||
// } else if (e.keyCode === Key.KEY_Y) {
|
||||
// palettes.forEach(releasePalette);
|
||||
// palettes.length = 0;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
@@ -0,0 +1,99 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { PopoverModule } from 'ngx-bootstrap/popover';
|
||||
import { TypeaheadModule } from 'ngx-bootstrap/typeahead';
|
||||
import { ButtonsModule } from 'ngx-bootstrap/buttons';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
import { ErrorReporter } from '../services/errorReporter';
|
||||
|
||||
import { ToolsRange } from './shared/tools-range/tools-range';
|
||||
import { ToolsFrame } from './shared/tools-frame/tools-frame';
|
||||
import { ToolsOffset } from './shared/tools-offset/tools-offset';
|
||||
import { ToolsXY } from './shared/tools-xy/tools-xy';
|
||||
|
||||
import { ToolsExpressions } from './tools-expressions/tools-expressions';
|
||||
import { ToolsAnimation } from './tools-animation/tools-animation';
|
||||
import { ToolsChat } from './tools-chat/tools-chat';
|
||||
import { ToolsVariants } from './tools-variants/tools-variants';
|
||||
import { ToolsWebgl } from './tools-webgl/tools-webgl';
|
||||
import { ToolsPalette } from './tools-palette/tools-palette';
|
||||
import { ToolsPerf } from './tools-perf/tools-perf';
|
||||
import { ToolsRegions } from './tools-regions/tools-regions';
|
||||
import { ToolsEntity } from './tools-entity/tools-entity';
|
||||
import { ToolsSheet } from './tools-sheet/tools-sheet';
|
||||
import { ToolsStates } from './tools-states/tools-states';
|
||||
import { ToolsUI } from './tools-ui/tools-ui';
|
||||
import { ToolsCollisions } from './tools-collisions/tools-collisions';
|
||||
import { ToolsMap } from './tools-map/tools-map';
|
||||
import { ToolsIndex } from './tools-index/tools-index';
|
||||
import { ToolsApp } from './tools';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: ToolsIndex },
|
||||
{ path: 'sheet', component: ToolsSheet },
|
||||
{ path: 'states', component: ToolsStates },
|
||||
{ path: 'variants', component: ToolsVariants },
|
||||
{ path: 'webgl', component: ToolsWebgl },
|
||||
{ path: 'animation/:id', component: ToolsAnimation },
|
||||
{ path: 'animation', component: ToolsAnimation },
|
||||
{ path: 'chat', component: ToolsChat },
|
||||
{ path: 'expressions', component: ToolsExpressions },
|
||||
{ path: 'entity', component: ToolsEntity },
|
||||
{ path: 'palette', component: ToolsPalette },
|
||||
{ path: 'perf', component: ToolsPerf },
|
||||
{ path: 'regions', component: ToolsRegions },
|
||||
{ path: 'ui', component: ToolsUI },
|
||||
{ path: 'collisions', component: ToolsCollisions },
|
||||
{ path: 'map', component: ToolsMap },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
SharedModule,
|
||||
PopoverModule.forRoot(),
|
||||
TypeaheadModule.forRoot(),
|
||||
ButtonsModule.forRoot(),
|
||||
RouterModule.forRoot(routes),
|
||||
FontAwesomeModule,
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
declarations: [
|
||||
ToolsRange,
|
||||
ToolsFrame,
|
||||
ToolsOffset,
|
||||
ToolsXY,
|
||||
ToolsExpressions,
|
||||
ToolsAnimation,
|
||||
ToolsChat,
|
||||
ToolsVariants,
|
||||
ToolsWebgl,
|
||||
ToolsPalette,
|
||||
ToolsPerf,
|
||||
ToolsRegions,
|
||||
ToolsEntity,
|
||||
ToolsSheet,
|
||||
ToolsStates,
|
||||
ToolsCollisions,
|
||||
ToolsMap,
|
||||
ToolsUI,
|
||||
ToolsIndex,
|
||||
ToolsApp,
|
||||
],
|
||||
providers: [
|
||||
ErrorReporter,
|
||||
],
|
||||
bootstrap: [ToolsApp],
|
||||
})
|
||||
export class ToolsAppModule {
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
router-outlet
|
||||
draggable-outlet
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TooltipConfig } from 'ngx-bootstrap/tooltip';
|
||||
import { PopoverConfig } from 'ngx-bootstrap/popover';
|
||||
|
||||
export function tooltipConfig() {
|
||||
return Object.assign(new TooltipConfig(), { container: 'body' });
|
||||
}
|
||||
|
||||
export function popoverConfig() {
|
||||
return Object.assign(new PopoverConfig(), { container: 'body' });
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'pony-town-app',
|
||||
templateUrl: 'tools.pug',
|
||||
providers: [
|
||||
{ provide: TooltipConfig, useFactory: tooltipConfig },
|
||||
{ provide: PopoverConfig, useFactory: popoverConfig },
|
||||
]
|
||||
})
|
||||
export class ToolsApp {
|
||||
}
|
||||
Reference in New Issue
Block a user