@penabt/pixi-expo 0.2.0 → 0.3.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/README.md CHANGED
@@ -143,21 +143,92 @@ import {
143
143
 
144
144
  ## Loading Assets
145
145
 
146
- ### Bundled Assets (require)
146
+ All asset loading goes through the Expo-compatible loader. PixiJS's browser-dependent `loadTextures` parser is replaced automatically — you don't need to do anything special.
147
+
148
+ Supported formats: `.png`, `.jpg`, `.jpeg`, `.webp`, `.avif`, `.gif`, `data:image/*`
149
+
150
+ ### Direct Loading — `Assets.load()`
151
+
152
+ Works with both `require()` (local bundled) and string URLs (remote):
153
+
154
+ ```tsx
155
+ import { Assets, Sprite } from '@penabt/pixi-expo';
156
+
157
+ // Local asset via require()
158
+ const bunny = await Assets.load(require('./assets/bunny.png'));
159
+ const sprite = new Sprite(bunny);
160
+
161
+ // Remote asset via URL
162
+ const remote = await Assets.load('https://example.com/sprite.png');
163
+
164
+ // Multiple assets
165
+ const [a, b] = await Assets.load([require('./assets/a.png'), 'https://example.com/b.png']);
166
+ ```
167
+
168
+ ### Manifest & Bundles — `createExpoManifest()`
169
+
170
+ For larger projects, group assets into bundles and load them on demand:
171
+
172
+ ```tsx
173
+ import { Assets, createExpoManifest } from '@penabt/pixi-expo';
174
+
175
+ const manifest = createExpoManifest({
176
+ bundles: [
177
+ {
178
+ name: 'load-screen',
179
+ assets: [{ alias: 'logo', src: require('./assets/logo.png') }],
180
+ },
181
+ {
182
+ name: 'game',
183
+ assets: [
184
+ { alias: 'hero', src: require('./assets/hero.png') },
185
+ { alias: 'enemy', src: 'https://cdn.example.com/enemy.png' },
186
+ ],
187
+ },
188
+ ],
189
+ });
190
+
191
+ // Initialize once
192
+ await Assets.init({ manifest });
193
+
194
+ // Load bundles on demand
195
+ const loadAssets = await Assets.loadBundle('load-screen');
196
+ const gameAssets = await Assets.loadBundle('game');
197
+ const heroSprite = new Sprite(gameAssets.hero);
198
+ ```
199
+
200
+ ### Dynamic Bundles — `createExpoBundle()`
201
+
202
+ Register bundles at runtime:
147
203
 
148
204
  ```tsx
149
- import { loadTexture, Sprite } from '@penabt/pixi-expo';
205
+ import { Assets, createExpoBundle } from '@penabt/pixi-expo';
206
+
207
+ Assets.addBundle(
208
+ 'powerups',
209
+ createExpoBundle([
210
+ { alias: 'shield', src: require('./assets/shield.png') },
211
+ { alias: 'speed', src: 'https://cdn.example.com/speed.png' },
212
+ ]),
213
+ );
150
214
 
151
- // Load a bundled image
152
- const texture = await loadTexture(require('./assets/bunny.png'));
153
- const sprite = new Sprite(texture);
215
+ const powerups = await Assets.loadBundle('powerups');
154
216
  ```
155
217
 
156
- ### Remote Assets (URL)
218
+ ### BitmapFont
219
+
220
+ Load `.fnt` or `.xml` bitmap fonts — the atlas texture is loaded automatically:
157
221
 
158
222
  ```tsx
159
- // Load from URL
160
- const texture = await Assets.load('https://example.com/sprite.png');
223
+ import { Assets, BitmapText } from '@penabt/pixi-expo';
224
+
225
+ await Assets.load('https://example.com/fonts/myfont.xml');
226
+
227
+ const score = new BitmapText({
228
+ text: 'Score: 0',
229
+ style: { fontFamily: 'MyFont', fontSize: 32 },
230
+ });
231
+ app.stage.addChild(score);
161
232
  ```
162
233
 
163
234
  ## Performance Tips
@@ -174,9 +245,9 @@ const texture = await Assets.load('https://example.com/sprite.png');
174
245
 
175
246
  ## Limitations
176
247
 
177
- - **No Canvas 2D** - expo-gl only supports WebGL, not Canvas 2D context
178
- - **No HTMLText** - HTML-based text rendering is not available
179
- - **Font Loading** - Use expo-font for loading custom fonts
248
+ - **No Canvas 2D** expo-gl only supports WebGL, not Canvas 2D context
249
+ - **No Text** `Text` (canvas-based) and `HTMLText` are not available. Use `BitmapText` instead
250
+ - **Font Loading** Use `BitmapFont` (`.fnt`/`.xml` + atlas) for in-game text, or `expo-font` for system fonts
180
251
 
181
252
  ## Compatibility
182
253
 
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ExpoWebGLRenderingContext } from 'expo-gl';
2
- import { LoaderParser, Texture, Application, Container } from 'pixi.js';
3
- export { AnimatedSprite, Application, ApplicationOptions, Assets, Batcher, BitmapText, BlurFilter, Circle, Color, ColorMatrixFilter, Container, DOMAdapter, DisplacementFilter, Ellipse, EventBoundary, ExtensionType, FederatedPointerEvent, Filter, FilterOptions, Graphics, Matrix, Mesh, NineSliceSprite, NoiseFilter, ObservablePoint, Point, Polygon, Rectangle, RenderTexture, Renderer, RoundedRectangle, Sprite, Spritesheet, SpritesheetData, Text, TextStyle, Texture, TextureSource, TextureSourceOptions, Ticker, TilingSprite, extensions } from 'pixi.js';
2
+ import { LoaderParser, Texture, ArrayOr, AssetsBundle, AssetsManifest, UnresolvedAsset, Application, Container } from 'pixi.js';
3
+ export { AnimatedSprite, Application, ApplicationOptions, Assets, AssetsBundle, AssetsManifest, Batcher, BitmapText, BlurFilter, Circle, Color, ColorMatrixFilter, Container, DOMAdapter, DisplacementFilter, Ellipse, EventBoundary, ExtensionType, FederatedPointerEvent, Filter, FilterOptions, Graphics, Matrix, Mesh, NineSliceSprite, NoiseFilter, ObservablePoint, Point, Polygon, Rectangle, RenderTexture, Renderer, RoundedRectangle, Sprite, Spritesheet, SpritesheetData, Text, TextStyle, Texture, TextureSource, TextureSourceOptions, Ticker, TilingSprite, UnresolvedAsset, extensions } from 'pixi.js';
4
4
  import * as react from 'react';
5
5
  import { ViewStyle, NativeTouchEvent, GestureResponderEvent } from 'react-native';
6
6
 
@@ -499,6 +499,78 @@ declare const loadExpoAsset: LoaderParser<Texture>;
499
499
  */
500
500
  declare const loadExpoFont: LoaderParser<string>;
501
501
 
