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