feat: 动画

This commit is contained in:
unanmed 2024-08-27 18:34:03 +08:00
parent 63765ab626
commit d0ff2f1dc5
7 changed files with 391 additions and 17 deletions

View File

@ -1,5 +1,6 @@
type AdapterFunction<T> = (item: T, ...params: any[]) => Promise<any>; type AdapterFunction<T> = (item: T, ...params: any[]) => Promise<any>;
type SyncAdapterFunction<T> = (item: T, ...params: any[]) => 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 execute: Map<string, AdapterFunction<T>> = new Map();
private syncExecutes: Map<string, SyncAdapterFunction<T>> = new Map(); private syncExecutes: Map<string, SyncAdapterFunction<T>> = new Map();
private globalExecutes: Map<string, GlobalAdapterFunction> = new Map();
constructor(id: string) { constructor(id: string) {
this.id = id; this.id = id;
@ -36,11 +38,20 @@ export class RenderAdapter<T> {
this.items.delete(item); this.items.delete(item);
} }
/**
*
* @param id
* @param fn
*/
receiveGlobal(id: string, fn: GlobalAdapterFunction) {
this.globalExecutes.set(id, fn);
}
/** /**
* *
* @param fn * @param fn
*/ */
recieve(id: string, fn: AdapterFunction<T>): void { receive(id: string, fn: AdapterFunction<T>): void {
this.execute.set(id, fn); this.execute.set(id, fn);
} }
@ -48,7 +59,7 @@ export class RenderAdapter<T> {
* *
* @param fn * @param fn
*/ */
recieveSync(id: string, fn: SyncAdapterFunction<T>): void { receiveSync(id: string, fn: SyncAdapterFunction<T>): void {
this.syncExecutes.set(id, fn); 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 * adapter
*/ */

View File

@ -6,6 +6,7 @@ import { LayerGroup, FloorLayer } from './preset/layer';
import { MotaRenderer } from './render'; import { MotaRenderer } from './render';
import { LayerShadowExtends } from '../fx/shadow'; import { LayerShadowExtends } from '../fx/shadow';
import { LayerGroupFilter } from '@/plugin/fx/gameCanvas'; import { LayerGroupFilter } from '@/plugin/fx/gameCanvas';
import { LayerGroupAnimate } from './preset/animate';
let main: MotaRenderer; let main: MotaRenderer;
@ -31,12 +32,14 @@ Mota.require('var', 'loading').once('loaded', () => {
const door = new LayerDoorAnimate(); const door = new LayerDoorAnimate();
const shadow = new LayerShadowExtends(); const shadow = new LayerShadowExtends();
const filter = new LayerGroupFilter(); const filter = new LayerGroupFilter();
const animate = new LayerGroupAnimate();
layer.extends(damage); layer.extends(damage);
layer.extends(detail); layer.extends(detail);
layer.extends(filter); layer.extends(filter);
layer.getLayer('event')?.extends(hero); layer.getLayer('event')?.extends(hero);
layer.getLayer('event')?.extends(door); layer.getLayer('event')?.extends(door);
layer.getLayer('event')?.extends(shadow); layer.getLayer('event')?.extends(shadow);
layer.extends(animate);
render.appendChild(layer); render.appendChild(layer);
}); });

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);
});

View File

@ -381,9 +381,9 @@ export class LayerDoorAnimate implements ILayerRenderExtends {
} }
const doorAdapter = new RenderAdapter<LayerDoorAnimate>('door-animate'); const doorAdapter = new RenderAdapter<LayerDoorAnimate>('door-animate');
doorAdapter.recieve('openDoor', (item, block: Block) => { doorAdapter.receive('openDoor', (item, block: Block) => {
return item.openDoor(block); return item.openDoor(block);
}); });
doorAdapter.recieve('closeDoor', (item, block: Block) => { doorAdapter.receive('closeDoor', (item, block: Block) => {
return item.closeDoor(block); return item.closeDoor(block);
}); });

View File

@ -359,42 +359,42 @@ export class HeroRenderer
} }
const adapter = new RenderAdapter<HeroRenderer>('hero-adapter'); const adapter = new RenderAdapter<HeroRenderer>('hero-adapter');
adapter.recieve('readyMove', item => { adapter.receive('readyMove', item => {
item.readyMove(); item.readyMove();
return Promise.resolve(); return Promise.resolve();
}); });
adapter.recieve('move', (item, dir: Dir) => { adapter.receive('move', (item, dir: Dir) => {
return item.move(dir); return item.move(dir);
}); });
adapter.recieve('endMove', item => { adapter.receive('endMove', item => {
return item.endMove(); return item.endMove();
}); });
adapter.recieve( adapter.receive(
'moveAs', 'moveAs',
(item, x: number, y: number, time: number, fn: TimingFn<3>) => { (item, x: number, y: number, time: number, fn: TimingFn<3>) => {
return item.moveAs(x, y, time, fn); return item.moveAs(x, y, time, fn);
} }
); );
adapter.recieve('setMoveSpeed', (item, speed: number) => { adapter.receive('setMoveSpeed', (item, speed: number) => {
item.setMoveSpeed(speed); item.setMoveSpeed(speed);
return Promise.resolve(); return Promise.resolve();
}); });
adapter.recieve('setHeroLoc', (item, x?: number, y?: number) => { adapter.receive('setHeroLoc', (item, x?: number, y?: number) => {
item.setHeroLoc(x, y); item.setHeroLoc(x, y);
return Promise.resolve(); return Promise.resolve();
}); });
adapter.recieve('turn', (item, dir: Dir2) => { adapter.receive('turn', (item, dir: Dir2) => {
item.turn(dir); item.turn(dir);
return Promise.resolve(); return Promise.resolve();
}); });
adapter.recieve('setImage', (item, image: SizedCanvasImageSource) => { adapter.receive('setImage', (item, image: SizedCanvasImageSource) => {
item.setImage(image); item.setImage(image);
return Promise.resolve(); return Promise.resolve();
}); });
// 同步fallback用于适配现在的样板之后会删除 // 同步fallback用于适配现在的样板之后会删除
adapter.recieveSync('setHeroLoc', (item, x?: number, y?: number) => { adapter.receiveSync('setHeroLoc', (item, x?: number, y?: number) => {
item.setHeroLoc(x, y); item.setHeroLoc(x, y);
}); });
adapter.recieveSync('turn', (item, dir: Dir2) => { adapter.receiveSync('turn', (item, dir: Dir2) => {
item.turn(dir); item.turn(dir);
}); });

