3d-spinner 0.9.4 → 0.9.6

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.
Files changed (54) hide show
  1. package/dist/animations/charged-orb.js +4 -2
  2. package/dist/animations/ghost-train.d.ts +1 -1
  3. package/dist/animations/ghost-train.js +3 -1
  4. package/dist/animations/grid-assembly.d.ts +1 -1
  5. package/dist/animations/spin.d.ts +3 -1
  6. package/dist/animations/spin.js +6 -1
  7. package/dist/cjs/animations/charged-orb.cjs +177 -30
  8. package/dist/cjs/animations/grid-assembly.cjs +171 -28
  9. package/dist/cjs/animations/object-motion.cjs +163 -26
  10. package/dist/cjs/animations/particles.cjs +181 -30
  11. package/dist/cjs/animations/spin.cjs +176 -29
  12. package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +200 -47
  13. package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +51 -13
  14. package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +56 -6
  15. package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +69 -10
  16. package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +64 -11
  17. package/dist/cjs/prefabs/prefabs.cjs +200 -44
  18. package/dist/engines/little-3d-engine/core/geometry.d.ts +15 -2
  19. package/dist/engines/little-3d-engine/core/geometry.js +25 -3
  20. package/dist/engines/little-3d-engine/core/light.d.ts +28 -4
  21. package/dist/engines/little-3d-engine/core/light.js +48 -7
  22. package/dist/engines/little-3d-engine/core/mesh.d.ts +33 -0
  23. package/dist/engines/little-3d-engine/core/mesh.js +12 -0
  24. package/dist/engines/little-3d-engine/little-3d-engine.d.ts +2 -2
  25. package/dist/engines/little-3d-engine/little-3d-engine.js +1 -1
  26. package/dist/engines/little-3d-engine/loaders/obj.d.ts +8 -7
  27. package/dist/engines/little-3d-engine/loaders/obj.js +74 -20
  28. package/dist/engines/little-3d-engine/renderers/canvas2d.js +23 -1
  29. package/dist/engines/little-3d-engine/renderers/webgl-textured.js +8 -3
  30. package/dist/engines/little-3d-engine/renderers/webgl.js +41 -7
  31. package/dist/engines/little-3d-engine/renderers/webgpu.js +43 -10
  32. package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +8 -3
  33. package/dist/engines/little-3d-engine/shapes/complex/plane.js +10 -4
  34. package/dist/engines/little-3d-engine/shapes/primitives/cube.d.ts +3 -2
  35. package/dist/engines/little-3d-engine/shapes/primitives/cube.js +4 -2
  36. package/dist/engines/little-3d-engine/shapes/primitives/octahedron.d.ts +3 -2
  37. package/dist/engines/little-3d-engine/shapes/primitives/octahedron.js +4 -2
  38. package/dist/engines/little-3d-engine/shapes/primitives/pyramid.d.ts +3 -2
  39. package/dist/engines/little-3d-engine/shapes/primitives/pyramid.js +4 -2
  40. package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +3 -2
  41. package/dist/engines/little-3d-engine/shapes/primitives/quad.js +4 -2
  42. package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.d.ts +3 -2
  43. package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.js +4 -2
  44. package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.d.ts +3 -2
  45. package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.js +4 -2
  46. package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.d.ts +3 -2
  47. package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.js +4 -2
  48. package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.d.ts +3 -2
  49. package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.js +4 -2
  50. package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.d.ts +3 -2
  51. package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.js +4 -2
  52. package/dist/umd/spinner.global.js +271 -67
  53. package/dist/umd/spinner.global.min.js +58 -17
  54. package/package.json +1 -1
