import { ERenderItemEvent, RenderItem, RenderItemPosition, Transform, MotaOffscreenCanvas2D } from '@motajs/render-core'; import { Font } from '@motajs/render-style'; import { logger } from '@motajs/common'; import { isNil } from 'lodash-es'; import { IAnimateFrame, renderEmits } from './frame'; import { AutotileRenderable, RenderableData, texture } from './cache'; import { SizedCanvasImageSource } from './types'; /** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */ const SAFE_PAD = 1; type CanvasStyle = string | CanvasGradient | CanvasPattern; export interface ETextEvent extends ERenderItemEvent { setText: [text: string, width: number, height: number]; } export class Text extends RenderItem { text: string; fillStyle?: CanvasStyle = '#fff'; strokeStyle?: CanvasStyle; font: Font = new Font(); strokeWidth: number = 1; private length: number = 0; private descent: number = 0; private static measureCanvas = new MotaOffscreenCanvas2D(); constructor(text: string = '', type: RenderItemPosition = 'static') { super(type, false); this.text = text; if (text.length > 0) { this.requestBeforeFrame(() => { this.calBox(); this.emit('setText', text, this.width, this.height); }); } } protected render( canvas: MotaOffscreenCanvas2D, _transform: Transform ): void { const ctx = canvas.ctx; const stroke = this.strokeWidth; ctx.textBaseline = 'bottom'; ctx.fillStyle = this.fillStyle ?? 'transparent'; ctx.strokeStyle = this.strokeStyle ?? 'transparent'; ctx.font = this.font.string(); ctx.lineWidth = this.strokeWidth; if (this.strokeStyle) { ctx.strokeText(this.text, stroke, this.descent + stroke + SAFE_PAD); } if (this.fillStyle) { ctx.fillText(this.text, stroke, this.descent + stroke + SAFE_PAD); } } /** * 获取文字的长度 */ measure() { const ctx = Text.measureCanvas.ctx; ctx.textBaseline = 'bottom'; ctx.font = this.font.string(); const res = ctx.measureText(this.text); return res; } /** * 设置显示文字 * @param text 显示的文字 */ setText(text: string) { this.text = text; this.calBox(); this.update(this); this.emit('setText', text, this.width, this.height); } /** * 设置使用的字体 * @param font 字体 */ setFont(font: Font) { this.font = font; this.calBox(); this.update(this); } /** * 设置字体样式 * @param fill 填充样式 * @param stroke 描边样式 */ setStyle(fill?: CanvasStyle, stroke?: CanvasStyle) { this.fillStyle = fill; this.strokeStyle = stroke; this.update(); } /** * 设置描边宽度 * @param width 宽度 */ setStrokeWidth(width: number) { const before = this.strokeWidth; this.strokeWidth = width; const dw = width - before; this.size(this.width + dw * 2, this.height + dw * 2); this.update(); } /** * 计算字体所占空间,从而确定这个元素的大小 */ calBox() { const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } = this.measure(); this.length = width; this.descent = actualBoundingBoxAscent; const height = actualBoundingBoxAscent + actualBoundingBoxDescent; const stroke = this.strokeWidth * 2; this.size(width + stroke, height + stroke + SAFE_PAD * 2); } protected handleProps( key: string, _prevValue: any, nextValue: any ): boolean { switch (key) { case 'text': if (!this.assertType(nextValue, 'string', key)) return false; this.setText(nextValue); return true; case 'fillStyle': this.setStyle(nextValue, this.strokeStyle); return true; case 'strokeStyle': this.setStyle(this.fillStyle, nextValue); return true; case 'font': if (!this.assertType(nextValue, Font, key)) return false; this.setFont(nextValue); return true; case 'strokeWidth': this.setStrokeWidth(nextValue); return true; } return false; } } export interface EImageEvent extends ERenderItemEvent {} export class Image extends RenderItem { image: CanvasImageSource; constructor(image: CanvasImageSource, type: RenderItemPosition = 'static') { super(type); this.image = image; if (image instanceof VideoFrame || image instanceof SVGElement) { this.size(200, 200); } else { this.size(image.width, image.height); } } protected render( canvas: MotaOffscreenCanvas2D, _transform: Transform ): void { const ctx = canvas.ctx; ctx.drawImage(this.image, 0, 0, canvas.width, canvas.height); } /** * 设置图片资源 * @param image 图片资源 */ setImage(image: CanvasImageSource) { this.image = image; this.update(); } protected handleProps( key: string, _prevValue: any, nextValue: any ): boolean { switch (key) { case 'image': this.setImage(nextValue); return true; } return false; } } export class Comment extends RenderItem { readonly isComment: boolean = true; constructor(public text: string = '') { super('static', false, false); this.hide(); } getBoundingRect(): DOMRectReadOnly { return new DOMRectReadOnly(0, 0, 0, 0); } protected render( _canvas: MotaOffscreenCanvas2D, _transform: Transform ): void {} protected handleProps(): boolean { return false; } } export interface EIconEvent extends ERenderItemEvent {} export class Icon extends RenderItem implements IAnimateFrame { /** 图标id */ icon: AllNumbers = 0; /** 帧数 */ frame: number = 0; /** 是否启用动画 */ animate: boolean = false; /** 图标的渲染信息 */ private renderable?: RenderableData | AutotileRenderable; private pendingIcon?: AllNumbers; constructor(type: RenderItemPosition, cache?: boolean, fall?: boolean) { super(type, cache, fall); this.setAntiAliasing(false); this.setHD(false); } protected render( canvas: MotaOffscreenCanvas2D, _transform: Transform ): void { const ctx = canvas.ctx; const renderable = this.renderable; if (!renderable) return; const [x, y, w, h] = renderable.render[0]; const cw = this.width; const ch = this.height; const frame = this.animate ? RenderItem.animatedFrame % renderable.frame : this.frame; if (!this.animate) { if (renderable.autotile) { ctx.drawImage(renderable.image[0], x, y, w, h, 0, 0, cw, ch); } else { ctx.drawImage(renderable.image, x, y, w, h, 0, 0, cw, ch); } } else { const [x1, y1, w1, h1] = renderable.render[frame]; if (renderable.autotile) { const img = renderable.image[0]; ctx.drawImage(img, x1, y1, w1, h1, 0, 0, cw, ch); } else { ctx.drawImage(renderable.image, x1, y1, w1, h1, 0, 0, cw, ch); } } } /** * 设置图标 * @param id 图标id */ setIcon(id: AllIds | AllNumbers) { if (id === 0) { this.renderable = void 0; return; } const num = typeof id === 'number' ? id : texture.idNumberMap[id]; const { loading } = Mota.require('@user/data-base'); if (loading.loaded) { this.setIconRenderable(num); } else { if (isNil(this.pendingIcon)) { loading.once('loaded', () => { this.setIconRenderable(this.pendingIcon ?? 0); delete this.pendingIcon; }); } this.pendingIcon = num; } } private setIconRenderable(num: AllNumbers) { const renderable = texture.getRenderable(num); if (!renderable) { logger.warn(43, num.toString()); return; } else { this.icon = num; this.renderable = renderable; this.frame = renderable.frame; } this.update(); } /** * 更新动画帧 */ updateFrameAnimate(): void { if (this.animate) this.update(this); } destroy(): void { renderEmits.removeFramer(this); super.destroy(); } protected handleProps( key: string, _prevValue: any, nextValue: any ): boolean { switch (key) { case 'icon': this.setIcon(nextValue); return true; case 'animate': if (!this.assertType(nextValue, 'boolean', key)) return false; this.animate = nextValue; if (nextValue) renderEmits.addFramer(this); else renderEmits.removeFramer(this); this.update(); return true; case 'frame': if (!this.assertType(nextValue, 'number', key)) return false; this.frame = nextValue; this.update(); return true; } return false; } } interface WinskinPatterns { top: CanvasPattern; left: CanvasPattern; bottom: CanvasPattern; right: CanvasPattern; } export interface EWinskinEvent extends ERenderItemEvent {} export class Winskin extends RenderItem { image: SizedCanvasImageSource; /** 边框宽度,32表示原始宽度 */ borderSize: number = 32; /** 图片名称 */ imageName?: string; private pendingImage?: ImageIds; private patternCache?: WinskinPatterns; private patternTransform: DOMMatrix; private static patternMap: Map = new Map(); constructor( image: SizedCanvasImageSource, type: RenderItemPosition = 'static' ) { super(type, false, false); this.image = image; this.setAntiAliasing(false); if (window.DOMMatrix) { this.patternTransform = new DOMMatrix(); } else if (window.WebKitCSSMatrix) { this.patternTransform = new WebKitCSSMatrix(); } else { this.patternTransform = new SVGMatrix(); } } private generatePattern() { const pattern = this.requireCanvas(); const img = this.image; pattern.size(32, 16); pattern.withGameScale(false); pattern.setHD(false); pattern.setAntiAliasing(false); const ctx = pattern.ctx; ctx.drawImage(img, 144, 0, 32, 16, 0, 0, 32, 16); const topPattern = ctx.createPattern(pattern.canvas, 'repeat'); ctx.clearRect(0, 0, 32, 16); ctx.drawImage(img, 144, 48, 32, 16, 0, 0, 32, 16); const bottomPattern = ctx.createPattern(pattern.canvas, 'repeat'); ctx.clearRect(0, 0, 32, 16); pattern.size(16, 32); ctx.drawImage(img, 128, 16, 16, 32, 0, 0, 16, 32); const leftPattern = ctx.createPattern(pattern.canvas, 'repeat'); ctx.clearRect(0, 0, 16, 32); ctx.drawImage(img, 176, 16, 16, 32, 0, 0, 16, 32); const rightPattern = ctx.createPattern(pattern.canvas, 'repeat'); if (!topPattern || !bottomPattern || !leftPattern || !rightPattern) { return null; } const winskinPattern: WinskinPatterns = { top: topPattern, bottom: bottomPattern, left: leftPattern, right: rightPattern }; if (this.imageName) { Winskin.patternMap.set(this.imageName, winskinPattern); } this.patternCache = winskinPattern; this.deleteCanvas(pattern); return winskinPattern; } private getPattern() { if (!this.imageName) { if (this.patternCache) return this.patternCache; return this.generatePattern(); } else { const pattern = Winskin.patternMap.get(this.imageName); if (pattern) return pattern; return this.generatePattern(); } } protected render( canvas: MotaOffscreenCanvas2D, _transform: Transform ): void { const ctx = canvas.ctx; const img = this.image; const w = this.width; const h = this.height; const pad = this.borderSize / 2; // 背景 ctx.drawImage(img, 0, 0, 128, 128, 2, 2, w - 4, h - 4); const pattern = this.getPattern(); if (!pattern) return; const { top, left, right, bottom } = pattern; top.setTransform(this.patternTransform); left.setTransform(this.patternTransform); right.setTransform(this.patternTransform); bottom.setTransform(this.patternTransform); // 上下左右边框 ctx.save(); ctx.fillStyle = top; ctx.translate(pad, 0); ctx.fillRect(0, 0, w - pad * 2, pad); ctx.fillStyle = bottom; ctx.translate(0, h - pad); ctx.fillRect(0, 0, w - pad * 2, pad); ctx.fillStyle = left; ctx.translate(-pad, pad * 2 - h); ctx.fillRect(0, 0, pad, h - pad * 2); ctx.fillStyle = right; ctx.translate(w - pad, 0); ctx.fillRect(0, 0, pad, h - pad * 2); ctx.restore(); // 四个角的边框 ctx.drawImage(img, 128, 0, 16, 16, 0, 0, pad, pad); ctx.drawImage(img, 176, 0, 16, 16, w - pad, 0, pad, pad); ctx.drawImage(img, 128, 48, 16, 16, 0, h - pad, pad, pad); ctx.drawImage(img, 176, 48, 16, 16, w - pad, h - pad, pad, pad); } /** * 设置winskin图片 * @param image winskin图片 */ setImage(image: SizedCanvasImageSource) { this.image = image; this.patternCache = void 0; this.update(); } /** * 通过图片名称设置winskin * @param name 图片名称 */ setImageByName(name: ImageIds) { const { loading } = Mota.require('@user/data-base'); if (loading.loaded) { const image = core.material.images.images[name]; this.setImage(image); } else { if (isNil(this.pendingImage)) { loading.once('loaded', () => { const id = this.pendingImage; if (!id) return; const image = core.material.images.images[id]; this.setImage(image); delete this.pendingImage; }); } this.pendingImage = name; } this.imageName = name; } /** * 设置边框大小 * @param size 边框大小 */ setBorderSize(size: number) { this.borderSize = size; this.patternTransform.a = size / 32; this.patternTransform.d = size / 32; this.update(); } protected handleProps( key: string, _prevValue: any, nextValue: any ): boolean { switch (key) { case 'image': if (!this.assertType(nextValue, 'string', key)) return false; this.setImageByName(nextValue); return true; case 'borderSize': if (!this.assertType(nextValue, 'number', key)) return false; this.setBorderSize(nextValue); return true; } return false; } }