mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-25 22:25:53 +02:00
200 lines
5.9 KiB
TypeScript
200 lines
5.9 KiB
TypeScript
import {
|
|
Component, Input, ElementRef, AfterViewInit, OnDestroy, NgZone, ViewChild, OnChanges, HostListener, ChangeDetectionStrategy } from '@angular/core';
|
|
import { PonyInfo, PonyState } from '../../../common/interfaces';
|
|
import { toPalette } from '../../../common/ponyInfo';
|
|
import { GRASS_COLOR, TRANSPARENT } from '../../../common/colors';
|
|
import {
|
|
createCanvas, disableImageSmoothing, getPixelRatio, resizeCanvas, resizeCanvasWithRatio
|
|
} from '../../../common/canvasUtils';
|
|
import { BLINK_FRAMES } from '../../../common/ponyUtils';
|
|
import { defaultPonyState, defaultDrawPonyOptions } from '../../../common/ponyHelpers';
|
|
import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch';
|
|
import { colorToCSS } from '../../../common/color';
|
|
import { drawNamePlate, commonPalettes, DrawNameFlags } from '../../../graphics/graphicsUtils';
|
|
import { drawPony } from '../../../client/ponyDraw';
|
|
import { paletteSpriteSheet } from '../../../generated/sprites';
|
|
import { replaceEmojis } from '../../../common/emoji';
|
|
import { loadAndInitSpriteSheets } from '../../../client/loadSprites';
|
|
|
|
const DEFAULT_STATE = defaultPonyState();
|
|
const DEFAULT_OPTIONS = defaultDrawPonyOptions();
|
|
|
|
@Component({
|
|
changeDetection: ChangeDetectionStrategy.Eager,
|
|
standalone: false,
|
|
selector: 'character-preview',
|
|
template: '<canvas class="rounded" #canvas></canvas>',
|
|
styles: [`:host { display: block; } canvas { width: 100%; height: 100%; }`],
|
|
})
|
|
export class CharacterPreview implements OnDestroy, OnChanges, AfterViewInit {
|
|
@Input() scale = 3;
|
|
@Input() name?: string;
|
|
@Input() tag?: string;
|
|
@Input() pony?: PonyInfo;
|
|
@Input() state?: PonyState = defaultPonyState();
|
|
@Input() noBackground = false;
|
|
@Input() noOutline = false;
|
|
@Input() noShadow = false;
|
|
@Input() extra = false;
|
|
@Input() passive = false;
|
|
@Input() blinks = true;
|
|
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
|
private batch?: ContextSpriteBatch;
|
|
private nameBatch?: ContextSpriteBatch;
|
|
private frame = 0;
|
|
private lastFrame = 0;
|
|
private initialized = false;
|
|
private nextBlink = performance.now() + 2000;
|
|
private blinkFrame = -1;
|
|
constructor(private zone: NgZone) {
|
|
}
|
|
ngAfterViewInit() {
|
|
return loadAndInitSpriteSheets()
|
|
.then(() => this.initialized = true)
|
|
.then(() => this.ngOnChanges());
|
|
}
|
|
ngOnDestroy() {
|
|
cancelAnimationFrame(this.frame);
|
|
}
|
|
ngOnChanges() {
|
|
if (!this.frame) {
|
|
this.zone.runOutsideAngular(() => this.frame = requestAnimationFrame(this.onFrame));
|
|
}
|
|
}
|
|
@HostListener('window:resize')
|
|
redraw() {
|
|
this.tryDraw();
|
|
}
|
|
blink() {
|
|
this.nextBlink = performance.now();
|
|
}
|
|
private onFrame = () => {
|
|
if (this.passive && this.initialized) {
|
|
this.frame = 0;
|
|
this.tryDraw();
|
|
return;
|
|
}
|
|
|
|
this.frame = requestAnimationFrame(this.onFrame);
|
|
|
|
const now = performance.now();
|
|
|
|
if ((now - this.lastFrame) > (1000 / 24)) {
|
|
if (this.blinks) {
|
|
if (this.blinkFrame === -1) {
|
|
if (this.nextBlink < now) {
|
|
this.blinkFrame = 0;
|
|
}
|
|
}
|
|
else {
|
|
this.blinkFrame++;
|
|
|
|
if (this.blinkFrame >= BLINK_FRAMES.length) {
|
|
this.nextBlink = now + Math.random() * 2000 + 3000;
|
|
this.blinkFrame = -1;
|
|
}
|
|
}
|
|
|
|
if (this.state) {
|
|
this.state.blinkFrame = this.blinkFrame === -1 ? 1 : BLINK_FRAMES[this.blinkFrame];
|
|
}
|
|
}
|
|
|
|
this.lastFrame = now;
|
|
this.tryDraw();
|
|
}
|
|
};
|
|
private tryDraw() {
|
|
try {
|
|
this.draw();
|
|
}
|
|
catch { }
|
|
}
|
|
private draw() {
|
|
if (!this.initialized) {
|
|
return;
|
|
}
|
|
|
|
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
|
|
|
|
const { width, height } = canvas.getBoundingClientRect();
|
|
resizeCanvasWithRatio(canvas, width, height, false);
|
|
|
|
const scale = this.scale * getPixelRatio();
|
|
const bufferWidth = Math.round(canvas.width / scale);
|
|
const bufferHeight = Math.round(canvas.height / scale);
|
|
|
|
if (!bufferWidth || !bufferHeight) {
|
|
return;
|
|
}
|
|
|
|
this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferWidth, bufferHeight));
|
|
resizeCanvas(this.batch.canvas, bufferWidth, bufferHeight);
|
|
|
|
const x = Math.round(bufferWidth / 2);
|
|
const y = Math.round(bufferHeight / 2 + 28);
|
|
|
|
if (this.pony) {
|
|
this.batch.start(paletteSpriteSheet, this.noBackground ? TRANSPARENT : GRASS_COLOR);
|
|
|
|
try {
|
|
const options = { ...DEFAULT_OPTIONS, shadow: !this.noShadow, extra: !!this.extra };
|
|
drawPony(this.batch, toPalette(this.pony), this.state || DEFAULT_STATE, x, y, options);
|
|
}
|
|
catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
this.batch.end();
|
|
}
|
|
|
|
const viewContext = canvas.getContext('2d');
|
|
|
|
if (!viewContext) {
|
|
return;
|
|
}
|
|
|
|
disableImageSmoothing(viewContext);
|
|
|
|
if (this.noBackground) {
|
|
viewContext.clearRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
viewContext.save();
|
|
viewContext.scale(scale, scale);
|
|
|
|
// draw outline
|
|
if (this.pony && this.noShadow && this.noBackground && !this.noOutline) {
|
|
for (let x = -1; x <= 1; x++) {
|
|
for (let y = -1; y <= 1; y++) {
|
|
viewContext.drawImage(this.batch.canvas, x, y);
|
|
}
|
|
}
|
|
|
|
viewContext.globalCompositeOperation = 'source-in';
|
|
viewContext.fillStyle = colorToCSS(GRASS_COLOR);
|
|
viewContext.fillRect(0, 0, viewContext.canvas.width, viewContext.canvas.height);
|
|
viewContext.globalCompositeOperation = 'source-over';
|
|
}
|
|
|
|
viewContext.drawImage(this.batch.canvas, 0, 0);
|
|
viewContext.restore();
|
|
|
|
// draw name plate
|
|
if (!this.noShadow && this.name) {
|
|
const name = replaceEmojis(this.name);
|
|
const scale = 2 * getPixelRatio();
|
|
const nameBufferWidth = Math.round(canvas.width / scale);
|
|
this.nameBatch = this.nameBatch || new ContextSpriteBatch(createCanvas(nameBufferWidth, 25));
|
|
resizeCanvas(this.nameBatch.canvas, nameBufferWidth, 25);
|
|
this.nameBatch.start(paletteSpriteSheet, TRANSPARENT);
|
|
drawNamePlate(this.nameBatch, name, nameBufferWidth / 2, 11, DrawNameFlags.None, commonPalettes, this.tag);
|
|
this.nameBatch.end();
|
|
viewContext.save();
|
|
viewContext.scale(scale, scale);
|
|
viewContext.drawImage(this.nameBatch.canvas, 0, 10);
|
|
viewContext.restore();
|
|
}
|
|
}
|
|
}
|