skinview3d-node 3.4.2-node.1

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,2922 @@
1
+ 'use strict';
2
+
3
+ var skiaCanvas = require('skia-canvas');
4
+ var path = require('path');
5
+ var url = require('url');
6
+ var three = require('three');
7
+ var pngjs = require('pngjs');
8
+ var gl = require('gl');
9
+
10
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
+ function setUVs(box, u, v, width, height, depth, textureWidth, textureHeight) {
12
+ const toFaceVertices = (x1, y1, x2, y2) => [
13
+ new three.Vector2(x1 / textureWidth, 1.0 - y2 / textureHeight),
14
+ new three.Vector2(x2 / textureWidth, 1.0 - y2 / textureHeight),
15
+ new three.Vector2(x2 / textureWidth, 1.0 - y1 / textureHeight),
16
+ new three.Vector2(x1 / textureWidth, 1.0 - y1 / textureHeight)
17
+ ];
18
+ const top = toFaceVertices(u + depth, v, u + width + depth, v + depth);
19
+ const bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth);
20
+ const left = toFaceVertices(u, v + depth, u + depth, v + depth + height);
21
+ const front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height);
22
+ const right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth);
23
+ const back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth);
24
+ const uvAttr = box.attributes.uv;
25
+ const uvRight = [right[3], right[2], right[0], right[1]];
26
+ const uvLeft = [left[3], left[2], left[0], left[1]];
27
+ const uvTop = [top[3], top[2], top[0], top[1]];
28
+ const uvBottom = [bottom[0], bottom[1], bottom[3], bottom[2]];
29
+ const uvFront = [front[3], front[2], front[0], front[1]];
30
+ const uvBack = [back[3], back[2], back[0], back[1]];
31
+ // Create a new array to hold the modified UV data
32
+ const newUVData = [];
33
+ // Iterate over the arrays and copy the data to uvData
34
+ for (const uvArray of [uvRight, uvLeft, uvTop, uvBottom, uvFront, uvBack]) {
35
+ for (const uv of uvArray) {
36
+ newUVData.push(uv.x, uv.y);
37
+ }
38
+ }
39
+ uvAttr.set(new Float32Array(newUVData));
40
+ uvAttr.needsUpdate = true;
41
+ }
42
+ function setSkinUVs(box, u, v, width, height, depth) {
43
+ setUVs(box, u, v, width, height, depth, 64, 64);
44
+ }
45
+ function setCapeUVs(box, u, v, width, height, depth) {
46
+ setUVs(box, u, v, width, height, depth, 64, 32);
47
+ }
48
+ /**
49
+ * Notice that innerLayer and outerLayer may NOT be the direct children of the Group.
50
+ */
51
+ class BodyPart extends three.Group {
52
+ innerLayer;
53
+ outerLayer;
54
+ constructor(innerLayer, outerLayer) {
55
+ super();
56
+ this.innerLayer = innerLayer;
57
+ this.outerLayer = outerLayer;
58
+ innerLayer.name = 'inner';
59
+ outerLayer.name = 'outer';
60
+ }
61
+ }
62
+ class SkinObject extends three.Group {
63
+ // body parts
64
+ head;
65
+ body;
66
+ rightArm;
67
+ leftArm;
68
+ rightLeg;
69
+ leftLeg;
70
+ modelListeners = []; // called when model(slim property) is changed
71
+ slim = false;
72
+ _map = null;
73
+ layer1Material;
74
+ layer1MaterialBiased;
75
+ layer2Material;
76
+ layer2MaterialBiased;
77
+ constructor() {
78
+ super();
79
+ this.layer1Material = new three.MeshStandardMaterial({
80
+ side: three.FrontSide
81
+ });
82
+ this.layer2Material = new three.MeshStandardMaterial({
83
+ side: three.DoubleSide,
84
+ transparent: true,
85
+ alphaTest: 1e-5
86
+ });
87
+ this.layer1MaterialBiased = this.layer1Material.clone();
88
+ this.layer1MaterialBiased.polygonOffset = true;
89
+ this.layer1MaterialBiased.polygonOffsetFactor = 1.0;
90
+ this.layer1MaterialBiased.polygonOffsetUnits = 1.0;
91
+ this.layer2MaterialBiased = this.layer2Material.clone();
92
+ this.layer2MaterialBiased.polygonOffset = true;
93
+ this.layer2MaterialBiased.polygonOffsetFactor = 1.0;
94
+ this.layer2MaterialBiased.polygonOffsetUnits = 1.0;
95
+ // Head
96
+ const headBox = new three.BoxGeometry(8, 8, 8);
97
+ setSkinUVs(headBox, 0, 0, 8, 8, 8);
98
+ const headMesh = new three.Mesh(headBox, this.layer1Material);
99
+ const head2Box = new three.BoxGeometry(9, 9, 9);
100
+ setSkinUVs(head2Box, 32, 0, 8, 8, 8);
101
+ const head2Mesh = new three.Mesh(head2Box, this.layer2Material);
102
+ this.head = new BodyPart(headMesh, head2Mesh);
103
+ this.head.name = 'head';
104
+ this.head.add(headMesh, head2Mesh);
105
+ headMesh.position.y = 4;
106
+ head2Mesh.position.y = 4;
107
+ this.add(this.head);
108
+ // Body
109
+ const bodyBox = new three.BoxGeometry(8, 12, 4);
110
+ setSkinUVs(bodyBox, 16, 16, 8, 12, 4);
111
+ const bodyMesh = new three.Mesh(bodyBox, this.layer1Material);
112
+ const body2Box = new three.BoxGeometry(8.5, 12.5, 4.5);
113
+ setSkinUVs(body2Box, 16, 32, 8, 12, 4);
114
+ const body2Mesh = new three.Mesh(body2Box, this.layer2Material);
115
+ this.body = new BodyPart(bodyMesh, body2Mesh);
116
+ this.body.name = 'body';
117
+ this.body.add(bodyMesh, body2Mesh);
118
+ this.body.position.y = -6;
119
+ this.add(this.body);
120
+ // Right Arm
121
+ const rightArmBox = new three.BoxGeometry();
122
+ const rightArmMesh = new three.Mesh(rightArmBox, this.layer1MaterialBiased);
123
+ this.modelListeners.push(() => {
124
+ rightArmMesh.scale.x = this.slim ? 3 : 4;
125
+ rightArmMesh.scale.y = 12;
126
+ rightArmMesh.scale.z = 4;
127
+ setSkinUVs(rightArmBox, 40, 16, this.slim ? 3 : 4, 12, 4);
128
+ });
129
+ const rightArm2Box = new three.BoxGeometry();
130
+ const rightArm2Mesh = new three.Mesh(rightArm2Box, this.layer2MaterialBiased);
131
+ this.modelListeners.push(() => {
132
+ rightArm2Mesh.scale.x = this.slim ? 3.5 : 4.5;
133
+ rightArm2Mesh.scale.y = 12.5;
134
+ rightArm2Mesh.scale.z = 4.5;
135
+ setSkinUVs(rightArm2Box, 40, 32, this.slim ? 3 : 4, 12, 4);
136
+ });
137
+ const rightArmPivot = new three.Group();
138
+ rightArmPivot.add(rightArmMesh, rightArm2Mesh);
139
+ this.modelListeners.push(() => {
140
+ rightArmPivot.position.x = this.slim ? -0.5 : -1;
141
+ });
142
+ rightArmPivot.position.y = -4;
143
+ this.rightArm = new BodyPart(rightArmMesh, rightArm2Mesh);
144
+ this.rightArm.name = 'rightArm';
145
+ this.rightArm.add(rightArmPivot);
146
+ this.rightArm.position.x = -5;
147
+ this.rightArm.position.y = -2;
148
+ this.add(this.rightArm);
149
+ // Left Arm
150
+ const leftArmBox = new three.BoxGeometry();
151
+ const leftArmMesh = new three.Mesh(leftArmBox, this.layer1MaterialBiased);
152
+ this.modelListeners.push(() => {
153
+ leftArmMesh.scale.x = this.slim ? 3 : 4;
154
+ leftArmMesh.scale.y = 12;
155
+ leftArmMesh.scale.z = 4;
156
+ setSkinUVs(leftArmBox, 32, 48, this.slim ? 3 : 4, 12, 4);
157
+ });
158
+ const leftArm2Box = new three.BoxGeometry();
159
+ const leftArm2Mesh = new three.Mesh(leftArm2Box, this.layer2MaterialBiased);
160
+ this.modelListeners.push(() => {
161
+ leftArm2Mesh.scale.x = this.slim ? 3.5 : 4.5;
162
+ leftArm2Mesh.scale.y = 12.5;
163
+ leftArm2Mesh.scale.z = 4.5;
164
+ setSkinUVs(leftArm2Box, 48, 48, this.slim ? 3 : 4, 12, 4);
165
+ });
166
+ const leftArmPivot = new three.Group();
167
+ leftArmPivot.add(leftArmMesh, leftArm2Mesh);
168
+ this.modelListeners.push(() => {
169
+ leftArmPivot.position.x = this.slim ? 0.5 : 1;
170
+ });
171
+ leftArmPivot.position.y = -4;
172
+ this.leftArm = new BodyPart(leftArmMesh, leftArm2Mesh);
173
+ this.leftArm.name = 'leftArm';
174
+ this.leftArm.add(leftArmPivot);
175
+ this.leftArm.position.x = 5;
176
+ this.leftArm.position.y = -2;
177
+ this.add(this.leftArm);
178
+ // Right Leg
179
+ const rightLegBox = new three.BoxGeometry(4, 12, 4);
180
+ setSkinUVs(rightLegBox, 0, 16, 4, 12, 4);
181
+ const rightLegMesh = new three.Mesh(rightLegBox, this.layer1MaterialBiased);
182
+ const rightLeg2Box = new three.BoxGeometry(4.5, 12.5, 4.5);
183
+ setSkinUVs(rightLeg2Box, 0, 32, 4, 12, 4);
184
+ const rightLeg2Mesh = new three.Mesh(rightLeg2Box, this.layer2MaterialBiased);
185
+ const rightLegPivot = new three.Group();
186
+ rightLegPivot.add(rightLegMesh, rightLeg2Mesh);
187
+ rightLegPivot.position.y = -6;
188
+ this.rightLeg = new BodyPart(rightLegMesh, rightLeg2Mesh);
189
+ this.rightLeg.name = 'rightLeg';
190
+ this.rightLeg.add(rightLegPivot);
191
+ this.rightLeg.position.x = -1.9;
192
+ this.rightLeg.position.y = -12;
193
+ this.rightLeg.position.z = -0.1;
194
+ this.add(this.rightLeg);
195
+ // Left Leg
196
+ const leftLegBox = new three.BoxGeometry(4, 12, 4);
197
+ setSkinUVs(leftLegBox, 16, 48, 4, 12, 4);
198
+ const leftLegMesh = new three.Mesh(leftLegBox, this.layer1MaterialBiased);
199
+ const leftLeg2Box = new three.BoxGeometry(4.5, 12.5, 4.5);
200
+ setSkinUVs(leftLeg2Box, 0, 48, 4, 12, 4);
201
+ const leftLeg2Mesh = new three.Mesh(leftLeg2Box, this.layer2MaterialBiased);
202
+ const leftLegPivot = new three.Group();
203
+ leftLegPivot.add(leftLegMesh, leftLeg2Mesh);
204
+ leftLegPivot.position.y = -6;
205
+ this.leftLeg = new BodyPart(leftLegMesh, leftLeg2Mesh);
206
+ this.leftLeg.name = 'leftLeg';
207
+ this.leftLeg.add(leftLegPivot);
208
+ this.leftLeg.position.x = 1.9;
209
+ this.leftLeg.position.y = -12;
210
+ this.leftLeg.position.z = -0.1;
211
+ this.add(this.leftLeg);
212
+ this.modelType = 'default';
213
+ }
214
+ get map() {
215
+ return this._map;
216
+ }
217
+ set map(newMap) {
218
+ this._map = newMap;
219
+ this.layer1Material.map = newMap;
220
+ this.layer1Material.needsUpdate = true;
221
+ this.layer1MaterialBiased.map = newMap;
222
+ this.layer1MaterialBiased.needsUpdate = true;
223
+ this.layer2Material.map = newMap;
224
+ this.layer2Material.needsUpdate = true;
225
+ this.layer2MaterialBiased.map = newMap;
226
+ this.layer2MaterialBiased.needsUpdate = true;
227
+ }
228
+ get modelType() {
229
+ return this.slim ? 'slim' : 'default';
230
+ }
231
+ set modelType(value) {
232
+ this.slim = value === 'slim';
233
+ this.modelListeners.forEach(listener => listener());
234
+ }
235
+ getBodyParts() {
236
+ return this.children.filter(it => it instanceof BodyPart);
237
+ }
238
+ setInnerLayerVisible(value) {
239
+ this.getBodyParts().forEach(part => (part.innerLayer.visible = value));
240
+ }
241
+ setOuterLayerVisible(value) {
242
+ this.getBodyParts().forEach(part => (part.outerLayer.visible = value));
243
+ }
244
+ resetJoints() {
245
+ this.head.rotation.set(0, 0, 0);
246
+ this.leftArm.rotation.set(0, 0, 0);
247
+ this.rightArm.rotation.set(0, 0, 0);
248
+ this.leftLeg.rotation.set(0, 0, 0);
249
+ this.rightLeg.rotation.set(0, 0, 0);
250
+ this.body.rotation.set(0, 0, 0);
251
+ this.head.position.y = 0;
252
+ this.body.position.y = -6;
253
+ this.body.position.z = 0;
254
+ this.rightArm.position.x = -5;
255
+ this.rightArm.position.y = -2;
256
+ this.rightArm.position.z = 0;
257
+ this.leftArm.position.x = 5;
258
+ this.leftArm.position.y = -2;
259
+ this.leftArm.position.z = 0;
260
+ this.rightLeg.position.x = -1.9;
261
+ this.rightLeg.position.y = -12;
262
+ this.rightLeg.position.z = -0.1;
263
+ this.leftLeg.position.x = 1.9;
264
+ this.leftLeg.position.y = -12;
265
+ this.leftLeg.position.z = -0.1;
266
+ }
267
+ }
268
+ class CapeObject extends three.Group {
269
+ cape;
270
+ material;
271
+ constructor() {
272
+ super();
273
+ this.material = new three.MeshStandardMaterial({
274
+ side: three.DoubleSide,
275
+ transparent: true,
276
+ alphaTest: 1e-5
277
+ });
278
+ // +z (front) - inside of cape
279
+ // -z (back) - outside of cape
280
+ const capeBox = new three.BoxGeometry(10, 16, 1);
281
+ setCapeUVs(capeBox, 0, 0, 10, 16, 1);
282
+ this.cape = new three.Mesh(capeBox, this.material);
283
+ this.cape.position.y = -8;
284
+ this.cape.position.z = 0.5;
285
+ this.add(this.cape);
286
+ }
287
+ get map() {
288
+ return this.material.map;
289
+ }
290
+ set map(newMap) {
291
+ this.material.map = newMap;
292
+ this.material.needsUpdate = true;
293
+ }
294
+ }
295
+ class ElytraObject extends three.Group {
296
+ leftWing;
297
+ rightWing;
298
+ material;
299
+ constructor() {
300
+ super();
301
+ this.material = new three.MeshStandardMaterial({
302
+ side: three.DoubleSide,
303
+ transparent: true,
304
+ alphaTest: 1e-5
305
+ });
306
+ const leftWingBox = new three.BoxGeometry(12, 22, 4);
307
+ setCapeUVs(leftWingBox, 22, 0, 10, 20, 2);
308
+ const leftWingMesh = new three.Mesh(leftWingBox, this.material);
309
+ leftWingMesh.position.x = -5;
310
+ leftWingMesh.position.y = -10;
311
+ leftWingMesh.position.z = -1;
312
+ this.leftWing = new three.Group();
313
+ this.leftWing.add(leftWingMesh);
314
+ this.add(this.leftWing);
315
+ const rightWingBox = new three.BoxGeometry(12, 22, 4);
316
+ setCapeUVs(rightWingBox, 22, 0, 10, 20, 2);
317
+ const rightWingMesh = new three.Mesh(rightWingBox, this.material);
318
+ rightWingMesh.scale.x = -1;
319
+ rightWingMesh.position.x = 5;
320
+ rightWingMesh.position.y = -10;
321
+ rightWingMesh.position.z = -1;
322
+ this.rightWing = new three.Group();
323
+ this.rightWing.add(rightWingMesh);
324
+ this.add(this.rightWing);
325
+ this.leftWing.position.x = 5;
326
+ this.leftWing.rotation.x = 0.2617994;
327
+ this.resetJoints();
328
+ }
329
+ resetJoints() {
330
+ this.leftWing.rotation.y = 0.01; // to avoid z-fighting
331
+ this.leftWing.rotation.z = 0.2617994;
332
+ this.updateRightWing();
333
+ }
334
+ /**
335
+ * Mirrors the position & rotation of left wing,
336
+ * and apply them to the right wing.
337
+ */
338
+ updateRightWing() {
339
+ this.rightWing.position.x = -this.leftWing.position.x;
340
+ this.rightWing.position.y = this.leftWing.position.y;
341
+ this.rightWing.rotation.x = this.leftWing.rotation.x;
342
+ this.rightWing.rotation.y = -this.leftWing.rotation.y;
343
+ this.rightWing.rotation.z = -this.leftWing.rotation.z;
344
+ }
345
+ get map() {
346
+ return this.material.map;
347
+ }
348
+ set map(newMap) {
349
+ this.material.map = newMap;
350
+ this.material.needsUpdate = true;
351
+ }
352
+ }
353
+ class EarsObject extends three.Group {
354
+ rightEar;
355
+ leftEar;
356
+ material;
357
+ constructor() {
358
+ super();
359
+ this.material = new three.MeshStandardMaterial({
360
+ side: three.FrontSide
361
+ });
362
+ const earBox = new three.BoxGeometry(8, 8, 4 / 3);
363
+ setUVs(earBox, 0, 0, 6, 6, 1, 14, 7);
364
+ this.rightEar = new three.Mesh(earBox, this.material);
365
+ this.rightEar.name = 'rightEar';
366
+ this.rightEar.position.x = -6;
367
+ this.add(this.rightEar);
368
+ this.leftEar = new three.Mesh(earBox, this.material);
369
+ this.leftEar.name = 'leftEar';
370
+ this.leftEar.position.x = 6;
371
+ this.add(this.leftEar);
372
+ }
373
+ get map() {
374
+ return this.material.map;
375
+ }
376
+ set map(newMap) {
377
+ this.material.map = newMap;
378
+ this.material.needsUpdate = true;
379
+ }
380
+ }
381
+ const CapeDefaultAngle = (10.8 * Math.PI) / 180;
382
+ class PlayerObject extends three.Group {
383
+ skin;
384
+ cape;
385
+ elytra;
386
+ ears;
387
+ nameTag;
388
+ constructor() {
389
+ super();
390
+ this.skin = new SkinObject();
391
+ this.skin.name = 'skin';
392
+ this.skin.position.y = 8;
393
+ this.add(this.skin);
394
+ this.cape = new CapeObject();
395
+ this.cape.name = 'cape';
396
+ this.cape.position.y = 8;
397
+ this.cape.position.z = -2;
398
+ this.cape.rotation.x = CapeDefaultAngle;
399
+ this.cape.rotation.y = Math.PI;
400
+ this.add(this.cape);
401
+ this.elytra = new ElytraObject();
402
+ this.elytra.name = 'elytra';
403
+ this.elytra.position.y = 8;
404
+ this.elytra.position.z = -2;
405
+ this.elytra.visible = false;
406
+ this.add(this.elytra);
407
+ this.ears = new EarsObject();
408
+ this.ears.name = 'ears';
409
+ this.ears.position.y = 10;
410
+ this.ears.position.z = 2 / 3;
411
+ this.ears.visible = false;
412
+ this.skin.head.add(this.ears);
413
+ }
414
+ get backEquipment() {
415
+ if (this.cape.visible) {
416
+ return 'cape';
417
+ }
418
+ else if (this.elytra.visible) {
419
+ return 'elytra';
420
+ }
421
+ else {
422
+ return null;
423
+ }
424
+ }
425
+ set backEquipment(value) {
426
+ this.cape.visible = value === 'cape';
427
+ this.elytra.visible = value === 'elytra';
428
+ }
429
+ resetJoints() {
430
+ this.skin.resetJoints();
431
+ this.cape.rotation.x = CapeDefaultAngle;
432
+ this.cape.position.y = 8;
433
+ this.cape.position.z = -2;
434
+ this.elytra.position.y = 8;
435
+ this.elytra.position.z = -2;
436
+ this.elytra.rotation.x = 0;
437
+ this.elytra.resetJoints();
438
+ }
439
+ }
440
+
441
+ function isTextureSource(value) {
442
+ return (value instanceof skiaCanvas.Image ||
443
+ // value instanceof HTMLVideoElement ||
444
+ value instanceof skiaCanvas.Canvas //||
445
+ // (typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap) ||
446
+ // (typeof OffscreenCanvas !== 'undefined' && value instanceof OffscreenCanvas)
447
+ );
448
+ }
449
+
450
+ function hasTransparency(context, x0, y0, w, h) {
451
+ const imgData = context.getImageData(x0, y0, w, h);
452
+ for (let x = 0; x < w; x++) {
453
+ for (let y = 0; y < h; y++) {
454
+ const offset = (x + y * w) * 4;
455
+ if (imgData.data[offset + 3] !== 0xff) {
456
+ return true;
457
+ }
458
+ }
459
+ }
460
+ return false;
461
+ }
462
+ function computeSkinScale(width) {
463
+ return width / 64.0;
464
+ }
465
+ function fixOpaqueSkin(context, width, format1_8) {
466
+ // see https://github.com/bs-community/skinview3d/issues/15
467
+ // see https://github.com/bs-community/skinview3d/issues/93
468
+ // check whether the skin has opaque background
469
+ if (format1_8) {
470
+ if (hasTransparency(context, 0, 0, width, width))
471
+ return;
472
+ }
473
+ else {
474
+ if (hasTransparency(context, 0, 0, width, width / 2))
475
+ return;
476
+ }
477
+ const scale = computeSkinScale(width);
478
+ const clearArea = (x, y, w, h) => context.clearRect(x * scale, y * scale, w * scale, h * scale);
479
+ clearArea(40, 0, 8, 8); // Helm Top
480
+ clearArea(48, 0, 8, 8); // Helm Bottom
481
+ clearArea(32, 8, 8, 8); // Helm Right
482
+ clearArea(40, 8, 8, 8); // Helm Front
483
+ clearArea(48, 8, 8, 8); // Helm Left
484
+ clearArea(56, 8, 8, 8); // Helm Back
485
+ if (format1_8) {
486
+ clearArea(4, 32, 4, 4); // Right Leg Layer 2 Top
487
+ clearArea(8, 32, 4, 4); // Right Leg Layer 2 Bottom
488
+ clearArea(0, 36, 4, 12); // Right Leg Layer 2 Right
489
+ clearArea(4, 36, 4, 12); // Right Leg Layer 2 Front
490
+ clearArea(8, 36, 4, 12); // Right Leg Layer 2 Left
491
+ clearArea(12, 36, 4, 12); // Right Leg Layer 2 Back
492
+ clearArea(20, 32, 8, 4); // Torso Layer 2 Top
493
+ clearArea(28, 32, 8, 4); // Torso Layer 2 Bottom
494
+ clearArea(16, 36, 4, 12); // Torso Layer 2 Right
495
+ clearArea(20, 36, 8, 12); // Torso Layer 2 Front
496
+ clearArea(28, 36, 4, 12); // Torso Layer 2 Left
497
+ clearArea(32, 36, 8, 12); // Torso Layer 2 Back
498
+ clearArea(44, 32, 4, 4); // Right Arm Layer 2 Top
499
+ clearArea(48, 32, 4, 4); // Right Arm Layer 2 Bottom
500
+ clearArea(40, 36, 4, 12); // Right Arm Layer 2 Right
501
+ clearArea(44, 36, 4, 12); // Right Arm Layer 2 Front
502
+ clearArea(48, 36, 4, 12); // Right Arm Layer 2 Left
503
+ clearArea(52, 36, 12, 12); // Right Arm Layer 2 Back
504
+ clearArea(4, 48, 4, 4); // Left Leg Layer 2 Top
505
+ clearArea(8, 48, 4, 4); // Left Leg Layer 2 Bottom
506
+ clearArea(0, 52, 4, 12); // Left Leg Layer 2 Right
507
+ clearArea(4, 52, 4, 12); // Left Leg Layer 2 Front
508
+ clearArea(8, 52, 4, 12); // Left Leg Layer 2 Left
509
+ clearArea(12, 52, 4, 12); // Left Leg Layer 2 Back
510
+ clearArea(52, 48, 4, 4); // Left Arm Layer 2 Top
511
+ clearArea(56, 48, 4, 4); // Left Arm Layer 2 Bottom
512
+ clearArea(48, 52, 4, 12); // Left Arm Layer 2 Right
513
+ clearArea(52, 52, 4, 12); // Left Arm Layer 2 Front
514
+ clearArea(56, 52, 4, 12); // Left Arm Layer 2 Left
515
+ clearArea(60, 52, 4, 12); // Left Arm Layer 2 Back
516
+ }
517
+ }
518
+ function convertSkinTo1_8(context, width) {
519
+ // Copied parts are horizontally flipped
520
+ context.save();
521
+ context.scale(-1, 1);
522
+ const scale = computeSkinScale(width);
523
+ const copySkin = (sX, sY, w, h, dX, dY) => context.drawImage(context.canvas, sX * scale, sY * scale, w * scale, h * scale, -dX * scale - w * scale, dY * scale, w * scale, h * scale); // -w https://github.com/samizdatco/skia-canvas/issues/283
524
+ copySkin(4, 16, 4, 4, 20, 48); // Top Leg
525
+ copySkin(8, 16, 4, 4, 24, 48); // Bottom Leg
526
+ copySkin(0, 20, 4, 12, 24, 52); // Outer Leg
527
+ copySkin(4, 20, 4, 12, 20, 52); // Front Leg
528
+ copySkin(8, 20, 4, 12, 16, 52); // Inner Leg
529
+ copySkin(12, 20, 4, 12, 28, 52); // Back Leg
530
+ copySkin(44, 16, 4, 4, 36, 48); // Top Arm
531
+ copySkin(48, 16, 4, 4, 40, 48); // Bottom Arm
532
+ copySkin(40, 20, 4, 12, 40, 52); // Outer Arm
533
+ copySkin(44, 20, 4, 12, 36, 52); // Front Arm
534
+ copySkin(48, 20, 4, 12, 32, 52); // Inner Arm
535
+ copySkin(52, 20, 4, 12, 44, 52); // Back Arm
536
+ context.restore();
537
+ }
538
+ function loadSkinToCanvas(canvas, image) {
539
+ let isOldFormat = false;
540
+ if (image.width !== image.height) {
541
+ if (image.width === 2 * image.height) {
542
+ isOldFormat = true;
543
+ }
544
+ else {
545
+ throw new Error(`Bad skin size: ${image.width}x${image.height}`);
546
+ }
547
+ }
548
+ const context = canvas.getContext('2d' /* , { willReadFrequently: true } */);
549
+ if (isOldFormat) {
550
+ const sideLength = image.width;
551
+ canvas.width = sideLength;
552
+ canvas.height = sideLength;
553
+ context.clearRect(0, 0, sideLength, sideLength);
554
+ context.drawImage(image, 0, 0, sideLength, sideLength / 2.0);
555
+ convertSkinTo1_8(context, sideLength);
556
+ fixOpaqueSkin(context, canvas.width, false);
557
+ }
558
+ else {
559
+ canvas.width = image.width;
560
+ canvas.height = image.height;
561
+ context.clearRect(0, 0, image.width, image.height);
562
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
563
+ fixOpaqueSkin(context, canvas.width, true);
564
+ }
565
+ }
566
+ function computeCapeScale(image) {
567
+ if (image.width === 2 * image.height) {
568
+ // 64x32
569
+ return image.width / 64;
570
+ }
571
+ else if (image.width * 17 === image.height * 22) {
572
+ // 22x17
573
+ return image.width / 22;
574
+ }
575
+ else if (image.width * 11 === image.height * 23) {
576
+ // 46x22
577
+ return image.width / 46;
578
+ }
579
+ else {
580
+ throw new Error(`Bad cape size: ${image.width}x${image.height}`);
581
+ }
582
+ }
583
+ function loadCapeToCanvas(canvas, image) {
584
+ const scale = computeCapeScale(image);
585
+ canvas.width = 64 * scale;
586
+ canvas.height = 32 * scale;
587
+ const context = canvas.getContext('2d');
588
+ context.clearRect(0, 0, canvas.width, canvas.height);
589
+ context.drawImage(image, 0, 0, image.width, image.height);
590
+ }
591
+ function isAreaBlack(context, x0, y0, w, h) {
592
+ const imgData = context.getImageData(x0, y0, w, h);
593
+ for (let x = 0; x < w; x++) {
594
+ for (let y = 0; y < h; y++) {
595
+ const offset = (x + y * w) * 4;
596
+ if (!(imgData.data[offset + 0] === 0 && imgData.data[offset + 1] === 0 && imgData.data[offset + 2] === 0 && imgData.data[offset + 3] === 0xff)) {
597
+ return false;
598
+ }
599
+ }
600
+ }
601
+ return true;
602
+ }
603
+ function isAreaWhite(context, x0, y0, w, h) {
604
+ const imgData = context.getImageData(x0, y0, w, h);
605
+ for (let x = 0; x < w; x++) {
606
+ for (let y = 0; y < h; y++) {
607
+ const offset = (x + y * w) * 4;
608
+ if (!(imgData.data[offset + 0] === 0xff && imgData.data[offset + 1] === 0xff && imgData.data[offset + 2] === 0xff && imgData.data[offset + 3] === 0xff)) {
609
+ return false;
610
+ }
611
+ }
612
+ }
613
+ return true;
614
+ }
615
+ function inferModelType(canvas) {
616
+ // The right arm area of *default* skins:
617
+ // (44,16)->*-------*-------*
618
+ // (40,20) |top |bottom |
619
+ // \|/ |4x4 |4x4 |
620
+ // *-------*-------*-------*-------*
621
+ // |right |front |left |back |
622
+ // |4x12 |4x12 |4x12 |4x12 |
623
+ // *-------*-------*-------*-------*
624
+ // The right arm area of *slim* skins:
625
+ // (44,16)->*------*------*-*
626
+ // (40,20) |top |bottom| |<----[x0=50,y0=16,w=2,h=4]
627
+ // \|/ |3x4 |3x4 | |
628
+ // *-------*------*------***-----*-*
629
+ // |right |front |left |back | |<----[x0=54,y0=20,w=2,h=12]
630
+ // |4x12 |3x12 |4x12 |3x12 | |
631
+ // *-------*------*-------*------*-*
632
+ // Compared with default right arms, slim right arms have 2 unused areas.
633
+ //
634
+ // The same is true for left arm:
635
+ // The left arm area of *default* skins:
636
+ // (36,48)->*-------*-------*
637
+ // (32,52) |top |bottom |
638
+ // \|/ |4x4 |4x4 |
639
+ // *-------*-------*-------*-------*
640
+ // |right |front |left |back |
641
+ // |4x12 |4x12 |4x12 |4x12 |
642
+ // *-------*-------*-------*-------*
643
+ // The left arm area of *slim* skins:
644
+ // (36,48)->*------*------*-*
645
+ // (32,52) |top |bottom| |<----[x0=42,y0=48,w=2,h=4]
646
+ // \|/ |3x4 |3x4 | |
647
+ // *-------*------*------***-----*-*
648
+ // |right |front |left |back | |<----[x0=46,y0=52,w=2,h=12]
649
+ // |4x12 |3x12 |4x12 |3x12 | |
650
+ // *-------*------*-------*------*-*
651
+ //
652
+ // If there is a transparent pixel in any of the 4 unused areas, the skin must be slim,
653
+ // as transparent pixels are not allowed in the first layer.
654
+ // If the 4 areas are all black or all white, the skin is also considered as slim.
655
+ const scale = computeSkinScale(canvas.width);
656
+ const context = canvas.getContext('2d');
657
+ const checkTransparency = (x, y, w, h) => hasTransparency(context, x * scale, y * scale, w * scale, h * scale);
658
+ const checkBlack = (x, y, w, h) => isAreaBlack(context, x * scale, y * scale, w * scale, h * scale);
659
+ const checkWhite = (x, y, w, h) => isAreaWhite(context, x * scale, y * scale, w * scale, h * scale);
660
+ const isSlim = checkTransparency(50, 16, 2, 4) ||
661
+ checkTransparency(54, 20, 2, 12) ||
662
+ checkTransparency(42, 48, 2, 4) ||
663
+ checkTransparency(46, 52, 2, 12) ||
664
+ (checkBlack(50, 16, 2, 4) && checkBlack(54, 20, 2, 12) && checkBlack(42, 48, 2, 4) && checkBlack(46, 52, 2, 12)) ||
665
+ (checkWhite(50, 16, 2, 4) && checkWhite(54, 20, 2, 12) && checkWhite(42, 48, 2, 4) && checkWhite(46, 52, 2, 12));
666
+ return isSlim ? 'slim' : 'default';
667
+ }
668
+ function computeEarsScale(image) {
669
+ if (image.width === image.height * 2 && image.height % 7 === 0) {
670
+ return image.height / 7;
671
+ }
672
+ else {
673
+ throw new Error(`Bad ears size: ${image.width}x${image.height}`);
674
+ }
675
+ }
676
+ function loadEarsToCanvas(canvas, image) {
677
+ const scale = computeEarsScale(image);
678
+ canvas.width = 14 * scale;
679
+ canvas.height = 7 * scale;
680
+ const context = canvas.getContext('2d');
681
+ context.clearRect(0, 0, canvas.width, canvas.height);
682
+ context.drawImage(image, 0, 0, image.width, image.height);
683
+ }
684
+ function loadEarsToCanvasFromSkin(canvas, image) {
685
+ if (image.width !== image.height && image.width !== 2 * image.height) {
686
+ throw new Error(`Bad skin size: ${image.width}x${image.height}`);
687
+ }
688
+ const scale = computeSkinScale(image.width);
689
+ const w = 14 * scale;
690
+ const h = 7 * scale;
691
+ canvas.width = w;
692
+ canvas.height = h;
693
+ const context = canvas.getContext('2d');
694
+ context.clearRect(0, 0, w, h);
695
+ context.drawImage(image, 24 * scale, 0, w, h, 0, 0, w, h);
696
+ }
697
+
698
+ async function loadImage(source) {
699
+ const image = new skiaCanvas.Image();
700
+ return new Promise((resolve, reject) => {
701
+ image.onload = () => resolve(image);
702
+ image.onerror = reject;
703
+ if (typeof source === 'string') {
704
+ image.src = source;
705
+ }
706
+ else if (Buffer.isBuffer(source)) {
707
+ image.src = source;
708
+ }
709
+ else {
710
+ image.src = source.src;
711
+ }
712
+ });
713
+ }
714
+ function canvas2DataTexture(canvas) {
715
+ const png = pngjs.PNG.sync.read(canvas.toBufferSync('png'));
716
+ const texture = new three.DataTexture(Uint8Array.from(png.data), png.width, png.height, three.RGBAFormat);
717
+ texture.needsUpdate = true;
718
+ texture.flipY = true;
719
+ return texture;
720
+ }
721
+
722
+ /**
723
+ * Full-screen textured quad shader
724
+ */
725
+
726
+ const CopyShader = {
727
+
728
+ name: 'CopyShader',
729
+
730
+ uniforms: {
731
+
732
+ 'tDiffuse': { value: null },
733
+ 'opacity': { value: 1.0 }
734
+
735
+ },
736
+
737
+ vertexShader: /* glsl */`
738
+
739
+ varying vec2 vUv;
740
+
741
+ void main() {
742
+
743
+ vUv = uv;
744
+ gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
745
+
746
+ }`,
747
+
748
+ fragmentShader: /* glsl */`
749
+
750
+ uniform float opacity;
751
+
752
+ uniform sampler2D tDiffuse;
753
+
754
+ varying vec2 vUv;
755
+
756
+ void main() {
757
+
758
+ vec4 texel = texture2D( tDiffuse, vUv );
759
+ gl_FragColor = opacity * texel;
760
+
761
+
762
+ }`
763
+
764
+ };
765
+
766
+ class Pass {
767
+
768
+ constructor() {
769
+
770
+ this.isPass = true;
771
+
772
+ // if set to true, the pass is processed by the composer
773
+ this.enabled = true;
774
+
775
+ // if set to true, the pass indicates to swap read and write buffer after rendering
776
+ this.needsSwap = true;
777
+
778
+ // if set to true, the pass clears its buffer before rendering
779
+ this.clear = false;
780
+
781
+ // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
782
+ this.renderToScreen = false;
783
+
784
+ }
785
+
786
+ setSize( /* width, height */ ) {}
787
+
788
+ render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
789
+
790
+ console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
791
+
792
+ }
793
+
794
+ dispose() {}
795
+
796
+ }
797
+
798
+ // Helper for passes that need to fill the viewport with a single quad.
799
+
800
+ const _camera = new three.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
801
+
802
+ // https://github.com/mrdoob/three.js/pull/21358
803
+
804
+ const _geometry = new three.BufferGeometry();
805
+ _geometry.setAttribute( 'position', new three.Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
806
+ _geometry.setAttribute( 'uv', new three.Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
807
+
808
+ class FullScreenQuad {
809
+
810
+ constructor( material ) {
811
+
812
+ this._mesh = new three.Mesh( _geometry, material );
813
+
814
+ }
815
+
816
+ dispose() {
817
+
818
+ this._mesh.geometry.dispose();
819
+
820
+ }
821
+
822
+ render( renderer ) {
823
+
824
+ renderer.render( this._mesh, _camera );
825
+
826
+ }
827
+
828
+ get material() {
829
+
830
+ return this._mesh.material;
831
+
832
+ }
833
+
834
+ set material( value ) {
835
+
836
+ this._mesh.material = value;
837
+
838
+ }
839
+
840
+ }
841
+
842
+ class ShaderPass extends Pass {
843
+
844
+ constructor( shader, textureID ) {
845
+
846
+ super();
847
+
848
+ this.textureID = ( textureID !== undefined ) ? textureID : 'tDiffuse';
849
+
850
+ if ( shader instanceof three.ShaderMaterial ) {
851
+
852
+ this.uniforms = shader.uniforms;
853
+
854
+ this.material = shader;
855
+
856
+ } else if ( shader ) {
857
+
858
+ this.uniforms = three.UniformsUtils.clone( shader.uniforms );
859
+
860
+ this.material = new three.ShaderMaterial( {
861
+
862
+ name: ( shader.name !== undefined ) ? shader.name : 'unspecified',
863
+ defines: Object.assign( {}, shader.defines ),
864
+ uniforms: this.uniforms,
865
+ vertexShader: shader.vertexShader,
866
+ fragmentShader: shader.fragmentShader
867
+
868
+ } );
869
+
870
+ }
871
+
872
+ this.fsQuad = new FullScreenQuad( this.material );
873
+
874
+ }
875
+
876
+ render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
877
+
878
+ if ( this.uniforms[ this.textureID ] ) {
879
+
880
+ this.uniforms[ this.textureID ].value = readBuffer.texture;
881
+
882
+ }
883
+
884
+ this.fsQuad.material = this.material;
885
+
886
+ if ( this.renderToScreen ) {
887
+
888
+ renderer.setRenderTarget( null );
889
+ this.fsQuad.render( renderer );
890
+
891
+ } else {
892
+
893
+ renderer.setRenderTarget( writeBuffer );
894
+ // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
895
+ if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
896
+ this.fsQuad.render( renderer );
897
+
898
+ }
899
+
900
+ }
901
+
902
+ dispose() {
903
+
904
+ this.material.dispose();
905
+
906
+ this.fsQuad.dispose();
907
+
908
+ }
909
+
910
+ }
911
+
912
+ class MaskPass extends Pass {
913
+
914
+ constructor( scene, camera ) {
915
+
916
+ super();
917
+
918
+ this.scene = scene;
919
+ this.camera = camera;
920
+
921
+ this.clear = true;
922
+ this.needsSwap = false;
923
+
924
+ this.inverse = false;
925
+
926
+ }
927
+
928
+ render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
929
+
930
+ const context = renderer.getContext();
931
+ const state = renderer.state;
932
+
933
+ // don't update color or depth
934
+
935
+ state.buffers.color.setMask( false );
936
+ state.buffers.depth.setMask( false );
937
+
938
+ // lock buffers
939
+
940
+ state.buffers.color.setLocked( true );
941
+ state.buffers.depth.setLocked( true );
942
+
943
+ // set up stencil
944
+
945
+ let writeValue, clearValue;
946
+
947
+ if ( this.inverse ) {
948
+
949
+ writeValue = 0;
950
+ clearValue = 1;
951
+
952
+ } else {
953
+
954
+ writeValue = 1;
955
+ clearValue = 0;
956
+
957
+ }
958
+
959
+ state.buffers.stencil.setTest( true );
960
+ state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );
961
+ state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );
962
+ state.buffers.stencil.setClear( clearValue );
963
+ state.buffers.stencil.setLocked( true );
964
+
965
+ // draw into the stencil buffer
966
+
967
+ renderer.setRenderTarget( readBuffer );
968
+ if ( this.clear ) renderer.clear();
969
+ renderer.render( this.scene, this.camera );
970
+
971
+ renderer.setRenderTarget( writeBuffer );
972
+ if ( this.clear ) renderer.clear();
973
+ renderer.render( this.scene, this.camera );
974
+
975
+ // unlock color and depth buffer and make them writable for subsequent rendering/clearing
976
+
977
+ state.buffers.color.setLocked( false );
978
+ state.buffers.depth.setLocked( false );
979
+
980
+ state.buffers.color.setMask( true );
981
+ state.buffers.depth.setMask( true );
982
+
983
+ // only render where stencil is set to 1
984
+
985
+ state.buffers.stencil.setLocked( false );
986
+ state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1
987
+ state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );
988
+ state.buffers.stencil.setLocked( true );
989
+
990
+ }
991
+
992
+ }
993
+
994
+ class ClearMaskPass extends Pass {
995
+
996
+ constructor() {
997
+
998
+ super();
999
+
1000
+ this.needsSwap = false;
1001
+
1002
+ }
1003
+
1004
+ render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
1005
+
1006
+ renderer.state.buffers.stencil.setLocked( false );
1007
+ renderer.state.buffers.stencil.setTest( false );
1008
+
1009
+ }
1010
+
1011
+ }
1012
+
1013
+ class EffectComposer {
1014
+
1015
+ constructor( renderer, renderTarget ) {
1016
+
1017
+ this.renderer = renderer;
1018
+
1019
+ this._pixelRatio = renderer.getPixelRatio();
1020
+
1021
+ if ( renderTarget === undefined ) {
1022
+
1023
+ const size = renderer.getSize( new three.Vector2() );
1024
+ this._width = size.width;
1025
+ this._height = size.height;
1026
+
1027
+ renderTarget = new three.WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: three.HalfFloatType } );
1028
+ renderTarget.texture.name = 'EffectComposer.rt1';
1029
+
1030
+ } else {
1031
+
1032
+ this._width = renderTarget.width;
1033
+ this._height = renderTarget.height;
1034
+
1035
+ }
1036
+
1037
+ this.renderTarget1 = renderTarget;
1038
+ this.renderTarget2 = renderTarget.clone();
1039
+ this.renderTarget2.texture.name = 'EffectComposer.rt2';
1040
+
1041
+ this.writeBuffer = this.renderTarget1;
1042
+ this.readBuffer = this.renderTarget2;
1043
+
1044
+ this.renderToScreen = true;
1045
+
1046
+ this.passes = [];
1047
+
1048
+ this.copyPass = new ShaderPass( CopyShader );
1049
+ this.copyPass.material.blending = three.NoBlending;
1050
+
1051
+ this.clock = new three.Clock();
1052
+
1053
+ }
1054
+
1055
+ swapBuffers() {
1056
+
1057
+ const tmp = this.readBuffer;
1058
+ this.readBuffer = this.writeBuffer;
1059
+ this.writeBuffer = tmp;
1060
+
1061
+ }
1062
+
1063
+ addPass( pass ) {
1064
+
1065
+ this.passes.push( pass );
1066
+ pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
1067
+
1068
+ }
1069
+
1070
+ insertPass( pass, index ) {
1071
+
1072
+ this.passes.splice( index, 0, pass );
1073
+ pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
1074
+
1075
+ }
1076
+
1077
+ removePass( pass ) {
1078
+
1079
+ const index = this.passes.indexOf( pass );
1080
+
1081
+ if ( index !== - 1 ) {
1082
+
1083
+ this.passes.splice( index, 1 );
1084
+
1085
+ }
1086
+
1087
+ }
1088
+
1089
+ isLastEnabledPass( passIndex ) {
1090
+
1091
+ for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
1092
+
1093
+ if ( this.passes[ i ].enabled ) {
1094
+
1095
+ return false;
1096
+
1097
+ }
1098
+
1099
+ }
1100
+
1101
+ return true;
1102
+
1103
+ }
1104
+
1105
+ render( deltaTime ) {
1106
+
1107
+ // deltaTime value is in seconds
1108
+
1109
+ if ( deltaTime === undefined ) {
1110
+
1111
+ deltaTime = this.clock.getDelta();
1112
+
1113
+ }
1114
+
1115
+ const currentRenderTarget = this.renderer.getRenderTarget();
1116
+
1117
+ let maskActive = false;
1118
+
1119
+ for ( let i = 0, il = this.passes.length; i < il; i ++ ) {
1120
+
1121
+ const pass = this.passes[ i ];
1122
+
1123
+ if ( pass.enabled === false ) continue;
1124
+
1125
+ pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
1126
+ pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
1127
+
1128
+ if ( pass.needsSwap ) {
1129
+
1130
+ if ( maskActive ) {
1131
+
1132
+ const context = this.renderer.getContext();
1133
+ const stencil = this.renderer.state.buffers.stencil;
1134
+
1135
+ //context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
1136
+ stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );
1137
+
1138
+ this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );
1139
+
1140
+ //context.stencilFunc( context.EQUAL, 1, 0xffffffff );
1141
+ stencil.setFunc( context.EQUAL, 1, 0xffffffff );
1142
+
1143
+ }
1144
+
1145
+ this.swapBuffers();
1146
+
1147
+ }
1148
+
1149
+ if ( MaskPass !== undefined ) {
1150
+
1151
+ if ( pass instanceof MaskPass ) {
1152
+
1153
+ maskActive = true;
1154
+
1155
+ } else if ( pass instanceof ClearMaskPass ) {
1156
+
1157
+ maskActive = false;
1158
+
1159
+ }
1160
+
1161
+ }
1162
+
1163
+ }
1164
+
1165
+ this.renderer.setRenderTarget( currentRenderTarget );
1166
+
1167
+ }
1168
+
1169
+ reset( renderTarget ) {
1170
+
1171
+ if ( renderTarget === undefined ) {
1172
+
1173
+ const size = this.renderer.getSize( new three.Vector2() );
1174
+ this._pixelRatio = this.renderer.getPixelRatio();
1175
+ this._width = size.width;
1176
+ this._height = size.height;
1177
+
1178
+ renderTarget = this.renderTarget1.clone();
1179
+ renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
1180
+
1181
+ }
1182
+
1183
+ this.renderTarget1.dispose();
1184
+ this.renderTarget2.dispose();
1185
+ this.renderTarget1 = renderTarget;
1186
+ this.renderTarget2 = renderTarget.clone();
1187
+
1188
+ this.writeBuffer = this.renderTarget1;
1189
+ this.readBuffer = this.renderTarget2;
1190
+
1191
+ }
1192
+
1193
+ setSize( width, height ) {
1194
+
1195
+ this._width = width;
1196
+ this._height = height;
1197
+
1198
+ const effectiveWidth = this._width * this._pixelRatio;
1199
+ const effectiveHeight = this._height * this._pixelRatio;
1200
+
1201
+ this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
1202
+ this.renderTarget2.setSize( effectiveWidth, effectiveHeight );
1203
+
1204
+ for ( let i = 0; i < this.passes.length; i ++ ) {
1205
+
1206
+ this.passes[ i ].setSize( effectiveWidth, effectiveHeight );
1207
+
1208
+ }
1209
+
1210
+ }
1211
+
1212
+ setPixelRatio( pixelRatio ) {
1213
+
1214
+ this._pixelRatio = pixelRatio;
1215
+
1216
+ this.setSize( this._width, this._height );
1217
+
1218
+ }
1219
+
1220
+ dispose() {
1221
+
1222
+ this.renderTarget1.dispose();
1223
+ this.renderTarget2.dispose();
1224
+
1225
+ this.copyPass.dispose();
1226
+
1227
+ }
1228
+
1229
+ }
1230
+
1231
+ class RenderPass extends Pass {
1232
+
1233
+ constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {
1234
+
1235
+ super();
1236
+
1237
+ this.scene = scene;
1238
+ this.camera = camera;
1239
+
1240
+ this.overrideMaterial = overrideMaterial;
1241
+
1242
+ this.clearColor = clearColor;
1243
+ this.clearAlpha = clearAlpha;
1244
+
1245
+ this.clear = true;
1246
+ this.clearDepth = false;
1247
+ this.needsSwap = false;
1248
+ this._oldClearColor = new three.Color();
1249
+
1250
+ }
1251
+
1252
+ render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
1253
+
1254
+ const oldAutoClear = renderer.autoClear;
1255
+ renderer.autoClear = false;
1256
+
1257
+ let oldClearAlpha, oldOverrideMaterial;
1258
+
1259
+ if ( this.overrideMaterial !== null ) {
1260
+
1261
+ oldOverrideMaterial = this.scene.overrideMaterial;
1262
+
1263
+ this.scene.overrideMaterial = this.overrideMaterial;
1264
+
1265
+ }
1266
+
1267
+ if ( this.clearColor !== null ) {
1268
+
1269
+ renderer.getClearColor( this._oldClearColor );
1270
+ renderer.setClearColor( this.clearColor );
1271
+
1272
+ }
1273
+
1274
+ if ( this.clearAlpha !== null ) {
1275
+
1276
+ oldClearAlpha = renderer.getClearAlpha();
1277
+ renderer.setClearAlpha( this.clearAlpha );
1278
+
1279
+ }
1280
+
1281
+ if ( this.clearDepth == true ) {
1282
+
1283
+ renderer.clearDepth();
1284
+
1285
+ }
1286
+
1287
+ renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
1288
+
1289
+ if ( this.clear === true ) {
1290
+
1291
+ // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
1292
+ renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
1293
+
1294
+ }
1295
+
1296
+ renderer.render( this.scene, this.camera );
1297
+
1298
+ // restore
1299
+
1300
+ if ( this.clearColor !== null ) {
1301
+
1302
+ renderer.setClearColor( this._oldClearColor );
1303
+
1304
+ }
1305
+
1306
+ if ( this.clearAlpha !== null ) {
1307
+
1308
+ renderer.setClearAlpha( oldClearAlpha );
1309
+
1310
+ }
1311
+
1312
+ if ( this.overrideMaterial !== null ) {
1313
+
1314
+ this.scene.overrideMaterial = oldOverrideMaterial;
1315
+
1316
+ }
1317
+
1318
+ renderer.autoClear = oldAutoClear;
1319
+
1320
+ }
1321
+
1322
+ }
1323
+
1324
+ /**
1325
+ * NVIDIA FXAA by Timothy Lottes
1326
+ * https://developer.download.nvidia.com/assets/gamedev/files/sdk/11/FXAA_WhitePaper.pdf
1327
+ * - WebGL port by @supereggbert
1328
+ * http://www.glge.org/demos/fxaa/
1329
+ * Further improved by Daniel Sturk
1330
+ */
1331
+
1332
+ const FXAAShader = {
1333
+
1334
+ uniforms: {
1335
+
1336
+ 'tDiffuse': { value: null },
1337
+ 'resolution': { value: new three.Vector2( 1 / 1024, 1 / 512 ) }
1338
+
1339
+ },
1340
+
1341
+ vertexShader: /* glsl */`
1342
+
1343
+ varying vec2 vUv;
1344
+
1345
+ void main() {
1346
+
1347
+ vUv = uv;
1348
+ gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
1349
+
1350
+ }`,
1351
+
1352
+ fragmentShader: `
1353
+ precision highp float;
1354
+
1355
+ uniform sampler2D tDiffuse;
1356
+
1357
+ uniform vec2 resolution;
1358
+
1359
+ varying vec2 vUv;
1360
+
1361
+ // FXAA 3.11 implementation by NVIDIA, ported to WebGL by Agost Biro (biro@archilogic.com)
1362
+
1363
+ //----------------------------------------------------------------------------------
1364
+ // File: es3-kepler\FXAA\assets\shaders/FXAA_DefaultES.frag
1365
+ // SDK Version: v3.00
1366
+ // Email: gameworks@nvidia.com
1367
+ // Site: http://developer.nvidia.com/
1368
+ //
1369
+ // Copyright (c) 2014-2015, NVIDIA CORPORATION. All rights reserved.
1370
+ //
1371
+ // Redistribution and use in source and binary forms, with or without
1372
+ // modification, are permitted provided that the following conditions
1373
+ // are met:
1374
+ // * Redistributions of source code must retain the above copyright
1375
+ // notice, this list of conditions and the following disclaimer.
1376
+ // * Redistributions in binary form must reproduce the above copyright
1377
+ // notice, this list of conditions and the following disclaimer in the
1378
+ // documentation and/or other materials provided with the distribution.
1379
+ // * Neither the name of NVIDIA CORPORATION nor the names of its
1380
+ // contributors may be used to endorse or promote products derived
1381
+ // from this software without specific prior written permission.
1382
+ //
1383
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
1384
+ // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
1385
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
1386
+ // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
1387
+ // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
1388
+ // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
1389
+ // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
1390
+ // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
1391
+ // OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
1392
+ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
1393
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1394
+ //
1395
+ //----------------------------------------------------------------------------------
1396
+
1397
+ #ifndef FXAA_DISCARD
1398
+ //
1399
+ // Only valid for PC OpenGL currently.
1400
+ // Probably will not work when FXAA_GREEN_AS_LUMA = 1.
1401
+ //
1402
+ // 1 = Use discard on pixels which don't need AA.
1403
+ // For APIs which enable concurrent TEX+ROP from same surface.
1404
+ // 0 = Return unchanged color on pixels which don't need AA.
1405
+ //
1406
+ #define FXAA_DISCARD 0
1407
+ #endif
1408
+
1409
+ /*--------------------------------------------------------------------------*/
1410
+ #define FxaaTexTop(t, p) texture2D(t, p, -100.0)
1411
+ #define FxaaTexOff(t, p, o, r) texture2D(t, p + (o * r), -100.0)
1412
+ /*--------------------------------------------------------------------------*/
1413
+
1414
+ #define NUM_SAMPLES 5
1415
+
1416
+ // assumes colors have premultipliedAlpha, so that the calculated color contrast is scaled by alpha
1417
+ float contrast( vec4 a, vec4 b ) {
1418
+ vec4 diff = abs( a - b );
1419
+ return max( max( max( diff.r, diff.g ), diff.b ), diff.a );
1420
+ }
1421
+
1422
+ /*============================================================================
1423
+
1424
+ FXAA3 QUALITY - PC
1425
+
1426
+ ============================================================================*/
1427
+
1428
+ /*--------------------------------------------------------------------------*/
1429
+ vec4 FxaaPixelShader(
1430
+ vec2 posM,
1431
+ sampler2D tex,
1432
+ vec2 fxaaQualityRcpFrame,
1433
+ float fxaaQualityEdgeThreshold,
1434
+ float fxaaQualityinvEdgeThreshold
1435
+ ) {
1436
+ vec4 rgbaM = FxaaTexTop(tex, posM);
1437
+ vec4 rgbaS = FxaaTexOff(tex, posM, vec2( 0.0, 1.0), fxaaQualityRcpFrame.xy);
1438
+ vec4 rgbaE = FxaaTexOff(tex, posM, vec2( 1.0, 0.0), fxaaQualityRcpFrame.xy);
1439
+ vec4 rgbaN = FxaaTexOff(tex, posM, vec2( 0.0,-1.0), fxaaQualityRcpFrame.xy);
1440
+ vec4 rgbaW = FxaaTexOff(tex, posM, vec2(-1.0, 0.0), fxaaQualityRcpFrame.xy);
1441
+ // . S .
1442
+ // W M E
1443
+ // . N .
1444
+
1445
+ bool earlyExit = max( max( max(
1446
+ contrast( rgbaM, rgbaN ),
1447
+ contrast( rgbaM, rgbaS ) ),
1448
+ contrast( rgbaM, rgbaE ) ),
1449
+ contrast( rgbaM, rgbaW ) )
1450
+ < fxaaQualityEdgeThreshold;
1451
+ // . 0 .
1452
+ // 0 0 0
1453
+ // . 0 .
1454
+
1455
+ #if (FXAA_DISCARD == 1)
1456
+ if(earlyExit) FxaaDiscard;
1457
+ #else
1458
+ if(earlyExit) return rgbaM;
1459
+ #endif
1460
+
1461
+ float contrastN = contrast( rgbaM, rgbaN );
1462
+ float contrastS = contrast( rgbaM, rgbaS );
1463
+ float contrastE = contrast( rgbaM, rgbaE );
1464
+ float contrastW = contrast( rgbaM, rgbaW );
1465
+
1466
+ float relativeVContrast = ( contrastN + contrastS ) - ( contrastE + contrastW );
1467
+ relativeVContrast *= fxaaQualityinvEdgeThreshold;
1468
+
1469
+ bool horzSpan = relativeVContrast > 0.;
1470
+ // . 1 .
1471
+ // 0 0 0
1472
+ // . 1 .
1473
+
1474
+ // 45 deg edge detection and corners of objects, aka V/H contrast is too similar
1475
+ if( abs( relativeVContrast ) < .3 ) {
1476
+ // locate the edge
1477
+ vec2 dirToEdge;
1478
+ dirToEdge.x = contrastE > contrastW ? 1. : -1.;
1479
+ dirToEdge.y = contrastS > contrastN ? 1. : -1.;
1480
+ // . 2 . . 1 .
1481
+ // 1 0 2 ~= 0 0 1
1482
+ // . 1 . . 0 .
1483
+
1484
+ // tap 2 pixels and see which ones are "outside" the edge, to
1485
+ // determine if the edge is vertical or horizontal
1486
+
1487
+ vec4 rgbaAlongH = FxaaTexOff(tex, posM, vec2( dirToEdge.x, -dirToEdge.y ), fxaaQualityRcpFrame.xy);
1488
+ float matchAlongH = contrast( rgbaM, rgbaAlongH );
1489
+ // . 1 .
1490
+ // 0 0 1
1491
+ // . 0 H
1492
+
1493
+ vec4 rgbaAlongV = FxaaTexOff(tex, posM, vec2( -dirToEdge.x, dirToEdge.y ), fxaaQualityRcpFrame.xy);
1494
+ float matchAlongV = contrast( rgbaM, rgbaAlongV );
1495
+ // V 1 .
1496
+ // 0 0 1
1497
+ // . 0 .
1498
+
1499
+ relativeVContrast = matchAlongV - matchAlongH;
1500
+ relativeVContrast *= fxaaQualityinvEdgeThreshold;
1501
+
1502
+ if( abs( relativeVContrast ) < .3 ) { // 45 deg edge
1503
+ // 1 1 .
1504
+ // 0 0 1
1505
+ // . 0 1
1506
+
1507
+ // do a simple blur
1508
+ return mix(
1509
+ rgbaM,
1510
+ (rgbaN + rgbaS + rgbaE + rgbaW) * .25,
1511
+ .4
1512
+ );
1513
+ }
1514
+
1515
+ horzSpan = relativeVContrast > 0.;
1516
+ }
1517
+
1518
+ if(!horzSpan) rgbaN = rgbaW;
1519
+ if(!horzSpan) rgbaS = rgbaE;
1520
+ // . 0 . 1
1521
+ // 1 0 1 -> 0
1522
+ // . 0 . 1
1523
+
1524
+ bool pairN = contrast( rgbaM, rgbaN ) > contrast( rgbaM, rgbaS );
1525
+ if(!pairN) rgbaN = rgbaS;
1526
+
1527
+ vec2 offNP;
1528
+ offNP.x = (!horzSpan) ? 0.0 : fxaaQualityRcpFrame.x;
1529
+ offNP.y = ( horzSpan) ? 0.0 : fxaaQualityRcpFrame.y;
1530
+
1531
+ bool doneN = false;
1532
+ bool doneP = false;
1533
+
1534
+ float nDist = 0.;
1535
+ float pDist = 0.;
1536
+
1537
+ vec2 posN = posM;
1538
+ vec2 posP = posM;
1539
+
1540
+ int iterationsUsed = 0;
1541
+ int iterationsUsedN = 0;
1542
+ int iterationsUsedP = 0;
1543
+ for( int i = 0; i < NUM_SAMPLES; i++ ) {
1544
+ iterationsUsed = i;
1545
+
1546
+ float increment = float(i + 1);
1547
+
1548
+ if(!doneN) {
1549
+ nDist += increment;
1550
+ posN = posM + offNP * nDist;
1551
+ vec4 rgbaEndN = FxaaTexTop(tex, posN.xy);
1552
+ doneN = contrast( rgbaEndN, rgbaM ) > contrast( rgbaEndN, rgbaN );
1553
+ iterationsUsedN = i;
1554
+ }
1555
+
1556
+ if(!doneP) {
1557
+ pDist += increment;
1558
+ posP = posM - offNP * pDist;
1559
+ vec4 rgbaEndP = FxaaTexTop(tex, posP.xy);
1560
+ doneP = contrast( rgbaEndP, rgbaM ) > contrast( rgbaEndP, rgbaN );
1561
+ iterationsUsedP = i;
1562
+ }
1563
+
1564
+ if(doneN || doneP) break;
1565
+ }
1566
+
1567
+
1568
+ if ( !doneP && !doneN ) return rgbaM; // failed to find end of edge
1569
+
1570
+ float dist = min(
1571
+ doneN ? float( iterationsUsedN ) / float( NUM_SAMPLES - 1 ) : 1.,
1572
+ doneP ? float( iterationsUsedP ) / float( NUM_SAMPLES - 1 ) : 1.
1573
+ );
1574
+
1575
+ // hacky way of reduces blurriness of mostly diagonal edges
1576
+ // but reduces AA quality
1577
+ dist = pow(dist, .5);
1578
+
1579
+ dist = 1. - dist;
1580
+
1581
+ return mix(
1582
+ rgbaM,
1583
+ rgbaN,
1584
+ dist * .5
1585
+ );
1586
+ }
1587
+
1588
+ void main() {
1589
+ const float edgeDetectionQuality = .2;
1590
+ const float invEdgeDetectionQuality = 1. / edgeDetectionQuality;
1591
+
1592
+ gl_FragColor = FxaaPixelShader(
1593
+ vUv,
1594
+ tDiffuse,
1595
+ resolution,
1596
+ edgeDetectionQuality, // [0,1] contrast needed, otherwise early discard
1597
+ invEdgeDetectionQuality
1598
+ );
1599
+
1600
+ }
1601
+ `
1602
+
1603
+ };
1604
+
1605
+ /**
1606
+ * A Minecraft name tag, i.e. a text label with background.
1607
+ */
1608
+ class NameTagObject extends three.Sprite {
1609
+ /**
1610
+ * A promise that is resolved after the name tag is fully painted.
1611
+ *
1612
+ * This will be a resolved promise, if
1613
+ * {@link NameTagOptions.repaintAfterLoaded} is `false`, or
1614
+ * the desired font is available when the `NameTagObject` is created.
1615
+ *
1616
+ * If {@link NameTagOptions.repaintAfterLoaded} is `true`, and
1617
+ * the desired font hasn't been loaded when the `NameTagObject` is created,
1618
+ * the name tag will be painted with the fallback font first, and then
1619
+ * repainted with the desired font after it's loaded. This promise is
1620
+ * resolved after repainting is done.
1621
+ */
1622
+ painted;
1623
+ text;
1624
+ font;
1625
+ margin;
1626
+ textStyle;
1627
+ backgroundStyle;
1628
+ height;
1629
+ textMaterial;
1630
+ constructor(text = '', options = {}) {
1631
+ const material = new three.SpriteMaterial({
1632
+ transparent: true,
1633
+ alphaTest: 1e-5
1634
+ });
1635
+ super(material);
1636
+ this.textMaterial = material;
1637
+ this.text = text;
1638
+ this.font = options.font === undefined ? '48px Minecraft' : options.font;
1639
+ this.margin = options.margin === undefined ? [5, 10, 5, 10] : options.margin;
1640
+ this.textStyle = options.textStyle === undefined ? 'white' : options.textStyle;
1641
+ this.backgroundStyle = options.backgroundStyle === undefined ? 'rgba(0,0,0,.25)' : options.backgroundStyle;
1642
+ this.height = options.height === undefined ? 4.0 : options.height;
1643
+ const repaintAfterLoaded = options.repaintAfterLoaded ?? true;
1644
+ if (repaintAfterLoaded /* && !document.fonts.check(this.font, this.text) */) {
1645
+ this.paint();
1646
+ this.painted = this.loadAndPaint();
1647
+ }
1648
+ else {
1649
+ this.paint();
1650
+ this.painted = Promise.resolve();
1651
+ }
1652
+ }
1653
+ async loadAndPaint() {
1654
+ // await document.fonts.load(this.font, this.text);
1655
+ this.paint();
1656
+ }
1657
+ paint() {
1658
+ const canvas = new skiaCanvas.Canvas();
1659
+ // Measure the text size
1660
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1661
+ let ctx = canvas.getContext('2d');
1662
+ ctx.font = this.font;
1663
+ const metrics = ctx.measureText(this.text);
1664
+ // Compute canvas size
1665
+ canvas.width = this.margin[3] + metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight + this.margin[1];
1666
+ canvas.height = this.margin[0] + metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent + this.margin[2];
1667
+ // After change canvas size, the context needs to be re-created
1668
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1669
+ ctx = canvas.getContext('2d');
1670
+ ctx.font = this.font;
1671
+ // Fill background
1672
+ ctx.fillStyle = this.backgroundStyle;
1673
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1674
+ // Draw text
1675
+ ctx.fillStyle = this.textStyle;
1676
+ ctx.fillText(this.text, this.margin[3] + metrics.actualBoundingBoxLeft, this.margin[0] + metrics.actualBoundingBoxAscent);
1677
+ // Apply texture
1678
+ const texture = canvas2DataTexture(canvas);
1679
+ texture.magFilter = three.NearestFilter;
1680
+ texture.minFilter = three.NearestFilter;
1681
+ this.textMaterial.map = texture;
1682
+ this.textMaterial.needsUpdate = true;
1683
+ // Update size
1684
+ this.scale.x = (canvas.width / canvas.height) * this.height;
1685
+ this.scale.y = this.height;
1686
+ }
1687
+ }
1688
+
1689
+ /**
1690
+ * The SkinViewer renders the player on a canvas.
1691
+ */
1692
+ class SkinViewer {
1693
+ /**
1694
+ * The canvas where the renderer draws its output.
1695
+ */
1696
+ canvas;
1697
+ ctx;
1698
+ scene;
1699
+ camera;
1700
+ renderer;
1701
+ /**
1702
+ * The OrbitControls component which is used to implement the mouse control function.
1703
+ *
1704
+ * @see {@link https://threejs.org/docs/#examples/en/controls/OrbitControls | OrbitControls - three.js docs}
1705
+ */
1706
+ // readonly controls: OrbitControls;
1707
+ /**
1708
+ * The player object.
1709
+ */
1710
+ playerObject;
1711
+ /**
1712
+ * A group that wraps the player object.
1713
+ * It is used to center the player in the world.
1714
+ */
1715
+ playerWrapper;
1716
+ globalLight = new three.AmbientLight(0xffffff, 3);
1717
+ cameraLight = new three.PointLight(0xffffff, 0.6);
1718
+ composer;
1719
+ renderPass;
1720
+ fxaaPass;
1721
+ skinCanvas;
1722
+ capeCanvas;
1723
+ earsCanvas;
1724
+ skinTexture = null;
1725
+ capeTexture = null;
1726
+ earsTexture = null;
1727
+ backgroundTexture = null;
1728
+ _disposed = false;
1729
+ _zoom;
1730
+ /**
1731
+ * Whether to rotate the player along the y axis.
1732
+ *
1733
+ * @defaultValue `false`
1734
+ */
1735
+ autoRotate = false;
1736
+ /**
1737
+ * The angular velocity of the player, in rad/s.
1738
+ *
1739
+ * @defaultValue `1.0`
1740
+ * @see {@link autoRotate}
1741
+ */
1742
+ autoRotateSpeed = 1.0;
1743
+ _animation;
1744
+ // private clock: Clock;
1745
+ // private animationID: number | null;
1746
+ // private onContextLost: (event: Event) => void;
1747
+ // private onContextRestored: () => void;
1748
+ _pixelRatio;
1749
+ // private devicePixelRatioQuery: MediaQueryList | null;
1750
+ // private onDevicePixelRatioChange: () => void;
1751
+ _nameTag = null;
1752
+ nameTagYOffset = 20;
1753
+ _loadPromiseArr = [];
1754
+ /** 读取各部位 */
1755
+ ready;
1756
+ constructor(options = {}) {
1757
+ this.canvas = options.canvas || new skiaCanvas.Canvas();
1758
+ this.ctx = options.ctx || gl(300, 300, { preserveDrawingBuffer: options.preserveDrawingBuffer });
1759
+ this.skinCanvas = new skiaCanvas.Canvas();
1760
+ this.capeCanvas = new skiaCanvas.Canvas();
1761
+ this.earsCanvas = new skiaCanvas.Canvas();
1762
+ this.scene = new three.Scene();
1763
+ this.camera = new three.PerspectiveCamera();
1764
+ this.camera.add(this.cameraLight);
1765
+ this.scene.add(this.camera);
1766
+ this.scene.add(this.globalLight);
1767
+ three.ColorManagement.enabled = false;
1768
+ this.renderer = new three.WebGLRenderer({
1769
+ canvas: this.canvas,
1770
+ context: this.ctx,
1771
+ preserveDrawingBuffer: options.preserveDrawingBuffer === true // default: false
1772
+ });
1773
+ // this.onDevicePixelRatioChange = () => {
1774
+ // this.renderer.setPixelRatio(window.devicePixelRatio);
1775
+ // this.updateComposerSize();
1776
+ // if (this._pixelRatio === "match-device") {
1777
+ // this.devicePixelRatioQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
1778
+ // this.devicePixelRatioQuery.addEventListener("change", this.onDevicePixelRatioChange, { once: true });
1779
+ // }
1780
+ // };
1781
+ // if (options.pixelRatio === undefined || options.pixelRatio === "match-device") {
1782
+ // this._pixelRatio = "match-device";
1783
+ // this.devicePixelRatioQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
1784
+ // this.devicePixelRatioQuery.addEventListener("change", this.onDevicePixelRatioChange, { once: true });
1785
+ // this.renderer.setPixelRatio(window.devicePixelRatio);
1786
+ // } else {
1787
+ this._pixelRatio = options.pixelRatio || 1;
1788
+ // this.devicePixelRatioQuery = null;
1789
+ this.renderer.setPixelRatio(1);
1790
+ // }
1791
+ this.renderer.setClearColor(0, 0);
1792
+ // if (this.renderer.capabilities.isWebGL2) {
1793
+ // Use float precision depth if possible
1794
+ // see https://github.com/bs-community/skinview3d/issues/111
1795
+ const renderTarget = new three.WebGLRenderTarget(0, 0, {
1796
+ // depthTexture: new DepthTexture(0, 0, FloatType),
1797
+ minFilter: three.LinearFilter,
1798
+ magFilter: three.LinearFilter,
1799
+ format: three.RGBAFormat,
1800
+ type: three.UnsignedByteType
1801
+ });
1802
+ // }
1803
+ this.composer = new EffectComposer(this.renderer, renderTarget);
1804
+ this.renderPass = new RenderPass(this.scene, this.camera);
1805
+ this.fxaaPass = new ShaderPass(FXAAShader);
1806
+ this.composer.addPass(this.renderPass);
1807
+ this.composer.addPass(this.fxaaPass);
1808
+ this.playerObject = new PlayerObject();
1809
+ this.playerObject.name = 'player';
1810
+ this.playerObject.skin.visible = false;
1811
+ this.playerObject.cape.visible = false;
1812
+ this.playerWrapper = new three.Group();
1813
+ this.playerWrapper.add(this.playerObject);
1814
+ this.scene.add(this.playerWrapper);
1815
+ // this.controls = new OrbitControls(this.camera, this.canvas);
1816
+ // this.controls.enablePan = false; // disable pan by default
1817
+ // this.controls.minDistance = 10;
1818
+ // this.controls.maxDistance = 256;
1819
+ // if (options.enableControls === false) {
1820
+ // this.controls.enabled = false;
1821
+ // }
1822
+ if (options.skin !== undefined) {
1823
+ this._loadPromiseArr.push(this.loadSkin(options.skin, {
1824
+ model: options.model,
1825
+ ears: options.ears === 'current-skin'
1826
+ }));
1827
+ }
1828
+ if (options.cape !== undefined) {
1829
+ this._loadPromiseArr.push(this.loadCape(options.cape, options.capeLoadOptions));
1830
+ }
1831
+ if (options.ears !== undefined && options.ears !== 'current-skin') {
1832
+ this._loadPromiseArr.push(this.loadEars(options.ears.source, {
1833
+ textureType: options.ears.textureType
1834
+ }));
1835
+ }
1836
+ if (options.width !== undefined) {
1837
+ this.width = options.width;
1838
+ }
1839
+ if (options.height !== undefined) {
1840
+ this.height = options.height;
1841
+ }
1842
+ if (options.background !== undefined) {
1843
+ this.background = options.background;
1844
+ }
1845
+ if (options.panorama !== undefined) {
1846
+ this._loadPromiseArr.push(this.loadPanorama(options.panorama));
1847
+ }
1848
+ if (options.nameTag !== undefined) {
1849
+ this.nameTag = options.nameTag;
1850
+ }
1851
+ this.camera.position.z = 1;
1852
+ this._zoom = options.zoom === undefined ? 0.9 : options.zoom;
1853
+ this.fov = options.fov === undefined ? 50 : options.fov;
1854
+ this._animation = options.animation === undefined ? null : options.animation;
1855
+ // this.clock = new Clock();
1856
+ // this.onContextLost = (event: Event) => {
1857
+ // event.preventDefault();
1858
+ // if (this.animationID !== null) {
1859
+ // window.cancelAnimationFrame(this.animationID);
1860
+ // this.animationID = null;
1861
+ // }
1862
+ // };
1863
+ // this.onContextRestored = () => {
1864
+ // this.renderer.setClearColor(0, 0); // Clear color might be lost
1865
+ // if (!this._renderPaused && !this._disposed && this.animationID === null) {
1866
+ // this.animationID = window.requestAnimationFrame(() => this.draw());
1867
+ // }
1868
+ // };
1869
+ // this.canvas.addEventListener("webglcontextlost", this.onContextLost, false);
1870
+ // this.canvas.addEventListener("webglcontextrestored", this.onContextRestored, false);
1871
+ // this.canvas.addEventListener(
1872
+ // "mousedown",
1873
+ // () => {
1874
+ // this.isUserRotating = true;
1875
+ // },
1876
+ // false
1877
+ // );
1878
+ // this.canvas.addEventListener(
1879
+ // "mouseup",
1880
+ // () => {
1881
+ // this.isUserRotating = false;
1882
+ // },
1883
+ // false
1884
+ // );
1885
+ // this.canvas.addEventListener(
1886
+ // "touchmove",
1887
+ // e => {
1888
+ // if (e.touches.length === 1) {
1889
+ // this.isUserRotating = true;
1890
+ // } else {
1891
+ // this.isUserRotating = false;
1892
+ // }
1893
+ // },
1894
+ // false
1895
+ // );
1896
+ // this.canvas.addEventListener(
1897
+ // "touchend",
1898
+ // () => {
1899
+ // this.isUserRotating = false;
1900
+ // },
1901
+ // false
1902
+ // );
1903
+ this.ready = Promise.allSettled(this._loadPromiseArr);
1904
+ }
1905
+ updateComposerSize() {
1906
+ this.composer.setSize(this.width, this.height);
1907
+ const pixelRatio = this.renderer.getPixelRatio();
1908
+ this.composer.setPixelRatio(pixelRatio);
1909
+ this.fxaaPass.material.uniforms['resolution'].value.x = 1 / (this.width * pixelRatio);
1910
+ this.fxaaPass.material.uniforms['resolution'].value.y = 1 / (this.height * pixelRatio);
1911
+ }
1912
+ recreateSkinTexture() {
1913
+ if (this.skinTexture !== null) {
1914
+ this.skinTexture.dispose();
1915
+ }
1916
+ this.skinTexture = canvas2DataTexture(this.skinCanvas);
1917
+ this.skinTexture.magFilter = three.NearestFilter;
1918
+ this.skinTexture.minFilter = three.NearestFilter;
1919
+ this.playerObject.skin.map = this.skinTexture;
1920
+ }
1921
+ recreateCapeTexture() {
1922
+ if (this.capeTexture !== null) {
1923
+ this.capeTexture.dispose();
1924
+ }
1925
+ this.capeTexture = canvas2DataTexture(this.capeCanvas);
1926
+ this.capeTexture.magFilter = three.NearestFilter;
1927
+ this.capeTexture.minFilter = three.NearestFilter;
1928
+ this.playerObject.cape.map = this.capeTexture;
1929
+ this.playerObject.elytra.map = this.capeTexture;
1930
+ }
1931
+ recreateEarsTexture() {
1932
+ if (this.earsTexture !== null) {
1933
+ this.earsTexture.dispose();
1934
+ }
1935
+ this.earsTexture = canvas2DataTexture(this.earsCanvas);
1936
+ this.earsTexture.magFilter = three.NearestFilter;
1937
+ this.earsTexture.minFilter = three.NearestFilter;
1938
+ this.playerObject.ears.map = this.earsTexture;
1939
+ }
1940
+ loadSkin(source, options = {}) {
1941
+ if (source === null) {
1942
+ this.resetSkin();
1943
+ }
1944
+ else if (isTextureSource(source)) {
1945
+ loadSkinToCanvas(this.skinCanvas, source);
1946
+ this.recreateSkinTexture();
1947
+ if (options.model === undefined || options.model === 'auto-detect') {
1948
+ this.playerObject.skin.modelType = inferModelType(this.skinCanvas);
1949
+ }
1950
+ else {
1951
+ this.playerObject.skin.modelType = options.model;
1952
+ }
1953
+ if (options.makeVisible !== false) {
1954
+ this.playerObject.skin.visible = true;
1955
+ }
1956
+ if (options.ears === true || options.ears == 'load-only') {
1957
+ loadEarsToCanvasFromSkin(this.earsCanvas, source);
1958
+ this.recreateEarsTexture();
1959
+ if (options.ears === true) {
1960
+ this.playerObject.ears.visible = true;
1961
+ if (this._nameTag) {
1962
+ this.nameTagYOffset = 25;
1963
+ this._nameTag.position.y = this.nameTagYOffset;
1964
+ }
1965
+ }
1966
+ }
1967
+ }
1968
+ else {
1969
+ return loadImage(source).then(image => this.loadSkin(image, options));
1970
+ }
1971
+ }
1972
+ resetSkin() {
1973
+ this.playerObject.skin.visible = false;
1974
+ this.playerObject.skin.map = null;
1975
+ if (this.skinTexture !== null) {
1976
+ this.skinTexture.dispose();
1977
+ this.skinTexture = null;
1978
+ }
1979
+ }
1980
+ loadCape(source, options = {}) {
1981
+ if (source === null) {
1982
+ this.resetCape();
1983
+ }
1984
+ else if (isTextureSource(source)) {
1985
+ loadCapeToCanvas(this.capeCanvas, source);
1986
+ this.recreateCapeTexture();
1987
+ if (options.makeVisible !== false) {
1988
+ this.playerObject.backEquipment = options.backEquipment || 'cape';
1989
+ }
1990
+ }
1991
+ else {
1992
+ return loadImage(source).then(image => this.loadCape(image, options));
1993
+ }
1994
+ }
1995
+ resetCape() {
1996
+ this.playerObject.backEquipment = null;
1997
+ this.playerObject.cape.map = null;
1998
+ this.playerObject.elytra.map = null;
1999
+ if (this.capeTexture !== null) {
2000
+ this.capeTexture.dispose();
2001
+ this.capeTexture = null;
2002
+ }
2003
+ }
2004
+ loadEars(source, options = {}) {
2005
+ if (source === null) {
2006
+ this.resetEars();
2007
+ }
2008
+ else if (isTextureSource(source)) {
2009
+ if (options.textureType === 'skin') {
2010
+ loadEarsToCanvasFromSkin(this.earsCanvas, source);
2011
+ }
2012
+ else {
2013
+ loadEarsToCanvas(this.earsCanvas, source);
2014
+ }
2015
+ this.recreateEarsTexture();
2016
+ if (options.makeVisible !== false) {
2017
+ this.playerObject.ears.visible = true;
2018
+ if (this._nameTag) {
2019
+ this.nameTagYOffset = 25;
2020
+ this._nameTag.position.y = this.nameTagYOffset;
2021
+ }
2022
+ }
2023
+ }
2024
+ else {
2025
+ return loadImage(source).then(image => this.loadEars(image, options));
2026
+ }
2027
+ }
2028
+ resetEars() {
2029
+ this.playerObject.ears.visible = false;
2030
+ if (this._nameTag) {
2031
+ this.nameTagYOffset = 20;
2032
+ this._nameTag.position.y = this.nameTagYOffset;
2033
+ }
2034
+ this.playerObject.ears.map = null;
2035
+ if (this.earsTexture !== null) {
2036
+ this.earsTexture.dispose();
2037
+ this.earsTexture = null;
2038
+ }
2039
+ }
2040
+ loadPanorama(source) {
2041
+ return this.loadBackground(source, three.EquirectangularReflectionMapping);
2042
+ }
2043
+ loadBackground(source, mapping) {
2044
+ if (isTextureSource(source)) {
2045
+ if (this.backgroundTexture !== null) {
2046
+ this.backgroundTexture.dispose();
2047
+ }
2048
+ this.backgroundTexture = new three.Texture();
2049
+ this.backgroundTexture.image = source;
2050
+ if (mapping !== undefined) {
2051
+ this.backgroundTexture.mapping = mapping;
2052
+ }
2053
+ this.backgroundTexture.needsUpdate = true;
2054
+ this.scene.background = this.backgroundTexture;
2055
+ }
2056
+ else {
2057
+ return loadImage(source).then(image => this.loadBackground(image, mapping));
2058
+ }
2059
+ }
2060
+ toRGBA() {
2061
+ const buf = new Uint8Array(this.width * this.height * 4);
2062
+ this.ctx.readPixels(0, 0, this.width, this.height, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, buf);
2063
+ const res = new Uint8Array(this.width * this.height * 4);
2064
+ for (let y = 0; y < this.height; y++) {
2065
+ for (let x = 0; x < this.width; x++) {
2066
+ const srcIdx = ((this.height - 1 - y) * this.width + x) * 4;
2067
+ const dstIdx = (y * this.width + x) * 4;
2068
+ res[dstIdx] = buf[srcIdx];
2069
+ res[dstIdx + 1] = buf[srcIdx + 1];
2070
+ res[dstIdx + 2] = buf[srcIdx + 2];
2071
+ res[dstIdx + 3] = buf[srcIdx + 3];
2072
+ }
2073
+ }
2074
+ return res;
2075
+ }
2076
+ toBuffer(format = 'png', options) {
2077
+ const buf = this.toRGBA();
2078
+ const outCanvas = new skiaCanvas.Canvas(this.width, this.height);
2079
+ const ctx2d = outCanvas.getContext('2d');
2080
+ const imageData = ctx2d.createImageData(this.width, this.height);
2081
+ for (let y = 0; y < this.height; y++) {
2082
+ for (let x = 0; x < this.width; x++) {
2083
+ const srcIdx = (y * this.width + x) * 4;
2084
+ const dstIdx = (y * this.width + x) * 4;
2085
+ imageData.data[dstIdx] = buf[srcIdx];
2086
+ imageData.data[dstIdx + 1] = buf[srcIdx + 1];
2087
+ imageData.data[dstIdx + 2] = buf[srcIdx + 2];
2088
+ imageData.data[dstIdx + 3] = buf[srcIdx + 3];
2089
+ }
2090
+ }
2091
+ ctx2d.putImageData(imageData, 0, 0);
2092
+ return outCanvas.toBufferSync(format, options);
2093
+ }
2094
+ /**
2095
+ * Renders the scene to the canvas.
2096
+ * This method does not change the animation progress.
2097
+ */
2098
+ render() {
2099
+ this.composer.render();
2100
+ }
2101
+ renderAnimationFrame(progress, binary = false) {
2102
+ if (this._animation == null) {
2103
+ throw new Error('No animation.');
2104
+ }
2105
+ return () => {
2106
+ this._animation.render(this.playerObject, progress);
2107
+ this.render();
2108
+ return binary ? this.toBuffer('png') : this.toRGBA();
2109
+ };
2110
+ }
2111
+ renderAnimationLoop(frames = 30, binary = false) {
2112
+ if (this._animation == null) {
2113
+ throw new Error('No animation.');
2114
+ }
2115
+ return new Array(frames).fill(null).map((_, i) => {
2116
+ return () => {
2117
+ // this.playerObject.resetJoints();
2118
+ // this.playerObject.position.set(0, 0, 0);
2119
+ // this.playerObject.rotation.set(0, 0, 0);
2120
+ this._animation.render(this.playerObject, i / frames);
2121
+ this.render();
2122
+ return binary ? this.toBuffer('png') : this.toRGBA();
2123
+ };
2124
+ });
2125
+ }
2126
+ setSize(width, height) {
2127
+ this.camera.aspect = width / height;
2128
+ this.camera.updateProjectionMatrix();
2129
+ this.renderer.setSize(width, height);
2130
+ this.updateComposerSize();
2131
+ }
2132
+ dispose() {
2133
+ this._disposed = true;
2134
+ // this.canvas.removeEventListener("webglcontextlost", this.onContextLost, false);
2135
+ // this.canvas.removeEventListener("webglcontextrestored", this.onContextRestored, false);
2136
+ // if (this.devicePixelRatioQuery !== null) {
2137
+ // this.devicePixelRatioQuery.removeEventListener("change", this.onDevicePixelRatioChange);
2138
+ // this.devicePixelRatioQuery = null;
2139
+ // }
2140
+ // if (this.animationID !== null) {
2141
+ // window.cancelAnimationFrame(this.animationID);
2142
+ // this.animationID = null;
2143
+ // }
2144
+ // this.controls.dispose();
2145
+ try {
2146
+ this.renderer.dispose();
2147
+ }
2148
+ catch (error) {
2149
+ // https://github.com/mrdoob/three.js/issues/33157
2150
+ }
2151
+ this.resetSkin();
2152
+ this.resetCape();
2153
+ this.resetEars();
2154
+ this.background = null;
2155
+ this.fxaaPass.fsQuad.dispose();
2156
+ }
2157
+ get disposed() {
2158
+ return this._disposed;
2159
+ }
2160
+ /**
2161
+ * Whether rendering and animations are paused.
2162
+ * Setting this property to true will stop both rendering and animation loops.
2163
+ * Setting it back to false will resume them.
2164
+ */
2165
+ // get renderPaused(): boolean {
2166
+ // return this._renderPaused;
2167
+ // }
2168
+ // set renderPaused(value: boolean) {
2169
+ // this._renderPaused = value;
2170
+ // if (this._renderPaused && this.animationID !== null) {
2171
+ // window.cancelAnimationFrame(this.animationID);
2172
+ // this.animationID = null;
2173
+ // this.clock.stop();
2174
+ // this.clock.autoStart = true;
2175
+ // } else if (
2176
+ // !this._renderPaused &&
2177
+ // !this._disposed &&
2178
+ // !this.renderer.getContext().isContextLost() &&
2179
+ // this.animationID == null
2180
+ // ) {
2181
+ // this.animationID = window.requestAnimationFrame(() => this.draw());
2182
+ // }
2183
+ // }
2184
+ get width() {
2185
+ return this.renderer.getSize(new three.Vector2()).width;
2186
+ }
2187
+ set width(newWidth) {
2188
+ this.setSize(newWidth, this.height);
2189
+ }
2190
+ get height() {
2191
+ return this.renderer.getSize(new three.Vector2()).height;
2192
+ }
2193
+ set height(newHeight) {
2194
+ this.setSize(this.width, newHeight);
2195
+ }
2196
+ get background() {
2197
+ return this.scene.background;
2198
+ }
2199
+ set background(value) {
2200
+ if (value === null || value instanceof three.Color || value instanceof three.Texture) {
2201
+ this.scene.background = value;
2202
+ }
2203
+ else {
2204
+ this.scene.background = new three.Color(value);
2205
+ }
2206
+ if (this.backgroundTexture !== null && value !== this.backgroundTexture) {
2207
+ this.backgroundTexture.dispose();
2208
+ this.backgroundTexture = null;
2209
+ }
2210
+ }
2211
+ adjustCameraDistance() {
2212
+ let distance = 4.5 + 16.5 / Math.tan(((this.fov / 180) * Math.PI) / 2) / this.zoom;
2213
+ // limit distance between 10 ~ 256 (default min / max distance of OrbitControls)
2214
+ if (distance < 10) {
2215
+ distance = 10;
2216
+ }
2217
+ else if (distance > 256) {
2218
+ distance = 256;
2219
+ }
2220
+ this.camera.position.multiplyScalar(distance / this.camera.position.length());
2221
+ this.camera.updateProjectionMatrix();
2222
+ }
2223
+ resetCameraPose() {
2224
+ this.camera.position.set(0, 0, 1);
2225
+ this.camera.rotation.set(0, 0, 0);
2226
+ this.adjustCameraDistance();
2227
+ }
2228
+ get fov() {
2229
+ return this.camera.fov;
2230
+ }
2231
+ set fov(value) {
2232
+ this.camera.fov = value;
2233
+ this.adjustCameraDistance();
2234
+ }
2235
+ get zoom() {
2236
+ return this._zoom;
2237
+ }
2238
+ set zoom(value) {
2239
+ this._zoom = value;
2240
+ this.adjustCameraDistance();
2241
+ }
2242
+ get pixelRatio() {
2243
+ return this._pixelRatio;
2244
+ }
2245
+ set pixelRatio(newValue) {
2246
+ // if (newValue === "match-device") {
2247
+ // if (this._pixelRatio !== "match-device") {
2248
+ // this._pixelRatio = newValue;
2249
+ // this.onDevicePixelRatioChange();
2250
+ // }
2251
+ // } else {
2252
+ // if (this._pixelRatio === "match-device" && this.devicePixelRatioQuery !== null) {
2253
+ // this.devicePixelRatioQuery.removeEventListener("change", this.onDevicePixelRatioChange);
2254
+ // this.devicePixelRatioQuery = null;
2255
+ // }
2256
+ this._pixelRatio = newValue;
2257
+ this.renderer.setPixelRatio(newValue);
2258
+ this.updateComposerSize();
2259
+ // }
2260
+ }
2261
+ /**
2262
+ * The animation that is current playing, or `null` if no animation is playing.
2263
+ *
2264
+ * Setting this property to a different value will change the current animation.
2265
+ * The player's pose and the progress of the new animation will be reset before playing.
2266
+ *
2267
+ * Setting this property to `null` will stop the current animation and reset the player's pose.
2268
+ */
2269
+ get animation() {
2270
+ return this._animation;
2271
+ }
2272
+ set animation(animation) {
2273
+ if (this._animation !== animation) {
2274
+ this.playerObject.resetJoints();
2275
+ this.playerObject.position.set(0, 0, 0);
2276
+ this.playerObject.rotation.set(0, 0, 0);
2277
+ if (this._nameTag) {
2278
+ this._nameTag.position.y = this.nameTagYOffset;
2279
+ }
2280
+ // this.clock.stop();
2281
+ // this.clock.autoStart = true;
2282
+ }
2283
+ // if (animation !== null) {
2284
+ // animation.progress = 0;
2285
+ // }
2286
+ this._animation = animation;
2287
+ }
2288
+ /**
2289
+ * The name tag to display above the player, or `null` if there is none.
2290
+ *
2291
+ * When setting this property to a `string` value, a {@link NameTagObject}
2292
+ * will be automatically created with default options.
2293
+ *
2294
+ * @example
2295
+ * ```
2296
+ * skinViewer.nameTag = "hello";
2297
+ * skinViewer.nameTag = new NameTagObject("hello", { textStyle: "yellow" });
2298
+ * skinViewer.nameTag = null;
2299
+ * ```
2300
+ */
2301
+ get nameTag() {
2302
+ return this._nameTag;
2303
+ }
2304
+ set nameTag(newVal) {
2305
+ if (this._nameTag !== null) {
2306
+ // Remove the old name tag from the scene
2307
+ this.playerWrapper.remove(this._nameTag);
2308
+ }
2309
+ if (newVal !== null) {
2310
+ if (!(newVal instanceof three.Object3D)) {
2311
+ newVal = new NameTagObject(newVal);
2312
+ }
2313
+ // Add the new name tag to the scene
2314
+ this.playerWrapper.add(newVal);
2315
+ // Set y position
2316
+ this.nameTagYOffset = this.playerObject.ears.visible ? 25 : 20;
2317
+ newVal.position.y = this.nameTagYOffset;
2318
+ }
2319
+ this._nameTag = newVal;
2320
+ this.playerObject.nameTag = newVal || undefined;
2321
+ }
2322
+ }
2323
+
2324
+ const defaultParams = {
2325
+ multiple: {
2326
+ value: 1,
2327
+ desc: '整数倍频率'
2328
+ },
2329
+ delay: {
2330
+ value: 5,
2331
+ desc: '每帧延迟时间 1/100ms, 1=10ms'
2332
+ }
2333
+ };
2334
+ function getParamsValue(params) {
2335
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, value.value]));
2336
+ }
2337
+ class PlayerAnimation {
2338
+ static params = {
2339
+ ...defaultParams
2340
+ };
2341
+ // 这一层只需要暴露一个接口给外部调用
2342
+ render(player, progress) {
2343
+ // 确保 progress 在 0-1 之间 (可选,视需求而定)
2344
+ // const p = progress % 1.0;
2345
+ this.animate(player, progress);
2346
+ }
2347
+ }
2348
+ /**
2349
+ * A class that helps you create an animation from a function.
2350
+ *
2351
+ * @example
2352
+ * To create an animation that rotates the player:
2353
+ * ```
2354
+ * new FunctionAnimation((player, progress) => player.rotation.y = progress)
2355
+ * ```
2356
+ */
2357
+ class FunctionAnimation extends PlayerAnimation {
2358
+ static title = '函数';
2359
+ static params = {
2360
+ ...defaultParams
2361
+ };
2362
+ params = getParamsValue(PlayerAnimation.params);
2363
+ fn;
2364
+ constructor(fn) {
2365
+ super();
2366
+ this.fn = fn;
2367
+ }
2368
+ animate(player, progress) {
2369
+ this.fn(player, progress);
2370
+ }
2371
+ }
2372
+ class IdleAnimation extends PlayerAnimation {
2373
+ static title = '常态';
2374
+ static params = {
2375
+ ...defaultParams
2376
+ };
2377
+ params = getParamsValue(IdleAnimation.params);
2378
+ animate(player, progress) {
2379
+ // Multiply by animation's natural speed
2380
+ const t = progress * 2 * Math.PI * this.params.multiple;
2381
+ // Arm swing
2382
+ const basicArmRotationZ = Math.PI * 0.02;
2383
+ player.skin.leftArm.rotation.z = Math.cos(t) * 0.03 + basicArmRotationZ;
2384
+ player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ;
2385
+ // Always add an angle for cape around the x axis
2386
+ const basicCapeRotationX = Math.PI * 0.06;
2387
+ player.cape.rotation.x = Math.sin(t) * 0.01 + basicCapeRotationX;
2388
+ }
2389
+ }
2390
+ class WalkingAnimation extends PlayerAnimation {
2391
+ /**
2392
+ * Whether to shake head when walking.
2393
+ *
2394
+ * @defaultValue `true`
2395
+ */
2396
+ // headBobbing: boolean = true;
2397
+ static title = '行走';
2398
+ static params = {
2399
+ ...defaultParams,
2400
+ headBobbing: {
2401
+ value: true,
2402
+ desc: '是否摇晃头部'
2403
+ }
2404
+ };
2405
+ params = getParamsValue(WalkingAnimation.params);
2406
+ animate(player, progress) {
2407
+ // 基础周期:0 -> 2PI
2408
+ // 所有的动作都基于这个 base
2409
+ const t = progress * 2 * Math.PI * this.params.multiple;
2410
+ // 肢体摆动 (频率 1x)
2411
+ player.skin.leftLeg.rotation.x = Math.sin(t) * 0.5;
2412
+ player.skin.rightLeg.rotation.x = Math.sin(t + Math.PI) * 0.5;
2413
+ player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.5;
2414
+ player.skin.rightArm.rotation.x = Math.sin(t) * 0.5;
2415
+ const basicArmRotationZ = Math.PI * 0.02;
2416
+ player.skin.leftArm.rotation.z = Math.cos(t) * 0.03 + basicArmRotationZ;
2417
+ player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ;
2418
+ // 头部摇晃 (频率 2x - 保证在 0-1 周期内摇晃整数次)
2419
+ // 原版是 /4, /5,这里为了完美闭环,必须设为整数倍
2420
+ // 比如设定:走一圈,头晃 2 次
2421
+ if (this.params.headBobbing) {
2422
+ // Head shaking with different frequency & amplitude
2423
+ player.skin.head.rotation.y = Math.sin(t * 1) * 0.1; // 左右晃
2424
+ player.skin.head.rotation.x = Math.sin(t * 2) * 0.05; // 上下晃
2425
+ }
2426
+ else {
2427
+ player.skin.head.rotation.y = 0;
2428
+ player.skin.head.rotation.x = 0;
2429
+ }
2430
+ // 披风 (频率 1x 或 2x)
2431
+ player.cape.rotation.x = Math.sin(t) * 0.06 + Math.PI * 0.06;
2432
+ }
2433
+ }
2434
+ class RunningAnimation extends PlayerAnimation {
2435
+ static title = '跑步';
2436
+ static params = {
2437
+ ...defaultParams,
2438
+ multiply: {
2439
+ value: 3,
2440
+ desc: defaultParams.multiple.desc
2441
+ }
2442
+ // jump: {
2443
+ // value: true,
2444
+ // desc: '是否跳起'
2445
+ // }
2446
+ };
2447
+ params = getParamsValue(RunningAnimation.params);
2448
+ animate(player, progress) {
2449
+ // 基础周期:0 -> 2PI
2450
+ const t = progress * 2 * Math.PI * this.params.multiple + Math.PI * 0.5; // 保留相位偏移
2451
+ // Leg swing with larger amplitude
2452
+ player.skin.leftLeg.rotation.x = Math.cos(t + Math.PI) * 1.3;
2453
+ player.skin.rightLeg.rotation.x = Math.cos(t) * 1.3;
2454
+ // Arm swing
2455
+ player.skin.leftArm.rotation.x = Math.cos(t) * 1.5;
2456
+ player.skin.rightArm.rotation.x = Math.cos(t + Math.PI) * 1.5;
2457
+ // 身体跳动 (频率 2x)
2458
+ // 跑一步跳一下,左右各一步,所以跳两下 -> 2x
2459
+ player.position.y = Math.cos(t * 2);
2460
+ // Dodging when running
2461
+ player.position.x = Math.cos(t) * 0.15;
2462
+ // Slightly tilting when running
2463
+ player.rotation.z = Math.cos(t + Math.PI) * 0.01;
2464
+ // Apply higher swing frequency, lower amplitude,
2465
+ // and greater basic rotation around x axis,
2466
+ // to cape when running.
2467
+ // 披风 (频率 2x)
2468
+ player.cape.rotation.x = Math.sin(t * 2) * 0.1 + Math.PI * 0.3;
2469
+ // What about head shaking?
2470
+ // You shouldn't glance right and left when running dude :P
2471
+ if (player.nameTag)
2472
+ player.nameTag.position.y = player.position.y + 20;
2473
+ }
2474
+ }
2475
+ function clamp(num, min, max) {
2476
+ return Math.min(Math.max(num, min), max);
2477
+ }
2478
+ class FlyingAnimation extends PlayerAnimation {
2479
+ static title = '飞行';
2480
+ static params = {
2481
+ ...defaultParams,
2482
+ multiple: {
2483
+ value: 2,
2484
+ desc: defaultParams.multiple.desc
2485
+ }
2486
+ };
2487
+ params = getParamsValue(FlyingAnimation.params);
2488
+ animate(player, progress) {
2489
+ // Body rotation finishes in 0.5s
2490
+ // Elytra expansion finishes in 3.3s
2491
+ const t = progress * Math.PI * 2 * this.params.multiple;
2492
+ const startProgress = clamp((t * t) / 100, 0, 1);
2493
+ player.rotation.x = (startProgress * Math.PI) / 2;
2494
+ player.skin.head.rotation.x = startProgress > 0.5 ? Math.PI / 4 - player.rotation.x : 0;
2495
+ const basicArmRotationZ = Math.PI * 0.25 * startProgress;
2496
+ player.skin.leftArm.rotation.z = basicArmRotationZ;
2497
+ player.skin.rightArm.rotation.z = -basicArmRotationZ;
2498
+ const elytraRotationX = 0.34906584;
2499
+ const elytraRotationZ = Math.PI / 2;
2500
+ const interpolation = Math.pow(0.9, t);
2501
+ player.elytra.leftWing.rotation.x = elytraRotationX + interpolation * (0.2617994 - elytraRotationX);
2502
+ player.elytra.leftWing.rotation.z = elytraRotationZ + interpolation * (0.2617994 - elytraRotationZ);
2503
+ player.elytra.updateRightWing();
2504
+ }
2505
+ }
2506
+ class WaveAnimation extends PlayerAnimation {
2507
+ static title = '挥手';
2508
+ static params = {
2509
+ ...defaultParams,
2510
+ whichArm: {
2511
+ value: 'left',
2512
+ desc: '挥动哪个胳膊',
2513
+ choices: ['left', 'right', 'both']
2514
+ },
2515
+ sameDirection: {
2516
+ value: true,
2517
+ desc: '挥动方向相同'
2518
+ }
2519
+ };
2520
+ params = getParamsValue(WaveAnimation.params);
2521
+ animate(player, progress) {
2522
+ const t = progress * 2 * Math.PI * this.params.multiple;
2523
+ const targetArm = this.params.whichArm === 'left' ? player.skin.leftArm : player.skin.rightArm;
2524
+ targetArm.rotation.x = 180;
2525
+ targetArm.rotation.z = Math.sin(t) * 0.5;
2526
+ if (this.params.whichArm === 'both') {
2527
+ player.skin.leftArm.rotation.x = targetArm.rotation.x;
2528
+ player.skin.leftArm.rotation.z = this.params.sameDirection ? targetArm.rotation.z : -Math.sin(t) * 0.5;
2529
+ }
2530
+ }
2531
+ }
2532
+ class CrouchAnimation extends PlayerAnimation {
2533
+ static title = '蹲伏';
2534
+ static params = {
2535
+ ...defaultParams,
2536
+ showProgress: {
2537
+ value: false,
2538
+ desc: '是否启用平滑过渡。 false (默认): 像 Minecraft 游戏内一样,瞬间在站立和蹲下间切换。true: 平滑地蹲下和起立(适合展示动画)。'
2539
+ },
2540
+ isStatic: {
2541
+ value: false,
2542
+ desc: '是否保持静止的蹲下状态。为 true 则忽略 progress,始终保持完全蹲下的姿态。'
2543
+ },
2544
+ isRunningHitAnimation: {
2545
+ value: false,
2546
+ desc: '是否同时播放攻击(挥手)动画'
2547
+ },
2548
+ hitCycles: {
2549
+ value: 8,
2550
+ desc: '攻击动画的次数'
2551
+ }
2552
+ };
2553
+ params = getParamsValue(CrouchAnimation.params);
2554
+ animate(player, progress) {
2555
+ // 1. 计算蹲下系数 (crouchFactor)
2556
+ // 范围 0.0 (站立) 到 1.0 (完全蹲下)
2557
+ let crouchFactor;
2558
+ if (this.params.isStatic) {
2559
+ crouchFactor = 1.0; // 始终蹲下
2560
+ }
2561
+ else {
2562
+ // 将 0-1 的进度映射为 0-1-0 的正弦波 (蹲下再站起)
2563
+ // progress: 0 -> 0.5 -> 1.0
2564
+ // angle: 0 -> PI -> 2PI
2565
+ // sin: 0 -> 1 -> 0
2566
+ const t = progress * 2 * Math.PI * this.params.multiple;
2567
+ crouchFactor = Math.abs(Math.sin(t / 2));
2568
+ }
2569
+ // 2. 处理 "瞬间切换" 逻辑 (Minecraft 原版风格)
2570
+ if (!this.params.showProgress && !this.params.isStatic) {
2571
+ // 如果进度 > 0.5 则算蹲下,否则算站立
2572
+ crouchFactor = crouchFactor > (this.params.showProgress ? 0 : 0.4) ? 1.0 : 0.0;
2573
+ }
2574
+ // --- 应用身体位移和旋转 (使用 crouchFactor 插值) ---
2575
+ // 身体前倾
2576
+ player.skin.body.rotation.x = 0.4537860552 * crouchFactor;
2577
+ // 身体位置调整 (Y轴下降, Z轴后退)
2578
+ player.skin.body.position.y = -6 - 2.103677462 * crouchFactor;
2579
+ player.skin.body.position.z = 1.3256181 * crouchFactor - 3.4500310377 * crouchFactor;
2580
+ // 头部位置 (跟随身体下沉)
2581
+ player.skin.head.position.y = -3.618325234674 * crouchFactor;
2582
+ // 手臂位置和旋转
2583
+ // 基础位置偏移
2584
+ const armZ = 3.618325234674 * crouchFactor - 3.4500310377 * crouchFactor;
2585
+ const armY = -2 - 2.53943318 * crouchFactor;
2586
+ player.skin.leftArm.position.z = armZ;
2587
+ player.skin.rightArm.position.z = armZ;
2588
+ player.skin.leftArm.position.y = armY;
2589
+ player.skin.rightArm.position.y = armY;
2590
+ // 手臂旋转 (保持垂直或跟随身体)
2591
+ player.skin.leftArm.rotation.x = 0.410367746202 * crouchFactor;
2592
+ player.skin.rightArm.rotation.x = player.skin.leftArm.rotation.x;
2593
+ // 手臂微张
2594
+ player.skin.leftArm.rotation.z = 0.1;
2595
+ player.skin.rightArm.rotation.z = -0.1;
2596
+ // 腿部位置
2597
+ player.skin.leftLeg.position.z = -3.4500310377 * crouchFactor;
2598
+ player.skin.rightLeg.position.z = -3.4500310377 * crouchFactor;
2599
+ // 披风 (Cape) 调整
2600
+ player.cape.position.y = 8 - 1.851236166577372 * crouchFactor;
2601
+ player.cape.position.z = -2 + 3.786619432 * crouchFactor - 3.4500310377 * crouchFactor;
2602
+ player.cape.rotation.x = (10.8 * Math.PI) / 180 + 0.294220265771 * crouchFactor;
2603
+ // --- 鞘翅 (Elytra) 逻辑重写 ---
2604
+ // 移除 isCrouched 状态,直接基于 crouchFactor 计算位置
2605
+ player.elytra.position.x = player.cape.position.x;
2606
+ player.elytra.position.y = player.cape.position.y;
2607
+ player.elytra.position.z = player.cape.position.z;
2608
+ player.elytra.rotation.x = player.cape.rotation.x - (10.8 * Math.PI) / 180;
2609
+ // 鞘翅开合角度:
2610
+ // 站立时 (crouchFactor=0) -> 0.26 rad
2611
+ // 蹲下时 (crouchFactor=1) -> 0.72 rad (根据原代码逻辑推算)
2612
+ // 使用线性插值替代原有的复杂状态机
2613
+ const wingRotZ = 0.26179944 + 0.4582006 * crouchFactor;
2614
+ player.elytra.leftWing.rotation.z = wingRotZ;
2615
+ player.elytra.updateRightWing();
2616
+ // --- 攻击 (Hit) 动画逻辑 ---
2617
+ if (this.params.isRunningHitAnimation) {
2618
+ if (crouchFactor !== 1) {
2619
+ // 只在蹲下时播放
2620
+ if (crouchFactor === 0) {
2621
+ player.skin.body.rotation.y = 0;
2622
+ }
2623
+ return;
2624
+ }
2625
+ // 为了保证循环完美,攻击频率必须是主循环的整数倍。
2626
+ // 比如主循环是蹲下再起来 (1次),期间挥动 2 次手。
2627
+ const t = progress * 2 * Math.PI * this.params.hitCycles;
2628
+ // 基础手臂旋转 Z
2629
+ const basicArmRotationZ = 0.01 * Math.PI + 0.06;
2630
+ // 右手攻击
2631
+ // 叠加在蹲下的旋转基础上
2632
+ const crouchOffsetX = 0.4537860552 * crouchFactor;
2633
+ // Right Arm Swing
2634
+ player.skin.rightArm.rotation.x = -crouchOffsetX + 2 * Math.sin(t + Math.PI) * 0.3 - crouchOffsetX;
2635
+ player.skin.rightArm.rotation.z = -Math.cos(t) * 0.403 + basicArmRotationZ;
2636
+ // Body Twist
2637
+ player.skin.body.rotation.y = -Math.cos(t) * 0.06;
2638
+ // Left Arm Compensation (摆动平衡)
2639
+ player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.077 + 0.47 * crouchFactor;
2640
+ player.skin.leftArm.rotation.z = -Math.cos(t) * 0.015 + 0.13 - 0.05 * (1 - crouchFactor);
2641
+ // 左手位置微调 (仅在站立时明显,原代码逻辑)
2642
+ // 使用 (1 - crouchFactor) 来限制仅在站立附近生效
2643
+ const standFactor = 1 - crouchFactor;
2644
+ player.skin.leftArm.position.z += Math.cos(t) * 0.3 * standFactor;
2645
+ player.skin.leftArm.position.x = 5 - Math.cos(t) * 0.05 * standFactor;
2646
+ }
2647
+ }
2648
+ }
2649
+ class HitAnimation extends PlayerAnimation {
2650
+ // multiple: number = 2;
2651
+ static title = '击打';
2652
+ static params = {
2653
+ ...defaultParams,
2654
+ multiple: {
2655
+ value: 10,
2656
+ desc: defaultParams.multiple.desc
2657
+ }
2658
+ };
2659
+ params = getParamsValue(HitAnimation.params);
2660
+ animate(player, progress) {
2661
+ const t = progress * Math.PI * 2 * this.params.multiple;
2662
+ player.skin.rightArm.rotation.x = -0.4537860552 * 2 + 2 * Math.sin(t + Math.PI) * 0.3;
2663
+ const basicArmRotationZ = 0.01 * Math.PI + 0.06;
2664
+ player.skin.rightArm.rotation.z = -Math.cos(t) * 0.403 + basicArmRotationZ;
2665
+ player.skin.body.rotation.y = -Math.cos(t) * 0.06;
2666
+ player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.077;
2667
+ player.skin.leftArm.rotation.z = -Math.cos(t) * 0.015 + 0.13 - 0.05;
2668
+ player.skin.leftArm.position.z = Math.cos(t) * 0.3;
2669
+ player.skin.leftArm.position.x = 5 - Math.cos(t) * 0.05;
2670
+ }
2671
+ }
2672
+ class SwimAnimation extends PlayerAnimation {
2673
+ static title = '游泳';
2674
+ static params = {
2675
+ ...defaultParams,
2676
+ multiple: {
2677
+ value: 1,
2678
+ desc: defaultParams.multiple.desc
2679
+ }
2680
+ };
2681
+ params = getParamsValue(SwimAnimation.params);
2682
+ animate(player, progress) {
2683
+ player.position.y = -5;
2684
+ player.rotation.x = Math.PI / 2;
2685
+ player.skin.head.rotation.x = -Math.PI / 4;
2686
+ player.cape.rotation.x = Math.PI / 4;
2687
+ const loopProgress = (progress * this.params.multiple) % 1;
2688
+ // keyframe timing points
2689
+ const times = [0, 0.7 / 1.3, 1.1 / 1.3, 1.0];
2690
+ const leftEulerDeg = [
2691
+ { z: 180, y: 180, x: 0 },
2692
+ { z: 287.2, y: 180, x: 0 },
2693
+ { z: 180, y: 180, x: 90 },
2694
+ { z: 180, y: 180, x: 0 }
2695
+ ];
2696
+ const rightEulerDeg = [
2697
+ { z: -180, y: 180, x: 0 },
2698
+ { z: -287.2, y: 180, x: 0 },
2699
+ { z: -180, y: 180, x: 90 },
2700
+ { z: -180, y: 180, x: 0 }
2701
+ ];
2702
+ const toRad = Math.PI / 180;
2703
+ function eulerZYXToQuat(z, y, x) {
2704
+ const qz = new three.Quaternion().setFromAxisAngle(new three.Vector3(0, 0, 1), z);
2705
+ const qy = new three.Quaternion().setFromAxisAngle(new three.Vector3(0, 1, 0), y);
2706
+ const qx = new three.Quaternion().setFromAxisAngle(new three.Vector3(1, 0, 0), x);
2707
+ return qx.multiply(qy).multiply(qz);
2708
+ }
2709
+ const leftQuats = leftEulerDeg.map(e => eulerZYXToQuat(e.z * toRad, e.y * toRad, e.x * toRad));
2710
+ const rightQuats = rightEulerDeg.map(e => eulerZYXToQuat(e.z * toRad, e.y * toRad, e.x * toRad));
2711
+ function findSegment(t) {
2712
+ for (let i = 0; i < times.length - 1; i++) {
2713
+ if (t >= times[i] && t <= times[i + 1]) {
2714
+ return { i, t0: times[i], t1: times[i + 1] };
2715
+ }
2716
+ }
2717
+ return { i: times.length - 2, t0: times[times.length - 2], t1: times[times.length - 1] };
2718
+ }
2719
+ const seg = findSegment(loopProgress);
2720
+ const p = (loopProgress - seg.t0) / (seg.t1 - seg.t0);
2721
+ const i = seg.i;
2722
+ const qLeft = new three.Quaternion().copy(leftQuats[i]).slerp(leftQuats[i + 1], p);
2723
+ const qRight = new three.Quaternion().copy(rightQuats[i]).slerp(rightQuats[i + 1], p);
2724
+ player.skin.leftArm.quaternion.copy(qLeft);
2725
+ player.skin.rightArm.quaternion.copy(qRight);
2726
+ const legAmp = 17.2 * toRad;
2727
+ const legPhase = loopProgress * Math.PI * 2;
2728
+ const leftLegX = legAmp * Math.cos(legPhase + Math.PI);
2729
+ const rightLegX = legAmp * Math.cos(legPhase);
2730
+ player.skin.leftLeg.rotation.x = leftLegX;
2731
+ player.skin.leftLeg.rotation.y = -0.1 * toRad;
2732
+ player.skin.leftLeg.rotation.z = -0.1 * toRad;
2733
+ player.skin.rightLeg.rotation.x = rightLegX;
2734
+ player.skin.rightLeg.rotation.y = 0.1 * toRad;
2735
+ player.skin.rightLeg.rotation.z = 0.1 * toRad;
2736
+ }
2737
+ }
2738
+ class SpinUpAnimation extends PlayerAnimation {
2739
+ static title = '旋转起飞';
2740
+ static params = {
2741
+ ...defaultParams,
2742
+ maxHeight: {
2743
+ value: 90,
2744
+ desc: '最高高度'
2745
+ },
2746
+ rotationTurns: {
2747
+ value: 6,
2748
+ desc: '旋转圈数'
2749
+ },
2750
+ /**分头行动 */
2751
+ headOff: {
2752
+ value: false,
2753
+ desc: '分头行动,为 true 时只有头飞走'
2754
+ }
2755
+ };
2756
+ params = getParamsValue(SpinUpAnimation.params);
2757
+ animate(player, progress) {
2758
+ // --- 1. 手臂平举动画 (前 30% 时间) ---
2759
+ const armProgress = clamp(progress / 0.3, 0, 1);
2760
+ // 使用 sin(x * PI/2) 可以在 x=1 时精确到达 1,实现平滑过渡
2761
+ const armSpreadFactor = Math.sin((armProgress * Math.PI) / 2);
2762
+ player.skin.leftArm.rotation.z = armSpreadFactor * (Math.PI / 2);
2763
+ player.skin.rightArm.rotation.z = -armSpreadFactor * (Math.PI / 2);
2764
+ // --- 2. 身体旋转 ---
2765
+ player.rotation.y = Math.pow(progress, 2) * (Math.PI * 2) * this.params.rotationTurns;
2766
+ const a = Math.pow(clamp(progress * 2, 0, 1), 2);
2767
+ player.cape.rotation.x = a * (Math.PI / 2);
2768
+ player.elytra.rotation.x = a * (Math.PI / 2 - 0.2); // -0.2 让鞘翅看起来不偏上
2769
+ // --- 3. 身体上升 (后 70% 时间) ---
2770
+ const flyStartThreshold = 0.3;
2771
+ let riseHeight = 0;
2772
+ if (progress > flyStartThreshold) {
2773
+ // 将 0.3 ~ 1.0 映射为 0 ~ 1
2774
+ const flyProgress = (progress - flyStartThreshold) / (1 - flyStartThreshold);
2775
+ riseHeight = Math.pow(flyProgress, 3) * this.params.maxHeight;
2776
+ }
2777
+ if (this.params.headOff)
2778
+ player.skin.head.position.y = riseHeight;
2779
+ else
2780
+ player.position.y = riseHeight;
2781
+ if (player.nameTag)
2782
+ player.nameTag.position.y = riseHeight + 20;
2783
+ }
2784
+ }
2785
+ class RotateAnimation extends PlayerAnimation {
2786
+ static title = '旋转展示';
2787
+ static params = {
2788
+ ...defaultParams
2789
+ };
2790
+ params = getParamsValue(RotateAnimation.params);
2791
+ animate(player, progress) {
2792
+ const t = progress * 2 * Math.PI * this.params.multiple;
2793
+ player.rotation.y = t;
2794
+ player.skin.leftArm.rotation.z = 0.1;
2795
+ player.skin.rightArm.rotation.z = -0.1;
2796
+ }
2797
+ }
2798
+ class NodAnimation extends PlayerAnimation {
2799
+ // delay = 2;
2800
+ // amp = 0.5;
2801
+ static title = '点头';
2802
+ static params = {
2803
+ ...defaultParams,
2804
+ delay: {
2805
+ value: 2,
2806
+ desc: defaultParams.delay.desc
2807
+ },
2808
+ amp: {
2809
+ value: 0.5,
2810
+ desc: '点头幅度'
2811
+ }
2812
+ };
2813
+ params = getParamsValue(NodAnimation.params);
2814
+ animate(player, progress) {
2815
+ const t = progress * 2 * Math.PI * this.params.multiple;
2816
+ player.skin.head.rotation.x = Math.sin(t) * this.params.amp;
2817
+ }
2818
+ }
2819
+ // 辅助函数:线性插值
2820
+ // 当 t=0 返回 start,t=1 返回 end,中间平滑过渡
2821
+ function lerp(start, end, t) {
2822
+ return start + (end - start) * t;
2823
+ }
2824
+ class FlailAnimation extends PlayerAnimation {
2825
+ // multiple = 2;
2826
+ // delay = 4;
2827
+ static title = '手舞足蹈';
2828
+ static params = {
2829
+ ...defaultParams,
2830
+ multiple: {
2831
+ value: 2,
2832
+ desc: defaultParams.multiple.desc
2833
+ },
2834
+ delay: {
2835
+ value: 4,
2836
+ desc: defaultParams.delay.desc
2837
+ }
2838
+ };
2839
+ params = getParamsValue(FlailAnimation.params);
2840
+ animate(player, progress) {
2841
+ // 1. 定义时间参数
2842
+ // highFreq: 高频震动(用于手脚快速摆动)
2843
+ const highFreq = progress * 2 * Math.PI * this.params.multiple;
2844
+ // lowFreq: 低频变化(用于控制状态过渡),0 -> 1 -> 0
2845
+ // 使用 Math.sin(progress * Math.PI) 可以保证首尾都是 0 (跑步态),中间是 1 (发疯态)
2846
+ // 这样动画循环时是完美的:跑 -> 疯 -> 跑
2847
+ const blendFactor = Math.sin(progress * Math.PI);
2848
+ // 基础摆动幅度
2849
+ const swingRad = Math.sin(highFreq) * 1.9;
2850
+ // === 2. 腿部动作 (保持一直在跑) ===
2851
+ // 腿部不需要过渡,一直保持快速奔跑
2852
+ player.skin.leftLeg.rotation.x = -swingRad * 0.8;
2853
+ player.skin.rightLeg.rotation.x = swingRad * 0.8;
2854
+ // === 3. 手臂动作 (核心修改:平滑混合) ===
2855
+ // 状态 A: 跑步时手臂自然下垂 (Z轴接近0)
2856
+ const armZ_Run = 0.2;
2857
+ // 状态 B: 发疯时手臂平举 (Z轴 90度/PI/2)
2858
+ const armZ_Flail = Math.PI / 2 + 0.3;
2859
+ // 动态计算当前的 Z 轴角度:根据 blendFactor 在两者间平滑变化
2860
+ const currentArmZ = lerp(armZ_Run, armZ_Flail, blendFactor);
2861
+ player.skin.leftArm.rotation.z = currentArmZ;
2862
+ player.skin.rightArm.rotation.z = -currentArmZ;
2863
+ // 旋转轴混合:
2864
+ // 当 blendFactor 为 0 时,完全使用 X 轴旋转 (跑步摆臂)
2865
+ // 当 blendFactor 为 1 时,完全使用 Y 轴旋转 (直升机乱挥)
2866
+ // 中间状态会自动混合两个轴的旋转
2867
+ // X轴分量:跑步时满额,发疯时归零
2868
+ const rotX = swingRad * (1 - blendFactor);
2869
+ // Y轴分量:跑步时归零,发疯时满额
2870
+ const rotY = swingRad * blendFactor;
2871
+ player.skin.leftArm.rotation.x = rotX;
2872
+ player.skin.rightArm.rotation.x = -rotX;
2873
+ player.skin.leftArm.rotation.y = rotY;
2874
+ player.skin.rightArm.rotation.y = rotY;
2875
+ // === 4. 头部动作 (视线画圆) ===
2876
+ // 原理:X轴管上下,Y轴管左右。
2877
+ // 一个用 sin,一个用 cos,频率一致,就会形成圆周运动
2878
+ // 稍微降低一点频率(highFreq / 2),不要晃得太晕
2879
+ const headSpeed = highFreq * 0.5;
2880
+ const headAmp = 0.8; // 晃动幅度
2881
+ player.skin.head.rotation.y = Math.sin(headSpeed) * headAmp; // 左右
2882
+ player.skin.head.rotation.x = Math.cos(headSpeed) * headAmp; // 上下
2883
+ // 稍微加一点 Z 轴歪头,看起来更疯癫
2884
+ player.skin.head.rotation.z = Math.sin(headSpeed * 0.5) * 0.1;
2885
+ // === 5. 身体位移 ===
2886
+ // 上下跳动
2887
+ player.position.y = Math.sin(highFreq * 2) * 2;
2888
+ // 左右胡乱位移
2889
+ player.position.x = Math.cos(highFreq * 0.5) * 0.5;
2890
+ // 身体稍微前倾一点
2891
+ player.rotation.x = 0.15;
2892
+ player.cape.rotation.x = Math.sin(highFreq * 2) * 0.2 + Math.PI * 0.3;
2893
+ if (player.nameTag)
2894
+ player.nameTag.position.y = player.position.y + 20;
2895
+ }
2896
+ }
2897
+
2898
+ const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('skinview3d.cjs', document.baseURI).href))));
2899
+ skiaCanvas.FontLibrary.use([path.join(__dirname$1, '../assets/minecraft.woff2')]);
2900
+
2901
+ exports.BodyPart = BodyPart;
2902
+ exports.CapeObject = CapeObject;
2903
+ exports.CrouchAnimation = CrouchAnimation;
2904
+ exports.EarsObject = EarsObject;
2905
+ exports.ElytraObject = ElytraObject;
2906
+ exports.FlailAnimation = FlailAnimation;
2907
+ exports.FlyingAnimation = FlyingAnimation;
2908
+ exports.FunctionAnimation = FunctionAnimation;
2909
+ exports.HitAnimation = HitAnimation;
2910
+ exports.IdleAnimation = IdleAnimation;
2911
+ exports.NameTagObject = NameTagObject;
2912
+ exports.NodAnimation = NodAnimation;
2913
+ exports.PlayerAnimation = PlayerAnimation;
2914
+ exports.PlayerObject = PlayerObject;
2915
+ exports.RotateAnimation = RotateAnimation;
2916
+ exports.RunningAnimation = RunningAnimation;
2917
+ exports.SkinObject = SkinObject;
2918
+ exports.SkinViewer = SkinViewer;
2919
+ exports.SpinUpAnimation = SpinUpAnimation;
2920
+ exports.SwimAnimation = SwimAnimation;
2921
+ exports.WalkingAnimation = WalkingAnimation;
2922
+ exports.WaveAnimation = WaveAnimation;