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

View file

@ -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))

View file

@ -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<T extends AnimType> = AnimationProperties<T> | number
type AnimWithSmartAnimation<T extends AnimType> = [SmartAnimation | Anim<T>, ...Anim<T>[]]
export class Animation<T extends AnimType> extends Class {
protected keyframes: KeyframeController<AnimType>[] = []
protected loop = false
private smartAnimationMode: SmartAnimation = "none"
constructor(
readonly type: T,
...keyframes: (AnimationProperties<T> | number)[]
...keyframes: AnimWithSmartAnimation<T>
) {
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() {
@ -24,27 +38,90 @@ export class Animation<T extends AnimType> extends Class {
return this.keyframes[0]
}
addKeyframes(...keyframes: (AnimationProperties<T> | number)[]) {
for (const $ of keyframes) {
let keyframe: AnimationProperties<AnimType>, 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<T>) {
if (typeof keyframe === "number") {
return { type: AnimType.WAIT, properties: <AnimationProperties<T>>(<unknown>{ duration: keyframe }) }
} else {
return { type: this.type, properties: keyframe }
}
}
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 (prevKeyframe) prevKeyframe.setNext(keyframeController)
if (this.loop) keyframeController.setNext(this.firstKey() as KeyframeController<T>)
this.keyframes.push(keyframeController)
}
if (this.loop) this.lastKey().setNext(this.firstKey())
}
setLoop(boolean: boolean) {
@ -60,4 +137,19 @@ export class Animation<T extends AnimType> 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`
}
}

View file

@ -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<T extends AnimType> extends Class {
readonly name: string
readonly namespace: string
readonly extend?: AnimationKeyframe<T> | Animation<T>
constructor(
readonly type: T,
readonly properties: KeyframeAnimationProperties<T>,
@ -38,14 +41,28 @@ export class AnimationKeyframe<T extends AnimType> extends Class {
Memory.add(this)
}
setNext(keyframe: AnimationKeyframe<AnimType>) {
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(),
}
}
}

View file

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

View file

@ -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<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 { BagBinding } from "./BagBinding.js"
export { Binding } from "./Binding.js"
export { SmartAnimation } from "./SmartAnimation.js"

View file

@ -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<number>
@ -65,7 +66,7 @@ export interface 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]> &

View file

@ -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)