@buildcores/render-client 1.6.0 → 1.7.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 +34 -0
- package/dist/api.d.ts +2 -1
- package/dist/hooks/useSpriteRender.d.ts +8 -0
- package/dist/index.d.ts +88 -2
- package/dist/index.esm.js +154 -43
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +154 -43
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +64 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -213,17 +213,73 @@ const buildApiUrl = (endpoint, config) => {
|
|
|
213
213
|
}
|
|
214
214
|
return baseUrl;
|
|
215
215
|
};
|
|
216
|
+
const SESSION_TOKEN_REFRESH_WINDOW_MS = 15000;
|
|
217
|
+
const sessionTokenCache = new WeakMap();
|
|
218
|
+
const resolveAuthMode = (config) => config.authMode ?? (config.getRenderSessionToken ? "session" : "legacy");
|
|
219
|
+
const isSessionAuthMode = (config) => resolveAuthMode(config) === "session";
|
|
220
|
+
const parseExpiresAtMs = (expiresAt) => {
|
|
221
|
+
const parsed = Date.parse(expiresAt);
|
|
222
|
+
if (!Number.isFinite(parsed)) {
|
|
223
|
+
throw new Error("Invalid session token expiry (expiresAt)");
|
|
224
|
+
}
|
|
225
|
+
return parsed;
|
|
226
|
+
};
|
|
227
|
+
const resolveSessionToken = async (config, forceRefresh) => {
|
|
228
|
+
const supplier = config.getRenderSessionToken;
|
|
229
|
+
if (!supplier) {
|
|
230
|
+
throw new Error("authMode=session requires getRenderSessionToken");
|
|
231
|
+
}
|
|
232
|
+
const cached = sessionTokenCache.get(supplier);
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
if (!forceRefresh &&
|
|
235
|
+
cached &&
|
|
236
|
+
cached.expiresAtMs - SESSION_TOKEN_REFRESH_WINDOW_MS > now) {
|
|
237
|
+
return cached.token;
|
|
238
|
+
}
|
|
239
|
+
const session = await supplier();
|
|
240
|
+
if (!session?.token || !session?.expiresAt) {
|
|
241
|
+
throw new Error("getRenderSessionToken must return { token, expiresAt }");
|
|
242
|
+
}
|
|
243
|
+
const expiresAtMs = parseExpiresAtMs(session.expiresAt);
|
|
244
|
+
sessionTokenCache.set(supplier, {
|
|
245
|
+
token: session.token,
|
|
246
|
+
expiresAtMs,
|
|
247
|
+
});
|
|
248
|
+
return session.token;
|
|
249
|
+
};
|
|
250
|
+
const resolveAuthToken = async (config, forceRefresh) => {
|
|
251
|
+
if (isSessionAuthMode(config)) {
|
|
252
|
+
return resolveSessionToken(config, forceRefresh);
|
|
253
|
+
}
|
|
254
|
+
return config.authToken;
|
|
255
|
+
};
|
|
216
256
|
// Helper to build request headers with auth token
|
|
217
|
-
const buildHeaders = (config) => {
|
|
257
|
+
const buildHeaders = (config, authTokenOverride) => {
|
|
218
258
|
const headers = {
|
|
219
259
|
"Content-Type": "application/json",
|
|
220
260
|
accept: "application/json",
|
|
221
261
|
};
|
|
222
|
-
|
|
223
|
-
|
|
262
|
+
const authToken = authTokenOverride ?? config.authToken;
|
|
263
|
+
if (authToken) {
|
|
264
|
+
headers["Authorization"] = `Bearer ${authToken}`;
|
|
224
265
|
}
|
|
225
266
|
return headers;
|
|
226
267
|
};
|
|
268
|
+
const fetchWithApiAuth = async (url, init, config) => {
|
|
269
|
+
const firstToken = await resolveAuthToken(config, false);
|
|
270
|
+
const firstResponse = await fetch(url, {
|
|
271
|
+
...init,
|
|
272
|
+
headers: buildHeaders(config, firstToken),
|
|
273
|
+
});
|
|
274
|
+
if (!isSessionAuthMode(config) || firstResponse.status !== 401) {
|
|
275
|
+
return firstResponse;
|
|
276
|
+
}
|
|
277
|
+
const refreshedToken = await resolveAuthToken(config, true);
|
|
278
|
+
return fetch(url, {
|
|
279
|
+
...init,
|
|
280
|
+
headers: buildHeaders(config, refreshedToken),
|
|
281
|
+
});
|
|
282
|
+
};
|
|
227
283
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
228
284
|
// API Implementation
|
|
229
285
|
const renderBuildExperimental = async (request, config) => {
|
|
@@ -235,12 +291,16 @@ const renderBuildExperimental = async (request, config) => {
|
|
|
235
291
|
...(request.height !== undefined ? { height: request.height } : {}),
|
|
236
292
|
// Include profile if provided
|
|
237
293
|
...(request.profile ? { profile: request.profile } : {}),
|
|
294
|
+
...(request.scene ? { scene: request.scene } : {}),
|
|
295
|
+
...(request.showBackground !== undefined ? { showBackground: request.showBackground } : {}),
|
|
296
|
+
...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
|
|
297
|
+
...(request.winterMode !== undefined ? { winterMode: request.winterMode } : {}),
|
|
298
|
+
...(request.springMode !== undefined ? { springMode: request.springMode } : {}),
|
|
238
299
|
};
|
|
239
|
-
const response = await
|
|
300
|
+
const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
|
|
240
301
|
method: "POST",
|
|
241
|
-
headers: buildHeaders(config),
|
|
242
302
|
body: JSON.stringify(requestWithFormat),
|
|
243
|
-
});
|
|
303
|
+
}, config);
|
|
244
304
|
if (!response.ok) {
|
|
245
305
|
throw new Error(`Render build failed: ${response.status} ${response.statusText}`);
|
|
246
306
|
}
|
|
@@ -264,8 +324,12 @@ const createRenderBuildJob = async (request, config) => {
|
|
|
264
324
|
...(request.height !== undefined ? { height: request.height } : {}),
|
|
265
325
|
// Include profile if provided
|
|
266
326
|
...(request.profile ? { profile: request.profile } : {}),
|
|
327
|
+
...(request.scene ? { scene: request.scene } : {}),
|
|
328
|
+
...(request.showBackground !== undefined ? { showBackground: request.showBackground } : {}),
|
|
267
329
|
// Include composition settings
|
|
268
330
|
...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
|
|
331
|
+
...(request.winterMode !== undefined ? { winterMode: request.winterMode } : {}),
|
|
332
|
+
...(request.springMode !== undefined ? { springMode: request.springMode } : {}),
|
|
269
333
|
...(request.cameraOffsetX !== undefined ? { cameraOffsetX: request.cameraOffsetX } : {}),
|
|
270
334
|
...(request.gridSettings ? { gridSettings: request.gridSettings } : {}),
|
|
271
335
|
// Include frame quality for sprite rendering
|
|
@@ -273,11 +337,10 @@ const createRenderBuildJob = async (request, config) => {
|
|
|
273
337
|
// Include camera zoom for render-time scaling
|
|
274
338
|
...(request.cameraZoom !== undefined ? { cameraZoom: request.cameraZoom } : {}),
|
|
275
339
|
};
|
|
276
|
-
const response = await
|
|
340
|
+
const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
|
|
277
341
|
method: "POST",
|
|
278
|
-
headers: buildHeaders(config),
|
|
279
342
|
body: JSON.stringify(body),
|
|
280
|
-
});
|
|
343
|
+
}, config);
|
|
281
344
|
if (!response.ok) {
|
|
282
345
|
throw new Error(`Create render job failed: ${response.status} ${response.statusText}`);
|
|
283
346
|
}
|
|
@@ -289,10 +352,7 @@ const createRenderBuildJob = async (request, config) => {
|
|
|
289
352
|
};
|
|
290
353
|
const getRenderBuildStatus = async (jobId, config) => {
|
|
291
354
|
const url = buildApiUrl(`${API_ENDPOINTS.RENDER_BUILD}/${encodeURIComponent(jobId)}`, config);
|
|
292
|
-
const response = await
|
|
293
|
-
method: "GET",
|
|
294
|
-
headers: buildHeaders(config),
|
|
295
|
-
});
|
|
355
|
+
const response = await fetchWithApiAuth(url, { method: "GET" }, config);
|
|
296
356
|
if (response.status === 404) {
|
|
297
357
|
throw new Error("Render job not found");
|
|
298
358
|
}
|
|
@@ -337,12 +397,16 @@ const renderSpriteExperimental = async (request, config) => {
|
|
|
337
397
|
...(request.height !== undefined ? { height: request.height } : {}),
|
|
338
398
|
// Include profile if provided
|
|
339
399
|
...(request.profile ? { profile: request.profile } : {}),
|
|
400
|
+
...(request.scene ? { scene: request.scene } : {}),
|
|
401
|
+
...(request.showBackground !== undefined ? { showBackground: request.showBackground } : {}),
|
|
402
|
+
...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
|
|
403
|
+
...(request.winterMode !== undefined ? { winterMode: request.winterMode } : {}),
|
|
404
|
+
...(request.springMode !== undefined ? { springMode: request.springMode } : {}),
|
|
340
405
|
};
|
|
341
|
-
const response = await
|
|
406
|
+
const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
|
|
342
407
|
method: "POST",
|
|
343
|
-
headers: buildHeaders(config),
|
|
344
408
|
body: JSON.stringify(requestWithFormat),
|
|
345
|
-
});
|
|
409
|
+
}, config);
|
|
346
410
|
if (!response.ok) {
|
|
347
411
|
throw new Error(`Render sprite failed: ${response.status} ${response.statusText}`);
|
|
348
412
|
}
|
|
@@ -368,10 +432,7 @@ const getAvailableParts = async (category, config, options) => {
|
|
|
368
432
|
params.set("skip", String(options.skip));
|
|
369
433
|
const separator = base.includes("?") ? "&" : "?";
|
|
370
434
|
const url = `${base}${separator}${params.toString()}`;
|
|
371
|
-
const response = await
|
|
372
|
-
method: "GET",
|
|
373
|
-
headers: buildHeaders(config),
|
|
374
|
-
});
|
|
435
|
+
const response = await fetchWithApiAuth(url, { method: "GET" }, config);
|
|
375
436
|
if (!response.ok) {
|
|
376
437
|
throw new Error(`Get available parts failed: ${response.status} ${response.statusText}`);
|
|
377
438
|
}
|
|
@@ -404,10 +465,7 @@ const getAvailableParts = async (category, config, options) => {
|
|
|
404
465
|
*/
|
|
405
466
|
const getBuildByShareCode = async (shareCode, config) => {
|
|
406
467
|
const url = buildApiUrl(`${API_ENDPOINTS.BUILD}/${encodeURIComponent(shareCode)}`, config);
|
|
407
|
-
const response = await
|
|
408
|
-
method: "GET",
|
|
409
|
-
headers: buildHeaders(config),
|
|
410
|
-
});
|
|
468
|
+
const response = await fetchWithApiAuth(url, { method: "GET" }, config);
|
|
411
469
|
if (response.status === 404) {
|
|
412
470
|
throw new Error("Build not found");
|
|
413
471
|
}
|
|
@@ -437,11 +495,10 @@ const getBuildByShareCode = async (shareCode, config) => {
|
|
|
437
495
|
*/
|
|
438
496
|
const getPartsByIds = async (partIds, config) => {
|
|
439
497
|
const url = buildApiUrl(API_ENDPOINTS.PARTS, config);
|
|
440
|
-
const response = await
|
|
498
|
+
const response = await fetchWithApiAuth(url, {
|
|
441
499
|
method: "POST",
|
|
442
|
-
headers: buildHeaders(config),
|
|
443
500
|
body: JSON.stringify({ ids: partIds }),
|
|
444
|
-
});
|
|
501
|
+
}, config);
|
|
445
502
|
if (!response.ok) {
|
|
446
503
|
throw new Error(`Get parts by IDs failed: ${response.status} ${response.statusText}`);
|
|
447
504
|
}
|
|
@@ -464,17 +521,20 @@ const createRenderByShareCodeJob = async (shareCode, config, options) => {
|
|
|
464
521
|
...(options?.width !== undefined ? { width: options.width } : {}),
|
|
465
522
|
...(options?.height !== undefined ? { height: options.height } : {}),
|
|
466
523
|
...(options?.profile ? { profile: options.profile } : {}),
|
|
524
|
+
...(options?.scene ? { scene: options.scene } : {}),
|
|
525
|
+
...(options?.showBackground !== undefined ? { showBackground: options.showBackground } : {}),
|
|
467
526
|
...(options?.showGrid !== undefined ? { showGrid: options.showGrid } : {}),
|
|
527
|
+
...(options?.winterMode !== undefined ? { winterMode: options.winterMode } : {}),
|
|
528
|
+
...(options?.springMode !== undefined ? { springMode: options.springMode } : {}),
|
|
468
529
|
...(options?.cameraOffsetX !== undefined ? { cameraOffsetX: options.cameraOffsetX } : {}),
|
|
469
530
|
...(options?.gridSettings ? { gridSettings: options.gridSettings } : {}),
|
|
470
531
|
...(options?.frameQuality ? { frameQuality: options.frameQuality } : {}),
|
|
471
532
|
...(options?.cameraZoom !== undefined ? { cameraZoom: options.cameraZoom } : {}),
|
|
472
533
|
};
|
|
473
|
-
const response = await
|
|
534
|
+
const response = await fetchWithApiAuth(url, {
|
|
474
535
|
method: "POST",
|
|
475
|
-
headers: buildHeaders(config),
|
|
476
536
|
body: JSON.stringify(body),
|
|
477
|
-
});
|
|
537
|
+
}, config);
|
|
478
538
|
if (response.status === 404) {
|
|
479
539
|
throw new Error("Build not found");
|
|
480
540
|
}
|
|
@@ -683,7 +743,21 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
683
743
|
const [spriteMetadata, setSpriteMetadata] = react.useState(null);
|
|
684
744
|
const previousInputRef = react.useRef(null);
|
|
685
745
|
// Normalize input to SpriteRenderInput format
|
|
686
|
-
const normalizedInput = 'type' in input
|
|
746
|
+
const normalizedInput = 'type' in input
|
|
747
|
+
? input
|
|
748
|
+
: {
|
|
749
|
+
type: 'parts',
|
|
750
|
+
parts: input,
|
|
751
|
+
showGrid: input.showGrid,
|
|
752
|
+
scene: input.scene,
|
|
753
|
+
showBackground: input.showBackground,
|
|
754
|
+
winterMode: input.winterMode,
|
|
755
|
+
springMode: input.springMode,
|
|
756
|
+
cameraOffsetX: input.cameraOffsetX,
|
|
757
|
+
cameraZoom: input.cameraZoom,
|
|
758
|
+
gridSettings: input.gridSettings,
|
|
759
|
+
frameQuality: input.frameQuality
|
|
760
|
+
};
|
|
687
761
|
const fetchRenderSprite = react.useCallback(async (currentInput) => {
|
|
688
762
|
try {
|
|
689
763
|
setIsRenderingSprite(true);
|
|
@@ -695,6 +769,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
695
769
|
format: 'sprite',
|
|
696
770
|
profile: currentInput.profile,
|
|
697
771
|
showGrid: currentInput.showGrid,
|
|
772
|
+
scene: currentInput.scene,
|
|
773
|
+
showBackground: currentInput.showBackground,
|
|
774
|
+
winterMode: currentInput.winterMode,
|
|
775
|
+
springMode: currentInput.springMode,
|
|
698
776
|
cameraOffsetX: currentInput.cameraOffsetX,
|
|
699
777
|
cameraZoom: currentInput.cameraZoom,
|
|
700
778
|
gridSettings: currentInput.gridSettings,
|
|
@@ -721,6 +799,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
721
799
|
const response = await renderSpriteExperimental({
|
|
722
800
|
...currentParts,
|
|
723
801
|
showGrid: currentInput.showGrid,
|
|
802
|
+
scene: currentInput.scene,
|
|
803
|
+
showBackground: currentInput.showBackground,
|
|
804
|
+
winterMode: currentInput.winterMode,
|
|
805
|
+
springMode: currentInput.springMode,
|
|
724
806
|
cameraOffsetX: currentInput.cameraOffsetX,
|
|
725
807
|
cameraZoom: currentInput.cameraZoom,
|
|
726
808
|
gridSettings: currentInput.gridSettings,
|
|
@@ -747,6 +829,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
747
829
|
...currentParts,
|
|
748
830
|
format: "sprite",
|
|
749
831
|
showGrid: currentInput.showGrid,
|
|
832
|
+
scene: currentInput.scene,
|
|
833
|
+
showBackground: currentInput.showBackground,
|
|
834
|
+
winterMode: currentInput.winterMode,
|
|
835
|
+
springMode: currentInput.springMode,
|
|
750
836
|
cameraOffsetX: currentInput.cameraOffsetX,
|
|
751
837
|
cameraZoom: currentInput.cameraZoom,
|
|
752
838
|
gridSettings: currentInput.gridSettings,
|
|
@@ -781,6 +867,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
781
867
|
return a.shareCode === b.shareCode &&
|
|
782
868
|
a.profile === b.profile &&
|
|
783
869
|
a.showGrid === b.showGrid &&
|
|
870
|
+
a.scene === b.scene &&
|
|
871
|
+
a.showBackground === b.showBackground &&
|
|
872
|
+
a.winterMode === b.winterMode &&
|
|
873
|
+
a.springMode === b.springMode &&
|
|
784
874
|
a.cameraOffsetX === b.cameraOffsetX &&
|
|
785
875
|
a.cameraZoom === b.cameraZoom &&
|
|
786
876
|
a.frameQuality === b.frameQuality &&
|
|
@@ -791,6 +881,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
791
881
|
const gridSettingsEqual = JSON.stringify(a.gridSettings ?? {}) === JSON.stringify(b.gridSettings ?? {});
|
|
792
882
|
return arePartsEqual(a.parts, b.parts) &&
|
|
793
883
|
a.showGrid === b.showGrid &&
|
|
884
|
+
a.scene === b.scene &&
|
|
885
|
+
a.showBackground === b.showBackground &&
|
|
886
|
+
a.winterMode === b.winterMode &&
|
|
887
|
+
a.springMode === b.springMode &&
|
|
794
888
|
a.cameraOffsetX === b.cameraOffsetX &&
|
|
795
889
|
a.cameraZoom === b.cameraZoom &&
|
|
796
890
|
a.frameQuality === b.frameQuality &&
|
|
@@ -969,7 +1063,7 @@ const useZoomPan = ({ displayWidth, displayHeight, minScale = 0.5, maxScale = 2.
|
|
|
969
1063
|
};
|
|
970
1064
|
};
|
|
971
1065
|
|
|
972
|
-
const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, cameraOffsetX, cameraZoom, gridSettings, animationMode = 'bounce', spinDuration = 10000, interactive = true, frameQuality, zoom = 1, }) => {
|
|
1066
|
+
const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, scene, showBackground, winterMode, springMode, cameraOffsetX, cameraZoom, gridSettings, animationMode = 'bounce', spinDuration = 10000, interactive = true, frameQuality, zoom = 1, }) => {
|
|
973
1067
|
const canvasRef = react.useRef(null);
|
|
974
1068
|
const containerRef = react.useRef(null);
|
|
975
1069
|
const [img, setImg] = react.useState(null);
|
|
@@ -979,28 +1073,45 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
979
1073
|
const displayH = height ?? size ?? 300;
|
|
980
1074
|
// Build the render input - prefer shareCode if provided (preserves interactive state like case fan slots)
|
|
981
1075
|
const renderInput = react.useMemo(() => {
|
|
1076
|
+
const resolvedShowGrid = showGrid ?? parts?.showGrid;
|
|
1077
|
+
const resolvedScene = scene ?? parts?.scene;
|
|
1078
|
+
const resolvedShowBackground = showBackground ?? parts?.showBackground;
|
|
1079
|
+
const resolvedWinterMode = winterMode ?? parts?.winterMode;
|
|
1080
|
+
const resolvedSpringMode = springMode ?? parts?.springMode;
|
|
1081
|
+
const resolvedCameraOffsetX = cameraOffsetX ?? parts?.cameraOffsetX;
|
|
1082
|
+
const resolvedCameraZoom = cameraZoom ?? parts?.cameraZoom;
|
|
1083
|
+
const resolvedGridSettings = gridSettings ?? parts?.gridSettings;
|
|
1084
|
+
const resolvedFrameQuality = frameQuality ?? parts?.frameQuality;
|
|
982
1085
|
if (shareCode) {
|
|
983
1086
|
return {
|
|
984
1087
|
type: 'shareCode',
|
|
985
1088
|
shareCode,
|
|
986
1089
|
profile: parts?.profile,
|
|
987
|
-
showGrid,
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1090
|
+
showGrid: resolvedShowGrid,
|
|
1091
|
+
scene: resolvedScene,
|
|
1092
|
+
showBackground: resolvedShowBackground,
|
|
1093
|
+
winterMode: resolvedWinterMode,
|
|
1094
|
+
springMode: resolvedSpringMode,
|
|
1095
|
+
cameraOffsetX: resolvedCameraOffsetX,
|
|
1096
|
+
cameraZoom: resolvedCameraZoom,
|
|
1097
|
+
gridSettings: resolvedGridSettings,
|
|
1098
|
+
frameQuality: resolvedFrameQuality,
|
|
992
1099
|
};
|
|
993
1100
|
}
|
|
994
1101
|
return {
|
|
995
1102
|
type: 'parts',
|
|
996
1103
|
parts: parts,
|
|
997
|
-
showGrid,
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1104
|
+
showGrid: resolvedShowGrid,
|
|
1105
|
+
scene: resolvedScene,
|
|
1106
|
+
showBackground: resolvedShowBackground,
|
|
1107
|
+
winterMode: resolvedWinterMode,
|
|
1108
|
+
springMode: resolvedSpringMode,
|
|
1109
|
+
cameraOffsetX: resolvedCameraOffsetX,
|
|
1110
|
+
cameraZoom: resolvedCameraZoom,
|
|
1111
|
+
gridSettings: resolvedGridSettings,
|
|
1112
|
+
frameQuality: resolvedFrameQuality,
|
|
1002
1113
|
};
|
|
1003
|
-
}, [shareCode, parts, showGrid, cameraOffsetX, cameraZoom, gridSettings, frameQuality]);
|
|
1114
|
+
}, [shareCode, parts, showGrid, scene, showBackground, winterMode, springMode, cameraOffsetX, cameraZoom, gridSettings, frameQuality]);
|
|
1004
1115
|
// Use custom hook for sprite rendering
|
|
1005
1116
|
const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(renderInput, apiConfig, undefined, useSpriteRenderOptions);
|
|
1006
1117
|
const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
|