import { Animation, TimingFn, Transition } from 'mutate-animate'; import { RenderItem, Transform } from '@motajs/render-core'; import { logger } from '@motajs/common'; import EventEmitter from 'eventemitter3'; export interface ICameraTranslate { readonly type: 'translate'; readonly from: Camera; x: number; y: number; } export interface ICameraRotate { readonly type: 'rotate'; readonly from: Camera; /** 旋转角,单位弧度 */ angle: number; } export interface ICameraScale { readonly type: 'scale'; readonly from: Camera; x: number; y: number; } type CameraOperation = ICameraTranslate | ICameraScale | ICameraRotate; interface CameraEvent { destroy: []; } export class Camera extends EventEmitter { /** 当前绑定的渲染元素 */ readonly binded: RenderItem; /** 目标变换矩阵,默认与 `this.binded.transform` 同引用 */ transform: Transform; /** 委托ticker的id */ private delegation: number; /** 所有的动画id */ private animationIds: Set = new Set(); /** 是否需要更新视角 */ private needUpdate: boolean = false; /** 是否启用摄像机 */ private enabled: boolean = true; /** 变换操作列表,因为矩阵乘法跟顺序有关,因此需要把各个操作拆分成列表进行 */ protected operation: CameraOperation[] = []; /** 渲染元素到摄像机的映射 */ private static cameraMap: Map = new Map(); /** * 获取一个渲染元素的摄像机,如果不存在则为它创建一个并返回。注意使用`new Camera`创建的摄像机不在此列 * @param item 渲染元素 */ static for(item: RenderItem) { const camera = this.cameraMap.get(item); if (!camera) { const ca = new Camera(item); this.cameraMap.set(item, ca); return ca; } else { return camera; } } constructor(item: RenderItem) { super(); this.binded = item; this.delegation = item.delegateTicker(() => this.tick()); this.transform = item.transform; item.on('destroy', () => { this.destroy(); }); const ca = Camera.cameraMap.get(item); if (ca && ca.enabled) { logger.warn(22); } } private tick = () => { if (!this.needUpdate || !this.enabled) return; const trans = this.transform; trans.reset(); for (const o of this.operation) { if (o.type === 'translate') { trans.translate(-o.x, -o.y); } else if (o.type === 'rotate') { trans.rotate(o.angle); } else { trans.scale(o.x, o.y); } } this.binded.update(this.binded); this.needUpdate = false; }; /** * 禁用这个摄像机 */ disable() { this.enabled = false; } /** * 启用这个摄像机 */ enable() { this.enabled = true; } /** * 在下一帧进行强制更新 */ requestUpdate() { this.needUpdate = true; } /** * 移除一个变换操作 * @param operation 要移除的操作 */ removeOperation(operation: CameraOperation) { const index = this.operation.indexOf(operation); if (index === -1) return; this.operation.splice(index, 1); } /** * 清空变换操作列表 */ clearOperation() { this.operation.splice(0); } /** * 添加一个平移操作 * @returns 添加的平移变换操作 */ addTranslate(): ICameraTranslate { const item: ICameraTranslate = { type: 'translate', x: 0, y: 0, from: this }; this.operation.push(item); return item; } /** * 添加一个旋转操作 * @returns 添加的旋转变换操作 */ addRotate(): ICameraRotate { const item: ICameraRotate = { type: 'rotate', angle: 0, from: this }; this.operation.push(item); return item; } /** * 添加一个放缩操作 * @returns 添加的放缩变换操作 */ addScale(): ICameraScale { const item: ICameraScale = { type: 'scale', x: 1, y: 1, from: this }; this.operation.push(item); return item; } /** * 施加动画 * @param time 动画时长 * @param update 每帧的更新函数 */ applyAnimation(time: number, update: () => void) { const delegation = this.binded.delegateTicker( () => { update(); this.needUpdate = true; }, time, () => { update(); this.needUpdate = true; this.animationIds.delete(delegation); } ); this.animationIds.add(delegation); } /** * 为一个平移操作实施动画 * @param operation 平移操作 * @param animate 动画实例 * @param time 动画时长 */ applyTranslateAnimation( operation: ICameraTranslate, animate: Animation, time: number ) { if (operation.from !== this) { logger.warn(20); return; } const update = () => { operation.x = animate.x; operation.y = animate.y; }; this.applyAnimation(time, update); } /** * 为一个旋转操作实施动画 * @param operation 旋转操作 * @param animate 动画实例 * @param time 动画时长 */ applyRotateAnimation( operation: ICameraRotate, animate: Animation, time: number ) { if (operation.from !== this) { logger.warn(20); return; } const update = () => { operation.angle = animate.angle; }; this.applyAnimation(time, update); } /** * 为一个缩放操作实施动画 * @param operation 缩放操作 * @param animate 动画实例 * @param time 动画时长 */ applyScaleAnimation( operation: ICameraScale, animate: Animation, time: number ) { if (operation.from !== this) { logger.warn(20); return; } const update = () => { operation.x = animate.size; operation.y = animate.size; }; this.applyAnimation(time, update); } /** * 为一个平移操作实施渐变,使用渐变的 x,y 值,即`transition.value.x`与`transition.value.y` * @param operation 平移操作 * @param animate 渐变实例 * @param time 渐变时长 */ applyTranslateTransition( operation: ICameraTranslate, animate: Transition, time: number ) { if (operation.from !== this) { logger.warn(21); return; } const update = () => { operation.x = animate.value.x; operation.y = animate.value.y; }; this.applyAnimation(time, update); } /** * 为一个旋转操作实施渐变,使用渐变的 angle 值,即`transition.value.angle` * @param operation 旋转操作 * @param animate 渐变实例 * @param time 渐变时长 */ applyRotateTransition( operation: ICameraRotate, animate: Transition, time: number ) { if (operation.from !== this) { logger.warn(21); return; } const update = () => { operation.angle = animate.value.angle; }; this.applyAnimation(time, update); } /** * 为一个缩放操作实施渐变,使用渐变的 size 值,即`transition.value.size` * @param operation 缩放操作 * @param animate 渐变实例 * @param time 渐变时长 */ applyScaleTransition( operation: ICameraScale, animate: Transition, time: number ) { if (operation.from !== this) { logger.warn(21); return; } const update = () => { operation.x = animate.value.size; operation.y = animate.value.size; }; this.applyAnimation(time, update); } /** * 停止所有动画 */ stopAllAnimates() { this.animationIds.forEach(v => this.binded.removeTicker(v)); } /** * 摧毁这个摄像机,当绑定元素被摧毁之后摄像机会一并摧毁,如果这个摄像机不使用了,一定要将它摧毁 */ destroy() { this.binded.removeTicker(this.delegation); this.animationIds.forEach(v => this.binded.removeTicker(v)); Camera.cameraMap.delete(this.binded); this.emit('destroy'); } } interface CameraAnimationBase { type: string; time: number; start: number; } export interface TranslateAnimation extends CameraAnimationBase { type: 'translate'; timing: TimingFn; x: number; y: number; } export interface TranslateAsAnimation extends CameraAnimationBase { type: 'translateAs'; timing: TimingFn<2>; time: number; } export interface RotateAnimation extends CameraAnimationBase { type: 'rotate'; timing: TimingFn; angle: number; time: number; } export interface ScaleAnimation extends CameraAnimationBase { type: 'scale'; timing: TimingFn; scale: number; time: number; } export type CameraAnimationData = | TranslateAnimation | TranslateAsAnimation | RotateAnimation | ScaleAnimation; export interface CameraAnimationExecution { data: CameraAnimationData[]; animation: Animation; } interface CameraAnimationEvent { animate: [ operation: CameraOperation, execution: CameraAnimationExecution, item: CameraAnimationData ]; } export class CameraAnimation extends EventEmitter { camera: Camera; /** 动画开始时刻 */ private startTime: number = 0; /** 动画结束时刻 */ private endTime: number = 0; /** 委托ticker的id */ private delegation: number; /** 动画是否开始 */ private started: boolean = false; /** 每个摄像机操作的动画映射 */ private animateMap: Map = new Map(); constructor(camera: Camera) { super(); this.camera = camera; this.delegation = camera.binded.delegateTicker(this.tick); } private tick = () => { if (!this.started) return; const now = Date.now(); const time = now - this.startTime; if (now - this.startTime > this.endTime + 50) { this.destroy(); return; } this.animateMap.forEach((exe, ope) => { const data = exe.data; if (data.length === 0) return; const item = data[0]; if (item.start < time) { this.executeAnimate(exe, item); data.shift(); this.emit('animate', ope, exe, item); } }); this.camera.requestUpdate(); }; private executeAnimate( execution: CameraAnimationExecution, animate: CameraAnimationData ) { if (animate.type === 'translateAs') { const ani = this.ensureAnimate(execution); ani.time(animate.time).moveAs(animate.timing); } else if (animate.type === 'translate') { const ani = this.ensureAnimate(execution); const { x, y, time, timing } = animate; ani.mode(timing).time(time).move(x, y); } else if (animate.type === 'rotate') { const ani = this.ensureAnimate(execution); const { angle, time, timing } = animate; ani.mode(timing).time(time).rotate(angle); } else { const ani = this.ensureAnimate(execution); const { scale, time, timing } = animate; ani.mode(timing).time(time).scale(scale); } } private ensureAnimate(execution: CameraAnimationExecution) { if (execution.animation) return execution.animation; const ani = new Animation(); execution.animation = ani; return ani; } private ensureOperation(operation: CameraOperation) { if (!this.animateMap.has(operation)) { const data: CameraAnimationExecution = { data: [], animation: new Animation() }; this.animateMap.set(operation, data); return data; } else { return this.animateMap.get(operation)!; } } /** * 添加一个平移动画 * @param operation 摄像机操作对象 * @param x 目标横坐标 * @param y 目标纵坐标 * @param time 动画时长 * @param start 动画开始时间 * @param timing 动画的缓动函数 */ translate( operation: ICameraTranslate, x: number, y: number, time: number, start: number, timing: TimingFn ) { const exe = this.ensureOperation(operation); const data: TranslateAnimation = { type: 'translate', timing, x: x * 32, y: y * 32, time, start }; exe.data.push(data); } /** * 添加一个旋转动画 * @param operation 摄像机操作 * @param angle 目标旋转弧度,单位弧度 * @param time 动画时长 * @param start 动画开始时间 * @param timing 动画的缓动函数 */ rotate( operation: ICameraRotate, angle: number, time: number, start: number, timing: TimingFn ) { const exe = this.ensureOperation(operation); const data: RotateAnimation = { type: 'rotate', timing, angle, time, start }; exe.data.push(data); } /** * 添加一个缩放动画 * @param operation 摄像机操作 * @param scale 目标缩放倍率 * @param time 动画时长 * @param start 动画开始时间 * @param timing 动画的缓动函数 */ scale( operation: ICameraScale, scale: number, time: number, start: number, timing: TimingFn ) { const exe = this.ensureOperation(operation); const data: ScaleAnimation = { type: 'scale', timing, scale, time, start }; exe.data.push(data); } /** * 开始执行这个动画 */ start() { if (this.started) return; this.startTime = Date.now(); this.started = true; let endTime = 0; this.animateMap.forEach((exe, ope) => { const data = exe.data; data.sort((a, b) => a.start - b.start); const end = data.at(-1); if (!end) return; const t = end.start + end.time; if (t > endTime) endTime = t; const cam = this.camera; if (ope.type === 'translate') { cam.applyTranslateAnimation(ope, exe.animation, t + 100); } else if (ope.type === 'rotate') { cam.applyRotateAnimation(ope, exe.animation, t + 100); } else { cam.applyScaleAnimation(ope, exe.animation, t + 100); } }); this.endTime = endTime + this.startTime; this.tick(); } destroy() { this.camera.binded.removeTicker(this.delegation); this.camera.stopAllAnimates(); this.animateMap.forEach(v => { v.animation.ticker.destroy(); }); this.animateMap.clear(); } }