feat: smart animation mode

This commit is contained in:
Asaki Yuki 2026-01-26 15:35:55 +07:00
parent 7534a613cb
commit 1114828000
11 changed files with 207 additions and 44 deletions

View file

@ -0,0 +1 @@
export const isBuildMode = process.argv.includes("--build")

View file

@ -4,6 +4,7 @@ import { UI } from "../components/UI.js"
import { AnimType } from "../types/enums/AnimType.js" import { AnimType } from "../types/enums/AnimType.js"
import { Renderer } from "../types/enums/Renderer.js" import { Renderer } from "../types/enums/Renderer.js"
import { Type } from "../types/enums/Type.js" import { Type } from "../types/enums/Type.js"
import { isBuildMode } from "./Configuration.js"
type Element = UI<Type, Renderer | null> | AnimationKeyframe<AnimType> type Element = UI<Type, Renderer | null> | AnimationKeyframe<AnimType>
interface FileInterface { interface FileInterface {
@ -16,6 +17,7 @@ export class Memory extends Class {
protected static files: Files = new Map<string, FileInterface>() protected static files: Files = new Map<string, FileInterface>()
public static add(element: UI<Type, Renderer | null> | AnimationKeyframe<AnimType>) { public static add(element: UI<Type, Renderer | null> | AnimationKeyframe<AnimType>) {
if (!isBuildMode) return
let file = Memory.files.get(element.path) let file = Memory.files.get(element.path)
if (!file) { if (!file) {

View file

@ -1,7 +1,6 @@
import { isBuildMode } from "./Configuration.js"
import { Memory } from "./Memory.js" import { Memory } from "./Memory.js"
const isBuildMode = process.argv.includes("--build")
if (isBuildMode) { if (isBuildMode) {
process.on("beforeExit", () => { process.on("beforeExit", () => {
console.log(JSON.stringify(Memory.build(), null, 2)) console.log(JSON.stringify(Memory.build(), null, 2))

View file

@ -1,19 +1,33 @@
import { AnimType } from "../types/enums/AnimType.js" import { AnimType } from "../types/enums/AnimType.js"
import { SmartAnimation } from "../types/enums/SmartAnimation.js"
import { AnimationProperties } from "../types/properties/element/Animation.js" import { AnimationProperties } from "../types/properties/element/Animation.js"
import { Class } from "./Class.js" import { Class } from "./Class.js"
import { KeyframeController } from "./KeyframeController.js" import { KeyframeController } from "./KeyframeController.js"
import util from "node:util"
type Anim<T extends AnimType> = AnimationProperties<T> | number
type AnimWithSmartAnimation<T extends AnimType> = [SmartAnimation | Anim<T>, ...Anim<T>[]]
export class Animation<T extends AnimType> extends Class { export class Animation<T extends AnimType> extends Class {
protected keyframes: KeyframeController<AnimType>[] = [] protected keyframes: KeyframeController<AnimType>[] = []
protected loop = false protected loop = false
private smartAnimationMode: SmartAnimation = "none"
constructor( constructor(
readonly type: T, readonly type: T,
...keyframes: (AnimationProperties<T> | number)[] ...keyframes: AnimWithSmartAnimation<T>
) { ) {
super() 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<T>[]))
} else this.addKeyframes(...(keyframes as Anim<T>[]))
} }
protected lastKey() { protected lastKey() {
@ -24,27 +38,90 @@ export class Animation<T extends AnimType> extends Class {
return this.keyframes[0] return this.keyframes[0]
} }
addKeyframes(...keyframes: (AnimationProperties<T> | number)[]) { at(index: number) {
for (const $ of keyframes) { const frame = this.keyframes[index]
let keyframe: AnimationProperties<AnimType>, animType: AnimType if (frame) return frame
else throw new Error(`No frame at index ${index}`)
}
if (typeof $ === "number") { private transformKeyframe(keyframe: Anim<T>) {
keyframe = { duration: $ } if (typeof keyframe === "number") {
animType = AnimType.WAIT return { type: AnimType.WAIT, properties: <AnimationProperties<T>>(<unknown>{ duration: keyframe }) }
} else { } else {
keyframe = $ return { type: this.type, properties: keyframe }
animType = this.type }
}
private addKeyframes(...keyframes: Anim<T>[]) {
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 } }>(
(<unknown>this.transformKeyframe($))
)
if ((<{ duration?: number }>(<unknown>properties)).duration !== undefined)
lastDuration = (<{ duration?: number }>(<unknown>properties)).duration || 0
;(<{ duration?: number }>(<unknown>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, <AnimationProperties<T>>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 } }>(
(<unknown>this.transformKeyframe($))
)
if (properties.to === undefined && type !== AnimType.WAIT)
throw new Error(`To property is required in smooth mode`)
if ((<{ duration?: number }>(<unknown>properties)).duration !== undefined)
lastDuration = (<{ duration?: number }>(<unknown>properties)).duration || 0
;(<{ duration?: number }>(<unknown>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, <AnimationProperties<T>>properties)
const prevKeyframe = this.lastKey()
if (prevKeyframe) prevKeyframe.setNext(keyframeController)
this.keyframes.push(keyframeController)
} }
const keyframeController = new KeyframeController(animType, keyframe as AnimationProperties<T>) if (this.smartAnimationMode === "smooth_loop") {
const firstKey = <{ properties: { from?: unknown } }>(<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 (this.loop) this.lastKey().setNext(this.firstKey())
if (prevKeyframe) prevKeyframe.setNext(keyframeController)
if (this.loop) keyframeController.setNext(this.firstKey() as KeyframeController<T>)
this.keyframes.push(keyframeController)
}
} }
setLoop(boolean: boolean) { setLoop(boolean: boolean) {
@ -60,4 +137,19 @@ export class Animation<T extends AnimType> extends Class {
return this 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`
}
} }

View file

@ -2,6 +2,7 @@ import { FormatAnimationProperties } from "../compilers/FormatProperties.js"
import { Memory } from "../compilers/Memory.js" import { Memory } from "../compilers/Memory.js"
import { AnimType } from "../types/enums/AnimType.js" import { AnimType } from "../types/enums/AnimType.js"
import { KeyframeAnimationProperties } from "../types/properties/element/Animation.js" import { KeyframeAnimationProperties } from "../types/properties/element/Animation.js"
import { Animation } from "./Animation.js"
import { Class } from "./Class.js" import { Class } from "./Class.js"
import { RandomString } from "./Utils.js" import { RandomString } from "./Utils.js"
@ -12,6 +13,8 @@ export class AnimationKeyframe<T extends AnimType> extends Class {
readonly name: string readonly name: string
readonly namespace: string readonly namespace: string
readonly extend?: AnimationKeyframe<T> | Animation<T>
constructor( constructor(
readonly type: T, readonly type: T,
readonly properties: KeyframeAnimationProperties<T>, readonly properties: KeyframeAnimationProperties<T>,
@ -38,14 +41,28 @@ export class AnimationKeyframe<T extends AnimType> extends Class {
Memory.add(this) Memory.add(this)
} }
setNext(keyframe: AnimationKeyframe<AnimType>) {
this.properties.next = keyframe
return this
}
clearNext() {
delete this.properties.next
return this
}
protected toJsonUI() { protected toJsonUI() {
return FormatAnimationProperties(this.properties) return FormatAnimationProperties(this.properties)
} }
protected toJSON() { protected toJSON() {
return { if (this.extend) {
anim_type: this.type, return this.toJsonUI()
...this.toJsonUI(), } else {
return {
anim_type: this.type,
...this.toJsonUI(),
}
} }
} }

View file

@ -7,13 +7,7 @@ export class KeyframeController<T extends AnimType> extends AnimationKeyframe<T>
super(type, properties, name, namespace, path) super(type, properties, name, namespace, path)
} }
setNext(keyframe: AnimationKeyframe<AnimType>) { serialize() {
this.properties.next = keyframe return this.toJsonUI()
return this
}
clearNext() {
delete this.properties.next
return this
} }
} }

View file

@ -34,7 +34,9 @@ import { Parser } from "../compilers/bindings/Parser.js"
import { BindingType } from "../types/enums/BindingType.js" import { BindingType } from "../types/enums/BindingType.js"
import { AnimType } from "../types/enums/AnimType.js" import { AnimType } from "../types/enums/AnimType.js"
import { AnimationKeyframe } from "./AnimationKeyframe.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" const CHARS = "0123456789abcdefghijklmnopqrstuvwxyz"
type CompileBinding = `[${string}]` type CompileBinding = `[${string}]`
@ -315,3 +317,44 @@ export function KeyframeAsepriteFlipBook(
) { ) {
return new AnimationKeyframe(AnimType.ASEPRITE_FLIP_BOOK, properties || {}, name, namespace) return new AnimationKeyframe(AnimType.ASEPRITE_FLIP_BOOK, properties || {}, name, namespace)
} }
// Quick Animation
type Anim<T extends AnimType> = AnimationProperties<T> | number
type AnimWithSmartAnim<T extends AnimType> = [SmartAnimation | Anim<T>, ...Anim<T>[]]
export function AnimationOffset(...keyframes: AnimWithSmartAnim<AnimType.OFFSET>) {
return new Animation(AnimType.OFFSET, ...keyframes)
}
export function AnimationSize(...keyframes: AnimWithSmartAnim<AnimType.SIZE>) {
return new Animation(AnimType.SIZE, ...keyframes)
}
export function AnimationUV(...keyframes: AnimWithSmartAnim<AnimType.UV>) {
return new Animation(AnimType.UV, ...keyframes)
}
export function AnimationClip(...keyframes: AnimWithSmartAnim<AnimType.CLIP>) {
return new Animation(AnimType.CLIP, ...keyframes)
}
export function AnimationColor(...keyframes: AnimWithSmartAnim<AnimType.COLOR>) {
return new Animation(AnimType.COLOR, ...keyframes)
}
export function AnimationAlpha(...keyframes: AnimWithSmartAnim<AnimType.ALPHA>) {
return new Animation(AnimType.ALPHA, ...keyframes)
}
// Animation Extendof
export function AnimationExtendsOf<T extends AnimType>(
animation: AnimationKeyframe<T> | Animation<T>,
properties?: AnimationProperties<T>,
) {
const anim = new AnimationKeyframe(animation.type, properties || {})
// @ts-ignore
anim.extend = animation
return anim
}

View file

@ -0,0 +1 @@
export type SmartAnimation = "none" | "frame" | "smooth" | "smooth_loop"

View file

@ -28,3 +28,4 @@ export { SliderName } from "./SliderName.js"
export { ToggleName } from "./ToggleName.js" export { ToggleName } from "./ToggleName.js"
export { BagBinding } from "./BagBinding.js" export { BagBinding } from "./BagBinding.js"
export { Binding } from "./Binding.js" export { Binding } from "./Binding.js"
export { SmartAnimation } from "./SmartAnimation.js"

View file

@ -1,7 +1,8 @@
import { AnimationKeyframe } from "../../../components/AnimationKeyframe.js" import { AnimationKeyframe } from "../../../components/AnimationKeyframe.js"
import { AnimType } from "../../enums/AnimType.js" import { AnimType } from "../../enums/AnimType.js"
import { Easing } from "../../enums/Easing.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 { export interface DurationAnimation {
duration?: Value<number> duration?: Value<number>
@ -65,7 +66,7 @@ export interface AnimationPropertiesItem {
} }
export interface KeyframeAnimationPropertiesItem extends AnimationPropertiesItem { export interface KeyframeAnimationPropertiesItem extends AnimationPropertiesItem {
next?: Value<string | AnimationKeyframe<AnimType>> next?: Value<string | AnimationKeyframe<AnimType> | components.Animation<AnimType>>
} }
export type KeyframeAnimationProperties<T extends keyof AnimationValueType> = Partial<AnimationValueType[T]> & export type KeyframeAnimationProperties<T extends keyof AnimationValueType> = Partial<AnimationValueType[T]> &

View file

@ -1,10 +1,22 @@
import { Animation, AnimType } from ".." import { AnimationSize } from ".."
const animation = new Animation( const animation = AnimationSize(
AnimType.OFFSET, "smooth_loop",
{ {
from: [0, 0], to: [10, 10],
to: [100, 100], duration: 1.5,
}, },
123, {
).setLoop(false) to: [1, 1],
},
1,
{
from: [10, 10],
to: [20, 20],
},
{
to: [1, 1],
},
).setLoop(true)
console.log(animation)