@penabt/pixi-expo 0.3.0 → 0.4.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 +19 -2
- package/dist/index.d.mts +3 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +196 -36
- package/dist/index.mjs +196 -36
- package/package.json +1 -1
- package/src/adapter/ExpoAdapter.ts +11 -9
- package/src/adapter/ExpoCanvasElement.ts +17 -19
- package/src/adapter/canvas2d.ts +159 -0
- package/src/adapter/loadExpoAsset.ts +1 -1
- package/src/adapter/loadExpoFont.ts +3 -3
- package/src/adapter/polyfills.ts +18 -7
- package/src/components/PixiView.tsx +2 -2
- package/src/index.ts +5 -1
package/README.md
CHANGED
|
@@ -160,11 +160,28 @@ const sprite = new Sprite(bunny);
|
|
|
160
160
|
|
|
161
161
|
// Remote asset via URL
|
|
162
162
|
const remote = await Assets.load('https://example.com/sprite.png');
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Array Loading — `Assets.load([])`
|
|
166
|
+
|
|
167
|
+
Load multiple assets at once with array destructuring:
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { Assets, Sprite } from '@penabt/pixi-expo';
|
|
163
171
|
|
|
164
|
-
//
|
|
165
|
-
const [
|
|
172
|
+
// Mix require() and remote URLs in a single call
|
|
173
|
+
const [frame1, frame2, enemy] = await Assets.load([
|
|
174
|
+
require('./assets/frame-1.png'),
|
|
175
|
+
require('./assets/frame-2.png'),
|
|
176
|
+
'https://example.com/enemy.png',
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const sprite1 = new Sprite(frame1);
|
|
180
|
+
const sprite2 = new Sprite(frame2);
|
|
166
181
|
```
|
|
167
182
|
|
|
183
|
+
> **Note:** Unlike standard PixiJS (which returns a `Record<string, Texture>` for arrays), `@penabt/pixi-expo` returns an array in the same order as the input — enabling convenient destructuring.
|
|
184
|
+
|
|
168
185
|
### Manifest & Bundles — `createExpoManifest()`
|
|
169
186
|
|
|
170
187
|
For larger projects, group assets into bundles and load them on demand:
|
package/dist/index.d.mts
CHANGED
|
@@ -368,11 +368,9 @@ declare const ExpoAdapter: {
|
|
|
368
368
|
/**
|
|
369
369
|
* Get the Canvas 2D rendering context constructor.
|
|
370
370
|
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* expo-gl only provides WebGL contexts. For 2D canvas operations,
|
|
375
|
-
* consider using @shopify/react-native-skia as an alternative.
|
|
371
|
+
* Returns a mock constructor whose instances provide the mock Canvas2D
|
|
372
|
+
* context. PixiJS accesses `CanvasRenderingContext2D.prototype` for
|
|
373
|
+
* feature detection, so returning `null` causes a crash.
|
|
376
374
|
*/
|
|
377
375
|
getCanvasRenderingContext2D: () => any;
|
|
378
376
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -368,11 +368,9 @@ declare const ExpoAdapter: {
|
|
|
368
368
|
/**
|
|
369
369
|
* Get the Canvas 2D rendering context constructor.
|
|
370
370
|
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* expo-gl only provides WebGL contexts. For 2D canvas operations,
|
|
375
|
-
* consider using @shopify/react-native-skia as an alternative.
|
|
371
|
+
* Returns a mock constructor whose instances provide the mock Canvas2D
|
|
372
|
+
* context. PixiJS accesses `CanvasRenderingContext2D.prototype` for
|
|
373
|
+
* feature detection, so returning `null` causes a crash.
|
|
376
374
|
*/
|
|
377
375
|
getCanvasRenderingContext2D: () => any;
|
|
378
376
|
/**
|
package/dist/index.js
CHANGED
|
@@ -86,6 +86,160 @@ __export(index_exports, {
|
|
|
86
86
|
});
|
|
87
87
|
module.exports = __toCommonJS(index_exports);
|
|
88
88
|
|
|
89
|
+
// src/adapter/canvas2d.ts
|
|
90
|
+
function parseFontSize(font) {
|
|
91
|
+
const match = font.match(/(\d+(?:\.\d+)?)\s*px/);
|
|
92
|
+
return match ? parseFloat(match[1]) : 10;
|
|
93
|
+
}
|
|
94
|
+
function createMockGradient() {
|
|
95
|
+
return { addColorStop: () => {
|
|
96
|
+
} };
|
|
97
|
+
}
|
|
98
|
+
function createMockCanvas2DContext(canvas) {
|
|
99
|
+
let _font = "10px sans-serif";
|
|
100
|
+
const ctx = {
|
|
101
|
+
// -- Back-reference to owning canvas --
|
|
102
|
+
canvas,
|
|
103
|
+
// -- Font / text properties (critical for CanvasTextMetrics) --
|
|
104
|
+
get font() {
|
|
105
|
+
return _font;
|
|
106
|
+
},
|
|
107
|
+
set font(value) {
|
|
108
|
+
_font = value;
|
|
109
|
+
},
|
|
110
|
+
fillStyle: "#000",
|
|
111
|
+
strokeStyle: "#000",
|
|
112
|
+
lineWidth: 1,
|
|
113
|
+
lineCap: "butt",
|
|
114
|
+
lineJoin: "miter",
|
|
115
|
+
miterLimit: 10,
|
|
116
|
+
textAlign: "start",
|
|
117
|
+
textBaseline: "alphabetic",
|
|
118
|
+
direction: "ltr",
|
|
119
|
+
globalAlpha: 1,
|
|
120
|
+
globalCompositeOperation: "source-over",
|
|
121
|
+
imageSmoothingEnabled: true,
|
|
122
|
+
shadowBlur: 0,
|
|
123
|
+
shadowColor: "rgba(0, 0, 0, 0)",
|
|
124
|
+
shadowOffsetX: 0,
|
|
125
|
+
shadowOffsetY: 0,
|
|
126
|
+
letterSpacing: "0px",
|
|
127
|
+
wordSpacing: "0px",
|
|
128
|
+
fontKerning: "auto",
|
|
129
|
+
textRendering: "auto",
|
|
130
|
+
// -- Text measurement (primary reason this mock exists) --
|
|
131
|
+
measureText(text) {
|
|
132
|
+
const fontSize = parseFontSize(_font);
|
|
133
|
+
const width = text.length * fontSize * 0.6;
|
|
134
|
+
const ascent = fontSize * 0.8;
|
|
135
|
+
const descent = fontSize * 0.2;
|
|
136
|
+
return {
|
|
137
|
+
width,
|
|
138
|
+
actualBoundingBoxAscent: ascent,
|
|
139
|
+
actualBoundingBoxDescent: descent,
|
|
140
|
+
fontBoundingBoxAscent: ascent,
|
|
141
|
+
fontBoundingBoxDescent: descent,
|
|
142
|
+
actualBoundingBoxLeft: 0,
|
|
143
|
+
actualBoundingBoxRight: width,
|
|
144
|
+
alphabeticBaseline: 0,
|
|
145
|
+
emHeightAscent: ascent,
|
|
146
|
+
emHeightDescent: descent,
|
|
147
|
+
hangingBaseline: ascent * 0.8,
|
|
148
|
+
ideographicBaseline: -descent
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
// -- Text drawing (no-op) --
|
|
152
|
+
fillText: () => {
|
|
153
|
+
},
|
|
154
|
+
strokeText: () => {
|
|
155
|
+
},
|
|
156
|
+
// -- State stack --
|
|
157
|
+
save: () => {
|
|
158
|
+
},
|
|
159
|
+
restore: () => {
|
|
160
|
+
},
|
|
161
|
+
// -- Rect operations (no-op) --
|
|
162
|
+
clearRect: () => {
|
|
163
|
+
},
|
|
164
|
+
fillRect: () => {
|
|
165
|
+
},
|
|
166
|
+
strokeRect: () => {
|
|
167
|
+
},
|
|
168
|
+
// -- Path operations (no-op) --
|
|
169
|
+
beginPath: () => {
|
|
170
|
+
},
|
|
171
|
+
closePath: () => {
|
|
172
|
+
},
|
|
173
|
+
moveTo: () => {
|
|
174
|
+
},
|
|
175
|
+
lineTo: () => {
|
|
176
|
+
},
|
|
177
|
+
bezierCurveTo: () => {
|
|
178
|
+
},
|
|
179
|
+
quadraticCurveTo: () => {
|
|
180
|
+
},
|
|
181
|
+
arc: () => {
|
|
182
|
+
},
|
|
183
|
+
arcTo: () => {
|
|
184
|
+
},
|
|
185
|
+
ellipse: () => {
|
|
186
|
+
},
|
|
187
|
+
rect: () => {
|
|
188
|
+
},
|
|
189
|
+
roundRect: () => {
|
|
190
|
+
},
|
|
191
|
+
fill: () => {
|
|
192
|
+
},
|
|
193
|
+
stroke: () => {
|
|
194
|
+
},
|
|
195
|
+
clip: () => {
|
|
196
|
+
},
|
|
197
|
+
isPointInPath: () => false,
|
|
198
|
+
isPointInStroke: () => false,
|
|
199
|
+
// -- Transform (no-op) --
|
|
200
|
+
scale: () => {
|
|
201
|
+
},
|
|
202
|
+
rotate: () => {
|
|
203
|
+
},
|
|
204
|
+
translate: () => {
|
|
205
|
+
},
|
|
206
|
+
transform: () => {
|
|
207
|
+
},
|
|
208
|
+
setTransform: () => {
|
|
209
|
+
},
|
|
210
|
+
getTransform: () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }),
|
|
211
|
+
resetTransform: () => {
|
|
212
|
+
},
|
|
213
|
+
// -- Drawing images (no-op) --
|
|
214
|
+
drawImage: () => {
|
|
215
|
+
},
|
|
216
|
+
// -- Pixel manipulation --
|
|
217
|
+
createImageData: (w, h) => ({
|
|
218
|
+
width: w,
|
|
219
|
+
height: h,
|
|
220
|
+
data: new Uint8ClampedArray(w * h * 4)
|
|
221
|
+
}),
|
|
222
|
+
getImageData: (_x, _y, w, h) => ({
|
|
223
|
+
width: w,
|
|
224
|
+
height: h,
|
|
225
|
+
data: new Uint8ClampedArray(w * h * 4)
|
|
226
|
+
}),
|
|
227
|
+
putImageData: () => {
|
|
228
|
+
},
|
|
229
|
+
// -- Gradients / Patterns --
|
|
230
|
+
createLinearGradient: () => createMockGradient(),
|
|
231
|
+
createRadialGradient: () => createMockGradient(),
|
|
232
|
+
createConicGradient: () => createMockGradient(),
|
|
233
|
+
createPattern: () => null,
|
|
234
|
+
// -- Line dash --
|
|
235
|
+
setLineDash: () => {
|
|
236
|
+
},
|
|
237
|
+
getLineDash: () => [],
|
|
238
|
+
lineDashOffset: 0
|
|
239
|
+
};
|
|
240
|
+
return ctx;
|
|
241
|
+
}
|
|
242
|
+
|
|
89
243
|
// src/adapter/polyfills.ts
|
|
90
244
|
if (typeof globalThis.requestAnimationFrame === "undefined") {
|
|
91
245
|
globalThis.requestAnimationFrame = function requestAnimationFrame(callback) {
|
|
@@ -122,7 +276,7 @@ if (typeof globalThis.dispatchEvent === "undefined") {
|
|
|
122
276
|
try {
|
|
123
277
|
listener(event);
|
|
124
278
|
} catch (e) {
|
|
125
|
-
console.error("Error in event listener:", e);
|
|
279
|
+
if (__DEV__) console.error("Error in event listener:", e);
|
|
126
280
|
}
|
|
127
281
|
});
|
|
128
282
|
}
|
|
@@ -157,8 +311,10 @@ var createMockElement = (tagName) => {
|
|
|
157
311
|
const element = {
|
|
158
312
|
tagName: tagName.toUpperCase(),
|
|
159
313
|
style: createMockStyle(),
|
|
160
|
-
getContext: () =>
|
|
161
|
-
|
|
314
|
+
getContext: (type) => {
|
|
315
|
+
if (type === "2d") return createMockCanvas2DContext(element);
|
|
316
|
+
return null;
|
|
317
|
+
},
|
|
162
318
|
width: 0,
|
|
163
319
|
height: 0,
|
|
164
320
|
addEventListener: () => {
|
|
@@ -316,7 +472,7 @@ function dispatchWindowEvent(event) {
|
|
|
316
472
|
listener.handleEvent(event);
|
|
317
473
|
}
|
|
318
474
|
} catch (error) {
|
|
319
|
-
console.error("[Window] Error in event listener:", error);
|
|
475
|
+
if (__DEV__) console.error("[Window] Error in event listener:", error);
|
|
320
476
|
}
|
|
321
477
|
});
|
|
322
478
|
return true;
|
|
@@ -387,7 +543,8 @@ if (typeof globalThis.HTMLCanvasElement === "undefined") {
|
|
|
387
543
|
this.height = 0;
|
|
388
544
|
this.style = createMockStyle();
|
|
389
545
|
}
|
|
390
|
-
getContext() {
|
|
546
|
+
getContext(type) {
|
|
547
|
+
if (type === "2d") return createMockCanvas2DContext(this);
|
|
391
548
|
return null;
|
|
392
549
|
}
|
|
393
550
|
};
|
|
@@ -506,7 +663,7 @@ if (!("ontouchmove" in globalThis)) {
|
|
|
506
663
|
if (!("ontouchend" in globalThis)) {
|
|
507
664
|
globalThis.ontouchend = null;
|
|
508
665
|
}
|
|
509
|
-
console.log("PixiJS Expo Adapter: Polyfills loaded");
|
|
666
|
+
if (__DEV__) console.log("PixiJS Expo Adapter: Polyfills loaded");
|
|
510
667
|
|
|
511
668
|
// src/adapter/ExpoCanvasElement.ts
|
|
512
669
|
var MockCanvasStyle = class {
|
|
@@ -707,22 +864,21 @@ var ExpoCanvasElement = class {
|
|
|
707
864
|
case "webgl":
|
|
708
865
|
case "experimental-webgl":
|
|
709
866
|
if (!this._gl) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
867
|
+
if (__DEV__)
|
|
868
|
+
console.warn(
|
|
869
|
+
"ExpoCanvasElement: WebGL context not available. Make sure GLView.onContextCreate has been called first."
|
|
870
|
+
);
|
|
713
871
|
return null;
|
|
714
872
|
}
|
|
715
873
|
return this._gl;
|
|
716
874
|
case "2d":
|
|
717
|
-
|
|
718
|
-
"ExpoCanvasElement: 2D context is not supported in expo-gl. Consider using @shopify/react-native-skia for 2D rendering."
|
|
719
|
-
);
|
|
720
|
-
return null;
|
|
875
|
+
return createMockCanvas2DContext(this);
|
|
721
876
|
case "webgl2":
|
|
722
877
|
case "experimental-webgl2":
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
878
|
+
if (__DEV__)
|
|
879
|
+
console.warn(
|
|
880
|
+
"ExpoCanvasElement: WebGL2 is not fully supported in expo-gl. Falling back to WebGL1."
|
|
881
|
+
);
|
|
726
882
|
return this._gl;
|
|
727
883
|
default:
|
|
728
884
|
return null;
|
|
@@ -745,14 +901,15 @@ var ExpoCanvasElement = class {
|
|
|
745
901
|
*/
|
|
746
902
|
toDataURL(_type, _quality) {
|
|
747
903
|
if (!this._gl) {
|
|
748
|
-
console.warn("ExpoCanvasElement: Cannot create data URL without GL context");
|
|
904
|
+
if (__DEV__) console.warn("ExpoCanvasElement: Cannot create data URL without GL context");
|
|
749
905
|
return "";
|
|
750
906
|
}
|
|
751
907
|
const width = this._width;
|
|
752
908
|
const height = this._height;
|
|
753
909
|
const pixels = new Uint8Array(width * height * 4);
|
|
754
910
|
this._gl.readPixels(0, 0, width, height, this._gl.RGBA, this._gl.UNSIGNED_BYTE, pixels);
|
|
755
|
-
|
|
911
|
+
if (__DEV__)
|
|
912
|
+
console.warn("ExpoCanvasElement: toDataURL requires additional implementation for encoding");
|
|
756
913
|
return "";
|
|
757
914
|
}
|
|
758
915
|
/**
|
|
@@ -764,7 +921,7 @@ var ExpoCanvasElement = class {
|
|
|
764
921
|
* @param _quality - Image quality (ignored)
|
|
765
922
|
*/
|
|
766
923
|
toBlob(callback, _type, _quality) {
|
|
767
|
-
console.warn("ExpoCanvasElement: toBlob is not implemented");
|
|
924
|
+
if (__DEV__) console.warn("ExpoCanvasElement: toBlob is not implemented");
|
|
768
925
|
callback(null);
|
|
769
926
|
}
|
|
770
927
|
// ===========================================================================
|
|
@@ -833,7 +990,7 @@ var ExpoCanvasElement = class {
|
|
|
833
990
|
listener.handleEvent(event);
|
|
834
991
|
}
|
|
835
992
|
} catch (error) {
|
|
836
|
-
console.error("[ExpoCanvasElement] Error in event listener:", error);
|
|
993
|
+
if (__DEV__) console.error("[ExpoCanvasElement] Error in event listener:", error);
|
|
837
994
|
}
|
|
838
995
|
});
|
|
839
996
|
return true;
|
|
@@ -940,15 +1097,16 @@ var ExpoAdapter = {
|
|
|
940
1097
|
/**
|
|
941
1098
|
* Get the Canvas 2D rendering context constructor.
|
|
942
1099
|
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
* expo-gl only provides WebGL contexts. For 2D canvas operations,
|
|
947
|
-
* consider using @shopify/react-native-skia as an alternative.
|
|
1100
|
+
* Returns a mock constructor whose instances provide the mock Canvas2D
|
|
1101
|
+
* context. PixiJS accesses `CanvasRenderingContext2D.prototype` for
|
|
1102
|
+
* feature detection, so returning `null` causes a crash.
|
|
948
1103
|
*/
|
|
949
1104
|
getCanvasRenderingContext2D: () => {
|
|
950
|
-
|
|
951
|
-
|
|
1105
|
+
return class MockCanvasRenderingContext2D {
|
|
1106
|
+
constructor() {
|
|
1107
|
+
return createMockCanvas2DContext({ width: 0, height: 0 });
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
952
1110
|
},
|
|
953
1111
|
/**
|
|
954
1112
|
* Get the WebGL rendering context constructor.
|
|
@@ -1026,11 +1184,11 @@ var ExpoAdapter = {
|
|
|
1026
1184
|
return fetch(requestUrl, options);
|
|
1027
1185
|
}
|
|
1028
1186
|
if (requestUrl.startsWith("file://")) {
|
|
1029
|
-
console.warn("ExpoAdapter: Local file loading requires expo-file-system");
|
|
1187
|
+
if (__DEV__) console.warn("ExpoAdapter: Local file loading requires expo-file-system");
|
|
1030
1188
|
return fetch(requestUrl, options);
|
|
1031
1189
|
}
|
|
1032
1190
|
if (requestUrl.startsWith("asset://") || typeof url === "number") {
|
|
1033
|
-
console.warn("ExpoAdapter: Asset loading requires expo-asset");
|
|
1191
|
+
if (__DEV__) console.warn("ExpoAdapter: Asset loading requires expo-asset");
|
|
1034
1192
|
throw new Error("Asset loading not implemented");
|
|
1035
1193
|
}
|
|
1036
1194
|
return fetch(requestUrl, options);
|
|
@@ -1212,7 +1370,7 @@ var loadExpoAsset = {
|
|
|
1212
1370
|
});
|
|
1213
1371
|
return new import_pixi.Texture({ source });
|
|
1214
1372
|
} catch (error) {
|
|
1215
|
-
console.error(`[loadExpoAsset] Failed to load: ${url}`, error);
|
|
1373
|
+
if (__DEV__) console.error(`[loadExpoAsset] Failed to load: ${url}`, error);
|
|
1216
1374
|
throw error;
|
|
1217
1375
|
}
|
|
1218
1376
|
},
|
|
@@ -1282,10 +1440,10 @@ var loadExpoFont = {
|
|
|
1282
1440
|
await Font.loadAsync({
|
|
1283
1441
|
[familyName]: fontSource
|
|
1284
1442
|
});
|
|
1285
|
-
console.log(`Font loaded: ${familyName}`);
|
|
1443
|
+
if (__DEV__) console.log(`Font loaded: ${familyName}`);
|
|
1286
1444
|
return familyName;
|
|
1287
1445
|
} catch (error) {
|
|
1288
|
-
console.error(`Failed to load font: ${url}`, error);
|
|
1446
|
+
if (__DEV__) console.error(`Failed to load font: ${url}`, error);
|
|
1289
1447
|
throw error;
|
|
1290
1448
|
}
|
|
1291
1449
|
},
|
|
@@ -1294,7 +1452,7 @@ var loadExpoFont = {
|
|
|
1294
1452
|
* Note: expo-font doesn't have an unload API
|
|
1295
1453
|
*/
|
|
1296
1454
|
unload(fontFamily) {
|
|
1297
|
-
console.warn(`Font unloading is not supported in expo-font: ${fontFamily}`);
|
|
1455
|
+
if (__DEV__) console.warn(`Font unloading is not supported in expo-font: ${fontFamily}`);
|
|
1298
1456
|
}
|
|
1299
1457
|
};
|
|
1300
1458
|
|
|
@@ -1637,7 +1795,7 @@ var PixiView = (0, import_react.forwardRef)((props, ref) => {
|
|
|
1637
1795
|
});
|
|
1638
1796
|
onApplicationCreate?.(app);
|
|
1639
1797
|
} catch (error) {
|
|
1640
|
-
console.error("PixiJS initialization error:", error);
|
|
1798
|
+
if (__DEV__) console.error("PixiJS initialization error:", error);
|
|
1641
1799
|
onError?.(error);
|
|
1642
1800
|
}
|
|
1643
1801
|
},
|
|
@@ -1653,7 +1811,7 @@ var PixiView = (0, import_react.forwardRef)((props, ref) => {
|
|
|
1653
1811
|
try {
|
|
1654
1812
|
appRef.current.destroy(true, { children: true });
|
|
1655
1813
|
} catch (error) {
|
|
1656
|
-
console.warn("Error destroying PixiJS application:", error);
|
|
1814
|
+
if (__DEV__) console.warn("Error destroying PixiJS application:", error);
|
|
1657
1815
|
}
|
|
1658
1816
|
appRef.current = null;
|
|
1659
1817
|
}
|
|
@@ -1707,7 +1865,9 @@ import_pixi4.Assets.load = function patchedLoad(urls, onProgress) {
|
|
|
1707
1865
|
}
|
|
1708
1866
|
if (Array.isArray(urls)) {
|
|
1709
1867
|
const resolved = urls.map((u) => typeof u === "number" ? registerModuleId(u) : u);
|
|
1710
|
-
return _originalLoad(resolved, onProgress)
|
|
1868
|
+
return _originalLoad(resolved, onProgress).then((record) => {
|
|
1869
|
+
return resolved.map((key) => record[key]);
|
|
1870
|
+
});
|
|
1711
1871
|
}
|
|
1712
1872
|
return _originalLoad(urls, onProgress);
|
|
1713
1873
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,157 @@
|
|
|
1
|
+
// src/adapter/canvas2d.ts
|
|
2
|
+
function parseFontSize(font) {
|
|
3
|
+
const match = font.match(/(\d+(?:\.\d+)?)\s*px/);
|
|
4
|
+
return match ? parseFloat(match[1]) : 10;
|
|
5
|
+
}
|
|
6
|
+
function createMockGradient() {
|
|
7
|
+
return { addColorStop: () => {
|
|
8
|
+
} };
|
|
9
|
+
}
|
|
10
|
+
function createMockCanvas2DContext(canvas) {
|
|
11
|
+
let _font = "10px sans-serif";
|
|
12
|
+
const ctx = {
|
|
13
|
+
// -- Back-reference to owning canvas --
|
|
14
|
+
canvas,
|
|
15
|
+
// -- Font / text properties (critical for CanvasTextMetrics) --
|
|
16
|
+
get font() {
|
|
17
|
+
return _font;
|
|
18
|
+
},
|
|
19
|
+
set font(value) {
|
|
20
|
+
_font = value;
|
|
21
|
+
},
|
|
22
|
+
fillStyle: "#000",
|
|
23
|
+
strokeStyle: "#000",
|
|
24
|
+
lineWidth: 1,
|
|
25
|
+
lineCap: "butt",
|
|
26
|
+
lineJoin: "miter",
|
|
27
|
+
miterLimit: 10,
|
|
28
|
+
textAlign: "start",
|
|
29
|
+
textBaseline: "alphabetic",
|
|
30
|
+
direction: "ltr",
|
|
31
|
+
globalAlpha: 1,
|
|
32
|
+
globalCompositeOperation: "source-over",
|
|
33
|
+
imageSmoothingEnabled: true,
|
|
34
|
+
shadowBlur: 0,
|
|
35
|
+
shadowColor: "rgba(0, 0, 0, 0)",
|
|
36
|
+
shadowOffsetX: 0,
|
|
37
|
+
shadowOffsetY: 0,
|
|
38
|
+
letterSpacing: "0px",
|
|
39
|
+
wordSpacing: "0px",
|
|
40
|
+
fontKerning: "auto",
|
|
41
|
+
textRendering: "auto",
|
|
42
|
+
// -- Text measurement (primary reason this mock exists) --
|
|
43
|
+
measureText(text) {
|
|
44
|
+
const fontSize = parseFontSize(_font);
|
|
45
|
+
const width = text.length * fontSize * 0.6;
|
|
46
|
+
const ascent = fontSize * 0.8;
|
|
47
|
+
const descent = fontSize * 0.2;
|
|
48
|
+
return {
|
|
49
|
+
width,
|
|
50
|
+
actualBoundingBoxAscent: ascent,
|
|
51
|
+
actualBoundingBoxDescent: descent,
|
|
52
|
+
fontBoundingBoxAscent: ascent,
|
|
53
|
+
fontBoundingBoxDescent: descent,
|
|
54
|
+
actualBoundingBoxLeft: 0,
|
|
55
|
+
actualBoundingBoxRight: width,
|
|
56
|
+
alphabeticBaseline: 0,
|
|
57
|
+
emHeightAscent: ascent,
|
|
58
|
+
emHeightDescent: descent,
|
|
59
|
+
hangingBaseline: ascent * 0.8,
|
|
60
|
+
ideographicBaseline: -descent
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
// -- Text drawing (no-op) --
|
|
64
|
+
fillText: () => {
|
|
65
|
+
},
|
|
66
|
+
strokeText: () => {
|
|
67
|
+
},
|
|
68
|
+
// -- State stack --
|
|
69
|
+
save: () => {
|
|
70
|
+
},
|
|
71
|
+
restore: () => {
|
|
72
|
+
},
|
|
73
|
+
// -- Rect operations (no-op) --
|
|
74
|
+
clearRect: () => {
|
|
75
|
+
},
|
|
76
|
+
fillRect: () => {
|
|
77
|
+
},
|
|
78
|
+
strokeRect: () => {
|
|
79
|
+
},
|
|
80
|
+
// -- Path operations (no-op) --
|
|
81
|
+
beginPath: () => {
|
|
82
|
+
},
|
|
83
|
+
closePath: () => {
|
|
84
|
+
},
|
|
85
|
+
moveTo: () => {
|
|
86
|
+
},
|
|
87
|
+
lineTo: () => {
|
|
88
|
+
},
|
|
89
|
+
bezierCurveTo: () => {
|
|
90
|
+
},
|
|
91
|
+
quadraticCurveTo: () => {
|
|
92
|
+
},
|
|
93
|
+
arc: () => {
|
|
94
|
+
},
|
|
95
|
+
arcTo: () => {
|
|
96
|
+
},
|
|
97
|
+
ellipse: () => {
|
|
98
|
+
},
|
|
99
|
+
rect: () => {
|
|
100
|
+
},
|
|
101
|
+
roundRect: () => {
|
|
102
|
+
},
|
|
103
|
+
fill: () => {
|
|
104
|
+
},
|
|
105
|
+
stroke: () => {
|
|
106
|
+
},
|
|
107
|
+
clip: () => {
|
|
108
|
+
},
|
|
109
|
+
isPointInPath: () => false,
|
|
110
|
+
isPointInStroke: () => false,
|
|
111
|
+
// -- Transform (no-op) --
|
|
112
|
+
scale: () => {
|
|
113
|
+
},
|
|
114
|
+
rotate: () => {
|
|
115
|
+
},
|
|
116
|
+
translate: () => {
|
|
117
|
+
},
|
|
118
|
+
transform: () => {
|
|
119
|
+
},
|
|
120
|
+
setTransform: () => {
|
|
121
|
+
},
|
|
122
|
+
getTransform: () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }),
|
|
123
|
+
resetTransform: () => {
|
|
124
|
+
},
|
|
125
|
+
// -- Drawing images (no-op) --
|
|
126
|
+
drawImage: () => {
|
|
127
|
+
},
|
|
128
|
+
// -- Pixel manipulation --
|
|
129
|
+
createImageData: (w, h) => ({
|
|
130
|
+
width: w,
|
|
131
|
+
height: h,
|
|
132
|
+
data: new Uint8ClampedArray(w * h * 4)
|
|
133
|
+
}),
|
|
134
|
+
getImageData: (_x, _y, w, h) => ({
|
|
135
|
+
width: w,
|
|
136
|
+
height: h,
|
|
137
|
+
data: new Uint8ClampedArray(w * h * 4)
|
|
138
|
+
}),
|
|
139
|
+
putImageData: () => {
|
|
140
|
+
},
|
|
141
|
+
// -- Gradients / Patterns --
|
|
142
|
+
createLinearGradient: () => createMockGradient(),
|
|
143
|
+
createRadialGradient: () => createMockGradient(),
|
|
144
|
+
createConicGradient: () => createMockGradient(),
|
|
145
|
+
createPattern: () => null,
|
|
146
|
+
// -- Line dash --
|
|
147
|
+
setLineDash: () => {
|
|
148
|
+
},
|
|
149
|
+
getLineDash: () => [],
|
|
150
|
+
lineDashOffset: 0
|
|
151
|
+
};
|
|
152
|
+
return ctx;
|
|
153
|
+
}
|
|
154
|
+
|
|
1
155
|
// src/adapter/polyfills.ts
|
|
2
156
|
if (typeof globalThis.requestAnimationFrame === "undefined") {
|
|
3
157
|
globalThis.requestAnimationFrame = function requestAnimationFrame(callback) {
|
|
@@ -34,7 +188,7 @@ if (typeof globalThis.dispatchEvent === "undefined") {
|
|
|
34
188
|
try {
|
|
35
189
|
listener(event);
|
|
36
190
|
} catch (e) {
|
|
37
|
-
console.error("Error in event listener:", e);
|
|
191
|
+
if (__DEV__) console.error("Error in event listener:", e);
|
|
38
192
|
}
|
|
39
193
|
});
|
|
40
194
|
}
|
|
@@ -69,8 +223,10 @@ var createMockElement = (tagName) => {
|
|
|
69
223
|
const element = {
|
|
70
224
|
tagName: tagName.toUpperCase(),
|
|
71
225
|
style: createMockStyle(),
|
|
72
|
-
getContext: () =>
|
|
73
|
-
|
|
226
|
+
getContext: (type) => {
|
|
227
|
+
if (type === "2d") return createMockCanvas2DContext(element);
|
|
228
|
+
return null;
|
|
229
|
+
},
|
|
74
230
|
width: 0,
|
|
75
231
|
height: 0,
|
|
76
232
|
addEventListener: () => {
|
|
@@ -228,7 +384,7 @@ function dispatchWindowEvent(event) {
|
|
|
228
384
|
listener.handleEvent(event);
|
|
229
385
|
}
|
|
230
386
|
} catch (error) {
|
|
231
|
-
console.error("[Window] Error in event listener:", error);
|
|
387
|
+
if (__DEV__) console.error("[Window] Error in event listener:", error);
|
|
232
388
|
}
|
|
233
389
|
});
|
|
234
390
|
return true;
|
|
@@ -299,7 +455,8 @@ if (typeof globalThis.HTMLCanvasElement === "undefined") {
|
|
|
299
455
|
this.height = 0;
|
|
300
456
|
this.style = createMockStyle();
|
|
301
457
|
}
|
|
302
|
-
getContext() {
|
|
458
|
+
getContext(type) {
|
|
459
|
+
if (type === "2d") return createMockCanvas2DContext(this);
|
|
303
460
|
return null;
|
|
304
461
|
}
|
|
305
462
|
};
|
|
@@ -418,7 +575,7 @@ if (!("ontouchmove" in globalThis)) {
|
|
|
418
575
|
if (!("ontouchend" in globalThis)) {
|
|
419
576
|
globalThis.ontouchend = null;
|
|
420
577
|
}
|
|
421
|
-
console.log("PixiJS Expo Adapter: Polyfills loaded");
|
|
578
|
+
if (__DEV__) console.log("PixiJS Expo Adapter: Polyfills loaded");
|
|
422
579
|
|
|
423
580
|
// src/adapter/ExpoCanvasElement.ts
|
|
424
581
|
var MockCanvasStyle = class {
|
|
@@ -619,22 +776,21 @@ var ExpoCanvasElement = class {
|
|
|
619
776
|
case "webgl":
|
|
620
777
|
case "experimental-webgl":
|
|
621
778
|
if (!this._gl) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
779
|
+
if (__DEV__)
|
|
780
|
+
console.warn(
|
|
781
|
+
"ExpoCanvasElement: WebGL context not available. Make sure GLView.onContextCreate has been called first."
|
|
782
|
+
);
|
|
625
783
|
return null;
|
|
626
784
|
}
|
|
627
785
|
return this._gl;
|
|
628
786
|
case "2d":
|
|
629
|
-
|
|
630
|
-
"ExpoCanvasElement: 2D context is not supported in expo-gl. Consider using @shopify/react-native-skia for 2D rendering."
|
|
631
|
-
);
|
|
632
|
-
return null;
|
|
787
|
+
return createMockCanvas2DContext(this);
|
|
633
788
|
case "webgl2":
|
|
634
789
|
case "experimental-webgl2":
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
790
|
+
if (__DEV__)
|
|
791
|
+
console.warn(
|
|
792
|
+
"ExpoCanvasElement: WebGL2 is not fully supported in expo-gl. Falling back to WebGL1."
|
|
793
|
+
);
|
|
638
794
|
return this._gl;
|
|
639
795
|
default:
|
|
640
796
|
return null;
|
|
@@ -657,14 +813,15 @@ var ExpoCanvasElement = class {
|
|
|
657
813
|
*/
|
|
658
814
|
toDataURL(_type, _quality) {
|
|
659
815
|
if (!this._gl) {
|
|
660
|
-
console.warn("ExpoCanvasElement: Cannot create data URL without GL context");
|
|
816
|
+
if (__DEV__) console.warn("ExpoCanvasElement: Cannot create data URL without GL context");
|
|
661
817
|
return "";
|
|
662
818
|
}
|
|
663
819
|
const width = this._width;
|
|
664
820
|
const height = this._height;
|
|
665
821
|
const pixels = new Uint8Array(width * height * 4);
|
|
666
822
|
this._gl.readPixels(0, 0, width, height, this._gl.RGBA, this._gl.UNSIGNED_BYTE, pixels);
|
|
667
|
-
|
|
823
|
+
if (__DEV__)
|
|
824
|
+
console.warn("ExpoCanvasElement: toDataURL requires additional implementation for encoding");
|
|
668
825
|
return "";
|
|
669
826
|
}
|
|
670
827
|
/**
|
|
@@ -676,7 +833,7 @@ var ExpoCanvasElement = class {
|
|
|
676
833
|
* @param _quality - Image quality (ignored)
|
|
677
834
|
*/
|
|
678
835
|
toBlob(callback, _type, _quality) {
|
|
679
|
-
console.warn("ExpoCanvasElement: toBlob is not implemented");
|
|
836
|
+
if (__DEV__) console.warn("ExpoCanvasElement: toBlob is not implemented");
|
|
680
837
|
callback(null);
|
|
681
838
|
}
|
|
682
839
|
// ===========================================================================
|
|
@@ -745,7 +902,7 @@ var ExpoCanvasElement = class {
|
|
|
745
902
|
listener.handleEvent(event);
|
|
746
903
|
}
|
|
747
904
|
} catch (error) {
|
|
748
|
-
console.error("[ExpoCanvasElement] Error in event listener:", error);
|
|
905
|
+
if (__DEV__) console.error("[ExpoCanvasElement] Error in event listener:", error);
|
|
749
906
|
}
|
|
750
907
|
});
|
|
751
908
|
return true;
|
|
@@ -852,15 +1009,16 @@ var ExpoAdapter = {
|
|
|
852
1009
|
/**
|
|
853
1010
|
* Get the Canvas 2D rendering context constructor.
|
|
854
1011
|
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
* expo-gl only provides WebGL contexts. For 2D canvas operations,
|
|
859
|
-
* consider using @shopify/react-native-skia as an alternative.
|
|
1012
|
+
* Returns a mock constructor whose instances provide the mock Canvas2D
|
|
1013
|
+
* context. PixiJS accesses `CanvasRenderingContext2D.prototype` for
|
|
1014
|
+
* feature detection, so returning `null` causes a crash.
|
|
860
1015
|
*/
|
|
861
1016
|
getCanvasRenderingContext2D: () => {
|
|
862
|
-
|
|
863
|
-
|
|
1017
|
+
return class MockCanvasRenderingContext2D {
|
|
1018
|
+
constructor() {
|
|
1019
|
+
return createMockCanvas2DContext({ width: 0, height: 0 });
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
864
1022
|
},
|
|
865
1023
|
/**
|
|
866
1024
|
* Get the WebGL rendering context constructor.
|
|
@@ -938,11 +1096,11 @@ var ExpoAdapter = {
|
|
|
938
1096
|
return fetch(requestUrl, options);
|
|
939
1097
|
}
|
|
940
1098
|
if (requestUrl.startsWith("file://")) {
|
|
941
|
-
console.warn("ExpoAdapter: Local file loading requires expo-file-system");
|
|
1099
|
+
if (__DEV__) console.warn("ExpoAdapter: Local file loading requires expo-file-system");
|
|
942
1100
|
return fetch(requestUrl, options);
|
|
943
1101
|
}
|
|
944
1102
|
if (requestUrl.startsWith("asset://") || typeof url === "number") {
|
|
945
|
-
console.warn("ExpoAdapter: Asset loading requires expo-asset");
|
|
1103
|
+
if (__DEV__) console.warn("ExpoAdapter: Asset loading requires expo-asset");
|
|
946
1104
|
throw new Error("Asset loading not implemented");
|
|
947
1105
|
}
|
|
948
1106
|
return fetch(requestUrl, options);
|
|
@@ -1124,7 +1282,7 @@ var loadExpoAsset = {
|
|
|
1124
1282
|
});
|
|
1125
1283
|
return new Texture({ source });
|
|
1126
1284
|
} catch (error) {
|
|
1127
|
-
console.error(`[loadExpoAsset] Failed to load: ${url}`, error);
|
|
1285
|
+
if (__DEV__) console.error(`[loadExpoAsset] Failed to load: ${url}`, error);
|
|
1128
1286
|
throw error;
|
|
1129
1287
|
}
|
|
1130
1288
|
},
|
|
@@ -1194,10 +1352,10 @@ var loadExpoFont = {
|
|
|
1194
1352
|
await Font.loadAsync({
|
|
1195
1353
|
[familyName]: fontSource
|
|
1196
1354
|
});
|
|
1197
|
-
console.log(`Font loaded: ${familyName}`);
|
|
1355
|
+
if (__DEV__) console.log(`Font loaded: ${familyName}`);
|
|
1198
1356
|
return familyName;
|
|
1199
1357
|
} catch (error) {
|
|
1200
|
-
console.error(`Failed to load font: ${url}`, error);
|
|
1358
|
+
if (__DEV__) console.error(`Failed to load font: ${url}`, error);
|
|
1201
1359
|
throw error;
|
|
1202
1360
|
}
|
|
1203
1361
|
},
|
|
@@ -1206,7 +1364,7 @@ var loadExpoFont = {
|
|
|
1206
1364
|
* Note: expo-font doesn't have an unload API
|
|
1207
1365
|
*/
|
|
1208
1366
|
unload(fontFamily) {
|
|
1209
|
-
console.warn(`Font unloading is not supported in expo-font: ${fontFamily}`);
|
|
1367
|
+
if (__DEV__) console.warn(`Font unloading is not supported in expo-font: ${fontFamily}`);
|
|
1210
1368
|
}
|
|
1211
1369
|
};
|
|
1212
1370
|
|
|
@@ -1553,7 +1711,7 @@ var PixiView = forwardRef((props, ref) => {
|
|
|
1553
1711
|
});
|
|
1554
1712
|
onApplicationCreate?.(app);
|
|
1555
1713
|
} catch (error) {
|
|
1556
|
-
console.error("PixiJS initialization error:", error);
|
|
1714
|
+
if (__DEV__) console.error("PixiJS initialization error:", error);
|
|
1557
1715
|
onError?.(error);
|
|
1558
1716
|
}
|
|
1559
1717
|
},
|
|
@@ -1569,7 +1727,7 @@ var PixiView = forwardRef((props, ref) => {
|
|
|
1569
1727
|
try {
|
|
1570
1728
|
appRef.current.destroy(true, { children: true });
|
|
1571
1729
|
} catch (error) {
|
|
1572
|
-
console.warn("Error destroying PixiJS application:", error);
|
|
1730
|
+
if (__DEV__) console.warn("Error destroying PixiJS application:", error);
|
|
1573
1731
|
}
|
|
1574
1732
|
appRef.current = null;
|
|
1575
1733
|
}
|
|
@@ -1661,7 +1819,9 @@ Assets.load = function patchedLoad(urls, onProgress) {
|
|
|
1661
1819
|
}
|
|
1662
1820
|
if (Array.isArray(urls)) {
|
|
1663
1821
|
const resolved = urls.map((u) => typeof u === "number" ? registerModuleId(u) : u);
|
|
1664
|
-
return _originalLoad(resolved, onProgress)
|
|
1822
|
+
return _originalLoad(resolved, onProgress).then((record) => {
|
|
1823
|
+
return resolved.map((key) => record[key]);
|
|
1824
|
+
});
|
|
1665
1825
|
}
|
|
1666
1826
|
return _originalLoad(urls, onProgress);
|
|
1667
1827
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@penabt/pixi-expo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import type { ExpoWebGLRenderingContext } from 'expo-gl';
|
|
35
35
|
import { ExpoCanvasElement } from './ExpoCanvasElement';
|
|
36
|
+
import { createMockCanvas2DContext } from './canvas2d';
|
|
36
37
|
import { DOMParser } from '@xmldom/xmldom';
|
|
37
38
|
|
|
38
39
|
// =============================================================================
|
|
@@ -206,15 +207,16 @@ export const ExpoAdapter = {
|
|
|
206
207
|
/**
|
|
207
208
|
* Get the Canvas 2D rendering context constructor.
|
|
208
209
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* expo-gl only provides WebGL contexts. For 2D canvas operations,
|
|
213
|
-
* consider using @shopify/react-native-skia as an alternative.
|
|
210
|
+
* Returns a mock constructor whose instances provide the mock Canvas2D
|
|
211
|
+
* context. PixiJS accesses `CanvasRenderingContext2D.prototype` for
|
|
212
|
+
* feature detection, so returning `null` causes a crash.
|
|
214
213
|
*/
|
|
215
214
|
getCanvasRenderingContext2D: (): any => {
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
return class MockCanvasRenderingContext2D {
|
|
216
|
+
constructor() {
|
|
217
|
+
return createMockCanvas2DContext({ width: 0, height: 0 });
|
|
218
|
+
}
|
|
219
|
+
};
|
|
218
220
|
},
|
|
219
221
|
|
|
220
222
|
/**
|
|
@@ -303,13 +305,13 @@ export const ExpoAdapter = {
|
|
|
303
305
|
|
|
304
306
|
// Local file URL
|
|
305
307
|
if (requestUrl.startsWith('file://')) {
|
|
306
|
-
console.warn('ExpoAdapter: Local file loading requires expo-file-system');
|
|
308
|
+
if (__DEV__) console.warn('ExpoAdapter: Local file loading requires expo-file-system');
|
|
307
309
|
return fetch(requestUrl, options);
|
|
308
310
|
}
|
|
309
311
|
|
|
310
312
|
// Asset URL or require() number
|
|
311
313
|
if (requestUrl.startsWith('asset://') || typeof url === 'number') {
|
|
312
|
-
console.warn('ExpoAdapter: Asset loading requires expo-asset');
|
|
314
|
+
if (__DEV__) console.warn('ExpoAdapter: Asset loading requires expo-asset');
|
|
313
315
|
throw new Error('Asset loading not implemented');
|
|
314
316
|
}
|
|
315
317
|
|
|
@@ -33,8 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import type { ExpoWebGLRenderingContext } from 'expo-gl';
|
|
36
|
-
|
|
37
|
-
// ...
|
|
36
|
+
import { createMockCanvas2DContext } from './canvas2d';
|
|
38
37
|
|
|
39
38
|
// =============================================================================
|
|
40
39
|
// TYPE DEFINITIONS
|
|
@@ -332,27 +331,25 @@ export class ExpoCanvasElement {
|
|
|
332
331
|
case 'webgl':
|
|
333
332
|
case 'experimental-webgl':
|
|
334
333
|
if (!this._gl) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
'
|
|
338
|
-
|
|
334
|
+
if (__DEV__)
|
|
335
|
+
console.warn(
|
|
336
|
+
'ExpoCanvasElement: WebGL context not available. ' +
|
|
337
|
+
'Make sure GLView.onContextCreate has been called first.',
|
|
338
|
+
);
|
|
339
339
|
return null;
|
|
340
340
|
}
|
|
341
341
|
return this._gl as unknown as WebGLRenderingContext;
|
|
342
342
|
|
|
343
343
|
case '2d':
|
|
344
|
-
|
|
345
|
-
'ExpoCanvasElement: 2D context is not supported in expo-gl. ' +
|
|
346
|
-
'Consider using @shopify/react-native-skia for 2D rendering.',
|
|
347
|
-
);
|
|
348
|
-
return null;
|
|
344
|
+
return createMockCanvas2DContext(this) as any;
|
|
349
345
|
|
|
350
346
|
case 'webgl2':
|
|
351
347
|
case 'experimental-webgl2':
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
'
|
|
355
|
-
|
|
348
|
+
if (__DEV__)
|
|
349
|
+
console.warn(
|
|
350
|
+
'ExpoCanvasElement: WebGL2 is not fully supported in expo-gl. ' +
|
|
351
|
+
'Falling back to WebGL1.',
|
|
352
|
+
);
|
|
356
353
|
return this._gl as unknown as WebGLRenderingContext;
|
|
357
354
|
|
|
358
355
|
default:
|
|
@@ -378,7 +375,7 @@ export class ExpoCanvasElement {
|
|
|
378
375
|
*/
|
|
379
376
|
toDataURL(_type?: string, _quality?: number): string {
|
|
380
377
|
if (!this._gl) {
|
|
381
|
-
console.warn('ExpoCanvasElement: Cannot create data URL without GL context');
|
|
378
|
+
if (__DEV__) console.warn('ExpoCanvasElement: Cannot create data URL without GL context');
|
|
382
379
|
return '';
|
|
383
380
|
}
|
|
384
381
|
|
|
@@ -390,7 +387,8 @@ export class ExpoCanvasElement {
|
|
|
390
387
|
this._gl.readPixels(0, 0, width, height, this._gl.RGBA, this._gl.UNSIGNED_BYTE, pixels);
|
|
391
388
|
|
|
392
389
|
// Note: Full implementation would require encoding to PNG/JPEG
|
|
393
|
-
|
|
390
|
+
if (__DEV__)
|
|
391
|
+
console.warn('ExpoCanvasElement: toDataURL requires additional implementation for encoding');
|
|
394
392
|
|
|
395
393
|
return '';
|
|
396
394
|
}
|
|
@@ -404,7 +402,7 @@ export class ExpoCanvasElement {
|
|
|
404
402
|
* @param _quality - Image quality (ignored)
|
|
405
403
|
*/
|
|
406
404
|
toBlob(callback: (blob: Blob | null) => void, _type?: string, _quality?: number): void {
|
|
407
|
-
console.warn('ExpoCanvasElement: toBlob is not implemented');
|
|
405
|
+
if (__DEV__) console.warn('ExpoCanvasElement: toBlob is not implemented');
|
|
408
406
|
callback(null);
|
|
409
407
|
}
|
|
410
408
|
|
|
@@ -494,7 +492,7 @@ export class ExpoCanvasElement {
|
|
|
494
492
|
listener.handleEvent(event);
|
|
495
493
|
}
|
|
496
494
|
} catch (error) {
|
|
497
|
-
console.error('[ExpoCanvasElement] Error in event listener:', error);
|
|
495
|
+
if (__DEV__) console.error('[ExpoCanvasElement] Error in event listener:', error);
|
|
498
496
|
}
|
|
499
497
|
});
|
|
500
498
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mock Canvas 2D rendering context for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Provides a minimal CanvasRenderingContext2D implementation that satisfies
|
|
5
|
+
* PixiJS's text measurement pipeline (CanvasTextMetrics, BitmapFont.from,
|
|
6
|
+
* @pixi/ui text components). expo-gl only supports WebGL, so actual pixel
|
|
7
|
+
* drawing is not possible — but PixiJS needs the context to exist so it can
|
|
8
|
+
* set `context.font` and call `measureText()`.
|
|
9
|
+
*
|
|
10
|
+
* @module @penabt/pixi-expo/canvas2d
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Parse font size from a CSS font string (e.g. "bold 24px Arial" → 24). */
|
|
15
|
+
function parseFontSize(font: string): number {
|
|
16
|
+
const match = font.match(/(\d+(?:\.\d+)?)\s*px/);
|
|
17
|
+
return match ? parseFloat(match[1]) : 10;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** No-op gradient with addColorStop. */
|
|
21
|
+
function createMockGradient() {
|
|
22
|
+
return { addColorStop: () => {} };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a mock CanvasRenderingContext2D.
|
|
27
|
+
*
|
|
28
|
+
* The `measureText` implementation approximates character widths using
|
|
29
|
+
* `fontSize * 0.6` per character, which is close enough for layout purposes.
|
|
30
|
+
*
|
|
31
|
+
* @param canvas - The owning canvas element (or any object with width/height).
|
|
32
|
+
*/
|
|
33
|
+
export function createMockCanvas2DContext(canvas: any) {
|
|
34
|
+
let _font = '10px sans-serif';
|
|
35
|
+
|
|
36
|
+
const ctx: any = {
|
|
37
|
+
// -- Back-reference to owning canvas --
|
|
38
|
+
canvas,
|
|
39
|
+
|
|
40
|
+
// -- Font / text properties (critical for CanvasTextMetrics) --
|
|
41
|
+
get font() {
|
|
42
|
+
return _font;
|
|
43
|
+
},
|
|
44
|
+
set font(value: string) {
|
|
45
|
+
_font = value;
|
|
46
|
+
},
|
|
47
|
+
fillStyle: '#000',
|
|
48
|
+
strokeStyle: '#000',
|
|
49
|
+
lineWidth: 1,
|
|
50
|
+
lineCap: 'butt',
|
|
51
|
+
lineJoin: 'miter',
|
|
52
|
+
miterLimit: 10,
|
|
53
|
+
textAlign: 'start' as CanvasTextAlign,
|
|
54
|
+
textBaseline: 'alphabetic' as CanvasTextBaseline,
|
|
55
|
+
direction: 'ltr',
|
|
56
|
+
globalAlpha: 1,
|
|
57
|
+
globalCompositeOperation: 'source-over',
|
|
58
|
+
imageSmoothingEnabled: true,
|
|
59
|
+
shadowBlur: 0,
|
|
60
|
+
shadowColor: 'rgba(0, 0, 0, 0)',
|
|
61
|
+
shadowOffsetX: 0,
|
|
62
|
+
shadowOffsetY: 0,
|
|
63
|
+
letterSpacing: '0px',
|
|
64
|
+
wordSpacing: '0px',
|
|
65
|
+
fontKerning: 'auto',
|
|
66
|
+
textRendering: 'auto',
|
|
67
|
+
|
|
68
|
+
// -- Text measurement (primary reason this mock exists) --
|
|
69
|
+
measureText(text: string) {
|
|
70
|
+
const fontSize = parseFontSize(_font);
|
|
71
|
+
const width = text.length * fontSize * 0.6;
|
|
72
|
+
const ascent = fontSize * 0.8;
|
|
73
|
+
const descent = fontSize * 0.2;
|
|
74
|
+
return {
|
|
75
|
+
width,
|
|
76
|
+
actualBoundingBoxAscent: ascent,
|
|
77
|
+
actualBoundingBoxDescent: descent,
|
|
78
|
+
fontBoundingBoxAscent: ascent,
|
|
79
|
+
fontBoundingBoxDescent: descent,
|
|
80
|
+
actualBoundingBoxLeft: 0,
|
|
81
|
+
actualBoundingBoxRight: width,
|
|
82
|
+
alphabeticBaseline: 0,
|
|
83
|
+
emHeightAscent: ascent,
|
|
84
|
+
emHeightDescent: descent,
|
|
85
|
+
hangingBaseline: ascent * 0.8,
|
|
86
|
+
ideographicBaseline: -descent,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// -- Text drawing (no-op) --
|
|
91
|
+
fillText: () => {},
|
|
92
|
+
strokeText: () => {},
|
|
93
|
+
|
|
94
|
+
// -- State stack --
|
|
95
|
+
save: () => {},
|
|
96
|
+
restore: () => {},
|
|
97
|
+
|
|
98
|
+
// -- Rect operations (no-op) --
|
|
99
|
+
clearRect: () => {},
|
|
100
|
+
fillRect: () => {},
|
|
101
|
+
strokeRect: () => {},
|
|
102
|
+
|
|
103
|
+
// -- Path operations (no-op) --
|
|
104
|
+
beginPath: () => {},
|
|
105
|
+
closePath: () => {},
|
|
106
|
+
moveTo: () => {},
|
|
107
|
+
lineTo: () => {},
|
|
108
|
+
bezierCurveTo: () => {},
|
|
109
|
+
quadraticCurveTo: () => {},
|
|
110
|
+
arc: () => {},
|
|
111
|
+
arcTo: () => {},
|
|
112
|
+
ellipse: () => {},
|
|
113
|
+
rect: () => {},
|
|
114
|
+
roundRect: () => {},
|
|
115
|
+
fill: () => {},
|
|
116
|
+
stroke: () => {},
|
|
117
|
+
clip: () => {},
|
|
118
|
+
isPointInPath: () => false,
|
|
119
|
+
isPointInStroke: () => false,
|
|
120
|
+
|
|
121
|
+
// -- Transform (no-op) --
|
|
122
|
+
scale: () => {},
|
|
123
|
+
rotate: () => {},
|
|
124
|
+
translate: () => {},
|
|
125
|
+
transform: () => {},
|
|
126
|
+
setTransform: () => {},
|
|
127
|
+
getTransform: () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }),
|
|
128
|
+
resetTransform: () => {},
|
|
129
|
+
|
|
130
|
+
// -- Drawing images (no-op) --
|
|
131
|
+
drawImage: () => {},
|
|
132
|
+
|
|
133
|
+
// -- Pixel manipulation --
|
|
134
|
+
createImageData: (w: number, h: number) => ({
|
|
135
|
+
width: w,
|
|
136
|
+
height: h,
|
|
137
|
+
data: new Uint8ClampedArray(w * h * 4),
|
|
138
|
+
}),
|
|
139
|
+
getImageData: (_x: number, _y: number, w: number, h: number) => ({
|
|
140
|
+
width: w,
|
|
141
|
+
height: h,
|
|
142
|
+
data: new Uint8ClampedArray(w * h * 4),
|
|
143
|
+
}),
|
|
144
|
+
putImageData: () => {},
|
|
145
|
+
|
|
146
|
+
// -- Gradients / Patterns --
|
|
147
|
+
createLinearGradient: () => createMockGradient(),
|
|
148
|
+
createRadialGradient: () => createMockGradient(),
|
|
149
|
+
createConicGradient: () => createMockGradient(),
|
|
150
|
+
createPattern: () => null,
|
|
151
|
+
|
|
152
|
+
// -- Line dash --
|
|
153
|
+
setLineDash: () => {},
|
|
154
|
+
getLineDash: () => [],
|
|
155
|
+
lineDashOffset: 0,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return ctx;
|
|
159
|
+
}
|
|
@@ -260,7 +260,7 @@ export const loadExpoAsset = {
|
|
|
260
260
|
|
|
261
261
|
return new Texture({ source });
|
|
262
262
|
} catch (error) {
|
|
263
|
-
console.error(`[loadExpoAsset] Failed to load: ${url}`, error);
|
|
263
|
+
if (__DEV__) console.error(`[loadExpoAsset] Failed to load: ${url}`, error);
|
|
264
264
|
throw error;
|
|
265
265
|
}
|
|
266
266
|
},
|
|
@@ -118,12 +118,12 @@ export const loadExpoFont = {
|
|
|
118
118
|
[familyName]: fontSource,
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
console.log(`Font loaded: ${familyName}`);
|
|
121
|
+
if (__DEV__) console.log(`Font loaded: ${familyName}`);
|
|
122
122
|
|
|
123
123
|
// Return the font family name
|
|
124
124
|
return familyName;
|
|
125
125
|
} catch (error) {
|
|
126
|
-
console.error(`Failed to load font: ${url}`, error);
|
|
126
|
+
if (__DEV__) console.error(`Failed to load font: ${url}`, error);
|
|
127
127
|
throw error;
|
|
128
128
|
}
|
|
129
129
|
},
|
|
@@ -133,7 +133,7 @@ export const loadExpoFont = {
|
|
|
133
133
|
* Note: expo-font doesn't have an unload API
|
|
134
134
|
*/
|
|
135
135
|
unload(fontFamily: string): void {
|
|
136
|
-
console.warn(`Font unloading is not supported in expo-font: ${fontFamily}`);
|
|
136
|
+
if (__DEV__) console.warn(`Font unloading is not supported in expo-font: ${fontFamily}`);
|
|
137
137
|
},
|
|
138
138
|
} as LoaderParser<string>;
|
|
139
139
|
|
package/src/adapter/polyfills.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* @remarks
|
|
19
19
|
* These polyfills are intentionally minimal. They provide just enough
|
|
20
20
|
* functionality for PixiJS to initialize and run, but not full browser
|
|
21
|
-
* compatibility.
|
|
21
|
+
* compatibility. A mock Canvas 2D context is provided for text measurement
|
|
22
|
+
* (CanvasTextMetrics, BitmapFont, @pixi/ui) but does not perform actual drawing.
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
// =============================================================================
|
|
@@ -74,7 +75,7 @@ if (typeof (globalThis as any).dispatchEvent === 'undefined') {
|
|
|
74
75
|
try {
|
|
75
76
|
listener(event);
|
|
76
77
|
} catch (e) {
|
|
77
|
-
console.error('Error in event listener:', e);
|
|
78
|
+
if (__DEV__) console.error('Error in event listener:', e);
|
|
78
79
|
}
|
|
79
80
|
});
|
|
80
81
|
}
|
|
@@ -115,6 +116,12 @@ const createMockStyle = () => ({
|
|
|
115
116
|
getPropertyValue: () => '',
|
|
116
117
|
});
|
|
117
118
|
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// MOCK CANVAS 2D CONTEXT (imported from shared module)
|
|
121
|
+
// =============================================================================
|
|
122
|
+
|
|
123
|
+
import { createMockCanvas2DContext } from './canvas2d';
|
|
124
|
+
|
|
118
125
|
// =============================================================================
|
|
119
126
|
// MOCK ELEMENT FACTORY
|
|
120
127
|
// Creates HTMLElement-like objects for document.createElement compatibility.
|
|
@@ -131,7 +138,10 @@ const createMockElement = (tagName: string) => {
|
|
|
131
138
|
const element: any = {
|
|
132
139
|
tagName: tagName.toUpperCase(),
|
|
133
140
|
style: createMockStyle(),
|
|
134
|
-
getContext: () =>
|
|
141
|
+
getContext: (type: string) => {
|
|
142
|
+
if (type === '2d') return createMockCanvas2DContext(element);
|
|
143
|
+
return null; // WebGL contexts are provided by expo-gl via ExpoCanvasElement
|
|
144
|
+
},
|
|
135
145
|
width: 0,
|
|
136
146
|
height: 0,
|
|
137
147
|
addEventListener: () => {},
|
|
@@ -306,7 +316,7 @@ export function dispatchWindowEvent(event: { type: string; [key: string]: any })
|
|
|
306
316
|
listener.handleEvent(event);
|
|
307
317
|
}
|
|
308
318
|
} catch (error) {
|
|
309
|
-
console.error('[Window] Error in event listener:', error);
|
|
319
|
+
if (__DEV__) console.error('[Window] Error in event listener:', error);
|
|
310
320
|
}
|
|
311
321
|
});
|
|
312
322
|
|
|
@@ -399,8 +409,9 @@ if (typeof (globalThis as any).HTMLCanvasElement === 'undefined') {
|
|
|
399
409
|
width = 0;
|
|
400
410
|
height = 0;
|
|
401
411
|
style = createMockStyle();
|
|
402
|
-
getContext() {
|
|
403
|
-
return
|
|
412
|
+
getContext(type: string) {
|
|
413
|
+
if (type === '2d') return createMockCanvas2DContext(this);
|
|
414
|
+
return null; // WebGL contexts are provided by expo-gl via ExpoCanvasElement
|
|
404
415
|
}
|
|
405
416
|
};
|
|
406
417
|
}
|
|
@@ -572,4 +583,4 @@ if (!('ontouchend' in globalThis)) {
|
|
|
572
583
|
// Confirm polyfills are loaded (useful for debugging).
|
|
573
584
|
// =============================================================================
|
|
574
585
|
|
|
575
|
-
console.log('PixiJS Expo Adapter: Polyfills loaded');
|
|
586
|
+
if (__DEV__) console.log('PixiJS Expo Adapter: Polyfills loaded');
|
|
@@ -490,7 +490,7 @@ export const PixiView = forwardRef<PixiViewHandle, PixiViewProps>((props, ref) =
|
|
|
490
490
|
// Notify application creation
|
|
491
491
|
onApplicationCreate?.(app);
|
|
492
492
|
} catch (error) {
|
|
493
|
-
console.error('PixiJS initialization error:', error);
|
|
493
|
+
if (__DEV__) console.error('PixiJS initialization error:', error);
|
|
494
494
|
onError?.(error as Error);
|
|
495
495
|
}
|
|
496
496
|
},
|
|
@@ -515,7 +515,7 @@ export const PixiView = forwardRef<PixiViewHandle, PixiViewProps>((props, ref) =
|
|
|
515
515
|
try {
|
|
516
516
|
appRef.current.destroy(true, { children: true });
|
|
517
517
|
} catch (error) {
|
|
518
|
-
console.warn('Error destroying PixiJS application:', error);
|
|
518
|
+
if (__DEV__) console.warn('Error destroying PixiJS application:', error);
|
|
519
519
|
}
|
|
520
520
|
appRef.current = null;
|
|
521
521
|
}
|
package/src/index.ts
CHANGED
|
@@ -108,7 +108,11 @@ const _originalLoad = Assets.load.bind(Assets);
|
|
|
108
108
|
}
|
|
109
109
|
if (Array.isArray(urls)) {
|
|
110
110
|
const resolved = urls.map((u: any) => (typeof u === 'number' ? registerModuleId(u) : u));
|
|
111
|
-
|
|
111
|
+
// Assets.load returns Record<string, T> for arrays. Convert to array so
|
|
112
|
+
// callers can destructure: const [a, b] = await Assets.load([url1, url2])
|
|
113
|
+
return _originalLoad(resolved, onProgress).then((record: any) => {
|
|
114
|
+
return resolved.map((key: string) => record[key]);
|
|
115
|
+
});
|
|
112
116
|
}
|
|
113
117
|
return _originalLoad(urls, onProgress);
|
|
114
118
|
};
|