502
+ /**
503
+ * PixiJS Manifest & Bundle utilities for Expo.
504
+ *
505
+ * Transforms Expo-flavored manifests (where `src` can be a require() numeric ID)
506
+ * into standard PixiJS manifests (where `src` is always a string).
507
+ */
508
+
509
+ /** An asset source that accepts Expo require() numeric IDs alongside strings. */
510
+ type ExpoAssetSrc = number | string;
511
+ /**
512
+ * An unresolved asset that accepts require() IDs in src.
513
+ * Mirrors PixiJS UnresolvedAsset but src can include numeric module IDs.
514
+ */
515
+ interface ExpoUnresolvedAsset<T = any> {
516
+ alias?: ArrayOr<string>;
517
+ src?: ExpoAssetSrc | ExpoAssetSrc[];
518
+ data?: T;
519
+ format?: string;
520
+ parser?: string;
521
+ [key: string]: any;
522
+ }
523
+ /** A bundle that accepts Expo require() IDs. */
524
+ interface ExpoAssetsBundle {
525
+ name: string;
526
+ assets: ExpoUnresolvedAsset[] | Record<string, ExpoAssetSrc | ExpoAssetSrc[] | ExpoUnresolvedAsset>;
527
+ }
528
+ /** A manifest that accepts Expo require() IDs. */
529
+ interface ExpoAssetsManifest {
530
+ bundles: ExpoAssetsBundle[];
531
+ }
532
+ /**
533
+ * Transform a single Expo asset entry into a standard PixiJS UnresolvedAsset.
534
+ * Numeric require() IDs are registered and converted to string keys.
535
+ */
536
+ declare function resolveExpoAsset<T = any>(asset: ExpoUnresolvedAsset<T>): UnresolvedAsset<T>;
537
+ /**
538
+ * Transform bundle assets for use with `Assets.addBundle()`.
539
+ * Handles both array and Record formats.
540
+ *
541
+ * @example
542
+ * ```ts
543
+ * Assets.addBundle('animals', createExpoBundle([
544
+ * { alias: 'bunny', src: require('./assets/bunny.png') },
545
+ * { alias: 'chicken', src: 'https://example.com/chicken.png' },
546
+ * ]));
547
+ * ```
548
+ */
549
+ declare function createExpoBundle(assets: ExpoAssetsBundle['assets']): AssetsBundle['assets'];
550
+ /**
551
+ * Transform an Expo-flavored manifest into a standard PixiJS AssetsManifest.
552
+ * Numeric require() IDs in `src` fields are registered and converted to string keys.
553
+ *
554
+ * @example
555
+ * ```ts
556
+ * const manifest = createExpoManifest({
557
+ * bundles: [
558
+ * {
559
+ * name: 'game-screen',
560
+ * assets: [
561
+ * { alias: 'character', src: require('./assets/robot.png') },
562
+ * { alias: 'enemy', src: 'https://example.com/bad-guy.png' },
563
+ * ],
564
+ * },
565
+ * ],
566
+ * });
567
+ *
568
+ * await Assets.init({ manifest });
569
+ * const assets = await Assets.loadBundle('game-screen');
570
+ * ```
571
+ */
572
+ declare function createExpoManifest(manifest: ExpoAssetsManifest): AssetsManifest;
573
+
502
574
  /**
503
575
  * Props for the PixiView component.
504
576
  */
@@ -744,4 +816,4 @@ declare function clearTouchTracking(): void;
744
816
  */
745
817
  declare function getActiveTouchCount(): number;
746
818
 
747
- export { ExpoAdapter, ExpoCanvasElement, type NativePointerEvent, PixiView, type PixiViewHandle, type PixiViewProps, type TouchEventBridgeOptions, clearActiveContext, clearTouchTracking, convertTouchToPointerEvents, getActiveCanvas, getActiveGL, getActiveTouchCount, loadExpoAsset, loadExpoFont, loadTexture, setActiveGLContext };
819
+ export { ExpoAdapter, type ExpoAssetSrc, type ExpoAssetsBundle, type ExpoAssetsManifest, ExpoCanvasElement, type ExpoUnresolvedAsset, type NativePointerEvent, PixiView, type PixiViewHandle, type PixiViewProps, type TouchEventBridgeOptions, clearActiveContext, clearTouchTracking, convertTouchToPointerEvents, createExpoBundle, createExpoManifest, getActiveCanvas, getActiveGL, getActiveTouchCount, loadExpoAsset, loadExpoFont, loadTexture, resolveExpoAsset, setActiveGLContext };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ExpoWebGLRenderingContext } from 'expo-gl';
2
- import { LoaderParser, Texture, Application, Container } from 'pixi.js';
3
- export { AnimatedSprite, Application, ApplicationOptions, Assets, Batcher, BitmapText, BlurFilter, Circle, Color, ColorMatrixFilter, Container, DOMAdapter, DisplacementFilter, Ellipse, EventBoundary, ExtensionType, FederatedPointerEvent, Filter, FilterOptions, Graphics, Matrix, Mesh, NineSliceSprite, NoiseFilter, ObservablePoint, Point, Polygon, Rectangle, RenderTexture, Renderer, RoundedRectangle, Sprite, Spritesheet, SpritesheetData, Text, TextStyle, Texture, TextureSource, TextureSourceOptions, Ticker, TilingSprite, extensions } from 'pixi.js';
2
+ import { LoaderParser, Texture, ArrayOr, AssetsBundle, AssetsManifest, UnresolvedAsset, Application, Container } from 'pixi.js';
3
+ export { AnimatedSprite, Application, ApplicationOptions, Assets, AssetsBundle, AssetsManifest, Batcher, BitmapText, BlurFilter, Circle, Color, ColorMatrixFilter, Container, DOMAdapter, DisplacementFilter, Ellipse, EventBoundary, ExtensionType, FederatedPointerEvent, Filter, FilterOptions, Graphics, Matrix, Mesh, NineSliceSprite, NoiseFilter, ObservablePoint, Point, Polygon, Rectangle, RenderTexture, Renderer, RoundedRectangle, Sprite, Spritesheet, SpritesheetData, Text, TextStyle, Texture, TextureSource, TextureSourceOptions, Ticker, TilingSprite, UnresolvedAsset, extensions } from 'pixi.js';
4
4
  import * as react from 'react';
5
5
  import { ViewStyle, NativeTouchEvent, GestureResponderEvent } from 'react-native';
6
6
 
@@ -499,6 +499,78 @@ declare const loadExpoAsset: LoaderParser<Texture>;
499
499
  */
500
500
  declare const loadExpoFont: LoaderParser<string>;
501
501
 
502
+ /**
503
+ * PixiJS Manifest & Bundle utilities for Expo.
504
+ *
505
+ * Transforms Expo-flavored manifests (where `src` can be a require() numeric ID)
506
+ * into standard PixiJS manifests (where `src` is always a string).
507
+ */
508
+
509
+ /** An asset source that accepts Expo require() numeric IDs alongside strings. */
510
+ type ExpoAssetSrc = number | string;
511
+ /**
512
+ * An unresolved asset that accepts require() IDs in src.
513
+ * Mirrors PixiJS UnresolvedAsset but src can include numeric module IDs.
514
+ */
515
+ interface ExpoUnresolvedAsset<T = any> {
516
+ alias?: ArrayOr<string>;
517
+ src?: ExpoAssetSrc | ExpoAssetSrc[];
518
+ data?: T;
519
+ format?: string;
520
+ parser?: string;
521
+ [key: string]: any;
522
+ }
523
+ /** A bundle that accepts Expo require() IDs. */
524
+ interface ExpoAssetsBundle {
525
+ name: string;
526
+ assets: ExpoUnresolvedAsset[] | Record<string, ExpoAssetSrc | ExpoAssetSrc[] | ExpoUnresolvedAsset>;
527
+ }
528
+ /** A manifest that accepts Expo require() IDs. */
529
+ interface ExpoAssetsManifest {
530
+ bundles: ExpoAssetsBundle[];
531
+ }
532
+ /**
533
+ * Transform a single Expo asset entry into a standard PixiJS UnresolvedAsset.
534
+ * Numeric require() IDs are registered and converted to string keys.
535
+ */
536
+ declare function resolveExpoAsset<T = any>(asset: ExpoUnresolvedAsset<T>): UnresolvedAsset<T>;
537
+ /**
538
+ * Transform bundle assets for use with `Assets.addBundle()`.
539
+ * Handles both array and Record formats.
540
+ *
541
+ * @example
542
+ * ```ts
543
+ * Assets.addBundle('animals', createExpoBundle([
544
+ * { alias: 'bunny', src: require('./assets/bunny.png') },
545
+ * { alias: 'chicken', src: 'https://example.com/chicken.png' },
546
+ * ]));
547
+ * ```
548
+ */
549
+ declare function createExpoBundle(assets: ExpoAssetsBundle['assets']): AssetsBundle['assets'];
550
+ /**
551
+ * Transform an Expo-flavored manifest into a standard PixiJS AssetsManifest.
552
+ * Numeric require() IDs in `src` fields are registered and converted to string keys.
553
+ *
554
+ * @example
555
+ * ```ts
556
+ * const manifest = createExpoManifest({
557
+ * bundles: [
558
+ * {
559
+ * name: 'game-screen',
560
+ * assets: [
561
+ * { alias: 'character', src: require('./assets/robot.png') },
562
+ * { alias: 'enemy', src: 'https://example.com/bad-guy.png' },
563
+ * ],
564
+ * },
565
+ * ],
566
+ * });
567
+ *
568
+ * await Assets.init({ manifest });
569
+ * const assets = await Assets.loadBundle('game-screen');
570
+ * ```
571
+ */
572
+ declare function createExpoManifest(manifest: ExpoAssetsManifest): AssetsManifest;
573
+
502
574
  /**
503
575
  * Props for the PixiView component.
504
576
  */
