diff --git a/examples/frames.js b/examples/frames.js new file mode 100644 index 00000000..76e00a3d --- /dev/null +++ b/examples/frames.js @@ -0,0 +1,29 @@ +// @ts-check + +kaplay({ + scale: 4, + background: [0, 0, 0], +}); + +// https://0x72.itch.io/dungeontileset-ii +loadSpriteAtlas("/examples/sprites/dungeon.png", { + wizard: { + x: 128, + y: 140, + width: 144, + height: 28, + sliceX: 9, + anims: { + bouncy: { + frames: [8, 5, 0, 3, 2, 3, 0, 5], + speed: 10, + loop: true + } + }, + }, +}); + +add([ + sprite("wizard", { anim: "bouncy" }), + pos(100, 100), +]); diff --git a/src/assets/sprite.ts b/src/assets/sprite.ts index 8410003e..b1f7f34c 100644 --- a/src/assets/sprite.ts +++ b/src/assets/sprite.ts @@ -14,11 +14,11 @@ export type SpriteAnim = number | { /** * The starting frame. */ - from: number; + from?: number; /** * The end frame. */ - to: number; + to?: number; /** * If this anim should be played in loop. */ @@ -31,6 +31,12 @@ export type SpriteAnim = number | { * This anim's speed in frames per second. */ speed?: number; + /** + * List of frames for the animation. + * + * If this property exists, **from, to, and pingpong will be ignored**. + */ + frames?: number[]; }; /** diff --git a/src/components/draw/sprite.ts b/src/components/draw/sprite.ts index e3265beb..4d6991c7 100644 --- a/src/components/draw/sprite.ts +++ b/src/components/draw/sprite.ts @@ -214,14 +214,43 @@ export function sprite( obj.width = spr.tex.width * q.w * scale.x; obj.height = spr.tex.height * q.h * scale.y; - if (opt.anim) { - obj.play(opt.anim); + if (spr.anims) { + for (let animName in spr.anims) { + const anim = spr.anims[animName]; + if (typeof anim !== "number") { + anim.frames = createAnimFrames(anim); + } + } } spriteData = spr; spriteLoadedEvent.trigger(spriteData); + + if (opt.anim) { + obj.play(opt.anim); + } }; + const createAnimFrames = (anim: Exclude) => { + if (anim.frames) { + return anim.frames; + } + const frames = []; + if (anim.from === undefined || anim.to === undefined) { + throw new Error("Sprite anim 'from' and 'to' must be defined if 'frames' is not defined"); + } + const frameSeqLength = Math.abs(anim.to - anim.from) + 1; + for (let i = 0; i < frameSeqLength; i++) { + frames.push(anim.from + i * Math.sign(anim.to - anim.from)); + } + if (anim.pingpong) { + for (let i = frameSeqLength - 2; i > 0; i--) { + frames.push(frames[i]); + } + } + return frames; + } + return { id: "sprite", // TODO: allow update @@ -258,6 +287,10 @@ export function sprite( return anim; } + if (anim.from === undefined || anim.to === undefined) { + return curAnim.frameIndex; + } + return this.frame - Math.min(anim.from, anim.to); }, @@ -371,44 +404,36 @@ export function sprite( if (curAnim.timer >= (1 / curAnim.speed)) { curAnim.timer = 0; - this.frame += curAnimDir; - - if ( - this.frame < Math.min(anim.from, anim.to) - || this.frame > Math.max(anim.from, anim.to) - ) { - if (curAnim.loop) { - if (curAnim.pingpong) { - this.frame -= curAnimDir; - curAnimDir *= -1; - this.frame += curAnimDir; - } - else { - this.frame = anim.from; - } + curAnim.frameIndex += curAnimDir; + + const frames = anim.frames!; + if (curAnim.frameIndex >= frames.length) { + if (curAnim.pingpong && !anim.pingpong) { + curAnimDir = -1; + curAnim.frameIndex = frames.length - 2; + } else if (curAnim.loop) { + curAnim.frameIndex = 0; + } else { + this.frame = frames.at(-1)!; + curAnim.onEnd(); + this.stop(); + return; } - else { - if (curAnim.pingpong) { - const isForward = curAnimDir - === Math.sign(anim.to - anim.from); - if (isForward) { - this.frame = anim.to; - curAnimDir *= -1; - this.frame += curAnimDir; - } - else { - this.frame = anim.from; - curAnim.onEnd(); - this.stop(); - } - } - else { - this.frame = anim.to; - curAnim.onEnd(); - this.stop(); - } + } else if (curAnim.frameIndex < 0) { + if (curAnim.pingpong && curAnim.loop) { + curAnimDir = 1; + curAnim.frameIndex = 1; + } else if (curAnim.loop) { + curAnim.frameIndex = frames.length - 1; + } else { + this.frame = frames[0]; + curAnim.onEnd(); + this.stop(); + return; } } + + this.frame = frames[curAnim.frameIndex]; } }, @@ -439,7 +464,8 @@ export function sprite( loop: false, pingpong: false, speed: 0, - onEnd: () => {}, + frameIndex: 0, + onEnd: () => { }, } : { name: name, @@ -447,18 +473,12 @@ export function sprite( loop: opt.loop ?? anim.loop ?? false, pingpong: opt.pingpong ?? anim.pingpong ?? false, speed: opt.speed ?? anim.speed ?? 10, - onEnd: opt.onEnd ?? (() => {}), + frameIndex: 0, + onEnd: opt.onEnd ?? (() => { }), }; - curAnimDir = typeof anim === "number" - ? null - : anim.from < anim.to - ? 1 - : -1; - - this.frame = typeof anim === "number" - ? anim - : anim.from; + curAnimDir = typeof anim === "number" ? null : 1; + this.frame = typeof anim === "number" ? anim : anim.frames![0]; this.trigger("animStart", name); }, diff --git a/src/types.ts b/src/types.ts index f0af3b2f..8084d6b9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7145,6 +7145,13 @@ export interface SpriteCurAnim { timer: number; loop: boolean; speed: number; + /** + * The current index relative to the start of the + * associated `frames` array for this animation. + * This may be greater than the number of frames + * in the sprite. + */ + frameIndex: number; pingpong: boolean; onEnd: () => void; }