feat: 动画

This commit is contained in:
AncTe committed 2024-08-27 18:34:03 +08:00
1 parent 63765ab626
commit d0ff2f1dc5
7 files changed
+391 -17

No files matched your search

+25 -2
View File
@@ -1,5 +1,6 @@
type AdapterFunction<T> = (item: T, ...params: any[]) => Promise<any>;
type SyncAdapterFunction<T> = (item: T, ...params: any[]) => any;
type GlobalAdapterFunction = (...params: any[]) => Promise<any>;
/**
* 渲染适配器,用作渲染层与数据层沟通的桥梁,用于在数据层等待渲染层执行,常用与动画等。
@@ -16,6 +17,7 @@ export class RenderAdapter<T> {
private execute: Map<string, AdapterFunction<T>> = new Map();
private syncExecutes: Map<string, SyncAdapterFunction<T>> = new Map();
private globalExecutes: Map<string, GlobalAdapterFunction> = new Map();
constructor(id: string) {
this.id = id;
@@ -36,11 +38,20 @@ export class RenderAdapter<T> {
this.items.delete(item);
}
/**
* 定义一个类似于静态函数的函数,只会调用一次,不会对每个元素执行
* @param id 函数名称
* @param fn 执行的函数
*/
receiveGlobal(id: string, fn: GlobalAdapterFunction) {
this.globalExecutes.set(id, fn);
}
/**
* 设置执行函数
* @param fn 对于每个元素执行的函数
*/
recieve(id: string, fn: AdapterFunction<T>): void {
receive(id: string, fn: AdapterFunction<T>): void {
this.execute.set(id, fn);
}
@@ -48,7 +59,7 @@ export class RenderAdapter<T> {
* 设置同步执行函数
* @param fn 对于每个元素执行的函数
*/
recieveSync(id: string, fn: SyncAdapterFunction<T>): void {
receiveSync(id: string, fn: SyncAdapterFunction<T>): void {
this.syncExecutes.set(id, fn);
}
@@ -96,6 +107,18 @@ export class RenderAdapter<T> {
}
}
/**
* 调用一个全局函数
*/
global<R = any>(id: string, ...params: any[]): Promise<R> {
const execute = this.globalExecutes.get(id);
if (!execute) {
return Promise.reject();
} else {
return execute(...params);
}
}
/**
* 销毁这个adapter
*/
+3
View File
@@ -6,6 +6,7 @@ import { LayerGroup, FloorLayer } from './preset/layer';
import { MotaRenderer } from './render';
import { LayerShadowExtends } from '../fx/shadow';
import { LayerGroupFilter } from '@/plugin/fx/gameCanvas';
import { LayerGroupAnimate } from './preset/animate';
let main: MotaRenderer;
@@ -31,12 +32,14 @@ Mota.require('var', 'loading').once('loaded', () => {
const door = new LayerDoorAnimate();
const shadow = new LayerShadowExtends();
const filter = new LayerGroupFilter();
const animate = new LayerGroupAnimate();
layer.extends(damage);
layer.extends(detail);
layer.extends(filter);
layer.getLayer('event')?.extends(hero);
layer.getLayer('event')?.extends(door);
layer.getLayer('event')?.extends(shadow);
layer.extends(animate);
render.appendChild(layer);
});
+262
View File
@@ -0,0 +1,262 @@
import { logger } from '@/core/common/logger';
import { RenderAdapter } from '../adapter';
import { Sprite } from '../sprite';
import { HeroRenderer } from './hero';
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { transformCanvas } from '../item';
export class LayerGroupAnimate implements ILayerGroupRenderExtends {
static animateList: Set<LayerGroupAnimate> = new Set();
id: string = 'animate';
group!: LayerGroup;
hero!: HeroRenderer;
animate!: Animate;
private animation: Set<AnimateData> = new Set();
/**
* 绘制一个跟随勇士的动画
* @param name 动画id
*/
async drawHeroAnimate(name: AnimationIds) {
const animate = this.animate.animate(name, 0, 0);
this.updatePosition(animate);
await this.animate.draw(animate);
this.animation.delete(animate);
}
private updatePosition(animate: AnimateData) {
if (!this.hero.renderable) return;
const { x, y } = this.hero.renderable;
const cell = this.group.cellSize;
const half = cell / 2;
animate.centerX = x * cell + half;
animate.centerY = y * cell + half;
}
private onMoveTick = (x: number, y: number) => {
const cell = this.group.cellSize;
const half = cell / 2;
const ax = x * cell + half;
const ay = y * cell + half;
this.animation.forEach(v => {
v.centerX = ax;
v.centerY = ay;
});
};
private listen() {
this.hero.on('moveTick', this.onMoveTick);
}
awake(group: LayerGroup): void {
this.group = group;
const ex = group.getLayer('event')?.getExtends('floor-hero');
if (ex instanceof HeroRenderer) {
this.hero = ex;
this.animate = new Animate();
this.animate.size(group.width, group.height);
this.animate.setHD(true);
this.animate.setZIndex(100);
group.appendChild(this.animate);
LayerGroupAnimate.animateList.add(this);
this.listen();
} else {
logger.error(
14,
`Animate extends needs 'floor-hero' extends as dependency.`
);
group.removeExtends('animate');
}
}
onDestroy(group: LayerGroup): void {
this.hero.off('moveTick', this.onMoveTick);
LayerGroupAnimate.animateList.delete(this);
}
}
interface AnimateData {
obj: globalThis.Animate;
/** 第一帧是全局第几帧 */
readonly start: number;
/** 当前是第几帧 */
index: number;
/** 是否需要播放音频 */
sound: boolean;
centerX: number;
centerY: number;
onEnd?: () => void;
readonly absolute: boolean;
}
export class Animate extends Sprite {
/** 绝对位置的动画 */
private absoluteAnimates: Set<AnimateData> = new Set();
/** 静态位置的动画 */
private staticAnimates: Set<AnimateData> = new Set();
private delegation: number;
private frame: number = 0;
private lastTime: number = 0;
constructor() {
super('absolute', false);
this.setRenderFn((canvas, transform) => {
const { ctx } = canvas;
ctx.save();
this.drawAnimates(this.absoluteAnimates, canvas);
transformCanvas(canvas, transform);
this.drawAnimates(this.staticAnimates, canvas);
ctx.restore();
});
this.delegation = this.delegateTicker(time => {
if (time - this.lastTime < 50) return;
this.lastTime = time;
this.frame++;
if (
this.absoluteAnimates.size > 0 ||
this.staticAnimates.size > 0
) {
this.update(this);
}
});
adapter.add(this);
}
private drawAnimates(
data: Set<AnimateData>,
canvas: MotaOffscreenCanvas2D
) {
if (data.size === 0) return;
const { ctx } = canvas;
const toDelete = new Set<AnimateData>();
data.forEach(v => {
const obj = v.obj;
const index = v.index;
const frame = obj.frames[index];
const ratio = obj.ratio;
if (!v.sound) {
const se = (index % obj.frame) + 1;
core.playSound(v.obj.se[se], v.obj.pitch[se]);
v.sound = true;
}
const centerX = v.centerX;
const centerY = v.centerY;
frame.forEach(v => {
const img = obj.images[v.index];
if (!img) return;
const realWidth = (img.width * ratio * v.zoom) / 100;
const realHeight = (img.height * ratio * v.zoom) / 100;
ctx.globalAlpha = v.opacity / 255;
const cx = centerX + v.x;
const cy = centerY + v.y;
const ix = -realWidth / 2;
const iy = -realHeight / 2;
const angle = v.angle ? (-v.angle * Math.PI) / 180 : 0;
ctx.save();
ctx.translate(cx, cy);
if (v.mirror) {
ctx.scale(-1, 1);
}
ctx.rotate(angle);
ctx.drawImage(img, ix, iy, realWidth, realHeight);
ctx.restore();
});
const now = this.frame - v.start;
if (now !== v.index) v.sound = true;
v.index = now;
if (v.index === v.obj.frame) {
toDelete.add(v);
}
});
toDelete.forEach(v => {
data.delete(v);
v.onEnd?.();
});
}
/**
* 创建一个可以被执行的动画
* @param name 动画名称
* @param absolute 是否是绝对定位,绝对定位不会受到transform的影响
*/
animate(
name: AnimationIds,
x: number,
y: number,
absolute: boolean = false
) {
const animate = core.material.animates[name];
const data: AnimateData = {
index: 0,
start: this.frame,
obj: animate,
centerX: x,
centerY: y,
absolute,
sound: false
};
return data;
}
/**
* 绘制动画,动画结束时兑现返回的Promise
* @param animate 动画信息
* @returns
*/
draw(animate: AnimateData): Promise<void> {
return new Promise(res => {
if (animate.absolute) {
this.absoluteAnimates.add(animate);
} else {
this.staticAnimates.add(animate);
}
animate.onEnd = () => {
res();
};
});
}
/**
* 根据动画名称、坐标、定位绘制动画
* @param name 动画名称
* @param absolute 是否是绝对定位
*/
drawAnimate(
name: AnimationIds,
x: number,
y: number,
absolute: boolean = false
) {
return this.draw(this.animate(name, x, y, absolute));
}
destroy(): void {
super.destroy();
this.removeTicker(this.delegation);
adapter.remove(this);
}
}
const adapter = new RenderAdapter<Animate>('animate');
adapter.receive('drawAnimate', (item, name, x, y, absolute) => {
return item.drawAnimate(name, x, y, absolute);
});
adapter.receiveGlobal('drawHeroAnimate', name => {
const execute: Promise<void>[] = [];
LayerGroupAnimate.animateList.forEach(v => {
execute.push(v.drawHeroAnimate(name));
});
return Promise.all(execute);
});
+2 -2
View File
@@ -381,9 +381,9 @@ export class LayerDoorAnimate implements ILayerRenderExtends {
}
const doorAdapter = new RenderAdapter<LayerDoorAnimate>('door-animate');
doorAdapter.recieve('openDoor', (item, block: Block) => {
doorAdapter.receive('openDoor', (item, block: Block) => {
return item.openDoor(block);
});
doorAdapter.recieve('closeDoor', (item, block: Block) => {
doorAdapter.receive('closeDoor', (item, block: Block) => {
return item.closeDoor(block);
});
+10 -10
View File
@@ -359,42 +359,42 @@ export class HeroRenderer
}
const adapter = new RenderAdapter<HeroRenderer>('hero-adapter');
adapter.recieve('readyMove', item => {
adapter.receive('readyMove', item => {
item.readyMove();
return Promise.resolve();
});
adapter.recieve('move', (item, dir: Dir) => {
adapter.receive('move', (item, dir: Dir) => {
return item.move(dir);
});
adapter.recieve('endMove', item => {
adapter.receive('endMove', item => {
return item.endMove();
});
adapter.recieve(
adapter.receive(
'moveAs',
(item, x: number, y: number, time: number, fn: TimingFn<3>) => {
return item.moveAs(x, y, time, fn);
}
);
adapter.recieve('setMoveSpeed', (item, speed: number) => {
adapter.receive('setMoveSpeed', (item, speed: number) => {
item.setMoveSpeed(speed);
return Promise.resolve();
});
adapter.recieve('setHeroLoc', (item, x?: number, y?: number) => {
adapter.receive('setHeroLoc', (item, x?: number, y?: number) => {
item.setHeroLoc(x, y);
return Promise.resolve();
});
adapter.recieve('turn', (item, dir: Dir2) => {
adapter.receive('turn', (item, dir: Dir2) => {
item.turn(dir);
return Promise.resolve();
});
adapter.recieve('setImage', (item, image: SizedCanvasImageSource) => {
adapter.receive('setImage', (item, image: SizedCanvasImageSource) => {
item.setImage(image);
return Promise.resolve();
});
// 同步fallback,用于适配现在的样板,之后会删除
adapter.recieveSync('setHeroLoc', (item, x?: number, y?: number) => {
adapter.receiveSync('setHeroLoc', (item, x?: number, y?: number) => {
item.setHeroLoc(x, y);
});
adapter.recieveSync('turn', (item, dir: Dir2) => {
adapter.receiveSync('turn', (item, dir: Dir2) => {
item.turn(dir);
});