View File

@ -1,26 +1,30 @@
import type { RenderAdapter } from '@/core/render/adapter'; import type { RenderAdapter } from '@/core/render/adapter';
import type { LayerGroupAnimate } from '@/core/render/preset/animate';
import type { LayerDoorAnimate } from '@/core/render/preset/floor'; import type { LayerDoorAnimate } from '@/core/render/preset/floor';
import type { HeroRenderer } from '@/core/render/preset/hero'; import type { HeroRenderer } from '@/core/render/preset/hero';
import { hook } from '@/game/game';
interface Adapters { interface Adapters {
'hero-adapter'?: RenderAdapter<HeroRenderer>; 'hero-adapter'?: RenderAdapter<HeroRenderer>;
'door-animate'?: RenderAdapter<LayerDoorAnimate>; 'door-animate'?: RenderAdapter<LayerDoorAnimate>;
animate?: RenderAdapter<LayerGroupAnimate>;
} }
const adapters: Adapters = {}; const adapters: Adapters = {};
export function init() { export function init() {
const hook = Mota.require('var', 'hook'); const hook = Mota.require('var', 'hook');
const loading = Mota.require('var', 'loading');
let fallbackIds: number = 1e8; let fallbackIds: number = 1e8;
if (!main.replayChecking && main.mode === 'play') { if (!main.replayChecking && main.mode === 'play') {
const Adapter = Mota.require('module', 'Render').RenderAdapter; const Adapter = Mota.require('module', 'Render').RenderAdapter;
const hero = Adapter.get<HeroRenderer>('hero-adapter'); const hero = Adapter.get<HeroRenderer>('hero-adapter');
const doorAnimate = Adapter.get<LayerDoorAnimate>('door-animate'); const doorAnimate = Adapter.get<LayerDoorAnimate>('door-animate');
const animate = Adapter.get<LayerGroupAnimate>('animate');
adapters['hero-adapter'] = hero; adapters['hero-adapter'] = hero;
adapters['door-animate'] = doorAnimate; adapters['door-animate'] = doorAnimate;
adapters['animate'] = animate;
} }
let moving: boolean = false; let moving: boolean = false;
@ -391,6 +395,87 @@ export function init() {
this._openDoor_animate(block, x, y, callback); this._openDoor_animate(block, x, y, callback);
} }
}; };
// ----- animate & hero animate
////// 绘制动画 //////
maps.prototype.drawAnimate = function (
name: AnimationIds,
x: number,
y: number,
alignWindow?: boolean,
callback?: () => void
) {
// @ts-ignore
name = core.getMappedName(name);
// 正在播放录像:不显示动画
if (
core.isReplaying() ||
!core.material.animates[name] ||
x == null ||
y == null
) {
if (callback) callback();
return -1;
}
adapters.animate
?.all(
'drawAnimate',
name,
x * 32 + 16,
y * 32 + 16,
alignWindow ?? false
)
.then(() => {
callback?.();
});
};
maps.prototype.drawHeroAnimate = function (
name: AnimationIds,
callback?: () => void
) {
// @ts-ignore
name = core.getMappedName(name);
// 正在播放录像或动画不存在:不显示动画
if (core.isReplaying() || !core.material.animates[name]) {
if (callback) callback();
return -1;
}
adapters.animate?.global('drawHeroAnimate', name).then(() => {
callback?.();
});
// 开始绘制
// var animate = core.material.animates[name];
// animate.se = animate.se || {};
// if (typeof animate.se == 'string') animate.se = { 1: animate.se };
// var id = setTimeout(null);
// core.status.animateObjs.push({
// name: name,
// id: id,
// animate: animate,
// hero: true,
// index: 0,
// callback: callback
// });
// return id;
};
});
loading.once('loaded', () => {
for (const animate of Object.values(core.material.animates)) {
animate.se ??= {};
if (typeof animate.se === 'string') {
animate.se = { 1: animate.se };
}
animate.pitch ??= {};
}
}); });
return { readyMove, endMove, move }; return { readyMove, endMove, move };

5
src/types/util.d.ts vendored
View File

@ -753,7 +753,7 @@ type EventValuePreffix =
interface Animate { interface Animate {
/** /**
* s *
*/ */
frame: number; frame: number;
@ -775,7 +775,8 @@ interface Animate {
/** /**
* *
*/ */
se: string; se: any;
pitch: any;
} }
type Save = DeepReadonly<{ type Save = DeepReadonly<{