Archive commit

This commit is contained in:
Erik McClure
2019-08-28 21:15:02 -07:00
commit 735845746e
1435 changed files with 140724 additions and 0 deletions
@@ -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();
}
}
+358
View File
@@ -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") &times;
| {{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)") &times;2
a.dropdown-item((click)="png(3)") &times;3
a.dropdown-item((click)="png(4)") &times;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)") &times;2
a.dropdown-item((click)="gif(3)") &times;3
a.dropdown-item((click)="gif(4)") &times;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)
| &times;{{scale}}
.dropdown-menu(*dropdownMenu)
a.dropdown-item((click)="scale = 1; redraw()") &times;1
a.dropdown-item((click)="scale = 2; redraw()") &times;2
a.dropdown-item((click)="scale = 3; redraw()") &times;3
a.dropdown-item((click)="scale = 4; redraw()") &times;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;
// }
// });
// }
+99
View File
@@ -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 {
}
+2
View File
@@ -0,0 +1,2 @@
router-outlet
draggable-outlet
+22
View File
@@ -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 {
}