UNPKG

@spearwolf/twopoint5d

Version:

Create 2.5D realtime graphics and pixelart with WebGL and three.js

384 lines 18 kB
import { describe, expect, test } from 'vitest'; import { FrameBasedAnimations } from './FrameBasedAnimations.js'; import { TextureAtlas } from './TextureAtlas.js'; import { TextureCoords } from './TextureCoords.js'; import { TileSet } from './TileSet.js'; const AnimSymbol = Symbol('anim'); describe('FrameBasedAnimations', () => { describe('construction', () => { test('create instance', () => { const animations = new FrameBasedAnimations(); expect(animations).toBeDefined(); expect(animations).toBeInstanceOf(FrameBasedAnimations); }); }); describe('add with TextureCoords array', () => { test('add animation with name and TextureCoords array', () => { const animations = new FrameBasedAnimations(); const frames = [ new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32), new TextureCoords(64, 0, 32, 32), ]; const id = animations.add('walk', 1.0, frames); expect(id).toBe(0); expect(animations.animId('walk')).toBe(0); }); test('add animation without name (using Symbol)', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32)]; const id = animations.add(undefined, 0.5, frames); expect(id).toBe(0); expect(typeof id).toBe('number'); }); test('add animation with symbol name', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32)]; const id = animations.add(AnimSymbol, 2.0, frames); expect(id).toBe(0); expect(animations.animId(AnimSymbol)).toBe(0); }); test('add multiple animations', () => { const animations = new FrameBasedAnimations(); const id0 = animations.add('idle', 1.0, [new TextureCoords(0, 0, 32, 32)]); const id1 = animations.add('walk', 0.5, [ new TextureCoords(32, 0, 32, 32), new TextureCoords(64, 0, 32, 32), ]); const id2 = animations.add('run', 0.3, [ new TextureCoords(0, 32, 32, 32), new TextureCoords(32, 32, 32, 32), new TextureCoords(64, 32, 32, 32), ]); expect(id0).toBe(0); expect(id1).toBe(1); expect(id2).toBe(2); expect(animations.animId('idle')).toBe(0); expect(animations.animId('walk')).toBe(1); expect(animations.animId('run')).toBe(2); }); test('throw error on duplicate name', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32)]; animations.add('walk', 1.0, frames); expect(() => { animations.add('walk', 1.0, frames); }).toThrow("name='walk' must be unique!"); }); }); describe('add with TextureAtlas', () => { test('add animation from TextureAtlas with frameNameQuery', () => { const animations = new FrameBasedAnimations(); const atlas = new TextureAtlas(); atlas.add('sprite_001', new TextureCoords(0, 0, 32, 32)); atlas.add('sprite_002', new TextureCoords(32, 0, 32, 32)); atlas.add('sprite_003', new TextureCoords(64, 0, 32, 32)); atlas.add('other_001', new TextureCoords(0, 32, 32, 32)); const id = animations.add('walk', 1.0, atlas, 'sprite_.*'); expect(id).toBe(0); expect(animations.animId('walk')).toBe(0); }); test('add animation from TextureAtlas without frameNameQuery', () => { const animations = new FrameBasedAnimations(); const atlas = new TextureAtlas(); atlas.add('frame_001', new TextureCoords(0, 0, 32, 32)); atlas.add('frame_002', new TextureCoords(32, 0, 32, 32)); const id = animations.add('all', 1.0, atlas); expect(id).toBe(0); expect(animations.animId('all')).toBe(0); }); }); describe('add with TileSet', () => { test('add animation from TileSet with firstTileId and tileCount', () => { const animations = new FrameBasedAnimations(); const baseCoords = new TextureCoords(0, 0, 128, 64); const tileSet = new TileSet(baseCoords, { tileWidth: 32, tileHeight: 32, firstId: 1, }); const id = animations.add('walk', 1.0, tileSet, 1, 3); expect(id).toBe(0); expect(animations.animId('walk')).toBe(0); }); test('add animation from TileSet with default firstTileId and tileCount', () => { const animations = new FrameBasedAnimations(); const baseCoords = new TextureCoords(0, 0, 128, 32); const tileSet = new TileSet(baseCoords, { tileWidth: 32, tileHeight: 32, firstId: 5, tileCount: 4, }); const id = animations.add('idle', 0.8, tileSet); expect(id).toBe(0); expect(animations.animId('idle')).toBe(0); }); test('add animation from TileSet with tileIds array', () => { const animations = new FrameBasedAnimations(); const baseCoords = new TextureCoords(0, 0, 128, 64); const tileSet = new TileSet(baseCoords, { tileWidth: 32, tileHeight: 32, firstId: 1, }); const id = animations.add('custom', 1.5, tileSet, [1, 3, 2, 4]); expect(id).toBe(0); expect(animations.animId('custom')).toBe(0); }); }); describe('animId', () => { test('get animation id by name', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32)]; animations.add('walk', 1.0, frames); animations.add('run', 0.5, frames); expect(animations.animId('walk')).toBe(0); expect(animations.animId('run')).toBe(1); }); test('get animation id by symbol', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32)]; animations.add(AnimSymbol, 1.0, frames); expect(animations.animId(AnimSymbol)).toBe(0); }); }); describe('bakeDataTexture', () => { test('bake DataTexture without includeTextureSize option', () => { const animations = new FrameBasedAnimations(); const frames = [ new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32), new TextureCoords(64, 0, 32, 32), ]; animations.add('walk', 1.0, frames); const dataTexture = animations.bakeDataTexture(); expect(dataTexture).toBeDefined(); expect(dataTexture.image).toBeDefined(); expect(dataTexture.image.data).toBeInstanceOf(Float32Array); }); test('bake DataTexture with includeTextureSize option', () => { const animations = new FrameBasedAnimations(); const frames = [ new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32), ]; animations.add('walk', 1.0, frames); const dataTexture = animations.bakeDataTexture({ includeTextureSize: true }); expect(dataTexture).toBeDefined(); expect(dataTexture.image).toBeDefined(); expect(dataTexture.image.data).toBeInstanceOf(Float32Array); }); test('bake DataTexture with multiple animations', () => { const animations = new FrameBasedAnimations(); animations.add('idle', 1.0, [new TextureCoords(0, 0, 32, 32)]); animations.add('walk', 0.5, [ new TextureCoords(32, 0, 32, 32), new TextureCoords(64, 0, 32, 32), ]); animations.add('run', 0.3, [ new TextureCoords(0, 32, 32, 32), new TextureCoords(32, 32, 32, 32), new TextureCoords(64, 32, 32, 32), ]); const dataTexture = animations.bakeDataTexture(); expect(dataTexture).toBeDefined(); expect(dataTexture.image.data).toBeInstanceOf(Float32Array); const buffer = dataTexture.image.data; expect(buffer.length).toBeGreaterThan(0); expect(buffer[0]).toBe(1); expect(buffer[1]).toBe(1.0); expect(buffer[2]).toBeGreaterThanOrEqual(0); expect(buffer[4]).toBe(2); expect(buffer[5]).toBe(0.5); expect(buffer[6]).toBeGreaterThan(buffer[2]); expect(buffer[8]).toBe(3); expect(buffer[9]).toBeCloseTo(0.3, 5); expect(buffer[10]).toBeGreaterThan(buffer[6]); }); test('bake empty DataTexture', () => { const animations = new FrameBasedAnimations(); const dataTexture = animations.bakeDataTexture(); expect(dataTexture).toBeDefined(); expect(dataTexture.image.data).toBeInstanceOf(Float32Array); }); }); describe('edge cases', () => { test('add animation with single frame', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32)]; const id = animations.add('single', 1.0, frames); expect(id).toBe(0); expect(animations.animId('single')).toBe(0); }); test('add animation with zero duration', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32)]; const id = animations.add('zero', 0, frames); expect(id).toBe(0); expect(animations.animId('zero')).toBe(0); }); test('add animation with very long duration', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32)]; const id = animations.add('long', 999.99, frames); expect(id).toBe(0); expect(animations.animId('long')).toBe(0); }); test('sequential animation ids', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32)]; const ids = []; for (let i = 0; i < 10; i++) { ids.push(animations.add(`anim_${i}`, 1.0, frames)); } expect(ids).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); }); }); describe('buffer size calculation', () => { test('should handle reasonable number of animations', () => { const animations = new FrameBasedAnimations(); for (let i = 0; i < 100; i++) { const animFrames = []; for (let j = 0; j < 10; j++) { animFrames.push(new TextureCoords(j * 32, i * 32, 32, 32)); } animations.add(`anim_${i}`, 1.0, animFrames); } const dataTexture = animations.bakeDataTexture(); expect(dataTexture).toBeDefined(); expect(dataTexture.image.data).toBeInstanceOf(Float32Array); }); test('should handle animations with many frames', () => { const animations = new FrameBasedAnimations(); const frames = []; for (let i = 0; i < 100; i++) { frames.push(new TextureCoords(i * 32, 0, 32, 32)); } animations.add('long_anim', 10.0, frames); const dataTexture = animations.bakeDataTexture(); expect(dataTexture).toBeDefined(); expect(dataTexture.image.data).toBeInstanceOf(Float32Array); }); }); describe('frameRate support', () => { test('add animation with frameRate option', () => { const animations = new FrameBasedAnimations(); const frames = [ new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32), new TextureCoords(64, 0, 32, 32), new TextureCoords(96, 0, 32, 32), ]; const id = animations.add('walk', { frameRate: 4 }, frames); expect(id).toBe(0); expect(animations.animId('walk')).toBe(0); }); test('frameRate correctly calculates duration in baked texture', () => { const animations = new FrameBasedAnimations(); const frames = [ new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32), new TextureCoords(64, 0, 32, 32), new TextureCoords(96, 0, 32, 32), new TextureCoords(128, 0, 32, 32), ]; animations.add('run', { frameRate: 10 }, frames); const dataTexture = animations.bakeDataTexture(); const buffer = dataTexture.image.data; expect(buffer[0]).toBe(5); expect(buffer[1]).toBeCloseTo(0.5, 5); }); test('add animation with duration option object', () => { const animations = new FrameBasedAnimations(); const frames = [ new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32), ]; const id = animations.add('idle', { duration: 2.5 }, frames); expect(id).toBe(0); const dataTexture = animations.bakeDataTexture(); const buffer = dataTexture.image.data; expect(buffer[1]).toBeCloseTo(2.5, 5); }); test('mix duration number and frameRate options', () => { const animations = new FrameBasedAnimations(); const frames2 = [new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32)]; const frames4 = [ new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32), new TextureCoords(64, 0, 32, 32), new TextureCoords(96, 0, 32, 32), ]; animations.add('idle', 1.0, frames2); animations.add('walk', { frameRate: 8 }, frames4); animations.add('run', { duration: 0.25 }, frames2); const dataTexture = animations.bakeDataTexture(); const buffer = dataTexture.image.data; expect(buffer[0]).toBe(2); expect(buffer[1]).toBeCloseTo(1.0, 5); expect(buffer[4]).toBe(4); expect(buffer[5]).toBeCloseTo(0.5, 5); expect(buffer[8]).toBe(2); expect(buffer[9]).toBeCloseTo(0.25, 5); }); test('frameRate with TextureAtlas', () => { const animations = new FrameBasedAnimations(); const atlas = new TextureAtlas(); atlas.add('sprite_001', new TextureCoords(0, 0, 32, 32)); atlas.add('sprite_002', new TextureCoords(32, 0, 32, 32)); atlas.add('sprite_003', new TextureCoords(64, 0, 32, 32)); const id = animations.add('walk', { frameRate: 6 }, atlas, 'sprite_.*'); expect(id).toBe(0); const dataTexture = animations.bakeDataTexture(); const buffer = dataTexture.image.data; expect(buffer[0]).toBe(3); expect(buffer[1]).toBeCloseTo(0.5, 5); }); test('frameRate with TileSet', () => { const animations = new FrameBasedAnimations(); const baseCoords = new TextureCoords(0, 0, 128, 64); const tileSet = new TileSet(baseCoords, { tileWidth: 32, tileHeight: 32, firstId: 1, }); const id = animations.add('walk', { frameRate: 12 }, tileSet, 1, 4); expect(id).toBe(0); const dataTexture = animations.bakeDataTexture(); const buffer = dataTexture.image.data; expect(buffer[0]).toBe(4); expect(buffer[1]).toBeCloseTo(1 / 3, 5); }); test('frameRate with TileSet using tileIds array', () => { const animations = new FrameBasedAnimations(); const baseCoords = new TextureCoords(0, 0, 128, 64); const tileSet = new TileSet(baseCoords, { tileWidth: 32, tileHeight: 32, firstId: 1, }); const id = animations.add('custom', { frameRate: 20 }, tileSet, [1, 3, 2, 4]); expect(id).toBe(0); const dataTexture = animations.bakeDataTexture(); const buffer = dataTexture.image.data; expect(buffer[0]).toBe(4); expect(buffer[1]).toBeCloseTo(0.2, 5); }); test('throw error for zero frameRate', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32)]; expect(() => { animations.add('invalid', { frameRate: 0 }, frames); }).toThrow('frameRate must be greater than 0'); }); test('throw error for negative frameRate', () => { const animations = new FrameBasedAnimations(); const frames = [new TextureCoords(0, 0, 32, 32), new TextureCoords(32, 0, 32, 32)]; expect(() => { animations.add('invalid', { frameRate: -5 }, frames); }).toThrow('frameRate must be greater than 0'); }); }); }); //# sourceMappingURL=FrameBasedAnimations.spec.js.map