@@ -744,4 +816,4 @@ declare function clearTouchTracking(): void;
744
816
  */
745
817
  declare function getActiveTouchCount(): number;
746
818
 
747
- export { ExpoAdapter, ExpoCanvasElement, type NativePointerEvent, PixiView, type PixiViewHandle, type PixiViewProps, type TouchEventBridgeOptions, clearActiveContext, clearTouchTracking, convertTouchToPointerEvents, getActiveCanvas, getActiveGL, getActiveTouchCount, loadExpoAsset, loadExpoFont, loadTexture, setActiveGLContext };
819
+ export { ExpoAdapter, type ExpoAssetSrc, type ExpoAssetsBundle, type ExpoAssetsManifest, ExpoCanvasElement, type ExpoUnresolvedAsset, type NativePointerEvent, PixiView, type PixiViewHandle, type PixiViewProps, type TouchEventBridgeOptions, clearActiveContext, clearTouchTracking, convertTouchToPointerEvents, createExpoBundle, createExpoManifest, getActiveCanvas, getActiveGL, getActiveTouchCount, loadExpoAsset, loadExpoFont, loadTexture, resolveExpoAsset, setActiveGLContext };
package/dist/index.js CHANGED
@@ -72,6 +72,8 @@ __export(index_exports, {
72
72
  clearActiveContext: () => clearActiveContext,
73
73
  clearTouchTracking: () => clearTouchTracking,
74
74
  convertTouchToPointerEvents: () => convertTouchToPointerEvents,
75
+ createExpoBundle: () => createExpoBundle,
76
+ createExpoManifest: () => createExpoManifest,
75
77
  extensions: () => import_pixi5.extensions,
76
78
  getActiveCanvas: () => getActiveCanvas,
77
79
  getActiveGL: () => getActiveGL,
@@ -79,6 +81,7 @@ __export(index_exports, {
79
81
  loadExpoAsset: () => loadExpoAsset,
80
82
  loadExpoFont: () => loadExpoFont,
81
83
  loadTexture: () => loadTexture,
84
+ resolveExpoAsset: () => resolveExpoAsset,
82
85
  setActiveGLContext: () => setActiveGLContext
83
86
  });
84
87
  module.exports = __toCommonJS(index_exports);
@@ -300,7 +303,6 @@ function windowRemoveEventListener(type, listener, _options) {
300
303
  function dispatchWindowEvent(event) {
301
304
  const listeners = windowListeners.get(event.type);
302
305
  if (__DEV__) {
303
- console.log(`[Window] dispatchEvent: ${event.type}, listeners: ${listeners?.size ?? 0}`);
304
306
  }
305
307
  if (!listeners || listeners.size === 0) {
306
308
  return false;
@@ -1064,9 +1066,30 @@ var import_pixi4 = require("pixi.js");
1064
1066
  var import_expo_asset = require("expo-asset");
1065
1067
  var import_react_native = require("react-native");
1066
1068
  var import_pixi = require("pixi.js");
1067
- var validImageExtensions = [".png", ".jpg", ".jpeg", ".webp", ".gif"];
1069
+ var validImageExtensions = [".png", ".jpg", ".jpeg", ".webp", ".avif", ".gif"];
1070
+ var validImageMIMEs = [
1071
+ "image/png",
1072
+ "image/jpg",
1073
+ "image/jpeg",
1074
+ "image/webp",
1075
+ "image/avif",
1076
+ "image/gif"
1077
+ ];
1068
1078
  var MODULE_PREFIX = "__expo_module_";
1079
+ var URL_PREFIX = "__expo_url_";
1069
1080
  var moduleIdRegistry = /* @__PURE__ */ new Map();
1081
+ var urlRegistry = /* @__PURE__ */ new Map();
1082
+ var urlCounter = 0;
1083
+ function registerModuleId(moduleId) {
1084
+ const key = `${MODULE_PREFIX}${moduleId}`;
1085
+ moduleIdRegistry.set(key, moduleId);
1086
+ return key;
1087
+ }
1088
+ function registerAssetUrl(url) {
1089
+ const key = `${URL_PREFIX}${urlCounter++}`;
1090
+ urlRegistry.set(key, url);
1091
+ return key;
1092
+ }
1070
1093
  function getExtension(url) {
1071
1094
  const cleanUrl = url.split("?")[0].split("#")[0];
1072
1095
  const lastDot = cleanUrl.lastIndexOf(".");
@@ -1082,13 +1105,12 @@ function getImageSize(uri) {
1082
1105
  });
1083
1106
  }
1084
1107
  async function loadTexture(source) {
1085
- const { Assets: Assets2 } = await import("pixi.js");
1108
+ const { Assets: Assets3 } = await import("pixi.js");
1086
1109
  if (typeof source === "number") {
1087
- const key = `${MODULE_PREFIX}${source}`;
1088
- moduleIdRegistry.set(key, source);
1089
- return Assets2.load(key);
1110
+ const key = registerModuleId(source);
1111
+ return Assets3.load(key);
1090
1112
  }
1091
- return Assets2.load(source);
1113
+ return Assets3.load(source);
1092
1114
  }
1093
1115
  var loadExpoAsset = {
1094
1116
  extension: {
@@ -1102,43 +1124,77 @@ var loadExpoAsset = {
1102
1124
  */
1103
1125
  test(url) {
1104
1126
  if (url.startsWith(MODULE_PREFIX)) {
1127
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") \u2192 true (module)`);
1128
+ return true;
1129
+ }
1130
+ if (url.startsWith(URL_PREFIX)) {
1131
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") \u2192 true (registered url)`);
1105
1132
  return true;
1106
1133
  }
1107
1134
  if (url.startsWith("file://")) {
1135
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") \u2192 true (file)`);
1108
1136
  return true;
1109
1137
  }
1138
+ if (url.startsWith("data:")) {
1139
+ const result2 = validImageMIMEs.some((mime) => url.startsWith(`data:${mime}`));
1140
+ if (__DEV__)
1141
+ console.log(`[loadExpoAsset] test("${url.slice(0, 40)}...") \u2192 ${result2} (data url)`);
1142
+ return result2;
1143
+ }
1110
1144
  const ext = getExtension(url);
1111
- return validImageExtensions.includes(ext);
1145
+ const result = validImageExtensions.includes(ext);
1146
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") ext="${ext}" \u2192 ${result}`);
1147
+ return result;
1112
1148
  },
1113
1149
  /**
1114
1150
  * Load an asset and create a PixiJS Texture
1115
1151
  */
1116
1152
  async load(url, _asset) {
1117
- let expoAsset;
1118
1153
  try {
1119
- if (url.startsWith(MODULE_PREFIX)) {
1120
- const moduleId = moduleIdRegistry.get(url);
1154
+ if (__DEV__) {
1155
+ console.log(`[loadExpoAsset] Loading: ${url}`);
1156
+ }
1157
+ let localUri;
1158
+ let width;
1159
+ let height;
1160
+ const resolvedUrl = url.startsWith(URL_PREFIX) ? urlRegistry.get(url) ?? url : url;
1161
+ if (url.startsWith(URL_PREFIX) && !urlRegistry.has(url)) {
1162
+ throw new Error(`URL not found in registry for key: ${url}`);
1163
+ }
1164
+ if (resolvedUrl.startsWith(MODULE_PREFIX)) {
1165
+ const moduleId = moduleIdRegistry.get(resolvedUrl);
1121
1166
  if (moduleId === void 0) {
1122
- throw new Error(`Module ID not found in registry for key: ${url}`);
1167
+ throw new Error(`Module ID not found in registry for key: ${resolvedUrl}`);
1123
1168
  }
1124
- expoAsset = import_expo_asset.Asset.fromModule(moduleId);
1125
- } else if (isRemoteUrl(url)) {
1126
- expoAsset = import_expo_asset.Asset.fromURI(url);
1169
+ const expoAsset = import_expo_asset.Asset.fromModule(moduleId);
1170
+ await expoAsset.downloadAsync();
1171
+ localUri = expoAsset.localUri || expoAsset.uri;
1172
+ width = expoAsset.width ?? void 0;
1173
+ height = expoAsset.height ?? void 0;
1174
+ } else if (isRemoteUrl(resolvedUrl)) {
1175
+ const expoAsset = import_expo_asset.Asset.fromURI(resolvedUrl);
1176
+ await expoAsset.downloadAsync();
1177
+ localUri = expoAsset.localUri || expoAsset.uri;
1178
+ width = expoAsset.width ?? void 0;
1179
+ height = expoAsset.height ?? void 0;
1127
1180
  } else {
1128
- expoAsset = import_expo_asset.Asset.fromURI(url);
1181
+ const expoAsset = import_expo_asset.Asset.fromURI(resolvedUrl);
1182
+ await expoAsset.downloadAsync();
1183
+ localUri = expoAsset.localUri || expoAsset.uri;
1184
+ width = expoAsset.width ?? void 0;
1185
+ height = expoAsset.height ?? void 0;
1129
1186
  }
1130
- await expoAsset.downloadAsync();
1131
- const localUri = expoAsset.localUri || expoAsset.uri;
1132
1187
  if (!localUri) {
1133
1188
  throw new Error(`Failed to get local URI for asset: ${url}`);
1134
1189
  }
1135
- let width = expoAsset.width;
1136
- let height = expoAsset.height;
1137
1190
  if (!width || !height) {
1138
1191
  const size = await getImageSize(localUri);
1139
1192
  width = size.width;
1140
1193
  height = size.height;
1141
1194
  }
1195
+ if (__DEV__) {
1196
+ console.log(`[loadExpoAsset] Resolved: ${localUri} (${width}x${height})`);
1197
+ }
1142
1198
  const MockImage = globalThis.HTMLImageElement;
1143
1199
  const img = new MockImage();
1144
1200
  img.width = width;
@@ -1156,7 +1212,7 @@ var loadExpoAsset = {
1156
1212
  });
1157
1213
  return new import_pixi.Texture({ source });
1158
1214
  } catch (error) {
1159
- console.error(`Failed to load asset: ${url}`, error);
1215
+ console.error(`[loadExpoAsset] Failed to load: ${url}`, error);
1160
1216
  throw error;
1161
1217
  }
1162
1218
  },
@@ -1242,6 +1298,54 @@ var loadExpoFont = {
1242
1298
  }
1243
1299
  };
1244
1300
 
1301
+ // src/adapter/expoManifest.ts
1302
+ function resolveExpoSrc(src) {
1303
+ if (typeof src === "number") {
1304
+ return registerModuleId(src);
1305
+ }
1306
+ return registerAssetUrl(src);
1307
+ }
1308
+ function resolveExpoSrcArray(src) {
1309
+ if (Array.isArray(src)) {
1310
+ return src.map(resolveExpoSrc);
1311
+ }
1312
+ return resolveExpoSrc(src);
1313
+ }
1314
+ function resolveExpoAsset(asset) {
1315
+ const { src, ...rest } = asset;
1316
+ if (src === void 0) {
1317
+ return rest;
1318
+ }
1319
+ return {
1320
+ ...rest,
1321
+ src: resolveExpoSrcArray(src)
1322
+ };
1323
+ }
1324
+ function createExpoBundle(assets) {
1325
+ if (Array.isArray(assets)) {
1326
+ return assets.map(resolveExpoAsset);
1327
+ }
1328
+ const result = {};
1329
+ for (const [key, value] of Object.entries(assets)) {
1330
+ if (typeof value === "number" || typeof value === "string") {
1331
+ result[key] = resolveExpoSrc(value);
1332
+ } else if (Array.isArray(value)) {
1333
+ result[key] = value.map(resolveExpoSrc);
1334
+ } else {
1335
+ result[key] = resolveExpoAsset(value);
1336
+ }
1337
+ }
1338
+ return result;
1339
+ }
1340
+ function createExpoManifest(manifest) {
1341
+ return {
1342
+ bundles: manifest.bundles.map((bundle) => ({
1343
+ name: bundle.name,
1344
+ assets: createExpoBundle(bundle.assets)
1345
+ }))
1346
+ };
1347
+ }
1348
+
1245
1349
  // src/components/PixiView.tsx
1246
1350
  var import_react = require("react");
1247
1351
  var import_react_native3 = require("react-native");
@@ -1593,8 +1697,20 @@ var styles = import_react_native3.StyleSheet.create({
1593
1697
  // src/index.ts
1594
1698
  var import_pixi5 = require("pixi.js");
1595
1699
  import_pixi4.DOMAdapter.set(ExpoAdapter);
1700
+ import_pixi4.extensions.remove(import_pixi4.loadTextures);
1596
1701
  import_pixi4.extensions.add(loadExpoAsset);
1597
1702
  import_pixi4.extensions.add(loadExpoFont);
1703
+ var _originalLoad = import_pixi4.Assets.load.bind(import_pixi4.Assets);
1704
+ import_pixi4.Assets.load = function patchedLoad(urls, onProgress) {
1705
+ if (typeof urls === "number") {
1706
+ return _originalLoad(registerModuleId(urls), onProgress);
1707
+ }
1708
+ if (Array.isArray(urls)) {
1709
+ const resolved = urls.map((u) => typeof u === "number" ? registerModuleId(u) : u);
1710
+ return _originalLoad(resolved, onProgress);
1711
+ }
1712
+ return _originalLoad(urls, onProgress);
1713
+ };
1598
1714
  // Annotate the CommonJS export names for ESM import in node:
1599
1715
  0 && (module.exports = {
1600
1716
  AnimatedSprite,
@@ -1639,6 +1755,8 @@ import_pixi4.extensions.add(loadExpoFont);
1639
1755
  clearActiveContext,
1640
1756
  clearTouchTracking,
1641
1757
  convertTouchToPointerEvents,
1758
+ createExpoBundle,
1759
+ createExpoManifest,
1642
1760
  extensions,
1643
1761
  getActiveCanvas,
1644
1762
  getActiveGL,
@@ -1646,6 +1764,7 @@ import_pixi4.extensions.add(loadExpoFont);
1646
1764
  loadExpoAsset,
1647
1765
  loadExpoFont,
1648
1766
  loadTexture,
1767
+ resolveExpoAsset,
1649
1768
  setActiveGLContext
1650
1769
  });
1651
1770
  /**
package/dist/index.mjs CHANGED
@@ -215,7 +215,6 @@ function windowRemoveEventListener(type, listener, _options) {
215
215
  function dispatchWindowEvent(event) {
216
216
  const listeners = windowListeners.get(event.type);
217
217
  if (__DEV__) {
218
- console.log(`[Window] dispatchEvent: ${event.type}, listeners: ${listeners?.size ?? 0}`);
219
218
  }
220
219
  if (!listeners || listeners.size === 0) {
221
220
  return false;
@@ -973,15 +972,36 @@ var ExpoAdapter = {
973
972
  };
974
973
 
975
974
  // src/index.ts
976
- import { DOMAdapter, extensions } from "pixi.js";
975
+ import { DOMAdapter, extensions, loadTextures, Assets } from "pixi.js";
977
976
 
978
977
  // src/adapter/loadExpoAsset.ts
979
978
  import { Asset } from "expo-asset";
980
979
  import { Image } from "react-native";
981
980
  import { ExtensionType, Texture, ImageSource, LoaderParserPriority } from "pixi.js";
982
- var validImageExtensions = [".png", ".jpg", ".jpeg", ".webp", ".gif"];
981
+ var validImageExtensions = [".png", ".jpg", ".jpeg", ".webp", ".avif", ".gif"];
982
+ var validImageMIMEs = [
983
+ "image/png",
984
+ "image/jpg",
985
+ "image/jpeg",
986
+ "image/webp",
987
+ "image/avif",
988
+ "image/gif"
989
+ ];
983
990
  var MODULE_PREFIX = "__expo_module_";
991
+ var URL_PREFIX = "__expo_url_";
984
992
  var moduleIdRegistry = /* @__PURE__ */ new Map();
993
+ var urlRegistry = /* @__PURE__ */ new Map();
994
+ var urlCounter = 0;
995
+ function registerModuleId(moduleId) {
996
+ const key = `${MODULE_PREFIX}${moduleId}`;
997
+ moduleIdRegistry.set(key, moduleId);
998
+ return key;
999
+ }
1000
+ function registerAssetUrl(url) {
1001
+ const key = `${URL_PREFIX}${urlCounter++}`;
1002
+ urlRegistry.set(key, url);
1003
+ return key;
1004
+ }
985
1005
  function getExtension(url) {
986
1006
  const cleanUrl = url.split("?")[0].split("#")[0];
987
1007
  const lastDot = cleanUrl.lastIndexOf(".");
@@ -997,13 +1017,12 @@ function getImageSize(uri) {
997
1017
  });
998
1018
  }
999
1019
  async function loadTexture(source) {
1000
- const { Assets: Assets2 } = await import("pixi.js");
1020
+ const { Assets: Assets3 } = await import("pixi.js");
1001
1021
  if (typeof source === "number") {
1002
- const key = `${MODULE_PREFIX}${source}`;
1003
- moduleIdRegistry.set(key, source);
1004
- return Assets2.load(key);
1022
+ const key = registerModuleId(source);
1023
+ return Assets3.load(key);
1005
1024
  }
1006
- return Assets2.load(source);
1025
+ return Assets3.load(source);
1007
1026
  }
1008
1027
  var loadExpoAsset = {
1009
1028
  extension: {
@@ -1017,43 +1036,77 @@ var loadExpoAsset = {
1017
1036
  */
1018
1037
  test(url) {
1019
1038
  if (url.startsWith(MODULE_PREFIX)) {
1039
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") \u2192 true (module)`);
1040
+ return true;
1041
+ }
1042
+ if (url.startsWith(URL_PREFIX)) {
1043
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") \u2192 true (registered url)`);
1020
1044
  return true;
1021
1045
  }
1022
1046
  if (url.startsWith("file://")) {
1047
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") \u2192 true (file)`);
1023
1048
  return true;
1024
1049
  }
1050
+ if (url.startsWith("data:")) {
1051
+ const result2 = validImageMIMEs.some((mime) => url.startsWith(`data:${mime}`));
1052
+ if (__DEV__)
1053
+ console.log(`[loadExpoAsset] test("${url.slice(0, 40)}...") \u2192 ${result2} (data url)`);
1054
+ return result2;
1055
+ }
1025
1056
  const ext = getExtension(url);
1026
- return validImageExtensions.includes(ext);
1057
+ const result = validImageExtensions.includes(ext);
1058
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") ext="${ext}" \u2192 ${result}`);
1059
+ return result;
1027
1060
  },
1028
1061
  /**
1029
1062
  * Load an asset and create a PixiJS Texture
1030
1063
  */
1031
1064
  async load(url, _asset) {
1032
- let expoAsset;
1033
1065
  try {
1034
- if (url.startsWith(MODULE_PREFIX)) {
1035
- const moduleId = moduleIdRegistry.get(url);
1066
+ if (__DEV__) {
1067
+ console.log(`[loadExpoAsset] Loading: ${url}`);
1068
+ }
1069
+ let localUri;
1070
+ let width;
1071
+ let height;
1072
+ const resolvedUrl = url.startsWith(URL_PREFIX) ? urlRegistry.get(url) ?? url : url;
1073
+ if (url.startsWith(URL_PREFIX) && !urlRegistry.has(url)) {
1074
+ throw new Error(`URL not found in registry for key: ${url}`);
1075
+ }
1076
+ if (resolvedUrl.startsWith(MODULE_PREFIX)) {
1077
+ const moduleId = moduleIdRegistry.get(resolvedUrl);
1036
1078
  if (moduleId === void 0) {
1037
- throw new Error(`Module ID not found in registry for key: ${url}`);
1079
+ throw new Error(`Module ID not found in registry for key: ${resolvedUrl}`);
1038
1080
  }
1039
- expoAsset = Asset.fromModule(moduleId);
1040
- } else if (isRemoteUrl(url)) {
1041
- expoAsset = Asset.fromURI(url);
1081
+ const expoAsset = Asset.fromModule(moduleId);
1082
+ await expoAsset.downloadAsync();
1083
+ localUri = expoAsset.localUri || expoAsset.uri;
1084
+ width = expoAsset.width ?? void 0;
1085
+ height = expoAsset.height ?? void 0;
1086
+ } else if (isRemoteUrl(resolvedUrl)) {
1087
+ const expoAsset = Asset.fromURI(resolvedUrl);
1088
+ await expoAsset.downloadAsync();
1089
+ localUri = expoAsset.localUri || expoAsset.uri;
1090
+ width = expoAsset.width ?? void 0;
1091
+ height = expoAsset.height ?? void 0;
1042
1092
  } else {
1043
- expoAsset = Asset.fromURI(url);
1093
+ const expoAsset = Asset.fromURI(resolvedUrl);
1094
+ await expoAsset.downloadAsync();
1095
+ localUri = expoAsset.localUri || expoAsset.uri;
1096
+ width = expoAsset.width ?? void 0;
1097
+ height = expoAsset.height ?? void 0;
1044
1098
  }
1045
- await expoAsset.downloadAsync();
1046
- const localUri = expoAsset.localUri || expoAsset.uri;
1047
1099
  if (!localUri) {
1048
1100
  throw new Error(`Failed to get local URI for asset: ${url}`);
1049
1101
  }
1050
- let width = expoAsset.width;
1051
- let height = expoAsset.height;
1052
1102
  if (!width || !height) {
1053
1103
  const size = await getImageSize(localUri);
1054
1104
  width = size.width;
1055
1105
  height = size.height;
1056
1106
  }
1107
+ if (__DEV__) {
1108
+ console.log(`[loadExpoAsset] Resolved: ${localUri} (${width}x${height})`);
1109
+ }
1057
1110
  const MockImage = globalThis.HTMLImageElement;
1058
1111
  const img = new MockImage();
1059
1112
  img.width = width;
@@ -1071,7 +1124,7 @@ var loadExpoAsset = {
1071
1124
  });
1072
1125
  return new Texture({ source });
1073
1126
  } catch (error) {
1074
- console.error(`Failed to load asset: ${url}`, error);
1127
+ console.error(`[loadExpoAsset] Failed to load: ${url}`, error);
1075
1128
  throw error;
1076
1129
  }
1077
1130
  },
@@ -1157,6 +1210,54 @@ var loadExpoFont = {
1157
1210
  }
1158
1211
  };
1159
1212
 
1213
+ // src/adapter/expoManifest.ts
1214
+ function resolveExpoSrc(src) {
1215
+ if (typeof src === "number") {
1216
+ return registerModuleId(src);
1217
+ }
1218
+ return registerAssetUrl(src);
1219
+ }
1220
+ function resolveExpoSrcArray(src) {
1221
+ if (Array.isArray(src)) {
1222
+ return src.map(resolveExpoSrc);
1223
+ }
1224
+ return resolveExpoSrc(src);
1225
+ }
1226
+ function resolveExpoAsset(asset) {
1227
+ const { src, ...rest } = asset;
1228
+ if (src === void 0) {
1229
+ return rest;
1230
+ }
1231
+ return {
1232
+ ...rest,
1233
+ src: resolveExpoSrcArray(src)
1234
+ };
1235
+ }
1236
+ function createExpoBundle(assets) {
1237
+ if (Array.isArray(assets)) {
1238
+ return assets.map(resolveExpoAsset);
1239
+ }
1240
+ const result = {};
1241
+ for (const [key, value] of Object.entries(assets)) {
1242
+ if (typeof value === "number" || typeof value === "string") {
1243
+ result[key] = resolveExpoSrc(value);
1244
+ } else if (Array.isArray(value)) {
1245
+ result[key] = value.map(resolveExpoSrc);
1246
+ } else {
1247
+ result[key] = resolveExpoAsset(value);
1248
+ }
1249
+ }
1250
+ return result;
1251
+ }
1252
+ function createExpoManifest(manifest) {
1253
+ return {
1254
+ bundles: manifest.bundles.map((bundle) => ({
1255
+ name: bundle.name,
1256
+ assets: createExpoBundle(bundle.assets)
1257
+ }))
1258
+ };
1259
+ }
1260
+
1160
1261
  // src/components/PixiView.tsx
1161
1262
  import { useCallback, useRef, useEffect, useImperativeHandle, forwardRef } from "react";
1162
1263
  import {
@@ -1524,7 +1625,7 @@ import {
1524
1625
  TextureSource,
1525
1626
  Spritesheet,
1526
1627
  RenderTexture,
1527
- Assets,
1628
+ Assets as Assets2,
1528
1629
  Matrix,
1529
1630
  Point,
1530
1631
  ObservablePoint,
@@ -1550,12 +1651,24 @@ import {
1550
1651
  DOMAdapter as DOMAdapter2
1551
1652
  } from "pixi.js";
1552
1653
  DOMAdapter.set(ExpoAdapter);
1654
+ extensions.remove(loadTextures);
1553
1655
  extensions.add(loadExpoAsset);
1554
1656
  extensions.add(loadExpoFont);
1657
+ var _originalLoad = Assets.load.bind(Assets);
1658
+ Assets.load = function patchedLoad(urls, onProgress) {
1659
+ if (typeof urls === "number") {
1660
+ return _originalLoad(registerModuleId(urls), onProgress);
1661
+ }
1662
+ if (Array.isArray(urls)) {
1663
+ const resolved = urls.map((u) => typeof u === "number" ? registerModuleId(u) : u);
1664
+ return _originalLoad(resolved, onProgress);
1665
+ }
1666
+ return _originalLoad(urls, onProgress);
1667
+ };
1555
1668
  export {
1556
1669
  AnimatedSprite,
1557
1670
  Application2 as Application,
1558
- Assets,
1671
+ Assets2 as Assets,
1559
1672
  Batcher,
1560
1673
  BitmapText,
1561
1674
  BlurFilter,
@@ -1595,6 +1708,8 @@ export {
1595
1708
  clearActiveContext,
1596
1709
  clearTouchTracking,
1597
1710
  convertTouchToPointerEvents,
1711
+ createExpoBundle,
1712
+ createExpoManifest,
1598
1713
  extensions2 as extensions,
1599
1714
  getActiveCanvas,
1600
1715
  getActiveGL,
@@ -1602,6 +1717,7 @@ export {
1602
1717
  loadExpoAsset,
1603
1718
  loadExpoFont,
1604
1719
  loadTexture,
1720
+ resolveExpoAsset,
1605
1721
  setActiveGLContext
1606
1722
  };
1607
1723
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@penabt/pixi-expo",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "PixiJS v8 adapter for React Native Expo. Enables hardware-accelerated 2D graphics using expo-gl WebGL context.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -31,7 +31,7 @@
31
31
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
32
32
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
33
33
  "prepublishOnly": "npm run build",
34
- "clean": "rm -rf lib",
34
+ "clean": "rm -rf dist",
35
35
  "typecheck": "tsc --noEmit",
36
36
  "lint": "eslint src --ext .ts,.tsx",
37
37
  "format": "prettier --write ."
@@ -0,0 +1,143 @@
1
+ /**
2
+ * PixiJS Manifest & Bundle utilities for Expo.
3
+ *
4
+ * Transforms Expo-flavored manifests (where `src` can be a require() numeric ID)
5
+ * into standard PixiJS manifests (where `src` is always a string).
6
+ */
7
+
8
+ import type { UnresolvedAsset, AssetsBundle, AssetsManifest, ArrayOr } from 'pixi.js';
9
+ import { registerModuleId, registerAssetUrl } from './loadExpoAsset';
10
+
11
+ // =============================================================================
12
+ // TYPES
13
+ // =============================================================================
14
+
15
+ /** An asset source that accepts Expo require() numeric IDs alongside strings. */
16
+ export type ExpoAssetSrc = number | string;
17
+
18
+ /**
19
+ * An unresolved asset that accepts require() IDs in src.
20
+ * Mirrors PixiJS UnresolvedAsset but src can include numeric module IDs.
21
+ */
22
+ export interface ExpoUnresolvedAsset<T = any> {
23
+ alias?: ArrayOr<string>;
24
+ src?: ExpoAssetSrc | ExpoAssetSrc[];
25
+ data?: T;
26
+ format?: string;
27
+ parser?: string;
28
+ [key: string]: any;
29
+ }
30
+
31
+ /** A bundle that accepts Expo require() IDs. */
32
+ export interface ExpoAssetsBundle {
33
+ name: string;
34
+ assets:
35
+ | ExpoUnresolvedAsset[]
36
+ | Record<string, ExpoAssetSrc | ExpoAssetSrc[] | ExpoUnresolvedAsset>;
37
+ }
38
+
39
+ /** A manifest that accepts Expo require() IDs. */
40
+ export interface ExpoAssetsManifest {
41
+ bundles: ExpoAssetsBundle[];
42
+ }
43
+
44
+ // =============================================================================
45
+ // INTERNAL HELPERS
46
+ // =============================================================================
47
+
48
+ function resolveExpoSrc(src: ExpoAssetSrc): string {
49
+ if (typeof src === 'number') {
50
+ return registerModuleId(src);
51
+ }
52
+ // Wrap string URLs with a registry key to prevent PixiJS's resolver
53
+ // from routing them to built-in browser-dependent parsers
54
+ return registerAssetUrl(src);
55
+ }
56
+
57
+ function resolveExpoSrcArray(src: ExpoAssetSrc | ExpoAssetSrc[]): string | string[] {
58
+ if (Array.isArray(src)) {
59
+ return src.map(resolveExpoSrc);
60
+ }
61
+ return resolveExpoSrc(src);
62
+ }
63
+
64
+ // =============================================================================
65
+ // PUBLIC API
66
+ // =============================================================================
67
+
68
+ /**
69
+ * Transform a single Expo asset entry into a standard PixiJS UnresolvedAsset.
70
+ * Numeric require() IDs are registered and converted to string keys.
71
+ */
72
+ export function resolveExpoAsset<T = any>(asset: ExpoUnresolvedAsset<T>): UnresolvedAsset<T> {
73
+ const { src, ...rest } = asset;
74
+ if (src === undefined) {
75
+ return rest as UnresolvedAsset<T>;
76
+ }
77
+ return {
78
+ ...rest,
79
+ src: resolveExpoSrcArray(src),
80
+ } as UnresolvedAsset<T>;
81
+ }
82
+
83
+ /**
84
+ * Transform bundle assets for use with `Assets.addBundle()`.
85
+ * Handles both array and Record formats.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * Assets.addBundle('animals', createExpoBundle([
90
+ * { alias: 'bunny', src: require('./assets/bunny.png') },
91
+ * { alias: 'chicken', src: 'https://example.com/chicken.png' },
92
+ * ]));
93
+ * ```
94
+ */
95
+ export function createExpoBundle(assets: ExpoAssetsBundle['assets']): AssetsBundle['assets'] {
96
+ if (Array.isArray(assets)) {
97
+ return assets.map(resolveExpoAsset);
98
+ }
99
+
100
+ // Record format: Record<string, ExpoAssetSrc | ExpoAssetSrc[] | ExpoUnresolvedAsset>
101
+ const result: Record<string, ArrayOr<string> | UnresolvedAsset> = {};
102
+ for (const [key, value] of Object.entries(assets)) {
103
+ if (typeof value === 'number' || typeof value === 'string') {
104
+ result[key] = resolveExpoSrc(value);
105
+ } else if (Array.isArray(value)) {
106
+ result[key] = value.map(resolveExpoSrc);
107
+ } else {
108
+ result[key] = resolveExpoAsset(value);
109
+ }
110
+ }
111
+ return result;
112
+ }
113
+
114
+ /**
115
+ * Transform an Expo-flavored manifest into a standard PixiJS AssetsManifest.
116
+ * Numeric require() IDs in `src` fields are registered and converted to string keys.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * const manifest = createExpoManifest({
121
+ * bundles: [
122
+ * {
123
+ * name: 'game-screen',
124
+ * assets: [
125
+ * { alias: 'character', src: require('./assets/robot.png') },
126
+ * { alias: 'enemy', src: 'https://example.com/bad-guy.png' },
127
+ * ],
128
+ * },
129
+ * ],
130
+ * });
131
+ *
132
+ * await Assets.init({ manifest });
133
+ * const assets = await Assets.loadBundle('game-screen');
134
+ * ```
135
+ */
136
+ export function createExpoManifest(manifest: ExpoAssetsManifest): AssetsManifest {
137
+ return {
138
+ bundles: manifest.bundles.map((bundle) => ({
139
+ name: bundle.name,
140
+ assets: createExpoBundle(bundle.assets),
141
+ })),
142
+ };
143
+ }
@@ -14,9 +14,18 @@ import { ExtensionType, Texture, ImageSource, LoaderParserPriority } from 'pixi.
14
14
 
15
15
  import type { LoaderParser, ResolvedAsset } from 'pixi.js';
16
16
 
17
- const validImageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
17
+ const validImageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.avif', '.gif'];
18
+ const validImageMIMEs = [
19
+ 'image/png',
20
+ 'image/jpg',
21
+ 'image/jpeg',
22
+ 'image/webp',
23
+ 'image/avif',
24
+ 'image/gif',
25
+ ];
18
26
 
19
27
  const MODULE_PREFIX = '__expo_module_';
28
+ const URL_PREFIX = '__expo_url_';
20
29
 
21
30
  /**
22
31
  * Registry mapping stringified keys to original require() module IDs.
@@ -25,6 +34,37 @@ const MODULE_PREFIX = '__expo_module_';
25
34
  */
26
35
  const moduleIdRegistry = new Map<string, number>();
27
36
 
37
+ /**
38
+ * Registry mapping stringified keys to original string URLs.
39
+ * PixiJS's Resolver auto-assigns built-in parsers for known extensions (e.g. .png → loadTextures),
40
+ * bypassing our custom loader's test(). Wrapping URLs with a prefix ensures they always
41
+ * route through loadExpoAsset.
42
+ */
43
+ const urlRegistry = new Map<string, string>();
44
+ let urlCounter = 0;
45
+
46
+ /**
47
+ * Register a require() module ID in the registry and return the string key.
48
+ * This key can be used as a `src` in PixiJS Assets system and will be
49
+ * intercepted by the loadExpoAsset loader.
50
+ */
51
+ export function registerModuleId(moduleId: number): string {
52
+ const key = `${MODULE_PREFIX}${moduleId}`;
53
+ moduleIdRegistry.set(key, moduleId);
54
+ return key;
55
+ }
56
+
57
+ /**
58
+ * Register a string URL in the registry and return a prefixed key.
59
+ * This prevents PixiJS's resolver from routing the URL to built-in
60
+ * browser-dependent parsers that don't work in React Native.
61
+ */
62
+ export function registerAssetUrl(url: string): string {
63
+ const key = `${URL_PREFIX}${urlCounter++}`;
64
+ urlRegistry.set(key, url);
65
+ return key;
66
+ }
67
+
28
68
  /**
29
69
  * Get file extension from a URL or path
30
70
  */
@@ -69,8 +109,7 @@ export async function loadTexture(source: number | string): Promise<Texture> {
69
109
  const { Assets } = await import('pixi.js');
70
110
 
71
111
  if (typeof source === 'number') {
72
- const key = `${MODULE_PREFIX}${source}`;
73
- moduleIdRegistry.set(key, source);
112
+ const key = registerModuleId(source);
74
113
  return Assets.load(key);
75
114
  }
76
115
 
@@ -100,61 +139,105 @@ export const loadExpoAsset = {
100
139
  test(url: string): boolean {
101
140
  // Handle registered module IDs (from loadTexture helper)
102
141
  if (url.startsWith(MODULE_PREFIX)) {
142
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") → true (module)`);
143
+ return true;
144
+ }
145
+
146
+ // Handle registered URL keys (from createExpoManifest / registerAssetUrl)
147
+ if (url.startsWith(URL_PREFIX)) {
148
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") → true (registered url)`);
103
149
  return true;
104
150
  }
105
151
 
106
152
  // Handle local file URIs
107
153
  if (url.startsWith('file://')) {
154
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") → true (file)`);
108
155
  return true;
109
156
  }
110
157
 
158
+ // Handle data URLs with valid image MIME types
159
+ if (url.startsWith('data:')) {
160
+ const result = validImageMIMEs.some((mime) => url.startsWith(`data:${mime}`));
161
+ if (__DEV__)
162
+ console.log(`[loadExpoAsset] test("${url.slice(0, 40)}...") → ${result} (data url)`);
163
+ return result;
164
+ }
165
+
111
166
  // Handle URLs with valid image extensions
112
167
  const ext = getExtension(url);
168
+ const result = validImageExtensions.includes(ext);
113
169
 
114
- return validImageExtensions.includes(ext);
170
+ if (__DEV__) console.log(`[loadExpoAsset] test("${url}") ext="${ext}" → ${result}`);
171
+ return result;
115
172
  },
116
173
 
117
174
  /**
118
175
  * Load an asset and create a PixiJS Texture
119
176
  */
120
177
  async load(url: string, _asset?: ResolvedAsset): Promise<Texture> {
121
- let expoAsset: Asset;
122
-
123
178
  try {
124
- if (url.startsWith(MODULE_PREFIX)) {
179
+ if (__DEV__) {
180
+ console.log(`[loadExpoAsset] Loading: ${url}`);
181
+ }
182
+
183
+ let localUri: string;
184
+ let width: number | undefined;
185
+ let height: number | undefined;
186
+
187
+ // Resolve registered URL keys back to original URLs
188
+ const resolvedUrl = url.startsWith(URL_PREFIX) ? (urlRegistry.get(url) ?? url) : url;
189
+
190
+ if (url.startsWith(URL_PREFIX) && !urlRegistry.has(url)) {
191
+ throw new Error(`URL not found in registry for key: ${url}`);
192
+ }
193
+
194
+ if (resolvedUrl.startsWith(MODULE_PREFIX)) {
125
195
  // Recover the original require() module ID from registry
126
- const moduleId = moduleIdRegistry.get(url);
196
+ const moduleId = moduleIdRegistry.get(resolvedUrl);
127
197
 
128
198
  if (moduleId === undefined) {
129
- throw new Error(`Module ID not found in registry for key: ${url}`);
199
+ throw new Error(`Module ID not found in registry for key: ${resolvedUrl}`);
130
200
  }
131
201
 
132
- expoAsset = Asset.fromModule(moduleId);
133
- } else if (isRemoteUrl(url)) {
134
- expoAsset = Asset.fromURI(url);
135
- } else {
136
- expoAsset = Asset.fromURI(url);
137
- }
202
+ const expoAsset = Asset.fromModule(moduleId);
203
+ await expoAsset.downloadAsync();
204
+
205
+ localUri = expoAsset.localUri || expoAsset.uri;
206
+ width = expoAsset.width ?? undefined;
207
+ height = expoAsset.height ?? undefined;
208
+ } else if (isRemoteUrl(resolvedUrl)) {
209
+ // Download remote image to local cache via expo-asset
210
+ const expoAsset = Asset.fromURI(resolvedUrl);
211
+ await expoAsset.downloadAsync();
138
212
 
139
- // Download the asset to local storage
140
- await expoAsset.downloadAsync();
213
+ localUri = expoAsset.localUri || expoAsset.uri;
214
+ width = expoAsset.width ?? undefined;
215
+ height = expoAsset.height ?? undefined;
216
+ } else {
217
+ // Local file URI
218
+ const expoAsset = Asset.fromURI(resolvedUrl);
219
+ await expoAsset.downloadAsync();
141
220
 
142
- const localUri = expoAsset.localUri || expoAsset.uri;
221
+ localUri = expoAsset.localUri || expoAsset.uri;
222
+ width = expoAsset.width ?? undefined;
223
+ height = expoAsset.height ?? undefined;
224
+ }
143
225
 
144
226
  if (!localUri) {
145
227
  throw new Error(`Failed to get local URI for asset: ${url}`);
146
228
  }
147
229
 
148
- // Get dimensions from the asset metadata or via Image.getSize
149
- let width = expoAsset.width;
150
- let height = expoAsset.height;
151
-
230
+ // Get dimensions via Image.getSize if not already known
152
231
  if (!width || !height) {
153
232
  const size = await getImageSize(localUri);
154
233
  width = size.width;
155
234
  height = size.height;
156
235
  }
157
236
 
237
+ if (__DEV__) {
238
+ console.log(`[loadExpoAsset] Resolved: ${localUri} (${width}x${height})`);
239
+ }
240
+
158
241
  // Create an HTMLImageElement instance that:
159
242
  // 1. Passes ImageSource.test() (instanceof HTMLImageElement)
160
243
  // 2. Has `localUri` so expo-gl's patched texImage2D loads the image natively
@@ -177,7 +260,7 @@ export const loadExpoAsset = {
177
260
 
178
261
  return new Texture({ source });
179
262
  } catch (error) {
180
- console.error(`Failed to load asset: ${url}`, error);
263
+ console.error(`[loadExpoAsset] Failed to load: ${url}`, error);
181
264
  throw error;
182
265
  }
183
266
  },
@@ -290,7 +290,7 @@ export function dispatchWindowEvent(event: { type: string; [key: string]: any })
290
290
  const listeners = windowListeners.get(event.type);
291
291
 
292
292
  if (__DEV__) {
293
- console.log(`[Window] dispatchEvent: ${event.type}, listeners: ${listeners?.size ?? 0}`);
293
+ //console.log(`[Window] dispatchEvent: ${event.type}, listeners: ${listeners?.size ?? 0}`);
294
294
  }
295
295
 
296
296
  if (!listeners || listeners.size === 0) {
package/src/index.ts CHANGED
@@ -76,23 +76,43 @@ import { ExpoCanvasElement } from './adapter';
76
76
  // and configure it to use our custom adapter.
77
77
  // =============================================================================
78
78
 
79
- import { DOMAdapter, extensions } from 'pixi.js';
79
+ import { DOMAdapter, extensions, loadTextures, Assets } from 'pixi.js';
80
80
 
81
81
  // Set the custom adapter before any PixiJS rendering occurs
82
82
  DOMAdapter.set(ExpoAdapter as any);
83
83
 
84
84
  // =============================================================================
85
85
  // PHASE 4: ASSET LOADERS
86
- // Register custom loaders that use Expo's asset system.
86
+ // Remove PixiJS's built-in texture loader (uses createImageBitmap which doesn't
87
+ // exist in React Native) and register our Expo-compatible loaders.
87
88
  // These must be registered after DOMAdapter is configured.
88
89
  // =============================================================================
89
90
 
90
- import { loadExpoAsset, loadTexture } from './adapter/loadExpoAsset';
91
+ import { loadExpoAsset, loadTexture, registerModuleId } from './adapter/loadExpoAsset';
91
92
  import { loadExpoFont } from './adapter/loadExpoFont';
92
93
 
94
+ extensions.remove(loadTextures);
93
95
  extensions.add(loadExpoAsset);
94
96
  extensions.add(loadExpoFont);
95
97
 
98
+ // =============================================================================
99
+ // PHASE 5: PATCH Assets.load TO ACCEPT require() IDs
100
+ // PixiJS's Assets.load() only accepts strings. This patch allows passing
101
+ // numeric require() module IDs directly: Assets.load(require('./img.png'))
102
+ // =============================================================================
103
+
104
+ const _originalLoad = Assets.load.bind(Assets);
105
+ (Assets as any).load = function patchedLoad(urls: any, onProgress?: any): Promise<any> {
106
+ if (typeof urls === 'number') {
107
+ return _originalLoad(registerModuleId(urls), onProgress);
108
+ }
109
+ if (Array.isArray(urls)) {
110
+ const resolved = urls.map((u: any) => (typeof u === 'number' ? registerModuleId(u) : u));
111
+ return _originalLoad(resolved, onProgress);
112
+ }
113
+ return _originalLoad(urls, onProgress);
114
+ };
115
+
96
116
  // =============================================================================
97
117
  // EXPORTS: ADAPTER UTILITIES
98
118
  // Low-level adapter components for advanced use cases.
@@ -127,6 +147,19 @@ export {
127
147
  loadExpoFont,
128
148
  };
129
149
 
150
+ // =============================================================================
151
+ // EXPORTS: MANIFEST & BUNDLE UTILITIES
152
+ // Transform Expo-flavored manifests (with require() IDs) for PixiJS Assets.
153
+ // =============================================================================
154
+
155
+ export { createExpoManifest, createExpoBundle, resolveExpoAsset } from './adapter/expoManifest';
156
+ export type {
157
+ ExpoAssetSrc,
158
+ ExpoUnresolvedAsset,
159
+ ExpoAssetsBundle,
160
+ ExpoAssetsManifest,
161
+ } from './adapter/expoManifest';
162
+
130
163
  // =============================================================================
131
164
  // EXPORTS: REACT COMPONENTS
132
165
  // High-level React Native components for easy integration.
@@ -301,4 +334,10 @@ export type {
301
334
  FilterOptions,
302
335
  /** Renderer type */
303
336
  Renderer,
337
+ /** Manifest format for Assets.init() */
338
+ AssetsManifest,
339
+ /** Bundle format within a manifest */
340
+ AssetsBundle,
341
+ /** Unresolved asset entry */
342
+ UnresolvedAsset,
304
343
  } from 'pixi.js';