From 1114828000734d356a2b84314692cfc85d90a7cf Mon Sep 17 00:00:00 2001 From: Asaki Yuki Date: Mon, 26 Jan 2026 15:35:55 +0700 Subject: [PATCH] feat: smart animation mode --- src/compilers/Configuration.ts | 1 + src/compilers/Memory.ts | 2 + src/compilers/RunEnd.ts | 3 +- src/components/Animation.ts | 132 ++++++++++++++++++---- src/components/AnimationKeyframe.ts | 23 +++- src/components/KeyframeController.ts | 10 +- src/components/Utils.ts | 45 +++++++- src/types/enums/SmartAnimation.ts | 1 + src/types/enums/index.ts | 3 +- src/types/properties/element/Animation.ts | 5 +- test/app.ts | 26 +++-- 11 files changed, 207 insertions(+), 44 deletions(-) create mode 100644 src/compilers/Configuration.ts create mode 100644 src/types/enums/SmartAnimation.ts diff --git a/src/compilers/Configuration.ts b/src/compilers/Configuration.ts new file mode 100644 index 0000000..32b62ef --- /dev/null +++ b/src/compilers/Configuration.ts @@ -0,0 +1 @@ +export const isBuildMode = process.argv.includes("--build") diff --git a/src/compilers/Memory.ts b/src/compilers/Memory.ts index 37a219b..5e8579f 100644 --- a/src/compilers/Memory.ts +++ b/src/compilers/Memory.ts @@ -4,6 +4,7 @@ import { UI } from "../components/UI.js" import { AnimType } from "../types/enums/AnimType.js" import { Renderer } from "../types/enums/Renderer.js" import { Type } from "../types/enums/Type.js" +import { isBuildMode } from "./Configuration.js" type Element = UI | AnimationKeyframe interface FileInterface { @@ -16,6 +17,7 @@ export class Memory extends Class { protected static files: Files = new Map() public static add(element: UI | AnimationKeyframe) { + if (!isBuildMode) return let file = Memory.files.get(element.path) if (!file) { diff --git a/src/compilers/RunEnd.ts b/src/compilers/RunEnd.ts index 88ee3fb..0383301 100644 --- a/src/compilers/RunEnd.ts +++ b/src/compilers/RunEnd.ts @@ -1,7 +1,6 @@ +import { isBuildMode } from "./Configuration.js" import { Memory } from "./Memory.js" -const isBuildMode = process.argv.includes("--build") - if (isBuildMode) { process.on("beforeExit", () => { console.log(JSON.stringify(Memory.build(), null, 2)) diff --git a/src/components/Animation.ts b/src/components/Animation.ts index d475dd1..39c5ce7 100644 --- a/src/components/Animation.ts +++ b/src/components/Animation.ts @@ -1,19 +1,33 @@ import { AnimType } from "../types/enums/AnimType.js" +import { SmartAnimation } from "../types/enums/SmartAnimation.js" import { AnimationProperties } from "../types/properties/element/Animation.js" import { Class } from "./Class.js" import { KeyframeController } from "./KeyframeController.js" +import util from "node:util" + +type Anim = AnimationProperties | number +type AnimWithSmartAnimation = [SmartAnimation | Anim, ...Anim[]] + export class Animation extends Class { protected keyframes: KeyframeController[] = [] protected loop = false + private smartAnimationMode: SmartAnimation = "none" constructor( readonly type: T, - ...keyframes: (AnimationProperties | number)[] + ...keyframes: AnimWithSmartAnimation ) { super() - if (type === AnimType.WAIT) console.warn("Why are you create a wait animation?") - this.addKeyframes(...keyframes) + + if ([AnimType.ASEPRITE_FLIP_BOOK, AnimType.FLIP_BOOK, AnimType.WAIT].includes(type)) { + throw new Error(`${type} is not need for Animation constructor, please use AnimetionKeyframe instead!`) + } + + if (typeof keyframes[0] === "string") { + this.smartAnimationMode = keyframes[0] + this.addKeyframes(...(keyframes.slice(1) as Anim[])) + } else this.addKeyframes(...(keyframes as Anim[])) } protected lastKey() { @@ -24,27 +38,90 @@ export class Animation extends Class { return this.keyframes[0] } - addKeyframes(...keyframes: (AnimationProperties | number)[]) { - for (const $ of keyframes) { - let keyframe: AnimationProperties, animType: AnimType + at(index: number) { + const frame = this.keyframes[index] + if (frame) return frame + else throw new Error(`No frame at index ${index}`) + } - if (typeof $ === "number") { - keyframe = { duration: $ } - animType = AnimType.WAIT - } else { - keyframe = $ - animType = this.type + private transformKeyframe(keyframe: Anim) { + if (typeof keyframe === "number") { + return { type: AnimType.WAIT, properties: >({ duration: keyframe }) } + } else { + return { type: this.type, properties: keyframe } + } + } + + private addKeyframes(...keyframes: Anim[]) { + if (this.smartAnimationMode === "none") { + for (const $ of keyframes) { + const { type, properties } = this.transformKeyframe($) + const keyframeController = new KeyframeController(type, properties) + const prevKeyframe = this.lastKey() + if (prevKeyframe) prevKeyframe.setNext(keyframeController) + this.keyframes.push(keyframeController) + } + } else if (this.smartAnimationMode === "frame") { + let lastDuration = 0 + + for (const $ of keyframes) { + const { type, properties } = <{ type: AnimType; properties: { from?: unknown; to?: unknown } }>( + (this.transformKeyframe($)) + ) + + if ((<{ duration?: number }>(properties)).duration !== undefined) + lastDuration = (<{ duration?: number }>(properties)).duration || 0 + ;(<{ duration?: number }>(properties)).duration = lastDuration + + if (type !== AnimType.WAIT) { + const { from, to } = properties + if (from === undefined) properties.from = to + else if (to === undefined) properties.to = from + } + + const keyframeController = new KeyframeController(type, >properties) + const prevKeyframe = this.lastKey() + if (prevKeyframe) prevKeyframe.setNext(keyframeController) + this.keyframes.push(keyframeController) + } + } else if (this.smartAnimationMode === "smooth" || this.smartAnimationMode === "smooth_loop") { + let lastDuration = 0, + lastTo + + for (const $ of keyframes) { + const { type, properties } = <{ type: AnimType; properties: { from?: unknown; to?: unknown } }>( + (this.transformKeyframe($)) + ) + + if (properties.to === undefined && type !== AnimType.WAIT) + throw new Error(`To property is required in smooth mode`) + + if ((<{ duration?: number }>(properties)).duration !== undefined) + lastDuration = (<{ duration?: number }>(properties)).duration || 0 + ;(<{ duration?: number }>(properties)).duration = lastDuration + + if (type !== AnimType.WAIT) { + if (properties.to) { + if (properties.from === undefined) properties.from = lastTo + lastTo = properties.to + } + } + + const keyframeController = new KeyframeController(type, >properties) + const prevKeyframe = this.lastKey() + if (prevKeyframe) prevKeyframe.setNext(keyframeController) + this.keyframes.push(keyframeController) } - const keyframeController = new KeyframeController(animType, keyframe as AnimationProperties) + if (this.smartAnimationMode === "smooth_loop") { + const firstKey = <{ properties: { from?: unknown } }>(this.firstKey()) + if (firstKey.properties.from === undefined && lastTo !== undefined) { + firstKey.properties.from = lastTo + } + } + } else throw new Error(`Unknown smart animation mode: ${this.smartAnimationMode}`) - const prevKeyframe = this.lastKey() - if (prevKeyframe) prevKeyframe.setNext(keyframeController) - - if (this.loop) keyframeController.setNext(this.firstKey() as KeyframeController) - - this.keyframes.push(keyframeController) - } + if (this.loop) this.lastKey().setNext(this.firstKey()) } setLoop(boolean: boolean) { @@ -60,4 +137,19 @@ export class Animation extends Class { return this } + + protected toString() { + return String(this.firstKey()) + } + + protected [util.inspect.custom]($: any, opts: any) { + const out = this.keyframes.map((v, i) => { + return ` \x1b[33m${i}\x1b[0m: \x1b[33mKeyframe\x1b[0m<\x1b[92m${v.type}\x1b[0m> \x1b[92m"${v}\x1b[92m"\x1b[0m ${util.inspect(v.serialize(), opts)}`.replace( + /\n/g, + "\n ", + ) + }) + + return `\x1b[33mAnimation\x1b[0m<\x1b[92m${this.type}\x1b[0m> \x1b[92m"${this}\x1b[92m"\x1b[0m {\n${out.join("\n")}\n}\n` + } } diff --git a/src/components/AnimationKeyframe.ts b/src/components/AnimationKeyframe.ts index 0eb5349..79ec74e 100644 --- a/src/components/AnimationKeyframe.ts +++ b/src/components/AnimationKeyframe.ts @@ -2,6 +2,7 @@ import { FormatAnimationProperties } from "../compilers/FormatProperties.js" import { Memory } from "../compilers/Memory.js" import { AnimType } from "../types/enums/AnimType.js" import { KeyframeAnimationProperties } from "../types/properties/element/Animation.js" +import { Animation } from "./Animation.js" import { Class } from "./Class.js" import { RandomString } from "./Utils.js" @@ -12,6 +13,8 @@ export class AnimationKeyframe extends Class { readonly name: string readonly namespace: string + readonly extend?: AnimationKeyframe | Animation + constructor( readonly type: T, readonly properties: KeyframeAnimationProperties, @@ -38,14 +41,28 @@ export class AnimationKeyframe extends Class { Memory.add(this) } + setNext(keyframe: AnimationKeyframe) { + this.properties.next = keyframe + return this + } + + clearNext() { + delete this.properties.next + return this + } + protected toJsonUI() { return FormatAnimationProperties(this.properties) } protected toJSON() { - return { - anim_type: this.type, - ...this.toJsonUI(), + if (this.extend) { + return this.toJsonUI() + } else { + return { + anim_type: this.type, + ...this.toJsonUI(), + } } } diff --git a/src/components/KeyframeController.ts b/src/components/KeyframeController.ts index ddfb688..d1bdc7a 100644 --- a/src/components/KeyframeController.ts +++ b/src/components/KeyframeController.ts @@ -7,13 +7,7 @@ export class KeyframeController extends AnimationKeyframe super(type, properties, name, namespace, path) } - setNext(keyframe: AnimationKeyframe) { - this.properties.next = keyframe - return this - } - - clearNext() { - delete this.properties.next - return this + serialize() { + return this.toJsonUI() } } diff --git a/src/components/Utils.ts b/src/components/Utils.ts index d18a570..ecd46f3 100644 --- a/src/components/Utils.ts +++ b/src/components/Utils.ts @@ -34,7 +34,9 @@ import { Parser } from "../compilers/bindings/Parser.js" import { BindingType } from "../types/enums/BindingType.js" import { AnimType } from "../types/enums/AnimType.js" import { AnimationKeyframe } from "./AnimationKeyframe.js" -import { KeyframeAnimationProperties } from "../types/properties/element/Animation.js" +import { AnimationProperties, KeyframeAnimationProperties } from "../types/properties/element/Animation.js" +import { Animation } from "./Animation.js" +import { SmartAnimation } from "../types/enums/SmartAnimation.js" const CHARS = "0123456789abcdefghijklmnopqrstuvwxyz" type CompileBinding = `[${string}]` @@ -315,3 +317,44 @@ export function KeyframeAsepriteFlipBook( ) { return new AnimationKeyframe(AnimType.ASEPRITE_FLIP_BOOK, properties || {}, name, namespace) } + +// Quick Animation +type Anim = AnimationProperties | number +type AnimWithSmartAnim = [SmartAnimation | Anim, ...Anim[]] + +export function AnimationOffset(...keyframes: AnimWithSmartAnim) { + return new Animation(AnimType.OFFSET, ...keyframes) +} + +export function AnimationSize(...keyframes: AnimWithSmartAnim) { + return new Animation(AnimType.SIZE, ...keyframes) +} + +export function AnimationUV(...keyframes: AnimWithSmartAnim) { + return new Animation(AnimType.UV, ...keyframes) +} + +export function AnimationClip(...keyframes: AnimWithSmartAnim) { + return new Animation(AnimType.CLIP, ...keyframes) +} + +export function AnimationColor(...keyframes: AnimWithSmartAnim) { + return new Animation(AnimType.COLOR, ...keyframes) +} + +export function AnimationAlpha(...keyframes: AnimWithSmartAnim) { + return new Animation(AnimType.ALPHA, ...keyframes) +} + +// Animation Extendof +export function AnimationExtendsOf( + animation: AnimationKeyframe | Animation, + properties?: AnimationProperties, +) { + const anim = new AnimationKeyframe(animation.type, properties || {}) + + // @ts-ignore + anim.extend = animation + + return anim +} diff --git a/src/types/enums/SmartAnimation.ts b/src/types/enums/SmartAnimation.ts new file mode 100644 index 0000000..13934a0 --- /dev/null +++ b/src/types/enums/SmartAnimation.ts @@ -0,0 +1 @@ +export type SmartAnimation = "none" | "frame" | "smooth" | "smooth_loop" diff --git a/src/types/enums/index.ts b/src/types/enums/index.ts index ad47853..3e12567 100644 --- a/src/types/enums/index.ts +++ b/src/types/enums/index.ts @@ -27,4 +27,5 @@ export { TextboxName } from "./TextboxName.js" export { SliderName } from "./SliderName.js" export { ToggleName } from "./ToggleName.js" export { BagBinding } from "./BagBinding.js" -export { Binding } from "./Binding.js" \ No newline at end of file +export { Binding } from "./Binding.js" +export { SmartAnimation } from "./SmartAnimation.js" diff --git a/src/types/properties/element/Animation.ts b/src/types/properties/element/Animation.ts index 68cb5ba..1a2222c 100644 --- a/src/types/properties/element/Animation.ts +++ b/src/types/properties/element/Animation.ts @@ -1,7 +1,8 @@ import { AnimationKeyframe } from "../../../components/AnimationKeyframe.js" import { AnimType } from "../../enums/AnimType.js" import { Easing } from "../../enums/Easing.js" -import { Array2, Array3, Value } from "../value.js" +import { Animation, Array2, Array3, Value } from "../value.js" +import * as components from "../../../components/Animation.js" export interface DurationAnimation { duration?: Value @@ -65,7 +66,7 @@ export interface AnimationPropertiesItem { } export interface KeyframeAnimationPropertiesItem extends AnimationPropertiesItem { - next?: Value> + next?: Value | components.Animation> } export type KeyframeAnimationProperties = Partial & diff --git a/test/app.ts b/test/app.ts index 9e8d6a8..0cd1597 100644 --- a/test/app.ts +++ b/test/app.ts @@ -1,10 +1,22 @@ -import { Animation, AnimType } from ".." +import { AnimationSize } from ".." -const animation = new Animation( - AnimType.OFFSET, +const animation = AnimationSize( + "smooth_loop", { - from: [0, 0], - to: [100, 100], + to: [10, 10], + duration: 1.5, }, - 123, -).setLoop(false) + { + to: [1, 1], + }, + 1, + { + from: [10, 10], + to: [20, 20], + }, + { + to: [1, 1], + }, +).setLoop(true) + +console.log(animation)