distark-render 1.0.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.
@@ -0,0 +1,1248 @@
1
+ /**
2
+ * Rendering-agnostic character rig data generator
3
+ * Computes all transforms and returns a data structure that can be used with any rendering library
4
+ */
5
+ // Import ImageLoader class
6
+ import { ImageLoader } from './imageLoad.js';
7
+ /**
8
+ * CharacterRigRenderer class - Handles character rig computation and rendering
9
+ */
10
+ export class CharacterRigRenderer {
11
+ /**
12
+ * Creates a new CharacterRigRenderer instance
13
+ * @param imageLoader - Optional ImageLoader instance (creates a new one if not provided)
14
+ */
15
+ constructor(imageLoader) {
16
+ this.imageLoader = imageLoader || new ImageLoader();
17
+ }
18
+ /**
19
+ * Get the ImageLoader instance
20
+ * @returns The ImageLoader instance
21
+ */
22
+ getImageLoader() {
23
+ return this.imageLoader;
24
+ }
25
+ /**
26
+ * Helper: apply parent transform to get world space position
27
+ */
28
+ applyTransform(parent, local) {
29
+ const cos = Math.cos(parent.rotation);
30
+ const sin = Math.sin(parent.rotation);
31
+ return {
32
+ x: parent.x + (local.x * cos - local.y * sin) * parent.scaleX,
33
+ y: parent.y + (local.x * sin + local.y * cos) * parent.scaleY,
34
+ rotation: parent.rotation + local.rotation,
35
+ scaleX: parent.scaleX * local.scaleX,
36
+ scaleY: parent.scaleY * local.scaleY
37
+ };
38
+ }
39
+ /**
40
+ * Computes all character rig objects with their final transforms and properties
41
+ */
42
+ computeCharacterRigData(rigData, options = {}) {
43
+ const { canvasWidth = 800, canvasHeight = 600, cameraOffset = { x: 0, y: 0 }, loadedImages = {} } = options;
44
+ // Get rendering parameters from rigData
45
+ const centerX = canvasWidth / 2 + cameraOffset.x;
46
+ const centerY = canvasHeight / 2 + 100 + cameraOffset.y;
47
+ const rotations = rigData.rotationValues || {};
48
+ const selfRotations = rigData.selfRotationValues || {};
49
+ const dimensions = rigData.dimensionValues || {};
50
+ const pivotPoints = rigData.pivotPoints || {};
51
+ const jointOffset = rigData.jointOffset || {};
52
+ const zIndexValues = rigData.zIndexValues || {};
53
+ const eyes = rigData.eyes || {};
54
+ const visibility = rigData.visibility || {}; // Default: all visible if not specified
55
+ const flipX = rigData.flipX || false; // No flip by default
56
+ const flipHead = rigData.flipHead || false; // No head flip by default
57
+ const imageScale = rigData.imageScale ?? 1.0; // Use imageScale from rigData, default to 1.0
58
+ // Create array of all limbs/components with their computed transforms
59
+ const allObjects = [];
60
+ // Root transform (character position in world space)
61
+ const rootTransform = {
62
+ x: centerX,
63
+ y: centerY,
64
+ rotation: 0,
65
+ scaleX: flipX ? -1 : 1,
66
+ scaleY: 1
67
+ };
68
+ // Torso
69
+ if (visibility.torso !== false) {
70
+ const torsoWidth = dimensions.torso?.width || 60;
71
+ const torsoHeight = dimensions.torso?.height || 120;
72
+ const selfRot = selfRotations["torso"] || 0;
73
+ allObjects.push({
74
+ name: 'torso',
75
+ type: 'limb',
76
+ zIndex: zIndexValues['torso'] || 1,
77
+ width: torsoWidth * imageScale,
78
+ height: torsoHeight * imageScale,
79
+ x: rootTransform.x,
80
+ y: rootTransform.y,
81
+ rotation: rootTransform.rotation + selfRot,
82
+ scaleX: rootTransform.scaleX,
83
+ scaleY: rootTransform.scaleY,
84
+ // Anchor point (where the image is drawn from - top center for torso)
85
+ anchorX: 0.5,
86
+ anchorY: 1,
87
+ imageKey: 'imagePaths.torso',
88
+ imageData: loadedImages['imagePaths.torso'],
89
+ selfRotation: selfRot
90
+ });
91
+ }
92
+ // Head
93
+ if (visibility.head !== false) {
94
+ const connectionKey = `torso_head`;
95
+ const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
96
+ const headJointOffset = jointOffset[connectionKey] || { x: 0, y: 0 };
97
+ const headWidth = dimensions.head?.width || 80;
98
+ const headHeight = dimensions.head?.height || 80;
99
+ const headRot = rotations["head"] || 0;
100
+ const selfRot = selfRotations["head"] || 0;
101
+ // Calculate head transform
102
+ const headParentTransform = this.applyTransform(rootTransform, {
103
+ x: pivotPoint.x || 0,
104
+ y: pivotPoint.y || 0,
105
+ rotation: headRot,
106
+ scaleX: flipHead ? -1 : 1,
107
+ scaleY: 1
108
+ });
109
+ const headTransform = this.applyTransform(headParentTransform, {
110
+ x: headJointOffset.x || 0,
111
+ y: (headJointOffset.y || 0) - headHeight / 2,
112
+ rotation: selfRot,
113
+ scaleX: 1,
114
+ scaleY: 1
115
+ });
116
+ allObjects.push({
117
+ name: 'head',
118
+ type: 'limb',
119
+ zIndex: zIndexValues['head'] || 1,
120
+ width: headWidth * imageScale,
121
+ height: headHeight * imageScale,
122
+ x: headTransform.x,
123
+ y: headTransform.y,
124
+ rotation: headTransform.rotation,
125
+ scaleX: headTransform.scaleX,
126
+ scaleY: headTransform.scaleY,
127
+ anchorX: 0.5,
128
+ anchorY: 0.5,
129
+ imageKey: 'imagePaths.head',
130
+ imageData: loadedImages['imagePaths.head'],
131
+ selfRotation: selfRot,
132
+ parentTransform: headParentTransform
133
+ });
134
+ }
135
+ // Mouth (child of head)
136
+ if (visibility.mouth !== false) {
137
+ const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
138
+ const mouthOffset = pivotPoints['head_mouth'] || { x: 0, y: 0 };
139
+ const mouthWidth = dimensions.mouth?.width || 40;
140
+ const mouthHeight = dimensions.mouth?.height || 30;
141
+ const selfRot = selfRotations["mouth"] || 0;
142
+ const headRot = rotations["head"] || 0;
143
+ const headBaseTransform = this.applyTransform(rootTransform, {
144
+ x: headPivot.x || 0,
145
+ y: headPivot.y || 0,
146
+ rotation: headRot,
147
+ scaleX: flipHead ? -1 : 1,
148
+ scaleY: 1
149
+ });
150
+ const offsetX = Number.isFinite(mouthOffset.x) ? mouthOffset.x : 0;
151
+ const offsetY = Number.isFinite(mouthOffset.y) ? mouthOffset.y : 0;
152
+ const mouthTransform = this.applyTransform(headBaseTransform, {
153
+ x: offsetX,
154
+ y: offsetY - mouthHeight / 2,
155
+ rotation: selfRot,
156
+ scaleX: 1,
157
+ scaleY: 1
158
+ });
159
+ allObjects.push({
160
+ name: 'mouth',
161
+ type: 'limb',
162
+ zIndex: zIndexValues['mouth'] || 1,
163
+ width: mouthWidth * imageScale,
164
+ height: mouthHeight * imageScale,
165
+ x: mouthTransform.x,
166
+ y: mouthTransform.y,
167
+ rotation: mouthTransform.rotation,
168
+ scaleX: mouthTransform.scaleX,
169
+ scaleY: mouthTransform.scaleY,
170
+ anchorX: 0.5,
171
+ anchorY: 0.5,
172
+ imageKey: 'imagePaths.mouth',
173
+ imageData: loadedImages['imagePaths.mouth'],
174
+ selfRotation: selfRot
175
+ });
176
+ }
177
+ // Left Eye
178
+ if (visibility.leftEye !== false && loadedImages['eyes.leftEyeImage'] && eyes) {
179
+ const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
180
+ const headRot = rotations["head"] || 0;
181
+ const headBaseTransform = this.applyTransform(rootTransform, {
182
+ x: headPivot.x || 0,
183
+ y: headPivot.y || 0,
184
+ rotation: headRot,
185
+ scaleX: flipHead ? -1 : 1,
186
+ scaleY: 1
187
+ });
188
+ const offsetX = Number.isFinite(eyes.leftEyeXCoor) ? eyes.leftEyeXCoor : -10;
189
+ const offsetY = Number.isFinite(eyes.leftEyeYCoor) ? eyes.leftEyeYCoor : -5;
190
+ const baseLeftEyeWidth = eyes.leftEyeImageWidth || 20;
191
+ const baseLeftEyeHeight = eyes.leftEyeImageHeight || 15;
192
+ const leftEyeWidth = baseLeftEyeWidth * (eyes.leftEyeWidthRatio || 1.0);
193
+ const leftEyeHeight = baseLeftEyeHeight * (eyes.leftEyeHeightRatio || 1.0);
194
+ const leftEyeTransform = this.applyTransform(headBaseTransform, {
195
+ x: offsetX,
196
+ y: offsetY,
197
+ rotation: 0,
198
+ scaleX: 1,
199
+ scaleY: 1
200
+ });
201
+ allObjects.push({
202
+ name: 'leftEye',
203
+ type: 'eye',
204
+ zIndex: zIndexValues['leftEye'] || 6,
205
+ width: leftEyeWidth,
206
+ height: leftEyeHeight,
207
+ x: leftEyeTransform.x,
208
+ y: leftEyeTransform.y,
209
+ rotation: leftEyeTransform.rotation,
210
+ scaleX: leftEyeTransform.scaleX,
211
+ scaleY: leftEyeTransform.scaleY,
212
+ anchorX: 0.5,
213
+ anchorY: 0.5,
214
+ imageKey: 'eyes.leftEyeImage',
215
+ imageData: loadedImages['eyes.leftEyeImage']
216
+ });
217
+ }
218
+ // Right Eye
219
+ if (visibility.rightEye !== false && loadedImages['eyes.rightEyeImage'] && eyes) {
220
+ const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
221
+ const headRot = rotations["head"] || 0;
222
+ const headBaseTransform = this.applyTransform(rootTransform, {
223
+ x: headPivot.x || 0,
224
+ y: headPivot.y || 0,
225
+ rotation: headRot,
226
+ scaleX: flipHead ? -1 : 1,
227
+ scaleY: 1
228
+ });
229
+ const offsetX = Number.isFinite(eyes.rightEyeXCoor) ? eyes.rightEyeXCoor : 10;
230
+ const offsetY = Number.isFinite(eyes.rightEyeYCoor) ? eyes.rightEyeYCoor : -5;
231
+ const baseRightEyeWidth = eyes.rightEyeImageWidth || 20;
232
+ const baseRightEyeHeight = eyes.rightEyeImageHeight || 15;
233
+ const rightEyeWidth = baseRightEyeWidth * (eyes.rightEyeWidthRatio || 1.0);
234
+ const rightEyeHeight = baseRightEyeHeight * (eyes.rightEyeHeightRatio || 1.0);
235
+ const rightEyeTransform = this.applyTransform(headBaseTransform, {
236
+ x: offsetX,
237
+ y: offsetY,
238
+ rotation: 0,
239
+ scaleX: 1,
240
+ scaleY: 1
241
+ });
242
+ allObjects.push({
243
+ name: 'rightEye',
244
+ type: 'eye',
245
+ zIndex: zIndexValues['rightEye'] || 3,
246
+ width: rightEyeWidth,
247
+ height: rightEyeHeight,
248
+ x: rightEyeTransform.x,
249
+ y: rightEyeTransform.y,
250
+ rotation: rightEyeTransform.rotation,
251
+ scaleX: rightEyeTransform.scaleX,
252
+ scaleY: rightEyeTransform.scaleY,
253
+ anchorX: 0.5,
254
+ anchorY: 0.5,
255
+ imageKey: 'eyes.rightEyeImage',
256
+ imageData: loadedImages['eyes.rightEyeImage']
257
+ });
258
+ }
259
+ // Left Iris
260
+ if (visibility.leftIris !== false && loadedImages['eyes.leftIris'] && eyes) {
261
+ const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
262
+ const headRot = rotations["head"] || 0;
263
+ const headBaseTransform = this.applyTransform(rootTransform, {
264
+ x: headPivot.x || 0,
265
+ y: headPivot.y || 0,
266
+ rotation: headRot,
267
+ scaleX: flipHead ? -1 : 1,
268
+ scaleY: 1
269
+ });
270
+ const offsetX = Number.isFinite(eyes.leftIrisXCoor) ? eyes.leftIrisXCoor : -10;
271
+ const offsetY = Number.isFinite(eyes.leftIrisYCoor) ? eyes.leftIrisYCoor : -5;
272
+ const baseLeftIrisWidth = eyes.leftIrisWidth || 10;
273
+ const baseLeftIrisHeight = eyes.leftIrisHeight || 10;
274
+ const leftIrisWidth = baseLeftIrisWidth * (eyes.leftIrisWidthRatio || 1.0);
275
+ const leftIrisHeight = baseLeftIrisHeight * (eyes.leftIrisHeightRatio || 1.0);
276
+ const leftIrisTransform = this.applyTransform(headBaseTransform, {
277
+ x: offsetX,
278
+ y: offsetY,
279
+ rotation: 0,
280
+ scaleX: 1,
281
+ scaleY: 1
282
+ });
283
+ allObjects.push({
284
+ name: 'leftIris',
285
+ type: 'iris',
286
+ zIndex: zIndexValues['leftIris'] || 7,
287
+ width: leftIrisWidth,
288
+ height: leftIrisHeight,
289
+ x: leftIrisTransform.x,
290
+ y: leftIrisTransform.y,
291
+ rotation: leftIrisTransform.rotation,
292
+ scaleX: leftIrisTransform.scaleX,
293
+ scaleY: leftIrisTransform.scaleY,
294
+ anchorX: 0.5,
295
+ anchorY: 0.5,
296
+ imageKey: 'eyes.leftIris',
297
+ imageData: loadedImages['eyes.leftIris']
298
+ });
299
+ }
300
+ // Right Iris
301
+ if (visibility.rightIris !== false && loadedImages['eyes.rightIris'] && eyes) {
302
+ const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
303
+ const headRot = rotations["head"] || 0;
304
+ const headBaseTransform = this.applyTransform(rootTransform, {
305
+ x: headPivot.x || 0,
306
+ y: headPivot.y || 0,
307
+ rotation: headRot,
308
+ scaleX: flipHead ? -1 : 1,
309
+ scaleY: 1
310
+ });
311
+ const offsetX = Number.isFinite(eyes.rightIrisXCoor) ? eyes.rightIrisXCoor : 10;
312
+ const offsetY = Number.isFinite(eyes.rightIrisYCoor) ? eyes.rightIrisYCoor : -5;
313
+ const baseRightIrisWidth = eyes.rightIrisWidth || 10;
314
+ const baseRightIrisHeight = eyes.rightIrisHeight || 10;
315
+ const rightIrisWidth = baseRightIrisWidth * (eyes.rightIrisWidthRatio || 1.0);
316
+ const rightIrisHeight = baseRightIrisHeight * (eyes.rightIrisHeightRatio || 1.0);
317
+ const rightIrisTransform = this.applyTransform(headBaseTransform, {
318
+ x: offsetX,
319
+ y: offsetY,
320
+ rotation: 0,
321
+ scaleX: 1,
322
+ scaleY: 1
323
+ });
324
+ allObjects.push({
325
+ name: 'rightIris',
326
+ type: 'iris',
327
+ zIndex: zIndexValues['rightIris'] || 4,
328
+ width: rightIrisWidth,
329
+ height: rightIrisHeight,
330
+ x: rightIrisTransform.x,
331
+ y: rightIrisTransform.y,
332
+ rotation: rightIrisTransform.rotation,
333
+ scaleX: rightIrisTransform.scaleX,
334
+ scaleY: rightIrisTransform.scaleY,
335
+ anchorX: 0.5,
336
+ anchorY: 0.5,
337
+ imageKey: 'eyes.rightIris',
338
+ imageData: loadedImages['eyes.rightIris']
339
+ });
340
+ }
341
+ // Left Eye Lid - select image based on current eyelid state
342
+ const eyelidState = eyes.eyelidState || 'open'; // Default to 'open'
343
+ // Select the correct image based on eyelid state
344
+ let leftEyeLidImage;
345
+ if (eyelidState === 'closed') {
346
+ leftEyeLidImage = loadedImages['eyes.leftEyeLidClosed'];
347
+ }
348
+ else if (eyelidState === 'half-closed') {
349
+ leftEyeLidImage = loadedImages['eyes.leftEyeLidHalfClosed'];
350
+ }
351
+ else {
352
+ leftEyeLidImage = loadedImages['eyes.leftEyeLidOpen'];
353
+ }
354
+ // Fallback to any available eyelid image if the specific one isn't loaded
355
+ if (!leftEyeLidImage) {
356
+ leftEyeLidImage = loadedImages['eyes.leftEyeLidOpen'] ||
357
+ loadedImages['eyes.leftEyeLidHalfClosed'] ||
358
+ loadedImages['eyes.leftEyeLidClosed'];
359
+ }
360
+ if (visibility.leftEyeLid !== false && leftEyeLidImage && eyes) {
361
+ const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
362
+ const headRot = rotations["head"] || 0;
363
+ const headBaseTransform = this.applyTransform(rootTransform, {
364
+ x: headPivot.x || 0,
365
+ y: headPivot.y || 0,
366
+ rotation: headRot,
367
+ scaleX: flipHead ? -1 : 1,
368
+ scaleY: 1
369
+ });
370
+ const offsetX = Number.isFinite(eyes.leftEyeLidXCoor) ? eyes.leftEyeLidXCoor : -10;
371
+ const offsetY = Number.isFinite(eyes.leftEyeLidYCoor) ? eyes.leftEyeLidYCoor : -5;
372
+ const baseLeftEyeLidWidth = eyes.leftEyeLidOpenWidth || eyes.leftEyeLidHalfClosedWidth || eyes.leftEyeLidClosedWidth || 20;
373
+ const baseLeftEyeLidHeight = eyes.leftEyeLidOpenHeight || eyes.leftEyeLidHalfClosedHeight || eyes.leftEyeLidClosedHeight || 15;
374
+ const leftEyeLidWidth = baseLeftEyeLidWidth * (eyes.leftEyeLidWidthRatio || 1.0);
375
+ const leftEyeLidHeight = baseLeftEyeLidHeight * (eyes.leftEyeLidHeightRatio || 1.0);
376
+ const leftEyeLidTransform = this.applyTransform(headBaseTransform, {
377
+ x: offsetX,
378
+ y: offsetY,
379
+ rotation: 0,
380
+ scaleX: 1,
381
+ scaleY: 1
382
+ });
383
+ allObjects.push({
384
+ name: 'leftEyeLid',
385
+ type: 'eyelid',
386
+ zIndex: zIndexValues['leftEyeLid'] || 8,
387
+ width: leftEyeLidWidth,
388
+ height: leftEyeLidHeight,
389
+ x: leftEyeLidTransform.x,
390
+ y: leftEyeLidTransform.y,
391
+ rotation: leftEyeLidTransform.rotation,
392
+ scaleX: leftEyeLidTransform.scaleX,
393
+ scaleY: leftEyeLidTransform.scaleY,
394
+ anchorX: 0.5,
395
+ anchorY: 0.5,
396
+ imageKey: 'eyes.leftEyeLid',
397
+ imageData: leftEyeLidImage
398
+ });
399
+ }
400
+ // Right Eye Lid - select image based on current eyelid state (same state for both eyes)
401
+ // Select the correct image based on eyelid state
402
+ let rightEyeLidImage;
403
+ if (eyelidState === 'closed') {
404
+ rightEyeLidImage = loadedImages['eyes.rightEyeLidClosed'];
405
+ }
406
+ else if (eyelidState === 'half-closed') {
407
+ rightEyeLidImage = loadedImages['eyes.rightEyeLidHalfClosed'];
408
+ }
409
+ else {
410
+ rightEyeLidImage = loadedImages['eyes.rightEyeLidOpen'];
411
+ }
412
+ // Fallback to any available eyelid image if the specific one isn't loaded
413
+ if (!rightEyeLidImage) {
414
+ rightEyeLidImage = loadedImages['eyes.rightEyeLidOpen'] ||
415
+ loadedImages['eyes.rightEyeLidHalfClosed'] ||
416
+ loadedImages['eyes.rightEyeLidClosed'];
417
+ }
418
+ if (visibility.rightEyeLid !== false && rightEyeLidImage && eyes) {
419
+ const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
420
+ const headRot = rotations["head"] || 0;
421
+ const headBaseTransform = this.applyTransform(rootTransform, {
422
+ x: headPivot.x || 0,
423
+ y: headPivot.y || 0,
424
+ rotation: headRot,
425
+ scaleX: flipHead ? -1 : 1,
426
+ scaleY: 1
427
+ });
428
+ const offsetX = Number.isFinite(eyes.rightEyeLidXCoor) ? eyes.rightEyeLidXCoor : 10;
429
+ const offsetY = Number.isFinite(eyes.rightEyeLidYCoor) ? eyes.rightEyeLidYCoor : -5;
430
+ const baseRightEyeLidWidth = eyes.rightEyeLidOpenWidth || 20;
431
+ const baseRightEyeLidHeight = eyes.rightEyeLidOpenHeight || 15;
432
+ const rightEyeLidWidth = baseRightEyeLidWidth * (eyes.rightEyeLidWidthRatio || 1.0);
433
+ const rightEyeLidHeight = baseRightEyeLidHeight * (eyes.rightEyeLidHeightRatio || 1.0);
434
+ const rightEyeLidTransform = this.applyTransform(headBaseTransform, {
435
+ x: offsetX,
436
+ y: offsetY,
437
+ rotation: 0,
438
+ scaleX: 1,
439
+ scaleY: 1
440
+ });
441
+ allObjects.push({
442
+ name: 'rightEyeLid',
443
+ type: 'eyelid',
444
+ zIndex: zIndexValues['rightEyeLid'] || 5,
445
+ width: rightEyeLidWidth,
446
+ height: rightEyeLidHeight,
447
+ x: rightEyeLidTransform.x,
448
+ y: rightEyeLidTransform.y,
449
+ rotation: rightEyeLidTransform.rotation,
450
+ scaleX: rightEyeLidTransform.scaleX,
451
+ scaleY: rightEyeLidTransform.scaleY,
452
+ anchorX: 0.5,
453
+ anchorY: 0.5,
454
+ imageKey: 'eyes.rightEyeLid',
455
+ imageData: rightEyeLidImage
456
+ });
457
+ }
458
+ // Left Arm chain (leftUpperArm, leftForearm, leftHand)
459
+ // Left Upper Arm
460
+ if (visibility.leftUpperArm !== false) {
461
+ const connectionKey = `torso_leftUpperArm`;
462
+ const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
463
+ const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
464
+ const width = dimensions.leftUpperArm?.width || 30;
465
+ const height = dimensions.leftUpperArm?.height || 50;
466
+ const limbRot = rotations['leftUpperArm'] || 0;
467
+ const selfRot = selfRotations['leftUpperArm'] || 0;
468
+ const upperArmParentTransform = this.applyTransform(rootTransform, {
469
+ x: pivotPoint.x || 0,
470
+ y: pivotPoint.y || 0,
471
+ rotation: limbRot,
472
+ scaleX: 1,
473
+ scaleY: 1
474
+ });
475
+ const upperArmTransform = this.applyTransform(upperArmParentTransform, {
476
+ x: jointOff.x || 0,
477
+ y: (jointOff.y || 0) - height / 2,
478
+ rotation: selfRot,
479
+ scaleX: 1,
480
+ scaleY: 1
481
+ });
482
+ allObjects.push({
483
+ name: 'leftUpperArm',
484
+ type: 'limb',
485
+ zIndex: zIndexValues['leftUpperArm'] || 1,
486
+ width: width * imageScale,
487
+ height: height * imageScale,
488
+ x: upperArmTransform.x,
489
+ y: upperArmTransform.y,
490
+ rotation: upperArmTransform.rotation,
491
+ scaleX: upperArmTransform.scaleX,
492
+ scaleY: upperArmTransform.scaleY,
493
+ anchorX: 0.5,
494
+ anchorY: 0.5,
495
+ imageKey: 'imagePaths.leftUpperArm',
496
+ imageData: loadedImages['imagePaths.leftUpperArm'],
497
+ selfRotation: selfRot,
498
+ parentTransform: upperArmParentTransform
499
+ });
500
+ }
501
+ // Left Forearm
502
+ if (visibility.leftForearm !== false) {
503
+ const upperConnectionKey = `torso_leftUpperArm`;
504
+ const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
505
+ const upperHeight = dimensions.leftUpperArm?.height || 50;
506
+ const upperRot = rotations["leftUpperArm"] || 0;
507
+ const connectionKey = `leftUpperArm_leftForearm`;
508
+ const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
509
+ const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
510
+ const width = dimensions.leftForearm?.width || 25;
511
+ const height = dimensions.leftForearm?.height || 45;
512
+ const limbRot = rotations["leftForearm"] || 0;
513
+ const selfRot = selfRotations["leftForearm"] || 0;
514
+ const upperArmBase = this.applyTransform(rootTransform, {
515
+ x: upperPivot.x || 0,
516
+ y: upperPivot.y || 0,
517
+ rotation: upperRot,
518
+ scaleX: 1,
519
+ scaleY: 1
520
+ });
521
+ const forearmParent = this.applyTransform(upperArmBase, {
522
+ x: pivotPoint.x || 0,
523
+ y: (pivotPoint.y || 0) - upperHeight,
524
+ rotation: limbRot,
525
+ scaleX: 1,
526
+ scaleY: 1
527
+ });
528
+ const forearmTransform = this.applyTransform(forearmParent, {
529
+ x: jointOff.x || 0,
530
+ y: (jointOff.y || 0) - height / 2,
531
+ rotation: selfRot,
532
+ scaleX: 1,
533
+ scaleY: 1
534
+ });
535
+ allObjects.push({
536
+ name: 'leftForearm',
537
+ type: 'limb',
538
+ zIndex: zIndexValues['leftForearm'] || 1,
539
+ width: width * imageScale,
540
+ height: height * imageScale,
541
+ x: forearmTransform.x,
542
+ y: forearmTransform.y,
543
+ rotation: forearmTransform.rotation,
544
+ scaleX: forearmTransform.scaleX,
545
+ scaleY: forearmTransform.scaleY,
546
+ anchorX: 0.5,
547
+ anchorY: 0.5,
548
+ imageKey: 'imagePaths.leftForearm',
549
+ imageData: loadedImages['imagePaths.leftForearm'],
550
+ selfRotation: selfRot,
551
+ parentTransform: forearmParent
552
+ });
553
+ }
554
+ // Left Hand
555
+ if (visibility.leftHand !== false) {
556
+ const upperConnectionKey = `torso_leftUpperArm`;
557
+ const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
558
+ const upperHeight = dimensions.leftUpperArm?.height || 50;
559
+ const upperRot = rotations["leftUpperArm"] || 0;
560
+ const foreConnectionKey = `leftUpperArm_leftForearm`;
561
+ const forePivot = pivotPoints[foreConnectionKey] || { x: 0, y: 0 };
562
+ const foreHeight = dimensions.leftForearm?.height || 45;
563
+ const foreRot = rotations["leftForearm"] || 0;
564
+ const handConnectionKey = `leftForearm_leftHand`;
565
+ const handPivot = pivotPoints[handConnectionKey] || { x: 0, y: 0 };
566
+ const handJointOff = jointOffset[handConnectionKey] || { x: 0, y: 0 };
567
+ const width = dimensions.leftHand?.width || 20;
568
+ const height = dimensions.leftHand?.height || 30;
569
+ const handRot = rotations["leftHand"] || 0;
570
+ const selfRot = selfRotations["leftHand"] || 0;
571
+ const upperArmBase = this.applyTransform(rootTransform, {
572
+ x: upperPivot.x || 0,
573
+ y: upperPivot.y || 0,
574
+ rotation: upperRot,
575
+ scaleX: 1,
576
+ scaleY: 1
577
+ });
578
+ const forearmBase = this.applyTransform(upperArmBase, {
579
+ x: forePivot.x || 0,
580
+ y: (forePivot.y || 0) - upperHeight,
581
+ rotation: foreRot,
582
+ scaleX: 1,
583
+ scaleY: 1
584
+ });
585
+ const handParent = this.applyTransform(forearmBase, {
586
+ x: handPivot.x || 0,
587
+ y: (handPivot.y || 0) - foreHeight,
588
+ rotation: handRot,
589
+ scaleX: 1,
590
+ scaleY: 1
591
+ });
592
+ const handTransform = this.applyTransform(handParent, {
593
+ x: handJointOff.x || 0,
594
+ y: (handJointOff.y || 0) - height / 2,
595
+ rotation: selfRot,
596
+ scaleX: 1,
597
+ scaleY: 1
598
+ });
599
+ allObjects.push({
600
+ name: 'leftHand',
601
+ type: 'limb',
602
+ zIndex: zIndexValues['leftHand'] || 1,
603
+ width: width * imageScale,
604
+ height: height * imageScale,
605
+ x: handTransform.x,
606
+ y: handTransform.y,
607
+ rotation: handTransform.rotation,
608
+ scaleX: handTransform.scaleX,
609
+ scaleY: handTransform.scaleY,
610
+ anchorX: 0.5,
611
+ anchorY: 0.5,
612
+ imageKey: 'imagePaths.leftHand',
613
+ imageData: loadedImages['imagePaths.leftHand'],
614
+ selfRotation: selfRot
615
+ });
616
+ }
617
+ // Right Arm chain (rightUpperArm, rightForearm, rightHand)
618
+ // Right Upper Arm
619
+ if (visibility.rightUpperArm !== false) {
620
+ const connectionKey = `torso_rightUpperArm`;
621
+ const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
622
+ const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
623
+ const width = dimensions.rightUpperArm?.width || 30;
624
+ const height = dimensions.rightUpperArm?.height || 50;
625
+ const limbRot = rotations['rightUpperArm'] || 0;
626
+ const selfRot = selfRotations['rightUpperArm'] || 0;
627
+ const upperArmParentTransform = this.applyTransform(rootTransform, {
628
+ x: pivotPoint.x || 0,
629
+ y: pivotPoint.y || 0,
630
+ rotation: limbRot,
631
+ scaleX: 1,
632
+ scaleY: 1
633
+ });
634
+ const upperArmTransform = this.applyTransform(upperArmParentTransform, {
635
+ x: jointOff.x || 0,
636
+ y: (jointOff.y || 0) - height / 2,
637
+ rotation: selfRot,
638
+ scaleX: 1,
639
+ scaleY: 1
640
+ });
641
+ allObjects.push({
642
+ name: 'rightUpperArm',
643
+ type: 'limb',
644
+ zIndex: zIndexValues['rightUpperArm'] || 1,
645
+ width: width * imageScale,
646
+ height: height * imageScale,
647
+ x: upperArmTransform.x,
648
+ y: upperArmTransform.y,
649
+ rotation: upperArmTransform.rotation,
650
+ scaleX: upperArmTransform.scaleX,
651
+ scaleY: upperArmTransform.scaleY,
652
+ anchorX: 0.5,
653
+ anchorY: 0.5,
654
+ imageKey: 'imagePaths.rightUpperArm',
655
+ imageData: loadedImages['imagePaths.rightUpperArm'],
656
+ selfRotation: selfRot,
657
+ parentTransform: upperArmParentTransform
658
+ });
659
+ }
660
+ // Right Forearm
661
+ if (visibility.rightForearm !== false) {
662
+ const upperConnectionKey = `torso_rightUpperArm`;
663
+ const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
664
+ const upperHeight = dimensions.rightUpperArm?.height || 50;
665
+ const upperRot = rotations["rightUpperArm"] || 0;
666
+ const connectionKey = `rightUpperArm_rightForearm`;
667
+ const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
668
+ const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
669
+ const width = dimensions.rightForearm?.width || 25;
670
+ const height = dimensions.rightForearm?.height || 45;
671
+ const limbRot = rotations["rightForearm"] || 0;
672
+ const selfRot = selfRotations["rightForearm"] || 0;
673
+ const upperArmBase = this.applyTransform(rootTransform, {
674
+ x: upperPivot.x || 0,
675
+ y: upperPivot.y || 0,
676
+ rotation: upperRot,
677
+ scaleX: 1,
678
+ scaleY: 1
679
+ });
680
+ const forearmParent = this.applyTransform(upperArmBase, {
681
+ x: pivotPoint.x || 0,
682
+ y: (pivotPoint.y || 0) - upperHeight,
683
+ rotation: limbRot,
684
+ scaleX: 1,
685
+ scaleY: 1
686
+ });
687
+ const forearmTransform = this.applyTransform(forearmParent, {
688
+ x: jointOff.x || 0,
689
+ y: (jointOff.y || 0) - height / 2,
690
+ rotation: selfRot,
691
+ scaleX: 1,
692
+ scaleY: 1
693
+ });
694
+ allObjects.push({
695
+ name: 'rightForearm',
696
+ type: 'limb',
697
+ zIndex: zIndexValues['rightForearm'] || 1,
698
+ width: width * imageScale,
699
+ height: height * imageScale,
700
+ x: forearmTransform.x,
701
+ y: forearmTransform.y,
702
+ rotation: forearmTransform.rotation,
703
+ scaleX: forearmTransform.scaleX,
704
+ scaleY: forearmTransform.scaleY,
705
+ anchorX: 0.5,
706
+ anchorY: 0.5,
707
+ imageKey: 'imagePaths.rightForearm',
708
+ imageData: loadedImages['imagePaths.rightForearm'],
709
+ selfRotation: selfRot,
710
+ parentTransform: forearmParent
711
+ });
712
+ }
713
+ // Right Hand
714
+ if (visibility.rightHand !== false) {
715
+ const upperConnectionKey = `torso_rightUpperArm`;
716
+ const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
717
+ const upperHeight = dimensions.rightUpperArm?.height || 50;
718
+ const upperRot = rotations["rightUpperArm"] || 0;
719
+ const foreConnectionKey = `rightUpperArm_rightForearm`;
720
+ const forePivot = pivotPoints[foreConnectionKey] || { x: 0, y: 0 };
721
+ const foreHeight = dimensions.rightForearm?.height || 45;
722
+ const foreRot = rotations["rightForearm"] || 0;
723
+ const handConnectionKey = `rightForearm_rightHand`;
724
+ const handPivot = pivotPoints[handConnectionKey] || { x: 0, y: 0 };
725
+ const handJointOff = jointOffset[handConnectionKey] || { x: 0, y: 0 };
726
+ const width = dimensions.rightHand?.width || 20;
727
+ const height = dimensions.rightHand?.height || 30;
728
+ const handRot = rotations["rightHand"] || 0;
729
+ const selfRot = selfRotations["rightHand"] || 0;
730
+ const upperArmBase = this.applyTransform(rootTransform, {
731
+ x: upperPivot.x || 0,
732
+ y: upperPivot.y || 0,
733
+ rotation: upperRot,
734
+ scaleX: 1,
735
+ scaleY: 1
736
+ });
737
+ const forearmBase = this.applyTransform(upperArmBase, {
738
+ x: forePivot.x || 0,
739
+ y: (forePivot.y || 0) - upperHeight,
740
+ rotation: foreRot,
741
+ scaleX: 1,
742
+ scaleY: 1
743
+ });
744
+ const handParent = this.applyTransform(forearmBase, {
745
+ x: handPivot.x || 0,
746
+ y: (handPivot.y || 0) - foreHeight,
747
+ rotation: handRot,
748
+ scaleX: 1,
749
+ scaleY: 1
750
+ });
751
+ const handTransform = this.applyTransform(handParent, {
752
+ x: handJointOff.x || 0,
753
+ y: (handJointOff.y || 0) - height / 2,
754
+ rotation: selfRot,
755
+ scaleX: 1,
756
+ scaleY: 1
757
+ });
758
+ allObjects.push({
759
+ name: 'rightHand',
760
+ type: 'limb',
761
+ zIndex: zIndexValues['rightHand'] || 1,
762
+ width: width * imageScale,
763
+ height: height * imageScale,
764
+ x: handTransform.x,
765
+ y: handTransform.y,
766
+ rotation: handTransform.rotation,
767
+ scaleX: handTransform.scaleX,
768
+ scaleY: handTransform.scaleY,
769
+ anchorX: 0.5,
770
+ anchorY: 0.5,
771
+ imageKey: 'imagePaths.rightHand',
772
+ imageData: loadedImages['imagePaths.rightHand'],
773
+ selfRotation: selfRot
774
+ });
775
+ }
776
+ // Left Leg chain (leftThigh, leftLeg)
777
+ // Left Thigh
778
+ if (visibility.leftThigh !== false) {
779
+ const connectionKey = `torso_leftThigh`;
780
+ const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
781
+ const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
782
+ const width = dimensions.leftThigh?.width || 35;
783
+ const height = dimensions.leftThigh?.height || 60;
784
+ const limbRot = rotations['leftThigh'] || 0;
785
+ const selfRot = selfRotations['leftThigh'] || 0;
786
+ const thighParentTransform = this.applyTransform(rootTransform, {
787
+ x: pivotPoint.x || 0,
788
+ y: pivotPoint.y || 0,
789
+ rotation: limbRot,
790
+ scaleX: 1,
791
+ scaleY: 1
792
+ });
793
+ const thighTransform = this.applyTransform(thighParentTransform, {
794
+ x: jointOff.x || 0,
795
+ y: (jointOff.y || 0) - height / 2,
796
+ rotation: selfRot,
797
+ scaleX: 1,
798
+ scaleY: 1
799
+ });
800
+ allObjects.push({
801
+ name: 'leftThigh',
802
+ type: 'limb',
803
+ zIndex: zIndexValues['leftThigh'] || 1,
804
+ width: width * imageScale,
805
+ height: height * imageScale,
806
+ x: thighTransform.x,
807
+ y: thighTransform.y,
808
+ rotation: thighTransform.rotation,
809
+ scaleX: thighTransform.scaleX,
810
+ scaleY: thighTransform.scaleY,
811
+ anchorX: 0.5,
812
+ anchorY: 0.5,
813
+ imageKey: 'imagePaths.leftThigh',
814
+ imageData: loadedImages['imagePaths.leftThigh'],
815
+ selfRotation: selfRot,
816
+ parentTransform: thighParentTransform
817
+ });
818
+ }
819
+ // Left Leg
820
+ if (visibility.leftLeg !== false) {
821
+ const upperConnectionKey = `torso_leftThigh`;
822
+ const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
823
+ const upperHeight = dimensions.leftThigh?.height || 60;
824
+ const upperRot = rotations["leftThigh"] || 0;
825
+ const connectionKey = `leftThigh_leftLeg`;
826
+ const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
827
+ const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
828
+ const width = dimensions.leftLeg?.width || 30;
829
+ const height = dimensions.leftLeg?.height || 55;
830
+ const limbRot = rotations["leftLeg"] || 0;
831
+ const selfRot = selfRotations["leftLeg"] || 0;
832
+ const thighBase = this.applyTransform(rootTransform, {
833
+ x: upperPivot.x || 0,
834
+ y: upperPivot.y || 0,
835
+ rotation: upperRot,
836
+ scaleX: 1,
837
+ scaleY: 1
838
+ });
839
+ const legParent = this.applyTransform(thighBase, {
840
+ x: pivotPoint.x || 0,
841
+ y: (pivotPoint.y || 0) - upperHeight,
842
+ rotation: limbRot,
843
+ scaleX: 1,
844
+ scaleY: 1
845
+ });
846
+ const legTransform = this.applyTransform(legParent, {
847
+ x: jointOff.x || 0,
848
+ y: (jointOff.y || 0) - height / 2,
849
+ rotation: selfRot,
850
+ scaleX: 1,
851
+ scaleY: 1
852
+ });
853
+ allObjects.push({
854
+ name: 'leftLeg',
855
+ type: 'limb',
856
+ zIndex: zIndexValues['leftLeg'] || 1,
857
+ width: width * imageScale,
858
+ height: height * imageScale,
859
+ x: legTransform.x,
860
+ y: legTransform.y,
861
+ rotation: legTransform.rotation,
862
+ scaleX: legTransform.scaleX,
863
+ scaleY: legTransform.scaleY,
864
+ anchorX: 0.5,
865
+ anchorY: 0.5,
866
+ imageKey: 'imagePaths.leftLeg',
867
+ imageData: loadedImages['imagePaths.leftLeg'],
868
+ selfRotation: selfRot
869
+ });
870
+ }
871
+ // Right Leg chain (rightThigh, rightLeg)
872
+ // Right Thigh
873
+ if (visibility.rightThigh !== false) {
874
+ const connectionKey = `torso_rightThigh`;
875
+ const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
876
+ const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
877
+ const width = dimensions.rightThigh?.width || 35;
878
+ const height = dimensions.rightThigh?.height || 60;
879
+ const limbRot = rotations['rightThigh'] || 0;
880
+ const selfRot = selfRotations['rightThigh'] || 0;
881
+ const thighParentTransform = this.applyTransform(rootTransform, {
882
+ x: pivotPoint.x || 0,
883
+ y: pivotPoint.y || 0,
884
+ rotation: limbRot,
885
+ scaleX: 1,
886
+ scaleY: 1
887
+ });
888
+ const thighTransform = this.applyTransform(thighParentTransform, {
889
+ x: jointOff.x || 0,
890
+ y: (jointOff.y || 0) - height / 2,
891
+ rotation: selfRot,
892
+ scaleX: 1,
893
+ scaleY: 1
894
+ });
895
+ allObjects.push({
896
+ name: 'rightThigh',
897
+ type: 'limb',
898
+ zIndex: zIndexValues['rightThigh'] || 1,
899
+ width: width * imageScale,
900
+ height: height * imageScale,
901
+ x: thighTransform.x,
902
+ y: thighTransform.y,
903
+ rotation: thighTransform.rotation,
904
+ scaleX: thighTransform.scaleX,
905
+ scaleY: thighTransform.scaleY,
906
+ anchorX: 0.5,
907
+ anchorY: 0.5,
908
+ imageKey: 'imagePaths.rightThigh',
909
+ imageData: loadedImages['imagePaths.rightThigh'],
910
+ selfRotation: selfRot,
911
+ parentTransform: thighParentTransform
912
+ });
913
+ }
914
+ // Right Leg
915
+ if (visibility.rightLeg !== false) {
916
+ const upperConnectionKey = `torso_rightThigh`;
917
+ const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
918
+ const upperHeight = dimensions.rightThigh?.height || 60;
919
+ const upperRot = rotations["rightThigh"] || 0;
920
+ const connectionKey = `rightThigh_rightLeg`;
921
+ const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
922
+ const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
923
+ const width = dimensions.rightLeg?.width || 30;
924
+ const height = dimensions.rightLeg?.height || 55;
925
+ const limbRot = rotations["rightLeg"] || 0;
926
+ const selfRot = selfRotations["rightLeg"] || 0;
927
+ const thighBase = this.applyTransform(rootTransform, {
928
+ x: upperPivot.x || 0,
929
+ y: upperPivot.y || 0,
930
+ rotation: upperRot,
931
+ scaleX: 1,
932
+ scaleY: 1
933
+ });
934
+ const legParent = this.applyTransform(thighBase, {
935
+ x: pivotPoint.x || 0,
936
+ y: (pivotPoint.y || 0) - upperHeight,
937
+ rotation: limbRot,
938
+ scaleX: 1,
939
+ scaleY: 1
940
+ });
941
+ const legTransform = this.applyTransform(legParent, {
942
+ x: jointOff.x || 0,
943
+ y: (jointOff.y || 0) - height / 2,
944
+ rotation: selfRot,
945
+ scaleX: 1,
946
+ scaleY: 1
947
+ });
948
+ allObjects.push({
949
+ name: 'rightLeg',
950
+ type: 'limb',
951
+ zIndex: zIndexValues['rightLeg'] || 1,
952
+ width: width * imageScale,
953
+ height: height * imageScale,
954
+ x: legTransform.x,
955
+ y: legTransform.y,
956
+ rotation: legTransform.rotation,
957
+ scaleX: legTransform.scaleX,
958
+ scaleY: legTransform.scaleY,
959
+ anchorX: 0.5,
960
+ anchorY: 0.5,
961
+ imageKey: 'imagePaths.rightLeg',
962
+ imageData: loadedImages['imagePaths.rightLeg'],
963
+ selfRotation: selfRot
964
+ });
965
+ }
966
+ // Sort all objects by zIndex
967
+ allObjects.sort((a, b) => a.zIndex - b.zIndex);
968
+ // Compute pivot points in world space
969
+ const computedPivotPoints = [];
970
+ // Head/torso pivot
971
+ const torsoHead = pivotPoints['torso_head'] || { x: 0, y: 0 };
972
+ const headPivotWorld = this.applyTransform(rootTransform, {
973
+ x: torsoHead.x || 0,
974
+ y: torsoHead.y || 0,
975
+ rotation: 0,
976
+ scaleX: 1,
977
+ scaleY: 1
978
+ });
979
+ computedPivotPoints.push({
980
+ name: 'torso_head',
981
+ x: headPivotWorld.x,
982
+ y: headPivotWorld.y
983
+ });
984
+ // Mouth pivot
985
+ const headAngle = rotations['head'] || 0;
986
+ const headAngleEff = flipHead ? -headAngle : headAngle;
987
+ const headOffX = 0; // head offset defaults to 0
988
+ const headOffY = 0; // head offset defaults to 0
989
+ const headOffEffX = flipHead ? -headOffX : headOffX;
990
+ const cosH = Math.cos(headAngleEff);
991
+ const sinH = Math.sin(headAngleEff);
992
+ const mouthOffset = pivotPoints['head_mouth'] || { x: 0, y: 0 };
993
+ const mouthOffX = flipHead ? -(mouthOffset.x || 0) : (mouthOffset.x || 0);
994
+ const mouthOffY = mouthOffset.y || 0;
995
+ computedPivotPoints.push({
996
+ name: 'head_mouth',
997
+ x: centerX + (torsoHead.x || 0) + headOffEffX + (mouthOffX * cosH - mouthOffY * sinH),
998
+ y: centerY + (torsoHead.y || 0) + headOffY + (mouthOffX * sinH + mouthOffY * cosH)
999
+ });
1000
+ // Left arm chain pivots
1001
+ const lShoulder = pivotPoints['torso_leftUpperArm'] || { x: 0, y: 0 };
1002
+ const lShoulderWorld = this.applyTransform(rootTransform, {
1003
+ x: lShoulder.x || 0,
1004
+ y: lShoulder.y || 0,
1005
+ rotation: 0,
1006
+ scaleX: 1,
1007
+ scaleY: 1
1008
+ });
1009
+ computedPivotPoints.push({ name: 'torso_leftUpperArm', x: lShoulderWorld.x, y: lShoulderWorld.y });
1010
+ const rL1 = rotations['leftUpperArm'] || 0;
1011
+ const leftUpperArmHeight = dimensions.leftUpperArm?.height || 50;
1012
+ const lElbowOff = pivotPoints['leftUpperArm_leftForearm'] || { x: 0, y: 0 };
1013
+ const lElbowBase = this.applyTransform(rootTransform, {
1014
+ x: lShoulder.x || 0,
1015
+ y: lShoulder.y || 0,
1016
+ rotation: rL1,
1017
+ scaleX: 1,
1018
+ scaleY: 1
1019
+ });
1020
+ const lElbowWorld = this.applyTransform(lElbowBase, {
1021
+ x: lElbowOff.x || 0,
1022
+ y: (lElbowOff.y || 0) - leftUpperArmHeight,
1023
+ rotation: 0,
1024
+ scaleX: 1,
1025
+ scaleY: 1
1026
+ });
1027
+ computedPivotPoints.push({ name: 'leftUpperArm_leftForearm', x: lElbowWorld.x, y: lElbowWorld.y });
1028
+ const leftForearmHeight = dimensions.leftForearm?.height || 45;
1029
+ const lWristOff = pivotPoints['leftForearm_leftHand'] || { x: 0, y: 0 };
1030
+ const lForearmBase = this.applyTransform(lElbowBase, {
1031
+ x: lElbowOff.x || 0,
1032
+ y: (lElbowOff.y || 0) - leftUpperArmHeight,
1033
+ rotation: rotations['leftForearm'] || 0,
1034
+ scaleX: 1,
1035
+ scaleY: 1
1036
+ });
1037
+ const lWristWorld = this.applyTransform(lForearmBase, {
1038
+ x: lWristOff.x || 0,
1039
+ y: (lWristOff.y || 0) - leftForearmHeight,
1040
+ rotation: 0,
1041
+ scaleX: 1,
1042
+ scaleY: 1
1043
+ });
1044
+ computedPivotPoints.push({ name: 'leftForearm_leftHand', x: lWristWorld.x, y: lWristWorld.y });
1045
+ // Right arm chain pivots
1046
+ const rShoulder = pivotPoints['torso_rightUpperArm'] || { x: 0, y: 0 };
1047
+ const rShoulderWorld = this.applyTransform(rootTransform, {
1048
+ x: rShoulder.x || 0,
1049
+ y: rShoulder.y || 0,
1050
+ rotation: 0,
1051
+ scaleX: 1,
1052
+ scaleY: 1
1053
+ });
1054
+ computedPivotPoints.push({ name: 'torso_rightUpperArm', x: rShoulderWorld.x, y: rShoulderWorld.y });
1055
+ const rR1 = rotations['rightUpperArm'] || 0;
1056
+ const rightUpperArmHeight = dimensions.rightUpperArm?.height || 50;
1057
+ const rElbowOff = pivotPoints['rightUpperArm_rightForearm'] || { x: 0, y: 0 };
1058
+ const rElbowBase = this.applyTransform(rootTransform, {
1059
+ x: rShoulder.x || 0,
1060
+ y: rShoulder.y || 0,
1061
+ rotation: rR1,
1062
+ scaleX: 1,
1063
+ scaleY: 1
1064
+ });
1065
+ const rElbowWorld = this.applyTransform(rElbowBase, {
1066
+ x: rElbowOff.x || 0,
1067
+ y: (rElbowOff.y || 0) - rightUpperArmHeight,
1068
+ rotation: 0,
1069
+ scaleX: 1,
1070
+ scaleY: 1
1071
+ });
1072
+ computedPivotPoints.push({ name: 'rightUpperArm_rightForearm', x: rElbowWorld.x, y: rElbowWorld.y });
1073
+ const rightForearmHeight = dimensions.rightForearm?.height || 45;
1074
+ const rWristOff = pivotPoints['rightForearm_rightHand'] || { x: 0, y: 0 };
1075
+ const rForearmBase = this.applyTransform(rElbowBase, {
1076
+ x: rElbowOff.x || 0,
1077
+ y: (rElbowOff.y || 0) - rightUpperArmHeight,
1078
+ rotation: rotations['rightForearm'] || 0,
1079
+ scaleX: 1,
1080
+ scaleY: 1
1081
+ });
1082
+ const rWristWorld = this.applyTransform(rForearmBase, {
1083
+ x: rWristOff.x || 0,
1084
+ y: (rWristOff.y || 0) - rightForearmHeight,
1085
+ rotation: 0,
1086
+ scaleX: 1,
1087
+ scaleY: 1
1088
+ });
1089
+ computedPivotPoints.push({ name: 'rightForearm_rightHand', x: rWristWorld.x, y: rWristWorld.y });
1090
+ // Left leg chain pivots
1091
+ const lHip = pivotPoints['torso_leftThigh'] || { x: 0, y: 0 };
1092
+ const lHipWorld = this.applyTransform(rootTransform, {
1093
+ x: lHip.x || 0,
1094
+ y: lHip.y || 0,
1095
+ rotation: 0,
1096
+ scaleX: 1,
1097
+ scaleY: 1
1098
+ });
1099
+ computedPivotPoints.push({ name: 'torso_leftThigh', x: lHipWorld.x, y: lHipWorld.y });
1100
+ const rLT1 = rotations['leftThigh'] || 0;
1101
+ const leftThighHeight = dimensions.leftThigh?.height || 60;
1102
+ const lKneeOff = pivotPoints['leftThigh_leftLeg'] || { x: 0, y: 0 };
1103
+ const lThighBase = this.applyTransform(rootTransform, {
1104
+ x: lHip.x || 0,
1105
+ y: lHip.y || 0,
1106
+ rotation: rLT1,
1107
+ scaleX: 1,
1108
+ scaleY: 1
1109
+ });
1110
+ const lKneeWorld = this.applyTransform(lThighBase, {
1111
+ x: lKneeOff.x || 0,
1112
+ y: (lKneeOff.y || 0) - leftThighHeight,
1113
+ rotation: 0,
1114
+ scaleX: 1,
1115
+ scaleY: 1
1116
+ });
1117
+ computedPivotPoints.push({ name: 'leftThigh_leftLeg', x: lKneeWorld.x, y: lKneeWorld.y });
1118
+ // Right leg chain pivots
1119
+ const rHip = pivotPoints['torso_rightThigh'] || { x: 0, y: 0 };
1120
+ const rHipWorld = this.applyTransform(rootTransform, {
1121
+ x: rHip.x || 0,
1122
+ y: rHip.y || 0,
1123
+ rotation: 0,
1124
+ scaleX: 1,
1125
+ scaleY: 1
1126
+ });
1127
+ computedPivotPoints.push({ name: 'torso_rightThigh', x: rHipWorld.x, y: rHipWorld.y });
1128
+ const rRT1 = rotations['rightThigh'] || 0;
1129
+ const rightThighHeight = dimensions.rightThigh?.height || 60;
1130
+ const rKneeOff = pivotPoints['rightThigh_rightLeg'] || { x: 0, y: 0 };
1131
+ const rThighBase = this.applyTransform(rootTransform, {
1132
+ x: rHip.x || 0,
1133
+ y: rHip.y || 0,
1134
+ rotation: rRT1,
1135
+ scaleX: 1,
1136
+ scaleY: 1
1137
+ });
1138
+ const rKneeWorld = this.applyTransform(rThighBase, {
1139
+ x: rKneeOff.x || 0,
1140
+ y: (rKneeOff.y || 0) - rightThighHeight,
1141
+ rotation: 0,
1142
+ scaleX: 1,
1143
+ scaleY: 1
1144
+ });
1145
+ computedPivotPoints.push({ name: 'rightThigh_rightLeg', x: rKneeWorld.x, y: rKneeWorld.y });
1146
+ // Return the complete data structure
1147
+ return {
1148
+ objects: allObjects,
1149
+ pivotPoints: computedPivotPoints
1150
+ };
1151
+ }
1152
+ /**
1153
+ * Universal Canvas renderer - handles both sync (with cache) and async (loads images)
1154
+ *
1155
+ * @param canvas - Canvas element to render to
1156
+ * @param rigData - Character rig data
1157
+ * @param loadedImages - Optional pre-loaded images. If not provided, will check cache or load from network
1158
+ * @param cameraOffset - Camera offset for positioning
1159
+ * @param showPivotPoints - Whether to show pivot points for debugging
1160
+ * @returns Promise that resolves when rendering is complete
1161
+ *
1162
+ * @example
1163
+ * // With pre-loaded images (fast, synchronous path)
1164
+ * await renderer.render(canvas, rigData, myImages);
1165
+ *
1166
+ * @example
1167
+ * // Without images - auto-loads if needed (async path)
1168
+ * await renderer.render(canvas, rigData);
1169
+ */
1170
+ async render(canvas, rigData, loadedImages, cameraOffset = { x: 0, y: 0 }, showPivotPoints = true) {
1171
+ // If no images provided, check cache or load
1172
+ if (!loadedImages) {
1173
+ const cache = this.imageLoader.getImageCache();
1174
+ // If cache is empty, load images from network
1175
+ if (cache.size === 0) {
1176
+ console.log('📦 Cache empty - loading images from network...');
1177
+ loadedImages = await this.imageLoader.loadAllRigImages(rigData, true);
1178
+ }
1179
+ else {
1180
+ // Use existing cache
1181
+ loadedImages = {};
1182
+ cache.forEach((img, key) => {
1183
+ loadedImages[key] = img;
1184
+ });
1185
+ console.log('📦 Using cached images for rendering');
1186
+ }
1187
+ }
1188
+ const ctx = canvas.getContext('2d');
1189
+ if (!ctx) {
1190
+ throw new Error('Failed to get 2D context from canvas');
1191
+ }
1192
+ // Clear canvas
1193
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1194
+ ctx.fillStyle = '#f0f0f0';
1195
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1196
+ // Compute the rig data
1197
+ const rigRenderData = this.computeCharacterRigData(rigData, {
1198
+ canvasWidth: canvas.width,
1199
+ canvasHeight: canvas.height,
1200
+ cameraOffset,
1201
+ loadedImages
1202
+ });
1203
+ // Render all objects
1204
+ rigRenderData.objects.forEach(obj => {
1205
+ // Skip rendering if no image data
1206
+ if (!obj.imageData) {
1207
+ return;
1208
+ }
1209
+ ctx.save();
1210
+ // Move to object position
1211
+ ctx.translate(obj.x, obj.y);
1212
+ // Apply rotation
1213
+ ctx.rotate(obj.rotation);
1214
+ // Apply scale
1215
+ ctx.scale(obj.scaleX, obj.scaleY);
1216
+ // Draw image from anchor point (center by default)
1217
+ const drawX = -obj.width * obj.anchorX;
1218
+ const drawY = -obj.height * obj.anchorY;
1219
+ ctx.drawImage(obj.imageData, drawX, drawY, obj.width, obj.height);
1220
+ ctx.restore();
1221
+ });
1222
+ // Draw pivot points if enabled
1223
+ if (showPivotPoints) {
1224
+ rigRenderData.pivotPoints.forEach(pivot => {
1225
+ ctx.save();
1226
+ ctx.fillStyle = 'rgba(0, 150, 255, 0.9)';
1227
+ ctx.strokeStyle = '#000';
1228
+ ctx.lineWidth = 1;
1229
+ ctx.beginPath();
1230
+ ctx.arc(pivot.x, pivot.y, 4, 0, Math.PI * 2);
1231
+ ctx.fill();
1232
+ ctx.stroke();
1233
+ ctx.restore();
1234
+ });
1235
+ }
1236
+ }
1237
+ }
1238
+ // Export a default renderer instance for convenience
1239
+ export const defaultRenderer = new CharacterRigRenderer();
1240
+ // For backwards compatibility - export standalone function
1241
+ export async function renderCharacterRig(canvas, rigData, loadedImages, cameraOffset = { x: 0, y: 0 }, showPivotPoints = true) {
1242
+ return defaultRenderer.render(canvas, rigData, loadedImages, cameraOffset, showPivotPoints);
1243
+ }
1244
+ // Export standalone function for computing rig data
1245
+ export function computeCharacterRigData(rigData, options = {}) {
1246
+ return defaultRenderer.computeCharacterRigData(rigData, options);
1247
+ }
1248
+ //# sourceMappingURL=renderRig.js.map