@@ -139,7 +139,10 @@ var Spinner3D = (() => {
139
139
  const positions = new Float32Array(triangles * 9);
140
140
  const normals = new Float32Array(triangles * 9);
141
141
  const colors = new Float32Array(triangles * 9);
142
+ const emissives = new Float32Array(triangles * 9);
143
+ const speculars = new Float32Array(triangles * 12);
142
144
  let o = 0;
145
+ let so = 0;
143
146
  for (const face of mesh.faces) {
144
147
  const v0 = mesh.vertices[face.indices[0]];
145
148
  const v1 = mesh.vertices[face.indices[1]];
@@ -149,6 +152,15 @@ var Spinner3D = (() => {
149
152
  const cr = r / 255;
150
153
  const cg = g / 255;
151
154
  const cb = b / 255;
155
+ const emissive = face.material?.emissive;
156
+ const er = emissive ? emissive[0] : 0;
157
+ const eg = emissive ? emissive[1] : 0;
158
+ const eb = emissive ? emissive[2] : 0;
159
+ const specular = face.material?.specular;
160
+ const sr = specular ? specular[0] : 0;
161
+ const sg = specular ? specular[1] : 0;
162
+ const sb = specular ? specular[2] : 0;
163
+ const sn = specular ? face.material?.shininess ?? 32 : 1;
152
164
  for (let k = 1; k < face.indices.length - 1; k++) {
153
165
  const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
154
166
  for (const index of tri) {
@@ -162,11 +174,19 @@ var Spinner3D = (() => {
162
174
  colors[o] = cr;
163
175
  colors[o + 1] = cg;
164
176
  colors[o + 2] = cb;
177
+ emissives[o] = er;
178
+ emissives[o + 1] = eg;
179
+ emissives[o + 2] = eb;
180
+ speculars[so] = sr;
181
+ speculars[so + 1] = sg;
182
+ speculars[so + 2] = sb;
183
+ speculars[so + 3] = sn;
165
184
  o += 3;
185
+ so += 4;
166
186
  }
167
187
  }
168
188
  }
169
- return { positions, normals, colors, count: positions.length / 3 };
189
+ return { positions, normals, colors, emissives, speculars, count: positions.length / 3 };
170
190
  }
171
191
  function midpoint(a, b) {
172
192
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
@@ -204,13 +224,42 @@ var Spinner3D = (() => {
204
224
  function clamp012(value) {
205
225
  return Math.min(1, Math.max(0, value));
206
226
  }
207
- function shadeColor(normal, color, light) {
227
+ function clamp255(value) {
228
+ return Math.round(Math.min(255, Math.max(0, value)));
229
+ }
230
+ function shade(normal, color, light, surface) {
208
231
  const lambert = Math.max(0, dot(normal, light.toLight));
209
232
  const brightness = clamp012(light.ambient + light.intensity * lambert);
210
- const [r, g, b] = parseColor(color);
211
- return `rgb(${Math.round(r * brightness)}, ${Math.round(g * brightness)}, ${Math.round(
212
- b * brightness
213
- )})`;
233
+ const [baseR, baseG, baseB] = parseColor(color);
234
+ let r = baseR * brightness;
235
+ let g = baseG * brightness;
236
+ let b = baseB * brightness;
237
+ const material = surface?.material;
238
+ const specular = material?.specular;
239
+ const viewDir = surface?.viewDir;
240
+ if (specular && viewDir && lambert > 0) {
241
+ const half = normalize({
242
+ x: light.toLight.x + viewDir.x,
243
+ y: light.toLight.y + viewDir.y,
244
+ z: light.toLight.z + viewDir.z
245
+ });
246
+ const shininess = material?.shininess ?? 32;
247
+ const highlight = Math.pow(Math.max(0, dot(normal, half)), shininess) * light.intensity * 255;
248
+ r += highlight * specular[0];
249
+ g += highlight * specular[1];
250
+ b += highlight * specular[2];
251
+ }
252
+ const emissive = material?.emissive;
253
+ if (emissive) {
254
+ r += emissive[0] * 255;
255
+ g += emissive[1] * 255;
256
+ b += emissive[2] * 255;
257
+ }
258
+ return [clamp255(r), clamp255(g), clamp255(b)];
259
+ }
260
+ function shadeColor(normal, color, light, surface) {
261
+ const [r, g, b] = shade(normal, color, light, surface);
262
+ return `rgb(${r}, ${g}, ${b})`;
214
263
  }
215
264
  var DEFAULTS2, Light;
216
265
  var init_light = __esm({
@@ -233,8 +282,8 @@ var Spinner3D = (() => {
233
282
  };
234
283
  }
235
284
  /** Convenience wrapper around {@link shadeColor} using this light. */
236
- shade(normal, color) {
237
- return shadeColor(normal, color, this.params);
285
+ shade(normal, color, surface) {
286
+ return shadeColor(normal, color, this.params, surface);
238
287
  }
239
288
  };
240
289
  }
@@ -274,28 +323,51 @@ var Spinner3D = (() => {
274
323
  in vec3 aPos;
275
324
  in vec3 aNormal;
276
325
  in vec3 aColor;
326
+ in vec3 aEmissive;
327
+ in vec4 aSpecular;
277
328
  uniform mat4 uViewProj;
278
329
  uniform mat4 uModel;
279
330
  out vec3 vNormal;
280
331
  out vec3 vColor;
332
+ out vec3 vEmissive;
333
+ out vec4 vSpecular;
334
+ out vec3 vWorldPos;
281
335
  void main() {
282
336
  vNormal = mat3(uModel) * aNormal;
283
337
  vColor = aColor;
284
- gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
338
+ vEmissive = aEmissive;
339
+ vSpecular = aSpecular;
340
+ vec4 world = uModel * vec4(aPos, 1.0);
341
+ vWorldPos = world.xyz;
342
+ gl_Position = uViewProj * world;
285
343
  }`;
286
344
  FRAGMENT_SHADER = `#version 300 es
287
345
  precision mediump float;
288
346
  in vec3 vNormal;
289
347
  in vec3 vColor;
348
+ in vec3 vEmissive;
349
+ in vec4 vSpecular;
350
+ in vec3 vWorldPos;
290
351
  uniform vec3 uToLight;
352
+ uniform vec3 uEye;
291
353
  uniform float uIntensity;
292
354
  uniform float uAmbient;
293
355
  uniform float uOpacity;
294
356
  out vec4 fragColor;
295
357
  void main() {
296
- float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
358
+ vec3 normal = normalize(vNormal);
359
+ vec3 toLight = normalize(uToLight);
360
+ float lambert = max(dot(normal, toLight), 0.0);
297
361
  float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
298
- fragColor = vec4(vColor * brightness, uOpacity);
362
+ vec3 lit = vColor * brightness;
363
+ if (lambert > 0.0) {
364
+ vec3 viewDir = normalize(uEye - vWorldPos);
365
+ vec3 halfVec = normalize(toLight + viewDir);
366
+ float highlight = pow(max(dot(normal, halfVec), 0.0), vSpecular.w) * uIntensity;
367
+ lit += highlight * vSpecular.xyz;
368
+ }
369
+ lit += vEmissive;
370
+ fragColor = vec4(lit, uOpacity);
299
371
  }`;
300
372
  WebGLRenderer = class {
301
373
  constructor(options = {}) {
@@ -316,9 +388,12 @@ void main() {
316
388
  aPos: gl.getAttribLocation(this.program, "aPos"),
317
389
  aNormal: gl.getAttribLocation(this.program, "aNormal"),
318
390
  aColor: gl.getAttribLocation(this.program, "aColor"),
391
+ aEmissive: gl.getAttribLocation(this.program, "aEmissive"),
392
+ aSpecular: gl.getAttribLocation(this.program, "aSpecular"),
319
393
  uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
320
394
  uModel: gl.getUniformLocation(this.program, "uModel"),
321
395
  uToLight: gl.getUniformLocation(this.program, "uToLight"),
396
+ uEye: gl.getUniformLocation(this.program, "uEye"),
322
397
  uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
323
398
  uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
324
399
  uOpacity: gl.getUniformLocation(this.program, "uOpacity")
@@ -342,19 +417,23 @@ void main() {
342
417
  const data = expandToTriangles(mesh);
343
418
  const vao = gl.createVertexArray();
344
419
  gl.bindVertexArray(vao);
345
- const attribute = (location, array) => {
420
+ const buffers = [];
421
+ const attribute = (location, array, size = 3) => {
346
422
  if (location < 0) return;
347
423
  const buffer = gl.createBuffer();
424
+ buffers.push(buffer);
348
425
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
349
426
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
350
427
  gl.enableVertexAttribArray(location);
351
- gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
428
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
352
429
  };
353
430
  attribute(loc.aPos, data.positions);
354
431
  attribute(loc.aNormal, data.normals);
355
432
  attribute(loc.aColor, data.colors);
433
+ attribute(loc.aEmissive, data.emissives);
434
+ attribute(loc.aSpecular, data.speculars, 4);
356
435
  gl.bindVertexArray(null);
357
- const result = { vao, count: data.count };
436
+ const result = { vao, buffers, count: data.count };
358
437
  this.cache.set(mesh, result);
359
438
  return result;
360
439
  }
@@ -367,6 +446,7 @@ void main() {
367
446
  gl.useProgram(this.program);
368
447
  gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
369
448
  gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
449
+ gl.uniform3f(loc.uEye, frame.eye.x, frame.eye.y, frame.eye.z);
370
450
  gl.uniform1f(loc.uIntensity, frame.light.intensity);
371
451
  gl.uniform1f(loc.uAmbient, frame.light.ambient);
372
452
  gl.disable(gl.BLEND);
@@ -411,7 +491,10 @@ void main() {
411
491
  destroy() {
412
492
  const gl = this.gl;
413
493
  if (gl) {
414
- for (const mesh of this.cache.values()) gl.deleteVertexArray(mesh.vao);
494
+ for (const mesh of this.cache.values()) {
495
+ gl.deleteVertexArray(mesh.vao);
496
+ for (const buffer of mesh.buffers) gl.deleteBuffer(buffer);
497
+ }
415
498
  if (this.program) gl.deleteProgram(this.program);
416
499
  }
417
500
  this.cache.clear();
@@ -441,6 +524,7 @@ struct Uniforms {
441
524
  model: mat4x4<f32>,
442
525
  toLight: vec4<f32>,
443
526
  params: vec4<f32>,
527
+ eye: vec4<f32>,
444
528
  };
445
529
  @group(0) @binding(0) var<uniform> u: Uniforms;
446
530
 
@@ -448,23 +532,40 @@ struct VSOut {
448
532
  @builtin(position) position: vec4<f32>,
449
533
  @location(0) normal: vec3<f32>,
450
534
  @location(1) color: vec3<f32>,
535
+ @location(2) emissive: vec3<f32>,
536
+ @location(3) specular: vec4<f32>,
537
+ @location(4) worldPos: vec3<f32>,
451
538
  };
452
539
 
453
540
  @vertex
454
- fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
541
+ fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>, @location(3) emissive: vec3<f32>, @location(4) specular: vec4<f32>) -> VSOut {
455
542
  var out: VSOut;
456
543
  let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
457
544
  out.normal = m * normal;
458
545
  out.color = color;
459
- out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
546
+ out.emissive = emissive;
547
+ out.specular = specular;
548
+ let world = u.model * vec4<f32>(pos, 1.0);
549
+ out.worldPos = world.xyz;
550
+ out.position = u.viewProj * world;
460
551
  return out;
461
552
  }
462
553
 
463
554
  @fragment
464
555
  fn fs(in: VSOut) -> @location(0) vec4<f32> {
465
- let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
556
+ let normal = normalize(in.normal);
557
+ let toLight = normalize(u.toLight.xyz);
558
+ let lambert = max(dot(normal, toLight), 0.0);
466
559
  let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
467
- return vec4<f32>(in.color * brightness, u.params.z);
560
+ var lit = in.color * brightness;
561
+ if (lambert > 0.0) {
562
+ let viewDir = normalize(u.eye.xyz - in.worldPos);
563
+ let halfVec = normalize(toLight + viewDir);
564
+ let highlight = pow(max(dot(normal, halfVec), 0.0), in.specular.w) * u.params.x;
565
+ lit = lit + highlight * in.specular.xyz;
566
+ }
567
+ lit = lit + in.emissive;
568
+ return vec4<f32>(lit, u.params.z);
468
569
  }
469
570
  `;
470
571
  CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
@@ -505,13 +606,15 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
505
606
  {
506
607
  binding: 0,
507
608
  visibility: stage.VERTEX | stage.FRAGMENT,
508
- buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
609
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 176 }
509
610
  }
510
611
  ]
511
612
  });
512
- const vertexBuffer = (location) => ({
513
- arrayStride: 12,
514
- attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
613
+ const vertexBuffer = (location, components = 3) => ({
614
+ arrayStride: components * 4,
615
+ attributes: [
616
+ { shaderLocation: location, offset: 0, format: `float32x${components}` }
617
+ ]
515
618
  });
516
619
  const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
517
620
  const blend = {
@@ -527,7 +630,13 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
527
630
  vertex: {
528
631
  module,
529
632
  entryPoint: "vs",
530
- buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
633
+ buffers: [
634
+ vertexBuffer(0),
635
+ vertexBuffer(1),
636
+ vertexBuffer(2),
637
+ vertexBuffer(3),
638
+ vertexBuffer(4, 4)
639
+ ]
531
640
  },
532
641
  fragment: {
533
642
  module,
@@ -580,6 +689,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
580
689
  position: upload(data.positions),
581
690
  normal: upload(data.normals),
582
691
  color: upload(data.colors),
692
+ emissive: upload(data.emissives),
693
+ specular: upload(data.speculars),
583
694
  count: data.count
584
695
  };
585
696
  this.cache.set(mesh, result);
@@ -631,7 +742,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
631
742
  const bindGroup = this.device.createBindGroup({
632
743
  layout,
633
744
  entries: [
634
- { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
745
+ { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 176 } }
635
746
  ]
636
747
  });
637
748
  draws.forEach((draw, i) => {
@@ -640,6 +751,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
640
751
  data.set(draw.item.model, 16);
641
752
  data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
642
753
  data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
754
+ data.set([frame.eye.x, frame.eye.y, frame.eye.z, 0], 40);
643
755
  this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
644
756
  });
645
757
  const encoder = this.device.createCommandEncoder();
@@ -666,6 +778,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
666
778
  pass.setVertexBuffer(0, mesh.position);
667
779
  pass.setVertexBuffer(1, mesh.normal);
668
780
  pass.setVertexBuffer(2, mesh.color);
781
+ pass.setVertexBuffer(3, mesh.emissive);
782
+ pass.setVertexBuffer(4, mesh.specular);
669
783
  pass.draw(mesh.count);
670
784
  });
671
785
  pass.end();
@@ -677,6 +791,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
677
791
  mesh.position.destroy?.();
678
792
  mesh.normal.destroy?.();
679
793
  mesh.color.destroy?.();
794
+ mesh.emissive.destroy?.();
795
+ mesh.specular.destroy?.();
680
796
  }
681
797
  this.cache.clear();
682
798
  this.uniformBuffer?.destroy?.();
@@ -759,9 +875,30 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
759
875
  depth += dot(d, d);
760
876
  }
761
877
  depth /= face.indices.length;
878
+ let surface;
879
+ const material = face.material;
880
+ if (material) {
881
+ if (material.specular) {
882
+ let cx = 0;
883
+ let cy = 0;
884
+ let cz = 0;
885
+ for (const i of face.indices) {
886
+ cx += world[i].x;
887
+ cy += world[i].y;
888
+ cz += world[i].z;
889
+ }
890
+ const inv = 1 / face.indices.length;
891
+ const viewDir = normalize(
892
+ subtract(frame.eye, { x: cx * inv, y: cy * inv, z: cz * inv })
893
+ );
894
+ surface = { material, viewDir };
895
+ } else {
896
+ surface = { material };
897
+ }
898
+ }
762
899
  polygons.push({
763
900
  points,
764
- color: shadeColor(normal, face.color, frame.light),
901
+ color: shadeColor(normal, face.color, frame.light, surface),
765
902
  depth,
766
903
  opacity: faceOpacity
767
904
  });
@@ -1299,9 +1436,11 @@ void main() {
1299
1436
  const data = expandToTriangles(mesh);
1300
1437
  const vao = gl.createVertexArray();
1301
1438
  gl.bindVertexArray(vao);
1439
+ const buffers = [];
1302
1440
  const attribute = (location, array, size) => {
1303
1441
  if (location < 0) return;
1304
1442
  const buffer = gl.createBuffer();
1443
+ buffers.push(buffer);
1305
1444
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1306
1445
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
1307
1446
  gl.enableVertexAttribArray(location);
@@ -1311,7 +1450,7 @@ void main() {
1311
1450
  attribute(loc.aColor, data.colors, 3);
1312
1451
  attribute(loc.aUV, planarUVs(mesh), 2);
1313
1452
  gl.bindVertexArray(null);
1314
- const result = { vao, count: data.count };
1453
+ const result = { vao, buffers, count: data.count };
1315
1454
  this.buffers.set(mesh, result);
1316
1455
  return result;
1317
1456
  }
@@ -1349,7 +1488,10 @@ void main() {
1349
1488
  const gl = this.gl;
1350
1489
  if (gl) {
1351
1490
  for (const texture of this.textures.values()) gl.deleteTexture(texture);
1352
- for (const buffers of this.buffers.values()) gl.deleteVertexArray(buffers.vao);
1491
+ for (const cached of this.buffers.values()) {
1492
+ gl.deleteVertexArray(cached.vao);
1493
+ for (const buffer of cached.buffers) gl.deleteBuffer(buffer);
1494
+ }
1353
1495
  if (this.program) gl.deleteProgram(this.program);
1354
1496
  }
1355
1497
  this.textures.clear();
@@ -1501,6 +1643,7 @@ void main() {
1501
1643
  var stdin_exports = {};
1502
1644
  __export(stdin_exports, {
1503
1645
  Camera: () => Camera,
1646
+ Canvas2DTexturedRenderer: () => Canvas2DTexturedRenderer,
1504
1647
  ChargedOrbAnimation: () => ChargedOrbAnimation,
1505
1648
  CompositeAnimation: () => CompositeAnimation,
1506
1649
  GridAssemblyAnimation: () => GridAssemblyAnimation,
@@ -1512,6 +1655,7 @@ void main() {
1512
1655
  SpinAnimation: () => SpinAnimation,
1513
1656
  WebGLTexturedRenderer: () => WebGLTexturedRenderer,
1514
1657
  WebGPUTexturedRenderer: () => WebGPUTexturedRenderer,
1658
+ attachMaterial: () => attachMaterial,
1515
1659
  centerAndScaleMesh: () => centerAndScaleMesh,
1516
1660
  chargedOrb: () => chargedOrb,
1517
1661
  circleMotion: () => circleMotion,
@@ -1729,6 +1873,12 @@ void main() {
1729
1873
  init_math();
1730
1874
 
1731
1875
  // src/engines/little-3d-engine/core/mesh.ts
1876
+ function attachMaterial(mesh, material) {
1877
+ if (material) {
1878
+ for (const face of mesh.faces) face.material = material;
1879
+ }
1880
+ return mesh;
1881
+ }
1732
1882
  function transform(init) {
1733
1883
  return {
1734
1884
  position: init?.position ?? { x: 0, y: 0, z: 0 },
@@ -1743,7 +1893,7 @@ void main() {
1743
1893
 
1744
1894
  // src/engines/little-3d-engine/shapes/primitives/cube.ts
1745
1895
  var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1746
- function cube(size = 1, colors = DEFAULT_COLORS) {
1896
+ function cube(size = 1, colors = DEFAULT_COLORS, material) {
1747
1897
  const h = size / 2;
1748
1898
  const vertices = [
1749
1899
  { x: -h, y: -h, z: h },
@@ -1763,12 +1913,12 @@ void main() {
1763
1913
  { indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
1764
1914
  { indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
1765
1915
  ];
1766
- return { vertices, faces };
1916
+ return attachMaterial({ vertices, faces }, material);
1767
1917
  }
1768
1918
 
1769
1919
  // src/engines/little-3d-engine/shapes/primitives/quad.ts
1770
1920
  var DEFAULT_COLORS2 = ["#3b82f6"];
1771
- function quad(size = 1, colors = DEFAULT_COLORS2) {
1921
+ function quad(size = 1, colors = DEFAULT_COLORS2, material) {
1772
1922
  const s = size / 2;
1773
1923
  const vertices = [
1774
1924
  { x: -s, y: -s, z: 0 },
@@ -1776,12 +1926,15 @@ void main() {
1776
1926
  { x: s, y: s, z: 0 },
1777
1927
  { x: -s, y: s, z: 0 }
1778
1928
  ];
1779
- return { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] };
1929
+ return attachMaterial(
1930
+ { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] },
1931
+ material
1932
+ );
1780
1933
  }
1781
1934
 
1782
1935
  // src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
1783
1936
  var DEFAULT_COLORS3 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
1784
- function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1937
+ function tetrahedron(size = 1, colors = DEFAULT_COLORS3, material) {
1785
1938
  const s = size / 2;
1786
1939
  const vertices = [
1787
1940
  { x: s, y: s, z: s },
@@ -1795,7 +1948,7 @@ void main() {
1795
1948
  { indices: [0, 2, 3], color: colors[2 % colors.length] },
1796
1949
  { indices: [1, 3, 2], color: colors[3 % colors.length] }
1797
1950
  ];
1798
- return { vertices, faces };
1951
+ return attachMaterial({ vertices, faces }, material);
1799
1952
  }
1800
1953
 
1801
1954
  // src/engines/little-3d-engine/shapes/primitives/octahedron.ts
@@ -1809,7 +1962,7 @@ void main() {
1809
1962
  "#06b6d4",
1810
1963
  "#eab308"
1811
1964
  ];
1812
- function octahedron(size = 1, colors = DEFAULT_COLORS4) {
1965
+ function octahedron(size = 1, colors = DEFAULT_COLORS4, material) {
1813
1966
  const r = size / 2;
1814
1967
  const vertices = [
1815
1968
  { x: r, y: 0, z: 0 },
@@ -1829,12 +1982,12 @@ void main() {
1829
1982
  { indices: [5, 3, 1], color: colors[6 % colors.length] },
1830
1983
  { indices: [5, 0, 3], color: colors[7 % colors.length] }
1831
1984
  ];
1832
- return { vertices, faces };
1985
+ return attachMaterial({ vertices, faces }, material);
1833
1986
  }
1834
1987
 
1835
1988
  // src/engines/little-3d-engine/shapes/primitives/pyramid.ts
1836
1989
  var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1837
- function pyramid(size = 1, colors = DEFAULT_COLORS5) {
1990
+ function pyramid(size = 1, colors = DEFAULT_COLORS5, material) {
1838
1991
  const h = size / 2;
1839
1992
  const vertices = [
1840
1993
  { x: -h, y: -h, z: h },
@@ -1850,12 +2003,12 @@ void main() {
1850
2003
  { indices: [4, 2, 3], color: colors[3 % colors.length] },
1851
2004
  { indices: [4, 3, 0], color: colors[4 % colors.length] }
1852
2005
  ];
1853
- return { vertices, faces };
2006
+ return attachMaterial({ vertices, faces }, material);
1854
2007
  }
1855
2008
 
1856
2009
  // src/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.ts
1857
2010
  var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1858
- function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
2011
+ function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6, material) {
1859
2012
  const r = size / 2;
1860
2013
  const d = Math.max(1, Math.floor(detail));
1861
2014
  const slices = Math.max(4, d * 4);
@@ -1894,7 +2047,7 @@ void main() {
1894
2047
  color: color()
1895
2048
  });
1896
2049
  }
1897
- return { vertices, faces };
2050
+ return attachMaterial({ vertices, faces }, material);
1898
2051
  }
1899
2052
 
1900
2053
  // src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
@@ -1937,8 +2090,11 @@ void main() {
1937
2090
  [8, 6, 7],
1938
2091
  [9, 8, 1]
1939
2092
  ];
1940
- function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7) {
1941
- return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
2093
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7, material) {
2094
+ return attachMaterial(
2095
+ sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors),
2096
+ material
2097
+ );
1942
2098
  }
1943
2099
 
1944
2100
  // src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
@@ -1962,8 +2118,11 @@ void main() {
1962
2118
  [5, 3, 1],
1963
2119
  [5, 0, 3]
1964
2120
  ];
1965
- function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8) {
1966
- return sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors);
2121
+ function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8, material) {
2122
+ return attachMaterial(
2123
+ sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors),
2124
+ material
2125
+ );
1967
2126
  }
1968
2127
 
1969
2128
  // src/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.ts
@@ -1976,7 +2135,7 @@ void main() {
1976
2135
  { normal: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
1977
2136
  { normal: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] }
1978
2137
  ];
1979
- function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9) {
2138
+ function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9, material) {
1980
2139
  const r = size / 2;
1981
2140
  const n = Math.max(1, Math.floor(detail));
1982
2141
  const vertices = [];
@@ -2005,13 +2164,13 @@ void main() {
2005
2164
  }
2006
2165
  }
2007
2166
  }
2008
- return { vertices, faces };
2167
+ return attachMaterial({ vertices, faces }, material);
2009
2168
  }
2010
2169
 
2011
2170
  // src/engines/little-3d-engine/shapes/complex/plane.ts
2012
2171
  var DEFAULT_COLORS10 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
2013
- function planeMesh(colors = DEFAULT_COLORS10) {
2014
- return {
2172
+ function planeMesh(colors = DEFAULT_COLORS10, material) {
2173
+ return attachMaterial({
2015
2174
  vertices: [
2016
2175
  { x: 0.9, y: 0, z: 0 },
2017
2176
  { x: -0.2, y: 0, z: 0.82 },
@@ -2039,7 +2198,7 @@ void main() {
2039
2198
  { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS10[0] },
2040
2199
  { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS10[1] }
2041
2200
  ]
2042
- };
2201
+ }, material);
2043
2202
  }
2044
2203
 
2045
2204
  // src/engines/little-3d-engine/textures/dynamic/canvas-texture.ts
@@ -2596,10 +2755,14 @@ void main() {
2596
2755
  const pick = Array.isArray(color) ? (i) => color[i % color.length] : () => color;
2597
2756
  return { vertices: mesh.vertices, faces: mesh.faces.map((f, i) => ({ ...f, color: pick(i) })) };
2598
2757
  }
2758
+ function applyMaterial(mesh, material) {
2759
+ if (!material) return mesh;
2760
+ return { vertices: mesh.vertices, faces: mesh.faces.map((f) => ({ ...f, material })) };
2761
+ }
2599
2762
  var SpinAnimation = class {
2600
2763
  constructor(options = {}) {
2601
2764
  this.exited = false;
2602
- this.mesh = applyColor(resolveMesh(options.shape), options.color);
2765
+ this.mesh = applyMaterial(applyColor(resolveMesh(options.shape), options.color), options.material);
2603
2766
  this.spinX = options.spinX ?? 7e-4;
2604
2767
  this.spinY = options.spinY ?? 11e-4;
2605
2768
  this.backend = options.backend;
@@ -3292,6 +3455,7 @@ void main() {
3292
3455
  var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
3293
3456
  var CENTER_POP_OUT_MS = 420;
3294
3457
  var PARKED = { x: 0, y: 0, z: 50 };
3458
+ var ORB_MATERIAL = { specular: [1, 1, 1], shininess: 28 };
3295
3459
  var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
3296
3460
  var MINI_COLORS = [
3297
3461
  ["#e0f2fe", "#bae6fd", "#7dd3fc"],
@@ -3320,9 +3484,9 @@ void main() {
3320
3484
  backend: this.backend,
3321
3485
  camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
3322
3486
  });
3323
- this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
3487
+ this.center = engine.add(icosphere(1, 2, CENTER_COLORS, ORB_MATERIAL), { scale: 0 });
3324
3488
  for (let i = 0; i < MINIS; i++) {
3325
- const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
3489
+ const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length], ORB_MATERIAL);
3326
3490
  this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
3327
3491
  }
3328
3492
  this.engine = engine;
@@ -3907,6 +4071,7 @@ void main() {
3907
4071
  var TRAIL_OUTRO_MS = 1200;
3908
4072
  var TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
3909
4073
  var CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
4074
+ var CAR_MATERIAL = { emissive: [0.1, 0.2, 0.3] };
3910
4075
  var WORLD_UP2 = { x: 0, y: 1, z: 0 };
3911
4076
  function clamp016(value) {
3912
4077
  return Math.max(0, Math.min(1, value));
@@ -3967,7 +4132,7 @@ void main() {
3967
4132
  backend: this.backend,
3968
4133
  camera: { position: { x: 0, y: 0, z: CAMERA_Z3 }, fov: FOV2 }
3969
4134
  });
3970
- const mesh = cube(1, CAR_COLORS);
4135
+ const mesh = cube(1, CAR_COLORS, CAR_MATERIAL);
3971
4136
  for (let i = 0; i < MAX_CARS; i++) {
3972
4137
  this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
3973
4138
  }
@@ -4649,32 +4814,69 @@ void main() {
4649
4814
 
4650
4815
  // <stdin>
4651
4816
  init_webgl_textured();
4817
+ init_canvas2d_textured();
4652
4818
  init_webgpu_textured();
4653
4819
 
4654
4820
  // src/engines/little-3d-engine/loaders/obj.ts
4655
4821
  var DEFAULT_COLORS12 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
4822
+ function clamp018(value) {
4823
+ return Math.min(1, Math.max(0, value));
4824
+ }
4656
4825
  function channelToHex(value) {
4657
4826
  const channel = Number.parseFloat(value);
4658
4827
  if (!Number.isFinite(channel)) return void 0;
4659
- return Math.round(Math.min(1, Math.max(0, channel)) * 255).toString(16).padStart(2, "0");
4660
- }
4661
- function parseMtlColors(text) {
4662
- const colors = /* @__PURE__ */ new Map();
4663
- let material;
4828
+ return Math.round(clamp018(channel) * 255).toString(16).padStart(2, "0");
4829
+ }
4830
+ function parseRgb(parts) {
4831
+ const channels = parts.slice(1, 4).map(Number.parseFloat);
4832
+ if (channels.length !== 3 || !channels.every(Number.isFinite)) return void 0;
4833
+ return [clamp018(channels[0]), clamp018(channels[1]), clamp018(channels[2])];
4834
+ }
4835
+ function toMaterial(surface) {
4836
+ const material = {};
4837
+ if (surface.specular) material.specular = surface.specular;
4838
+ if (surface.shininess !== void 0) material.shininess = surface.shininess;
4839
+ if (surface.emissive) material.emissive = surface.emissive;
4840
+ return Object.keys(material).length > 0 ? material : void 0;
4841
+ }
4842
+ function parseMtl(text) {
4843
+ const materials = /* @__PURE__ */ new Map();
4844
+ const surfaces = /* @__PURE__ */ new Map();
4845
+ let name;
4664
4846
  for (const line of text.split("\n")) {
4665
4847
  const trimmed = line.trim();
4666
4848
  if (trimmed === "" || trimmed.startsWith("#")) continue;
4667
4849
  const parts = trimmed.split(/\s+/);
4668
- if (parts[0] === "newmtl") {
4669
- material = parts.slice(1).join(" ");
4670
- } else if (parts[0] === "Kd" && material) {
4850
+ const keyword = parts[0];
4851
+ if (keyword === "newmtl") {
4852
+ name = parts.slice(1).join(" ");
4853
+ if (name && !materials.has(name)) {
4854
+ materials.set(name, {});
4855
+ surfaces.set(name, {});
4856
+ }
4857
+ continue;
4858
+ }
4859
+ if (!name) continue;
4860
+ const entry = materials.get(name);
4861
+ const surface = surfaces.get(name);
4862
+ if (keyword === "Kd") {
4671
4863
  const channels = parts.slice(1, 4).map(channelToHex);
4672
4864
  if (channels.length === 3 && channels.every((channel) => channel !== void 0)) {
4673
- colors.set(material, `#${channels.join("")}`);
4865
+ entry.color = `#${channels.join("")}`;
4674
4866
  }
4867
+ } else if (keyword === "Ks") {
4868
+ surface.specular = parseRgb(parts);
4869
+ } else if (keyword === "Ns") {
4870
+ const ns = Number.parseFloat(parts[1]);
4871
+ if (Number.isFinite(ns)) surface.shininess = Math.max(0, ns);
4872
+ } else if (keyword === "Ke") {
4873
+ surface.emissive = parseRgb(parts);
4675
4874
  }
4676
4875
  }
4677
- return colors;
4876
+ for (const [key, surface] of surfaces) {
4877
+ materials.get(key).material = toMaterial(surface);
4878
+ }
4879
+ return materials;
4678
4880
  }
4679
4881
  function resolveIndex(token, vertexCount) {
4680
4882
  const n = parseInt(token, 10);
@@ -4682,7 +4884,7 @@ void main() {
4682
4884
  }
4683
4885
  function parseObj(text, options = {}) {
4684
4886
  const colors = options.colors ?? DEFAULT_COLORS12;
4685
- const materialColors = options.useMtlColors && options.mtl ? parseMtlColors(options.mtl) : void 0;
4887
+ const materials = options.useMtlColors && options.mtl ? parseMtl(options.mtl) : void 0;
4686
4888
  const vertices = [];
4687
4889
  const faces = [];
4688
4890
  let material;
@@ -4706,9 +4908,11 @@ void main() {
4706
4908
  indices.push(resolveIndex(vertexToken, vertices.length));
4707
4909
  }
4708
4910
  if (indices.length >= 3) {
4709
- const mtlColor = material ? materialColors?.get(material) : void 0;
4710
- const color = mtlColor ?? (materialColors ? colors[0] ?? "#888888" : colors[faces.length % colors.length]);
4711
- faces.push({ indices, color });
4911
+ const entry = material ? materials?.get(material) : void 0;
4912
+ const color = entry?.color ?? (materials ? colors[0] ?? "#888888" : colors[faces.length % colors.length]);
4913
+ const face = { indices, color };
4914
+ if (entry?.material) face.material = entry.material;
4915
+ faces.push(face);
4712
4916
  }
4713
4917
  }
4714
4918
  }