@flighthq/spritesheet-formats 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asepriteParse.d.ts +18 -0
- package/dist/asepriteParse.d.ts.map +1 -0
- package/dist/asepriteParse.js +87 -0
- package/dist/asepriteParse.js.map +1 -0
- package/dist/asepriteSchema.d.ts +64 -0
- package/dist/asepriteSchema.d.ts.map +1 -0
- package/dist/asepriteSchema.js +6 -0
- package/dist/asepriteSchema.js.map +1 -0
- package/dist/asepriteSerialize.d.ts +14 -0
- package/dist/asepriteSerialize.d.ts.map +1 -0
- package/dist/asepriteSerialize.js +86 -0
- package/dist/asepriteSerialize.js.map +1 -0
- package/dist/cocosPlistParse.d.ts +15 -0
- package/dist/cocosPlistParse.d.ts.map +1 -0
- package/dist/cocosPlistParse.js +157 -0
- package/dist/cocosPlistParse.js.map +1 -0
- package/dist/cocosPlistSchema.d.ts +31 -0
- package/dist/cocosPlistSchema.d.ts.map +1 -0
- package/dist/cocosPlistSchema.js +6 -0
- package/dist/cocosPlistSchema.js.map +1 -0
- package/dist/cocosPlistSerialize.d.ts +8 -0
- package/dist/cocosPlistSerialize.d.ts.map +1 -0
- package/dist/cocosPlistSerialize.js +86 -0
- package/dist/cocosPlistSerialize.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/libgdxAtlasParse.d.ts +13 -0
- package/dist/libgdxAtlasParse.d.ts.map +1 -0
- package/dist/libgdxAtlasParse.js +209 -0
- package/dist/libgdxAtlasParse.js.map +1 -0
- package/dist/libgdxAtlasSchema.d.ts +38 -0
- package/dist/libgdxAtlasSchema.d.ts.map +1 -0
- package/dist/libgdxAtlasSchema.js +6 -0
- package/dist/libgdxAtlasSchema.js.map +1 -0
- package/dist/spritesheetDetect.d.ts +41 -0
- package/dist/spritesheetDetect.d.ts.map +1 -0
- package/dist/spritesheetDetect.js +101 -0
- package/dist/spritesheetDetect.js.map +1 -0
- package/dist/starlingParse.d.ts +20 -0
- package/dist/starlingParse.d.ts.map +1 -0
- package/dist/starlingParse.js +138 -0
- package/dist/starlingParse.js.map +1 -0
- package/dist/starlingSchema.d.ts +32 -0
- package/dist/starlingSchema.d.ts.map +1 -0
- package/dist/starlingSchema.js +6 -0
- package/dist/starlingSchema.js.map +1 -0
- package/dist/starlingSerialize.d.ts +8 -0
- package/dist/starlingSerialize.d.ts.map +1 -0
- package/dist/starlingSerialize.js +67 -0
- package/dist/starlingSerialize.js.map +1 -0
- package/dist/texturePackerParse.d.ts +15 -0
- package/dist/texturePackerParse.d.ts.map +1 -0
- package/dist/texturePackerParse.js +70 -0
- package/dist/texturePackerParse.js.map +1 -0
- package/dist/texturePackerSchema.d.ts +52 -0
- package/dist/texturePackerSchema.d.ts.map +1 -0
- package/dist/texturePackerSchema.js +5 -0
- package/dist/texturePackerSchema.js.map +1 -0
- package/dist/texturePackerSerialize.d.ts +14 -0
- package/dist/texturePackerSerialize.d.ts.map +1 -0
- package/dist/texturePackerSerialize.js +74 -0
- package/dist/texturePackerSerialize.js.map +1 -0
- package/package.json +41 -0
- package/src/asepriteParse.test.ts +235 -0
- package/src/asepriteSerialize.test.ts +218 -0
- package/src/cocosPlistParse.test.ts +131 -0
- package/src/cocosPlistSerialize.test.ts +56 -0
- package/src/libgdxAtlasParse.test.ts +100 -0
- package/src/spritesheetDetect.test.ts +217 -0
- package/src/starlingParse.test.ts +176 -0
- package/src/starlingSerialize.test.ts +117 -0
- package/src/texturePackerParse.test.ts +237 -0
- package/src/texturePackerSerialize.test.ts +200 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { parseAsepriteSpritesheet, parseAsepriteSpritesheetDocument } from './asepriteParse';
|
|
2
|
+
|
|
3
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const HASH_JSON = JSON.stringify({
|
|
6
|
+
frames: {
|
|
7
|
+
'sprite 0.aseprite': {
|
|
8
|
+
frame: { x: 0, y: 0, w: 32, h: 32 },
|
|
9
|
+
rotated: false,
|
|
10
|
+
trimmed: false,
|
|
11
|
+
spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
|
|
12
|
+
sourceSize: { w: 32, h: 32 },
|
|
13
|
+
duration: 100,
|
|
14
|
+
},
|
|
15
|
+
'sprite 1.aseprite': {
|
|
16
|
+
frame: { x: 32, y: 0, w: 32, h: 32 },
|
|
17
|
+
rotated: false,
|
|
18
|
+
trimmed: false,
|
|
19
|
+
spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
|
|
20
|
+
sourceSize: { w: 32, h: 32 },
|
|
21
|
+
duration: 150,
|
|
22
|
+
},
|
|
23
|
+
'sprite 2.aseprite': {
|
|
24
|
+
frame: { x: 64, y: 0, w: 30, h: 28 },
|
|
25
|
+
rotated: false,
|
|
26
|
+
trimmed: true,
|
|
27
|
+
spriteSourceSize: { x: 1, y: 2, w: 30, h: 28 },
|
|
28
|
+
sourceSize: { w: 32, h: 32 },
|
|
29
|
+
duration: 200,
|
|
30
|
+
},
|
|
31
|
+
'sprite 3.aseprite': {
|
|
32
|
+
frame: { x: 96, y: 0, w: 32, h: 32 },
|
|
33
|
+
rotated: false,
|
|
34
|
+
trimmed: false,
|
|
35
|
+
spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
|
|
36
|
+
sourceSize: { w: 32, h: 32 },
|
|
37
|
+
duration: 100,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
meta: {
|
|
41
|
+
app: 'https://www.aseprite.org/',
|
|
42
|
+
version: '1.3',
|
|
43
|
+
image: 'sprite.png',
|
|
44
|
+
format: 'RGBA8888',
|
|
45
|
+
size: { w: 128, h: 32 },
|
|
46
|
+
scale: '1',
|
|
47
|
+
frameTags: [
|
|
48
|
+
{ name: 'run', from: 0, to: 1, direction: 'forward' },
|
|
49
|
+
{ name: 'jump', from: 2, to: 3, direction: 'reverse' },
|
|
50
|
+
],
|
|
51
|
+
layers: [{ name: 'Layer 1', opacity: 255, blendMode: 'normal' }],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const ARRAY_JSON = JSON.stringify({
|
|
56
|
+
frames: [
|
|
57
|
+
{
|
|
58
|
+
filename: 'anim 0.aseprite',
|
|
59
|
+
frame: { x: 0, y: 0, w: 16, h: 16 },
|
|
60
|
+
rotated: false,
|
|
61
|
+
trimmed: false,
|
|
62
|
+
spriteSourceSize: { x: 0, y: 0, w: 16, h: 16 },
|
|
63
|
+
sourceSize: { w: 16, h: 16 },
|
|
64
|
+
duration: 80,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
filename: 'anim 1.aseprite',
|
|
68
|
+
frame: { x: 16, y: 0, w: 16, h: 16 },
|
|
69
|
+
rotated: false,
|
|
70
|
+
trimmed: false,
|
|
71
|
+
spriteSourceSize: { x: 0, y: 0, w: 16, h: 16 },
|
|
72
|
+
sourceSize: { w: 16, h: 16 },
|
|
73
|
+
duration: 80,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
meta: {
|
|
77
|
+
app: 'https://www.aseprite.org/',
|
|
78
|
+
version: '1.3',
|
|
79
|
+
image: 'anim.png',
|
|
80
|
+
format: 'RGBA8888',
|
|
81
|
+
size: { w: 32, h: 16 },
|
|
82
|
+
scale: '1',
|
|
83
|
+
frameTags: [{ name: 'idle', from: 0, to: 1, direction: 'pingpong' }],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const NO_TAGS_JSON = JSON.stringify({
|
|
88
|
+
frames: {
|
|
89
|
+
'solo 0.aseprite': {
|
|
90
|
+
frame: { x: 0, y: 0, w: 8, h: 8 },
|
|
91
|
+
rotated: false,
|
|
92
|
+
trimmed: false,
|
|
93
|
+
spriteSourceSize: { x: 0, y: 0, w: 8, h: 8 },
|
|
94
|
+
sourceSize: { w: 8, h: 8 },
|
|
95
|
+
duration: 100,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
meta: {
|
|
99
|
+
app: 'https://www.aseprite.org/',
|
|
100
|
+
version: '1.3',
|
|
101
|
+
image: 'solo.png',
|
|
102
|
+
format: 'RGBA8888',
|
|
103
|
+
size: { w: 8, h: 8 },
|
|
104
|
+
scale: '1',
|
|
105
|
+
frameTags: [],
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('parseAsepriteSpritesheet', () => {
|
|
110
|
+
it('returns a SpritesheetData (not a Parsed object)', () => {
|
|
111
|
+
const result = parseAsepriteSpritesheet(HASH_JSON);
|
|
112
|
+
expect(typeof result.frames).toBe('object');
|
|
113
|
+
expect((result as unknown as Record<string, unknown>).document).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('parses frame count from Hash variant', () => {
|
|
117
|
+
expect(parseAsepriteSpritesheet(HASH_JSON).frames).toHaveLength(4);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('parses frame count from Array variant', () => {
|
|
121
|
+
expect(parseAsepriteSpritesheet(ARRAY_JSON).frames).toHaveLength(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('maps frame name from Hash key', () => {
|
|
125
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
126
|
+
expect(data.frames[0].name).toBe('sprite 0.aseprite');
|
|
127
|
+
expect(data.frames[2].name).toBe('sprite 2.aseprite');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('maps frame name from Array filename field', () => {
|
|
131
|
+
const data = parseAsepriteSpritesheet(ARRAY_JSON);
|
|
132
|
+
expect(data.frames[0].name).toBe('anim 0.aseprite');
|
|
133
|
+
expect(data.frames[1].name).toBe('anim 1.aseprite');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('maps atlas position and size', () => {
|
|
137
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
138
|
+
expect(data.frames[0].x).toBe(0);
|
|
139
|
+
expect(data.frames[0].y).toBe(0);
|
|
140
|
+
expect(data.frames[0].width).toBe(32);
|
|
141
|
+
expect(data.frames[0].height).toBe(32);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('maps trim offset', () => {
|
|
145
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
146
|
+
const trimmed = data.frames[2];
|
|
147
|
+
expect(trimmed.offsetX).toBe(1);
|
|
148
|
+
expect(trimmed.offsetY).toBe(2);
|
|
149
|
+
expect(trimmed.sourceWidth).toBe(32);
|
|
150
|
+
expect(trimmed.sourceHeight).toBe(32);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('has null pivots (Aseprite format has no pivot data)', () => {
|
|
154
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
155
|
+
expect(data.frames[0].pivotX).toBeNull();
|
|
156
|
+
expect(data.frames[0].pivotY).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('maps image file and atlas size', () => {
|
|
160
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
161
|
+
expect(data.imageFile).toBe('sprite.png');
|
|
162
|
+
expect(data.imageWidth).toBe(128);
|
|
163
|
+
expect(data.imageHeight).toBe(32);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('parses animations from frameTags', () => {
|
|
167
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
168
|
+
expect(data.animations).toHaveLength(2);
|
|
169
|
+
expect(data.animations[0].name).toBe('run');
|
|
170
|
+
expect(data.animations[1].name).toBe('jump');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('maps animation direction', () => {
|
|
174
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
175
|
+
expect(data.animations[0].direction).toBe('forward');
|
|
176
|
+
expect(data.animations[1].direction).toBe('reverse');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('maps animation frameNames via tag from/to indices', () => {
|
|
180
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
181
|
+
expect(data.animations[0].frameNames).toEqual(['sprite 0.aseprite', 'sprite 1.aseprite']);
|
|
182
|
+
expect(data.animations[1].frameNames).toEqual(['sprite 2.aseprite', 'sprite 3.aseprite']);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('sets frameDurations when per-frame durations vary', () => {
|
|
186
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
187
|
+
const run = data.animations[0];
|
|
188
|
+
// frames 0 and 1 have durations 100 and 150 — they differ
|
|
189
|
+
expect(run.frameDurations).toEqual([100, 150]);
|
|
190
|
+
expect(run.frameDuration).toBe(100);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('sets frameDurations to null when all frame durations are uniform', () => {
|
|
194
|
+
const data = parseAsepriteSpritesheet(ARRAY_JSON);
|
|
195
|
+
const idle = data.animations[0];
|
|
196
|
+
// both frames have duration 80
|
|
197
|
+
expect(idle.frameDurations).toBeNull();
|
|
198
|
+
expect(idle.frameDuration).toBe(80);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('produces empty animations when frameTags array is empty', () => {
|
|
202
|
+
expect(parseAsepriteSpritesheet(NO_TAGS_JSON).animations).toHaveLength(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('maps pingpong direction from Array variant', () => {
|
|
206
|
+
const data = parseAsepriteSpritesheet(ARRAY_JSON);
|
|
207
|
+
expect(data.animations[0].direction).toBe('pingpong');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('parseAsepriteSpritesheetDocument', () => {
|
|
212
|
+
it('returns the same data as parseAsepriteSpritesheet', () => {
|
|
213
|
+
const parsed = parseAsepriteSpritesheet(HASH_JSON);
|
|
214
|
+
const { data } = parseAsepriteSpritesheetDocument(HASH_JSON);
|
|
215
|
+
expect(data.frames.length).toBe(parsed.frames.length);
|
|
216
|
+
expect(data.imageFile).toBe(parsed.imageFile);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('preserves the original Hash document', () => {
|
|
220
|
+
const { document } = parseAsepriteSpritesheetDocument(HASH_JSON);
|
|
221
|
+
expect(Array.isArray(document.frames)).toBe(false);
|
|
222
|
+
expect((document.frames as Record<string, unknown>)['sprite 0.aseprite']).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('preserves the original Array document', () => {
|
|
226
|
+
const { document } = parseAsepriteSpritesheetDocument(ARRAY_JSON);
|
|
227
|
+
expect(Array.isArray(document.frames)).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('preserves layers in document meta', () => {
|
|
231
|
+
const { document } = parseAsepriteSpritesheetDocument(HASH_JSON);
|
|
232
|
+
expect(document.meta.layers).toHaveLength(1);
|
|
233
|
+
expect(document.meta.layers![0].name).toBe('Layer 1');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { parseAsepriteSpritesheet, parseAsepriteSpritesheetDocument } from './asepriteParse';
|
|
2
|
+
import { serializeAsepriteSpritesheet } from './asepriteSerialize';
|
|
3
|
+
|
|
4
|
+
const HASH_JSON = JSON.stringify({
|
|
5
|
+
frames: {
|
|
6
|
+
'frame0.aseprite': {
|
|
7
|
+
duration: 100,
|
|
8
|
+
frame: { h: 32, w: 32, x: 0, y: 0 },
|
|
9
|
+
rotated: false,
|
|
10
|
+
sourceSize: { h: 32, w: 32 },
|
|
11
|
+
spriteSourceSize: { h: 32, w: 32, x: 0, y: 0 },
|
|
12
|
+
trimmed: false,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
meta: {
|
|
16
|
+
app: 'https://www.aseprite.org/',
|
|
17
|
+
format: 'RGBA8888',
|
|
18
|
+
frameTags: [],
|
|
19
|
+
image: 'sprite.png',
|
|
20
|
+
scale: '1',
|
|
21
|
+
size: { h: 32, w: 64 },
|
|
22
|
+
version: '1.3',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const ROUNDTRIP_HASH_JSON = JSON.stringify({
|
|
27
|
+
frames: {
|
|
28
|
+
'sprite 0.aseprite': {
|
|
29
|
+
frame: { x: 0, y: 0, w: 32, h: 32 },
|
|
30
|
+
rotated: false,
|
|
31
|
+
trimmed: false,
|
|
32
|
+
spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
|
|
33
|
+
sourceSize: { w: 32, h: 32 },
|
|
34
|
+
duration: 100,
|
|
35
|
+
},
|
|
36
|
+
'sprite 1.aseprite': {
|
|
37
|
+
frame: { x: 32, y: 0, w: 32, h: 32 },
|
|
38
|
+
rotated: false,
|
|
39
|
+
trimmed: false,
|
|
40
|
+
spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
|
|
41
|
+
sourceSize: { w: 32, h: 32 },
|
|
42
|
+
duration: 150,
|
|
43
|
+
},
|
|
44
|
+
'sprite 2.aseprite': {
|
|
45
|
+
frame: { x: 64, y: 0, w: 30, h: 28 },
|
|
46
|
+
rotated: false,
|
|
47
|
+
trimmed: true,
|
|
48
|
+
spriteSourceSize: { x: 1, y: 2, w: 30, h: 28 },
|
|
49
|
+
sourceSize: { w: 32, h: 32 },
|
|
50
|
+
duration: 200,
|
|
51
|
+
},
|
|
52
|
+
'sprite 3.aseprite': {
|
|
53
|
+
frame: { x: 96, y: 0, w: 32, h: 32 },
|
|
54
|
+
rotated: false,
|
|
55
|
+
trimmed: false,
|
|
56
|
+
spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
|
|
57
|
+
sourceSize: { w: 32, h: 32 },
|
|
58
|
+
duration: 100,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
meta: {
|
|
62
|
+
app: 'https://www.aseprite.org/',
|
|
63
|
+
version: '1.3',
|
|
64
|
+
image: 'sprite.png',
|
|
65
|
+
format: 'RGBA8888',
|
|
66
|
+
size: { w: 128, h: 32 },
|
|
67
|
+
scale: '1',
|
|
68
|
+
frameTags: [
|
|
69
|
+
{ name: 'run', from: 0, to: 1, direction: 'forward' },
|
|
70
|
+
{ name: 'jump', from: 2, to: 3, direction: 'reverse' },
|
|
71
|
+
],
|
|
72
|
+
layers: [{ name: 'Layer 1', opacity: 255, blendMode: 'normal' }],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const ROUNDTRIP_ARRAY_JSON = JSON.stringify({
|
|
77
|
+
frames: [
|
|
78
|
+
{
|
|
79
|
+
filename: 'anim 0.aseprite',
|
|
80
|
+
frame: { x: 0, y: 0, w: 16, h: 16 },
|
|
81
|
+
rotated: false,
|
|
82
|
+
trimmed: false,
|
|
83
|
+
spriteSourceSize: { x: 0, y: 0, w: 16, h: 16 },
|
|
84
|
+
sourceSize: { w: 16, h: 16 },
|
|
85
|
+
duration: 80,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
filename: 'anim 1.aseprite',
|
|
89
|
+
frame: { x: 16, y: 0, w: 16, h: 16 },
|
|
90
|
+
rotated: false,
|
|
91
|
+
trimmed: false,
|
|
92
|
+
spriteSourceSize: { x: 0, y: 0, w: 16, h: 16 },
|
|
93
|
+
sourceSize: { w: 16, h: 16 },
|
|
94
|
+
duration: 80,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
meta: {
|
|
98
|
+
app: 'https://www.aseprite.org/',
|
|
99
|
+
version: '1.3',
|
|
100
|
+
image: 'anim.png',
|
|
101
|
+
format: 'RGBA8888',
|
|
102
|
+
size: { w: 32, h: 16 },
|
|
103
|
+
scale: '1',
|
|
104
|
+
frameTags: [{ name: 'idle', from: 0, to: 1, direction: 'pingpong' }],
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const ROUNDTRIP_NO_TAGS_JSON = JSON.stringify({
|
|
109
|
+
frames: {
|
|
110
|
+
'solo 0.aseprite': {
|
|
111
|
+
frame: { x: 0, y: 0, w: 8, h: 8 },
|
|
112
|
+
rotated: false,
|
|
113
|
+
trimmed: false,
|
|
114
|
+
spriteSourceSize: { x: 0, y: 0, w: 8, h: 8 },
|
|
115
|
+
sourceSize: { w: 8, h: 8 },
|
|
116
|
+
duration: 100,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
meta: {
|
|
120
|
+
app: 'https://www.aseprite.org/',
|
|
121
|
+
version: '1.3',
|
|
122
|
+
image: 'solo.png',
|
|
123
|
+
format: 'RGBA8888',
|
|
124
|
+
size: { w: 8, h: 8 },
|
|
125
|
+
scale: '1',
|
|
126
|
+
frameTags: [],
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('serializeAsepriteSpritesheet', () => {
|
|
131
|
+
it('produces valid JSON that can be reparsed', () => {
|
|
132
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
133
|
+
const serialized = serializeAsepriteSpritesheet(data);
|
|
134
|
+
const reparsed = JSON.parse(serialized) as Record<string, unknown>;
|
|
135
|
+
expect(reparsed).toHaveProperty('frames');
|
|
136
|
+
expect(reparsed).toHaveProperty('meta');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('preserves frame count', () => {
|
|
140
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
141
|
+
const serialized = serializeAsepriteSpritesheet(data);
|
|
142
|
+
const reparsed = parseAsepriteSpritesheet(serialized);
|
|
143
|
+
expect(reparsed.frames).toHaveLength(data.frames.length);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('serializes array variant when option is set', () => {
|
|
147
|
+
const data = parseAsepriteSpritesheet(HASH_JSON);
|
|
148
|
+
const serialized = serializeAsepriteSpritesheet(data, undefined, { variant: 'array' });
|
|
149
|
+
const parsed = JSON.parse(serialized) as { frames: unknown[] };
|
|
150
|
+
expect(Array.isArray(parsed.frames)).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('round-trips frame names', () => {
|
|
154
|
+
const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
|
|
155
|
+
const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
|
|
156
|
+
expect(data2.frames.map((f) => f.name)).toEqual(data.frames.map((f) => f.name));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('round-trips frame positions', () => {
|
|
160
|
+
const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
|
|
161
|
+
const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
|
|
162
|
+
expect(data2.frames[0].x).toBe(data.frames[0].x);
|
|
163
|
+
expect(data2.frames[0].width).toBe(data.frames[0].width);
|
|
164
|
+
expect(data2.frames[2].offsetX).toBe(data.frames[2].offsetX);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('round-trips variable per-frame durations', () => {
|
|
168
|
+
const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
|
|
169
|
+
const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
|
|
170
|
+
expect(data2.animations[0].frameDurations).toEqual([100, 150]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('round-trips uniform durations as null frameDurations', () => {
|
|
174
|
+
const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_ARRAY_JSON);
|
|
175
|
+
const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
|
|
176
|
+
expect(data2.animations[0].frameDurations).toBeNull();
|
|
177
|
+
expect(data2.animations[0].frameDuration).toBe(80);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('round-trips animation names and directions', () => {
|
|
181
|
+
const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
|
|
182
|
+
const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
|
|
183
|
+
expect(data2.animations[0].name).toBe('run');
|
|
184
|
+
expect(data2.animations[1].name).toBe('jump');
|
|
185
|
+
expect(data2.animations[1].direction).toBe('reverse');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('preserves layer metadata through the document', () => {
|
|
189
|
+
const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
|
|
190
|
+
const json2 = serializeAsepriteSpritesheet(data, document);
|
|
191
|
+
const { document: doc2 } = parseAsepriteSpritesheetDocument(json2);
|
|
192
|
+
expect(doc2.meta.layers).toHaveLength(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('emits array variant when existing is array', () => {
|
|
196
|
+
const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_ARRAY_JSON);
|
|
197
|
+
const parsed = JSON.parse(serializeAsepriteSpritesheet(data, document));
|
|
198
|
+
expect(Array.isArray(parsed.frames)).toBe(true);
|
|
199
|
+
expect(parsed.frames[0].filename).toBeDefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('emits hash variant by default', () => {
|
|
203
|
+
const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
|
|
204
|
+
const parsed = JSON.parse(serializeAsepriteSpritesheet(data, document));
|
|
205
|
+
expect(Array.isArray(parsed.frames)).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('respects variant override option', () => {
|
|
209
|
+
const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
|
|
210
|
+
const parsed = JSON.parse(serializeAsepriteSpritesheet(data, document, { variant: 'array' }));
|
|
211
|
+
expect(Array.isArray(parsed.frames)).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('produces valid JSON', () => {
|
|
215
|
+
const data = parseAsepriteSpritesheet(ROUNDTRIP_NO_TAGS_JSON);
|
|
216
|
+
expect(() => JSON.parse(serializeAsepriteSpritesheet(data))).not.toThrow();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { parseCocosPlistSpritesheet, parseCocosPlistSpritesheetDocument } from './cocosPlistParse';
|
|
2
|
+
|
|
3
|
+
const MINIMAL_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
4
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
5
|
+
<plist version="1.0">
|
|
6
|
+
<dict>
|
|
7
|
+
<key>frames</key>
|
|
8
|
+
<dict>
|
|
9
|
+
<key>hero.png</key>
|
|
10
|
+
<dict>
|
|
11
|
+
<key>frame</key>
|
|
12
|
+
<string>{{0,0},{64,64}}</string>
|
|
13
|
+
<key>spriteOffset</key>
|
|
14
|
+
<string>{0,0}</string>
|
|
15
|
+
<key>spriteSize</key>
|
|
16
|
+
<string>{64,64}</string>
|
|
17
|
+
<key>spriteSourceSize</key>
|
|
18
|
+
<string>{64,64}</string>
|
|
19
|
+
<key>spriteTrimmed</key>
|
|
20
|
+
<false/>
|
|
21
|
+
<key>textureRotated</key>
|
|
22
|
+
<false/>
|
|
23
|
+
</dict>
|
|
24
|
+
<key>coin.png</key>
|
|
25
|
+
<dict>
|
|
26
|
+
<key>frame</key>
|
|
27
|
+
<string>{{64,0},{32,32}}</string>
|
|
28
|
+
<key>spriteOffset</key>
|
|
29
|
+
<string>{0,0}</string>
|
|
30
|
+
<key>spriteSize</key>
|
|
31
|
+
<string>{32,32}</string>
|
|
32
|
+
<key>spriteSourceSize</key>
|
|
33
|
+
<string>{32,32}</string>
|
|
34
|
+
<key>spriteTrimmed</key>
|
|
35
|
+
<false/>
|
|
36
|
+
<key>textureRotated</key>
|
|
37
|
+
<false/>
|
|
38
|
+
</dict>
|
|
39
|
+
</dict>
|
|
40
|
+
<key>metadata</key>
|
|
41
|
+
<dict>
|
|
42
|
+
<key>format</key>
|
|
43
|
+
<integer>3</integer>
|
|
44
|
+
<key>size</key>
|
|
45
|
+
<string>{256,128}</string>
|
|
46
|
+
<key>textureFileName</key>
|
|
47
|
+
<string>atlas.png</string>
|
|
48
|
+
</dict>
|
|
49
|
+
</dict>
|
|
50
|
+
</plist>`;
|
|
51
|
+
|
|
52
|
+
const ROTATED_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
53
|
+
<plist version="1.0">
|
|
54
|
+
<dict>
|
|
55
|
+
<key>frames</key>
|
|
56
|
+
<dict>
|
|
57
|
+
<key>rotated.png</key>
|
|
58
|
+
<dict>
|
|
59
|
+
<key>frame</key>
|
|
60
|
+
<string>{{0,0},{32,64}}</string>
|
|
61
|
+
<key>spriteOffset</key>
|
|
62
|
+
<string>{0,0}</string>
|
|
63
|
+
<key>spriteSize</key>
|
|
64
|
+
<string>{64,32}</string>
|
|
65
|
+
<key>spriteSourceSize</key>
|
|
66
|
+
<string>{64,32}</string>
|
|
67
|
+
<key>spriteTrimmed</key>
|
|
68
|
+
<false/>
|
|
69
|
+
<key>textureRotated</key>
|
|
70
|
+
<true/>
|
|
71
|
+
</dict>
|
|
72
|
+
</dict>
|
|
73
|
+
<key>metadata</key>
|
|
74
|
+
<dict>
|
|
75
|
+
<key>format</key>
|
|
76
|
+
<integer>3</integer>
|
|
77
|
+
<key>size</key>
|
|
78
|
+
<string>{128,128}</string>
|
|
79
|
+
<key>textureFileName</key>
|
|
80
|
+
<string>rotated.png</string>
|
|
81
|
+
</dict>
|
|
82
|
+
</dict>
|
|
83
|
+
</plist>`;
|
|
84
|
+
|
|
85
|
+
describe('parseCocosPlistSpritesheet', () => {
|
|
86
|
+
it('parses frame names', () => {
|
|
87
|
+
const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
|
|
88
|
+
const names = data.frames.map((f) => f.name).sort();
|
|
89
|
+
expect(names).toContain('hero.png');
|
|
90
|
+
expect(names).toContain('coin.png');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('parses frame positions', () => {
|
|
94
|
+
const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
|
|
95
|
+
const hero = data.frames.find((f) => f.name === 'hero.png');
|
|
96
|
+
expect(hero?.x).toBe(0);
|
|
97
|
+
expect(hero?.y).toBe(0);
|
|
98
|
+
expect(hero?.width).toBe(64);
|
|
99
|
+
expect(hero?.height).toBe(64);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('parses atlas image file and dimensions', () => {
|
|
103
|
+
const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
|
|
104
|
+
expect(data.imageFile).toBe('atlas.png');
|
|
105
|
+
expect(data.imageWidth).toBe(256);
|
|
106
|
+
expect(data.imageHeight).toBe(128);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('handles rotated frames with swapped dimensions', () => {
|
|
110
|
+
const data = parseCocosPlistSpritesheet(ROTATED_PLIST);
|
|
111
|
+
const frame = data.frames.find((f) => f.name === 'rotated.png');
|
|
112
|
+
expect(frame?.rotated).toBe(true);
|
|
113
|
+
// Rotated: atlas rect is 32x64, so in logical orientation it is 64x32
|
|
114
|
+
expect(frame?.width).toBe(64);
|
|
115
|
+
expect(frame?.height).toBe(32);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('produces empty animations array', () => {
|
|
119
|
+
const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
|
|
120
|
+
expect(data.animations).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('parseCocosPlistSpritesheetDocument', () => {
|
|
125
|
+
it('returns both data and document', () => {
|
|
126
|
+
const result = parseCocosPlistSpritesheetDocument(MINIMAL_PLIST);
|
|
127
|
+
expect(result.data.frames.length).toBeGreaterThan(0);
|
|
128
|
+
expect(result.document.metadata.textureFileName).toBe('atlas.png');
|
|
129
|
+
expect(result.document.metadata.format).toBe(3);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseCocosPlistSpritesheet } from './cocosPlistParse';
|
|
2
|
+
import { serializeCocosPlistSpritesheet } from './cocosPlistSerialize';
|
|
3
|
+
|
|
4
|
+
const MINIMAL_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
5
|
+
<plist version="1.0">
|
|
6
|
+
<dict>
|
|
7
|
+
<key>frames</key>
|
|
8
|
+
<dict>
|
|
9
|
+
<key>hero.png</key>
|
|
10
|
+
<dict>
|
|
11
|
+
<key>frame</key>
|
|
12
|
+
<string>{{0,0},{64,64}}</string>
|
|
13
|
+
<key>spriteOffset</key>
|
|
14
|
+
<string>{0,0}</string>
|
|
15
|
+
<key>spriteSize</key>
|
|
16
|
+
<string>{64,64}</string>
|
|
17
|
+
<key>spriteSourceSize</key>
|
|
18
|
+
<string>{64,64}</string>
|
|
19
|
+
<key>spriteTrimmed</key>
|
|
20
|
+
<false/>
|
|
21
|
+
<key>textureRotated</key>
|
|
22
|
+
<false/>
|
|
23
|
+
</dict>
|
|
24
|
+
</dict>
|
|
25
|
+
<key>metadata</key>
|
|
26
|
+
<dict>
|
|
27
|
+
<key>format</key>
|
|
28
|
+
<integer>3</integer>
|
|
29
|
+
<key>size</key>
|
|
30
|
+
<string>{128,64}</string>
|
|
31
|
+
<key>textureFileName</key>
|
|
32
|
+
<string>atlas.png</string>
|
|
33
|
+
</dict>
|
|
34
|
+
</dict>
|
|
35
|
+
</plist>`;
|
|
36
|
+
|
|
37
|
+
describe('serializeCocosPlistSpritesheet', () => {
|
|
38
|
+
it('produces XML with a plist root element', () => {
|
|
39
|
+
const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
|
|
40
|
+
const serialized = serializeCocosPlistSpritesheet(data);
|
|
41
|
+
expect(serialized).toContain('<plist');
|
|
42
|
+
expect(serialized).toContain('</plist>');
|
|
43
|
+
});
|
|
44
|
+
it('preserves frame count on round-trip', () => {
|
|
45
|
+
const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
|
|
46
|
+
const serialized = serializeCocosPlistSpritesheet(data);
|
|
47
|
+
const reparsed = parseCocosPlistSpritesheet(serialized);
|
|
48
|
+
expect(reparsed.frames).toHaveLength(data.frames.length);
|
|
49
|
+
});
|
|
50
|
+
it('preserves image file on round-trip', () => {
|
|
51
|
+
const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
|
|
52
|
+
const serialized = serializeCocosPlistSpritesheet(data);
|
|
53
|
+
const reparsed = parseCocosPlistSpritesheet(serialized);
|
|
54
|
+
expect(reparsed.imageFile).toBe(data.imageFile);
|
|
55
|
+
});
|
|
56
|
+
});
|