@vpmedia/phaser 1.117.0 → 1.118.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/index.js +140 -134
- package/dist/index.js.map +1 -1
- package/dist/phaser/core/animation_manager.d.ts +1 -1
- package/dist/phaser/core/animation_manager.d.ts.map +1 -1
- package/dist/phaser/core/cache.d.ts +70 -42
- package/dist/phaser/core/cache.d.ts.map +1 -1
- package/dist/phaser/core/input.d.ts +1 -1
- package/dist/phaser/core/input.d.ts.map +1 -1
- package/dist/phaser/core/input_mspointer.d.ts +8 -7
- package/dist/phaser/core/input_mspointer.d.ts.map +1 -1
- package/dist/phaser/core/loader.d.ts +30 -1
- package/dist/phaser/core/loader.d.ts.map +1 -1
- package/dist/phaser/core/loader_parser.d.ts +24 -4
- package/dist/phaser/core/loader_parser.d.ts.map +1 -1
- package/dist/phaser/core/sound.d.ts +13 -3
- package/dist/phaser/core/sound.d.ts.map +1 -1
- package/dist/phaser/core/tween.d.ts +3 -3
- package/dist/phaser/core/tween.d.ts.map +1 -1
- package/dist/phaser/core/tween_manager.d.ts +8 -6
- package/dist/phaser/core/tween_manager.d.ts.map +1 -1
- package/dist/phaser/display/bitmap_text.d.ts +13 -8
- package/dist/phaser/display/bitmap_text.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/phaser/core/animation_manager.ts +1 -1
- package/src/phaser/core/cache.test.ts +201 -0
- package/src/phaser/core/cache.ts +126 -86
- package/src/phaser/core/input.ts +1 -1
- package/src/phaser/core/input_mspointer.ts +10 -13
- package/src/phaser/core/loader.ts +82 -42
- package/src/phaser/core/loader_parser.test.ts +115 -0
- package/src/phaser/core/loader_parser.ts +64 -36
- package/src/phaser/core/sound.ts +29 -17
- package/src/phaser/core/sound_manager.ts +1 -1
- package/src/phaser/core/tween.ts +6 -6
- package/src/phaser/core/tween_manager.ts +42 -35
- package/src/phaser/display/bitmap_text.test.ts +212 -0
- package/src/phaser/display/bitmap_text.ts +39 -24
|
@@ -8,6 +8,37 @@ import { Signal } from './signal.js';
|
|
|
8
8
|
|
|
9
9
|
const TEXTURE_ATLAS_JSON_HASH = 1;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* What a completed load carries. Which member applies is decided by the file's type, so the
|
|
13
|
+
* completion handlers narrow before handing it to the cache.
|
|
14
|
+
*/
|
|
15
|
+
/** One entry in a pack manifest: the arguments for the loader call it stands for. */
|
|
16
|
+
export type PackEntry = {
|
|
17
|
+
type: string;
|
|
18
|
+
key: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
overwrite?: boolean;
|
|
21
|
+
frameWidth?: number;
|
|
22
|
+
frameHeight?: number;
|
|
23
|
+
frameMax?: number;
|
|
24
|
+
margin?: number;
|
|
25
|
+
spacing?: number;
|
|
26
|
+
urls?: string[] | string;
|
|
27
|
+
autoDecode?: boolean;
|
|
28
|
+
textureURL?: string;
|
|
29
|
+
atlasURL?: string;
|
|
30
|
+
atlasData?: unknown;
|
|
31
|
+
audioURL?: string;
|
|
32
|
+
jsonURL?: string;
|
|
33
|
+
jsonData?: unknown;
|
|
34
|
+
fontDataURL?: string;
|
|
35
|
+
xSpacing?: number;
|
|
36
|
+
ySpacing?: number;
|
|
37
|
+
format?: unknown;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type LoaderFileData = HTMLImageElement | XMLDocument | ArrayBuffer | string | Record<string, unknown> | null;
|
|
41
|
+
|
|
11
42
|
/** An audio source, either a plain URL or a URL paired with its format. */
|
|
12
43
|
export type AudioSource = string | { uri: string; type?: string };
|
|
13
44
|
|
|
@@ -21,7 +52,7 @@ export type LoaderFile = {
|
|
|
21
52
|
path: string;
|
|
22
53
|
url: string | AudioSource[];
|
|
23
54
|
syncPoint: boolean;
|
|
24
|
-
data:
|
|
55
|
+
data: LoaderFileData;
|
|
25
56
|
loading: boolean;
|
|
26
57
|
loaded: boolean;
|
|
27
58
|
error: boolean;
|
|
@@ -311,7 +342,7 @@ export class Loader {
|
|
|
311
342
|
url,
|
|
312
343
|
path: this.path,
|
|
313
344
|
syncPoint: true,
|
|
314
|
-
data: null as
|
|
345
|
+
data: null as LoaderFileData,
|
|
315
346
|
loading: false,
|
|
316
347
|
loaded: false,
|
|
317
348
|
error: false,
|
|
@@ -321,7 +352,7 @@ export class Loader {
|
|
|
321
352
|
if (typeof data === 'string') {
|
|
322
353
|
data = JSON.parse(data);
|
|
323
354
|
}
|
|
324
|
-
pack.data = data ?? {};
|
|
355
|
+
pack.data = (data ?? {}) as LoaderFileData;
|
|
325
356
|
pack.loaded = true;
|
|
326
357
|
}
|
|
327
358
|
for (let i = 0; i < this._fileList.length + 1; i += 1) {
|
|
@@ -755,52 +786,53 @@ export class Loader {
|
|
|
755
786
|
* @param {object} pack - The pack file object to process.
|
|
756
787
|
*/
|
|
757
788
|
public processPack(pack: LoaderFile): void {
|
|
758
|
-
const
|
|
789
|
+
const manifest = (pack.data ?? {}) as Record<string, unknown>;
|
|
790
|
+
const packData = manifest[pack.key] as { files?: unknown[] } | unknown[] | undefined;
|
|
759
791
|
if (!packData) {
|
|
760
792
|
this.game.logger.warn('Missing loader pack key', { key: pack.key });
|
|
761
793
|
return;
|
|
762
794
|
}
|
|
763
|
-
const packDataCompat = Array.isArray(packData) ? packData : packData.files;
|
|
795
|
+
const packDataCompat = (Array.isArray(packData) ? packData : (packData.files ?? [])) as PackEntry[];
|
|
764
796
|
for (const file of packDataCompat) {
|
|
765
797
|
switch (file.type) {
|
|
766
798
|
case 'image': {
|
|
767
|
-
this.image(file.key, file.url, file.overwrite);
|
|
799
|
+
this.image(file.key, file.url ?? file.key, file.overwrite);
|
|
768
800
|
break;
|
|
769
801
|
}
|
|
770
802
|
case 'text': {
|
|
771
|
-
this.text(file.key, file.url, file.overwrite);
|
|
803
|
+
this.text(file.key, file.url ?? file.key, file.overwrite);
|
|
772
804
|
break;
|
|
773
805
|
}
|
|
774
806
|
case 'json': {
|
|
775
|
-
this.json(file.key, file.url, file.overwrite);
|
|
807
|
+
this.json(file.key, file.url ?? file.key, file.overwrite);
|
|
776
808
|
break;
|
|
777
809
|
}
|
|
778
810
|
case 'xml': {
|
|
779
|
-
this.xml(file.key, file.url, file.overwrite);
|
|
811
|
+
this.xml(file.key, file.url ?? file.key, file.overwrite);
|
|
780
812
|
break;
|
|
781
813
|
}
|
|
782
814
|
case 'spritesheet': {
|
|
783
815
|
this.spritesheet(
|
|
784
816
|
file.key,
|
|
785
|
-
file.url,
|
|
786
|
-
file.frameWidth,
|
|
787
|
-
file.frameHeight,
|
|
788
|
-
file.frameMax,
|
|
817
|
+
file.url ?? file.key,
|
|
818
|
+
file.frameWidth ?? 0,
|
|
819
|
+
file.frameHeight ?? 0,
|
|
820
|
+
file.frameMax ?? -1,
|
|
789
821
|
file.margin,
|
|
790
822
|
file.spacing
|
|
791
823
|
);
|
|
792
824
|
break;
|
|
793
825
|
}
|
|
794
826
|
case 'audio': {
|
|
795
|
-
this.audio(file.key, file.urls ?? file.url);
|
|
827
|
+
this.audio(file.key, file.urls ?? file.url ?? file.key);
|
|
796
828
|
break;
|
|
797
829
|
}
|
|
798
830
|
case 'audiosprite': {
|
|
799
|
-
this.audioSprite(file.key, file.urls ?? file.url, file.jsonURL, file.jsonData);
|
|
831
|
+
this.audioSprite(file.key, String(file.urls ?? file.url ?? file.key), file.jsonURL ?? '', file.jsonData);
|
|
800
832
|
break;
|
|
801
833
|
}
|
|
802
834
|
case 'audioSprite': {
|
|
803
|
-
this.audioSprite(file.key, file.audioURL, file.jsonURL, file.jsonData);
|
|
835
|
+
this.audioSprite(file.key, file.audioURL ?? file.key, file.jsonURL ?? '', file.jsonData);
|
|
804
836
|
break;
|
|
805
837
|
}
|
|
806
838
|
case 'bitmapFont': {
|
|
@@ -815,7 +847,7 @@ export class Loader {
|
|
|
815
847
|
break;
|
|
816
848
|
}
|
|
817
849
|
case 'atlas': {
|
|
818
|
-
this.atlas(file.key, file.textureURL, file.atlasURL, file.atlasData, TEXTURE_ATLAS_JSON_HASH);
|
|
850
|
+
this.atlas(file.key, file.textureURL ?? file.key, file.atlasURL, file.atlasData, TEXTURE_ATLAS_JSON_HASH);
|
|
819
851
|
break;
|
|
820
852
|
}
|
|
821
853
|
}
|
|
@@ -898,31 +930,37 @@ export class Loader {
|
|
|
898
930
|
public loadImageTag(file: LoaderFile): void {
|
|
899
931
|
this.log('loadImageTag', file);
|
|
900
932
|
const scope = this;
|
|
901
|
-
|
|
902
|
-
file.data
|
|
903
|
-
|
|
904
|
-
|
|
933
|
+
const image = new globalThis.Image();
|
|
934
|
+
file.data = image;
|
|
935
|
+
image.name = file.key;
|
|
936
|
+
if (typeof this.crossOrigin === 'string' && this.crossOrigin) {
|
|
937
|
+
image.crossOrigin = this.crossOrigin;
|
|
905
938
|
}
|
|
906
|
-
|
|
907
|
-
if (
|
|
908
|
-
|
|
909
|
-
|
|
939
|
+
image.onload = (): void => {
|
|
940
|
+
if (image.onload) {
|
|
941
|
+
image.onload = null;
|
|
942
|
+
image.onerror = null;
|
|
910
943
|
scope.fileComplete(file);
|
|
911
944
|
}
|
|
912
945
|
};
|
|
913
|
-
|
|
946
|
+
image.onerror = (): void => {
|
|
914
947
|
if (scope.isUseRetry && (!file.numRetry || file.numRetry < scope.maxRetry)) {
|
|
915
948
|
setTimeout((): void => {
|
|
916
949
|
file.numRetry = !file.numRetry ? 1 : (file.numRetry += 1);
|
|
917
950
|
scope.loadImageTag(file);
|
|
918
951
|
}, 1000);
|
|
919
|
-
} else if (
|
|
920
|
-
|
|
921
|
-
|
|
952
|
+
} else if (image.onload) {
|
|
953
|
+
image.onload = null;
|
|
954
|
+
image.onerror = null;
|
|
922
955
|
scope.fileError(file);
|
|
923
956
|
}
|
|
924
957
|
};
|
|
925
|
-
|
|
958
|
+
const src = this.transformUrl(file.url, file);
|
|
959
|
+
if (src === false) {
|
|
960
|
+
this.fileError(file, null, 'Could not resolve the file URL');
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
image.src = src;
|
|
926
964
|
}
|
|
927
965
|
|
|
928
966
|
/**
|
|
@@ -1091,14 +1129,14 @@ export class Loader {
|
|
|
1091
1129
|
break;
|
|
1092
1130
|
}
|
|
1093
1131
|
case 'image': {
|
|
1094
|
-
this.cache.addImage(file.key, url, file.data);
|
|
1132
|
+
this.cache.addImage(file.key, url, file.data as HTMLImageElement);
|
|
1095
1133
|
break;
|
|
1096
1134
|
}
|
|
1097
1135
|
case 'spritesheet': {
|
|
1098
1136
|
this.cache.addSpriteSheet(
|
|
1099
1137
|
file.key,
|
|
1100
1138
|
url,
|
|
1101
|
-
file.data,
|
|
1139
|
+
file.data as HTMLImageElement,
|
|
1102
1140
|
file.frameWidth ?? 0,
|
|
1103
1141
|
file.frameHeight ?? 0,
|
|
1104
1142
|
file.frameMax ?? -1,
|
|
@@ -1109,7 +1147,7 @@ export class Loader {
|
|
|
1109
1147
|
}
|
|
1110
1148
|
case 'textureatlas': {
|
|
1111
1149
|
if (file.atlasURL == null) {
|
|
1112
|
-
this.cache.addTextureAtlas(file.key, url, file.data, file.atlasData);
|
|
1150
|
+
this.cache.addTextureAtlas(file.key, url, file.data as HTMLImageElement, file.atlasData);
|
|
1113
1151
|
} else {
|
|
1114
1152
|
loadNext = false;
|
|
1115
1153
|
if (file.format === TEXTURE_ATLAS_JSON_HASH) {
|
|
@@ -1125,7 +1163,7 @@ export class Loader {
|
|
|
1125
1163
|
this.cache.addBitmapFont(
|
|
1126
1164
|
file.key,
|
|
1127
1165
|
url,
|
|
1128
|
-
file.data,
|
|
1166
|
+
file.data as HTMLImageElement,
|
|
1129
1167
|
file.atlasData,
|
|
1130
1168
|
file.atlasType ?? 'xml',
|
|
1131
1169
|
file.xSpacing ?? 0,
|
|
@@ -1159,16 +1197,18 @@ export class Loader {
|
|
|
1159
1197
|
break;
|
|
1160
1198
|
}
|
|
1161
1199
|
case 'audio': {
|
|
1162
|
-
|
|
1163
|
-
|
|
1200
|
+
const audio = response.response as ArrayBuffer;
|
|
1201
|
+
file.data = audio;
|
|
1202
|
+
this.cache.addSound(file.key, url, audio);
|
|
1164
1203
|
if (file.autoDecode) {
|
|
1165
1204
|
this.game.sound.decode(file.key);
|
|
1166
1205
|
}
|
|
1167
1206
|
break;
|
|
1168
1207
|
}
|
|
1169
1208
|
case 'text': {
|
|
1170
|
-
|
|
1171
|
-
|
|
1209
|
+
const { responseText } = response;
|
|
1210
|
+
file.data = responseText;
|
|
1211
|
+
this.cache.addText(file.key, url, responseText);
|
|
1172
1212
|
break;
|
|
1173
1213
|
}
|
|
1174
1214
|
default: {
|
|
@@ -1193,7 +1233,7 @@ export class Loader {
|
|
|
1193
1233
|
this.cache.addBitmapFont(
|
|
1194
1234
|
file.key,
|
|
1195
1235
|
url,
|
|
1196
|
-
file.data,
|
|
1236
|
+
file.data as HTMLImageElement,
|
|
1197
1237
|
data,
|
|
1198
1238
|
file.atlasType ?? 'json',
|
|
1199
1239
|
file.xSpacing ?? 0,
|
|
@@ -1202,7 +1242,7 @@ export class Loader {
|
|
|
1202
1242
|
} else if (file.type === 'json') {
|
|
1203
1243
|
this.cache.addJSON(file.key, url, data);
|
|
1204
1244
|
} else {
|
|
1205
|
-
this.cache.addTextureAtlas(file.key, url, file.data, data);
|
|
1245
|
+
this.cache.addTextureAtlas(file.key, url, file.data as HTMLImageElement, data);
|
|
1206
1246
|
}
|
|
1207
1247
|
this.asyncComplete(file);
|
|
1208
1248
|
}
|
|
@@ -1235,14 +1275,14 @@ export class Loader {
|
|
|
1235
1275
|
this.cache.addBitmapFont(
|
|
1236
1276
|
file.key,
|
|
1237
1277
|
url,
|
|
1238
|
-
file.data,
|
|
1278
|
+
file.data as HTMLImageElement,
|
|
1239
1279
|
xml,
|
|
1240
1280
|
file.atlasType ?? 'xml',
|
|
1241
1281
|
file.xSpacing ?? 0,
|
|
1242
1282
|
file.ySpacing ?? 0
|
|
1243
1283
|
);
|
|
1244
1284
|
} else if (file.type === 'textureatlas') {
|
|
1245
|
-
this.cache.addTextureAtlas(file.key, url, file.data, xml);
|
|
1285
|
+
this.cache.addTextureAtlas(file.key, url, file.data as HTMLImageElement, xml);
|
|
1246
1286
|
} else if (file.type === 'xml') {
|
|
1247
1287
|
this.cache.addXML(file.key, url, xml);
|
|
1248
1288
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { BaseTexture } from '../display/webgl/base_texture.js';
|
|
3
|
+
import { bitmapFont, jsonBitmapFont, xmlBitmapFont } from './loader_parser.js';
|
|
4
|
+
|
|
5
|
+
const baseTexture = { width: 256, height: 256 } as unknown as BaseTexture;
|
|
6
|
+
|
|
7
|
+
const parseXml = (markup: string): XMLDocument => new DOMParser().parseFromString(markup, 'text/xml');
|
|
8
|
+
|
|
9
|
+
const FONT_XML = `<?xml version="1.0"?>
|
|
10
|
+
<font>
|
|
11
|
+
<info face="Arial" size="32" />
|
|
12
|
+
<common lineHeight="40" />
|
|
13
|
+
<chars>
|
|
14
|
+
<char id="65" x="0" y="0" width="20" height="30" xoffset="1" yoffset="2" xadvance="21" />
|
|
15
|
+
<char id="66" x="20" y="0" width="22" height="30" xoffset="0" yoffset="2" xadvance="23" />
|
|
16
|
+
</chars>
|
|
17
|
+
<kernings>
|
|
18
|
+
<kerning first="65" second="66" amount="-3" />
|
|
19
|
+
</kernings>
|
|
20
|
+
</font>`;
|
|
21
|
+
|
|
22
|
+
const FONT_JSON = {
|
|
23
|
+
font: {
|
|
24
|
+
info: { _face: 'Arial', _size: '32' },
|
|
25
|
+
common: { _lineHeight: '40' },
|
|
26
|
+
chars: {
|
|
27
|
+
char: [
|
|
28
|
+
{ _id: '65', _x: '0', _y: '0', _width: '20', _height: '30', _xoffset: '1', _yoffset: '2', _xadvance: '21' },
|
|
29
|
+
{ _id: '66', _x: '20', _y: '0', _width: '22', _height: '30', _xoffset: '0', _yoffset: '2', _xadvance: '23' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
kernings: { kerning: [{ _first: '65', _second: '66', _amount: '-3' }] },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('bitmap font parsing', () => {
|
|
37
|
+
describe('xmlBitmapFont', () => {
|
|
38
|
+
it('reads the font metrics', () => {
|
|
39
|
+
const data = xmlBitmapFont(parseXml(FONT_XML), baseTexture, 0, 0);
|
|
40
|
+
expect(data.font).toBe('Arial');
|
|
41
|
+
expect(data.size).toBe(32);
|
|
42
|
+
expect(data.lineHeight).toBe(40);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('reads every glyph, keyed by character code', () => {
|
|
46
|
+
const data = xmlBitmapFont(parseXml(FONT_XML), baseTexture, 0, 0);
|
|
47
|
+
expect(Object.keys(data.chars)).toStrictEqual(['65', '66']);
|
|
48
|
+
expect(data.chars[65]).toMatchObject({ x: 0, y: 0, width: 20, height: 30, xOffset: 1, yOffset: 2 });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('adds the spacing arguments to advance and line height', () => {
|
|
52
|
+
const data = xmlBitmapFont(parseXml(FONT_XML), baseTexture, 5, 7);
|
|
53
|
+
expect(data.chars[65]?.xAdvance).toBe(26);
|
|
54
|
+
expect(data.lineHeight).toBe(47);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('records kerning against the second character of the pair', () => {
|
|
58
|
+
const data = xmlBitmapFont(parseXml(FONT_XML), baseTexture, 0, 0);
|
|
59
|
+
expect(data.chars[66]?.kerning[65]).toBe(-3);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('attaches a texture to every glyph', () => {
|
|
63
|
+
const data = xmlBitmapFont(parseXml(FONT_XML), baseTexture, 0, 0);
|
|
64
|
+
expect(data.chars[65]?.texture).toBeDefined();
|
|
65
|
+
expect(data.chars[66]?.texture).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('survives a font with no kerning table', () => {
|
|
69
|
+
const markup = FONT_XML.replace(/<kernings>[\s\S]*<\/kernings>/, '');
|
|
70
|
+
expect(() => xmlBitmapFont(parseXml(markup), baseTexture, 0, 0)).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('ignores kerning that names a character the font does not have', () => {
|
|
74
|
+
const markup = FONT_XML.replace('second="66"', 'second="999"');
|
|
75
|
+
const data = xmlBitmapFont(parseXml(markup), baseTexture, 0, 0);
|
|
76
|
+
expect(data.chars[66]?.kerning).toStrictEqual({});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('falls back to an empty face when the info element is missing', () => {
|
|
80
|
+
const markup = FONT_XML.replace(/<info[^>]*\/>/, '');
|
|
81
|
+
expect(xmlBitmapFont(parseXml(markup), baseTexture, 0, 0).font).toBe('');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('jsonBitmapFont', () => {
|
|
86
|
+
it('reads the same metrics as the xml form', () => {
|
|
87
|
+
const data = jsonBitmapFont(FONT_JSON, baseTexture, 0, 0);
|
|
88
|
+
expect(data.font).toBe('Arial');
|
|
89
|
+
expect(data.size).toBe(32);
|
|
90
|
+
expect(data.lineHeight).toBe(40);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('produces the same glyphs as the xml form', () => {
|
|
94
|
+
const fromJson = jsonBitmapFont(FONT_JSON, baseTexture, 0, 0);
|
|
95
|
+
const fromXml = xmlBitmapFont(parseXml(FONT_XML), baseTexture, 0, 0);
|
|
96
|
+
for (const code of [65, 66]) {
|
|
97
|
+
expect(fromJson.chars[code]).toMatchObject({
|
|
98
|
+
x: fromXml.chars[code]!.x,
|
|
99
|
+
width: fromXml.chars[code]!.width,
|
|
100
|
+
xAdvance: fromXml.chars[code]!.xAdvance,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('records the same kerning as the xml form', () => {
|
|
106
|
+
expect(jsonBitmapFont(FONT_JSON, baseTexture, 0, 0).chars[66]?.kerning[65]).toBe(-3);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('bitmapFont', () => {
|
|
111
|
+
it('parses the xml form', () => {
|
|
112
|
+
expect(bitmapFont(parseXml(FONT_XML), baseTexture, 0, 0).font).toBe('Arial');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -2,17 +2,37 @@ import { Texture } from '../display/webgl/texture.js';
|
|
|
2
2
|
import { Rectangle } from '../geom/rectangle.js';
|
|
3
3
|
import type { BaseTexture } from '../display/webgl/base_texture.js';
|
|
4
4
|
|
|
5
|
+
/** One glyph in a bitmap font, as parsed from the font descriptor. */
|
|
6
|
+
export type BitmapFontChar = {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
xOffset: number;
|
|
12
|
+
yOffset: number;
|
|
13
|
+
xAdvance: number;
|
|
14
|
+
kerning: Record<number, number>;
|
|
15
|
+
texture?: Texture;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** A parsed bitmap font: its metrics and the glyphs it can draw. */
|
|
19
|
+
export type BitmapFontData = {
|
|
20
|
+
font: string;
|
|
21
|
+
size: number;
|
|
22
|
+
lineHeight: number;
|
|
23
|
+
chars: Record<number, BitmapFontChar>;
|
|
24
|
+
};
|
|
25
|
+
|
|
5
26
|
/**
|
|
6
27
|
* Finalizes bitmap font data by attaching textures to characters.
|
|
7
28
|
* @param {BaseTexture} baseTexture - The base texture for the font.
|
|
8
29
|
* @param {object} bitmapFontData - The bitmap font data to finalize.
|
|
9
30
|
* @returns {object} The finalized bitmap font data.
|
|
10
31
|
*/
|
|
11
|
-
export const finalizeBitmapFont = (baseTexture: BaseTexture, bitmapFontData:
|
|
12
|
-
Object.
|
|
13
|
-
const letter = bitmapFontData.chars[charCode];
|
|
32
|
+
export const finalizeBitmapFont = (baseTexture: BaseTexture, bitmapFontData: BitmapFontData): BitmapFontData => {
|
|
33
|
+
for (const letter of Object.values(bitmapFontData.chars)) {
|
|
14
34
|
letter.texture = new Texture(baseTexture, new Rectangle(letter.x, letter.y, letter.width, letter.height));
|
|
15
|
-
}
|
|
35
|
+
}
|
|
16
36
|
return bitmapFontData;
|
|
17
37
|
};
|
|
18
38
|
|
|
@@ -24,34 +44,38 @@ export const finalizeBitmapFont = (baseTexture: BaseTexture, bitmapFontData: any
|
|
|
24
44
|
* @param {number} ySpacing - Vertical spacing between characters.
|
|
25
45
|
* @returns {object} The parsed bitmap font data.
|
|
26
46
|
*/
|
|
27
|
-
export const xmlBitmapFont = (
|
|
28
|
-
|
|
47
|
+
export const xmlBitmapFont = (
|
|
48
|
+
xml: XMLDocument,
|
|
49
|
+
baseTexture: BaseTexture,
|
|
50
|
+
xSpacing: number,
|
|
51
|
+
ySpacing: number
|
|
52
|
+
): BitmapFontData => {
|
|
29
53
|
const info = xml.querySelectorAll('info')[0];
|
|
30
54
|
const common = xml.querySelectorAll('common')[0];
|
|
31
|
-
|
|
32
|
-
data
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
const attr = (element: Element | undefined, name: string): number => Math.trunc(Number(element?.getAttribute(name)));
|
|
56
|
+
const data: BitmapFontData = {
|
|
57
|
+
font: info?.getAttribute('face') ?? '',
|
|
58
|
+
size: attr(info, 'size'),
|
|
59
|
+
lineHeight: attr(common, 'lineHeight') + ySpacing,
|
|
60
|
+
chars: {},
|
|
61
|
+
};
|
|
62
|
+
for (const letter of xml.querySelectorAll('char')) {
|
|
63
|
+
data.chars[attr(letter, 'id')] = {
|
|
64
|
+
x: attr(letter, 'x'),
|
|
65
|
+
y: attr(letter, 'y'),
|
|
66
|
+
width: attr(letter, 'width'),
|
|
67
|
+
height: attr(letter, 'height'),
|
|
68
|
+
xOffset: attr(letter, 'xoffset'),
|
|
69
|
+
yOffset: attr(letter, 'yoffset'),
|
|
70
|
+
xAdvance: attr(letter, 'xadvance') + xSpacing,
|
|
46
71
|
kerning: {},
|
|
47
72
|
};
|
|
48
73
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
data.chars[second].kerning[first] = amount;
|
|
74
|
+
for (const kerning of xml.querySelectorAll('kerning')) {
|
|
75
|
+
const char = data.chars[attr(kerning, 'second')];
|
|
76
|
+
if (char) {
|
|
77
|
+
char.kerning[attr(kerning, 'first')] = attr(kerning, 'amount');
|
|
78
|
+
}
|
|
55
79
|
}
|
|
56
80
|
return finalizeBitmapFont(baseTexture, data);
|
|
57
81
|
};
|
|
@@ -64,8 +88,12 @@ export const xmlBitmapFont = (xml: any, baseTexture: BaseTexture, xSpacing: numb
|
|
|
64
88
|
* @param {number} ySpacing - Vertical spacing between characters.
|
|
65
89
|
* @returns {object} The parsed bitmap font data.
|
|
66
90
|
*/
|
|
67
|
-
export const bitmapFont = (
|
|
68
|
-
|
|
91
|
+
export const bitmapFont = (
|
|
92
|
+
xml: XMLDocument,
|
|
93
|
+
baseTexture: BaseTexture,
|
|
94
|
+
xSpacing: number,
|
|
95
|
+
ySpacing: number
|
|
96
|
+
): BitmapFontData => xmlBitmapFont(xml, baseTexture, xSpacing, ySpacing);
|
|
69
97
|
|
|
70
98
|
/**
|
|
71
99
|
* Parses JSON bitmap font data.
|
|
@@ -75,13 +103,13 @@ export const bitmapFont = (xml: any, baseTexture: BaseTexture, xSpacing: number,
|
|
|
75
103
|
* @param {number} ySpacing - Vertical spacing between characters.
|
|
76
104
|
* @returns {object} The parsed bitmap font data.
|
|
77
105
|
*/
|
|
78
|
-
export const jsonBitmapFont = (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
export const jsonBitmapFont = (
|
|
107
|
+
json: any,
|
|
108
|
+
baseTexture: BaseTexture,
|
|
109
|
+
xSpacing: number,
|
|
110
|
+
ySpacing: number
|
|
111
|
+
): BitmapFontData => {
|
|
112
|
+
const data: BitmapFontData = {
|
|
85
113
|
font: json.font.info._face,
|
|
86
114
|
size: Math.trunc(Number(json.font.info._size)),
|
|
87
115
|
lineHeight: Math.trunc(Number(json.font.common._lineHeight)) + ySpacing,
|
package/src/phaser/core/sound.ts
CHANGED
|
@@ -2,13 +2,24 @@ import { Signal } from './signal.js';
|
|
|
2
2
|
import type { Game } from './game.js';
|
|
3
3
|
import type { Tween } from './tween.js';
|
|
4
4
|
|
|
5
|
+
/** A named span within a sound file. */
|
|
6
|
+
export type SoundMarker = {
|
|
7
|
+
name: string;
|
|
8
|
+
start: number;
|
|
9
|
+
stop: number;
|
|
10
|
+
volume: number;
|
|
11
|
+
duration: number;
|
|
12
|
+
durationMS: number;
|
|
13
|
+
loop: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
5
16
|
export class Sound {
|
|
6
|
-
public _paused!:
|
|
17
|
+
public _paused!: boolean;
|
|
7
18
|
public game!: Game;
|
|
8
19
|
public name!: string;
|
|
9
20
|
public key!: string;
|
|
10
21
|
public loop!: boolean;
|
|
11
|
-
public markers!:
|
|
22
|
+
public markers!: Record<string, SoundMarker>;
|
|
12
23
|
public context!: AudioContext | null;
|
|
13
24
|
public autoplay!: boolean;
|
|
14
25
|
public totalDuration!: number;
|
|
@@ -124,8 +135,10 @@ export class Sound {
|
|
|
124
135
|
*/
|
|
125
136
|
public soundHasUnlocked(key: string): void {
|
|
126
137
|
if (key === this.key) {
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
// Unlocking parks the decoded buffer in _sound; play() replaces it with a real source node.
|
|
139
|
+
const buffer = this.game.cache.getSoundData(this.key) as AudioBuffer;
|
|
140
|
+
this._sound = buffer as unknown as AudioBufferSourceNode;
|
|
141
|
+
this.totalDuration = buffer.duration;
|
|
129
142
|
}
|
|
130
143
|
}
|
|
131
144
|
|
|
@@ -246,7 +259,7 @@ export class Sound {
|
|
|
246
259
|
* @param {boolean} forceRestart - Whether to force restarting the sound even if it's already playing.
|
|
247
260
|
* @returns {Sound} This Sound instance for chaining.
|
|
248
261
|
*/
|
|
249
|
-
public play(marker:
|
|
262
|
+
public play(marker: string | false | null = '', position = 0, volume = 1, loop = false, forceRestart = true): this {
|
|
250
263
|
if (marker === undefined || marker === false || marker === null) {
|
|
251
264
|
marker = '';
|
|
252
265
|
}
|
|
@@ -274,15 +287,16 @@ export class Sound {
|
|
|
274
287
|
// We should never play the entire thing
|
|
275
288
|
return this;
|
|
276
289
|
}
|
|
290
|
+
const markerData = marker === '' ? undefined : this.markers[marker];
|
|
277
291
|
if (marker !== '') {
|
|
278
|
-
if (
|
|
292
|
+
if (markerData) {
|
|
279
293
|
this.currentMarker = marker;
|
|
280
294
|
// Playing a marker? Then we default to the marker values
|
|
281
|
-
this.position =
|
|
282
|
-
this.volume =
|
|
283
|
-
this.loop =
|
|
284
|
-
this.duration =
|
|
285
|
-
this.durationMS =
|
|
295
|
+
this.position = markerData.start;
|
|
296
|
+
this.volume = markerData.volume;
|
|
297
|
+
this.loop = markerData.loop;
|
|
298
|
+
this.duration = markerData.duration;
|
|
299
|
+
this.durationMS = markerData.durationMS;
|
|
286
300
|
if (volume !== undefined) {
|
|
287
301
|
this.volume = volume;
|
|
288
302
|
}
|
|
@@ -298,10 +312,8 @@ export class Sound {
|
|
|
298
312
|
return this;
|
|
299
313
|
}
|
|
300
314
|
} else {
|
|
301
|
-
position
|
|
302
|
-
|
|
303
|
-
volume = this._volume;
|
|
304
|
-
}
|
|
315
|
+
position ??= 0;
|
|
316
|
+
volume ??= this._volume;
|
|
305
317
|
if (loop === undefined) {
|
|
306
318
|
({ loop } = this);
|
|
307
319
|
}
|
|
@@ -323,7 +335,7 @@ export class Sound {
|
|
|
323
335
|
} else {
|
|
324
336
|
this._sound.connect(this.gainNode);
|
|
325
337
|
}
|
|
326
|
-
this._buffer = this.game.cache.getSoundData(this.key);
|
|
338
|
+
this._buffer = this.game.cache.getSoundData(this.key) as AudioBuffer | null;
|
|
327
339
|
this._sound.buffer = this._buffer;
|
|
328
340
|
if (this.loop && marker === '') {
|
|
329
341
|
this._sound.loop = true;
|
|
@@ -351,7 +363,7 @@ export class Sound {
|
|
|
351
363
|
this.onPlay.dispatch(this);
|
|
352
364
|
} else {
|
|
353
365
|
this.pendingPlayback = true;
|
|
354
|
-
if (this.game.cache.getSound(this.key)
|
|
366
|
+
if (this.game.cache.getSound(this.key)?.isDecoding === false) {
|
|
355
367
|
this.game.sound.decode(this.key);
|
|
356
368
|
}
|
|
357
369
|
}
|
|
@@ -272,7 +272,7 @@ export class SoundManager {
|
|
|
272
272
|
*/
|
|
273
273
|
public decode(key: string): void {
|
|
274
274
|
const soundData = this.game.cache.getSoundData(key);
|
|
275
|
-
if (!soundData) {
|
|
275
|
+
if (!(soundData instanceof ArrayBuffer)) {
|
|
276
276
|
return;
|
|
277
277
|
}
|
|
278
278
|
if (this.game.cache.isSoundDecoded(key) === true) {
|