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
@@ -19,6 +19,8 @@ const REENTER_STAGGER_MS = 45;
19
19
  const CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
20
20
  const CENTER_POP_OUT_MS = 420;
21
21
  const PARKED = { x: 0, y: 0, z: 50 };
22
+ // A glossy white highlight over the cyan orbs, for a charged, energetic sheen.
23
+ const ORB_MATERIAL = { specular: [1, 1, 1], shininess: 28 };
22
24
  const CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
23
25
  const MINI_COLORS = [
24
26
  ["#e0f2fe", "#bae6fd", "#7dd3fc"],
@@ -58,9 +60,9 @@ export class ChargedOrbAnimation {
58
60
  backend: this.backend,
59
61
  camera: { position: { x: 0, y: 0, z: CAMERA_Z } },
60
62
  });
61
- this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
63
+ this.center = engine.add(icosphere(1, 2, CENTER_COLORS, ORB_MATERIAL), { scale: 0 });
62
64
  for (let i = 0; i < MINIS; i++) {
63
- const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
65
+ const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length], ORB_MATERIAL);
64
66
  this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
65
67
  }
66
68
  this.engine = engine;
@@ -4,7 +4,7 @@ import type { MotionController } from "../motion/controller.js";
4
4
  export interface GhostTrainOptions {
5
5
  /** How the convoy moves. Default a tilted square track. */
6
6
  motion?: MotionController;
7
- /** Uniform car size in scene units. Default `0.3`. */
7
+ /** Uniform car size in scene units. Default `0.15`. */
8
8
  size?: number;
9
9
  /** Rendering backend. Default `"canvas2d"`. */
10
10
  backend?: Backend;
@@ -18,6 +18,8 @@ const WARP_ACCEL = 1000; // extra path-milliseconds accrued, = 0.5 * WARP_ACCEL
18
18
  const TRAIL_OUTRO_MS = 1200; // how long the lead keeps shedding stars into the outro
19
19
  const TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
20
20
  const CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
21
+ // A faint icy self-glow so the translucent cars read as ghostly rather than flat.
22
+ const CAR_MATERIAL = { emissive: [0.1, 0.2, 0.3] };
21
23
  const WORLD_UP = { x: 0, y: 1, z: 0 };
22
24
  function clamp01(value) {
23
25
  return Math.max(0, Math.min(1, value));
@@ -92,7 +94,7 @@ export class GhostTrainAnimation {
92
94
  backend: this.backend,
93
95
  camera: { position: { x: 0, y: 0, z: CAMERA_Z }, fov: FOV },
94
96
  });
95
- const mesh = cube(1, CAR_COLORS);
97
+ const mesh = cube(1, CAR_COLORS, CAR_MATERIAL);
96
98
  for (let i = 0; i < MAX_CARS; i++) {
97
99
  this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
98
100
  }
@@ -1,7 +1,7 @@
1
1
  import type { AnimationFrame, AnimationLabel, SpinnerAnimation } from "../animation.js";
2
2
  import { type Backend, type Mesh } from "../engines/little-3d-engine/little-3d-engine.js";
3
3
  export interface GridAssemblyOptions {
4
- /** Meshes (or factories) cycled across the 25 shapes. Defaults to a mixed primitive set. */
4
+ /** Meshes (or factories) cycled across the 25 shapes. Defaults to a single pastel dark-blue cube. */
5
5
  meshes?: Array<Mesh | (() => Mesh)>;
6
6
  /** Uniform shape size in scene units. Default `0.34`. */
7
7
  size?: number;
@@ -1,11 +1,13 @@
1
1
  import type { AnimationFrame, SpinnerAnimation } from "../animation.js";
2
- import { type Backend, type Mesh, type Transparency } from "../engines/little-3d-engine/little-3d-engine.js";
2
+ import { type Backend, type Material, type Mesh, type Transparency } from "../engines/little-3d-engine/little-3d-engine.js";
3
3
  import { type ProgressAnimationOptions } from "../progress-animation.js";
4
4
  export interface SpinAnimationOptions {
5
5
  /** Shape to spin: a mesh, or a factory that returns one. Default: a cube. */
6
6
  shape?: Mesh | (() => Mesh);
7
7
  /** Face color(s): one color for every face, or an array applied per face. */
8
8
  color?: string | string[];
9
+ /** Surface material (specular, shininess, emissive) applied to every face. */
10
+ material?: Material;
9
11
  /** Rotation speed around the X axis, in radians per millisecond. Default `0.0007`. */
10
12
  spinX?: number;
11
13
  /** Rotation speed around the Y axis, in radians per millisecond. Default `0.0011`. */
@@ -24,6 +24,11 @@ function applyColor(mesh, color) {
24
24
  const pick = Array.isArray(color) ? (i) => color[i % color.length] : () => color;
25
25
  return { vertices: mesh.vertices, faces: mesh.faces.map((f, i) => ({ ...f, color: pick(i) })) };
26
26
  }
27
+ function applyMaterial(mesh, material) {
28
+ if (!material)
29
+ return mesh;
30
+ return { vertices: mesh.vertices, faces: mesh.faces.map((f) => ({ ...f, material })) };
31
+ }
27
32
  /**
28
33
  * A spinning, flat-lit 3D shape (a cube by default). With `progressAnimation`
29
34
  * set it pops in/out and its scale tracks progress, with an optional label;
@@ -32,7 +37,7 @@ function applyColor(mesh, color) {
32
37
  export class SpinAnimation {
33
38
  constructor(options = {}) {
34
39
  this.exited = false;
35
- this.mesh = applyColor(resolveMesh(options.shape), options.color);
40
+ this.mesh = applyMaterial(applyColor(resolveMesh(options.shape), options.color), options.material);
36
41
  this.spinX = options.spinX ?? 0.0007;
37
42
  this.spinY = options.spinY ?? 0.0011;
38
43
  this.backend = options.backend;
@@ -136,7 +136,10 @@ function expandToTriangles(mesh) {
136
136
  const positions = new Float32Array(triangles * 9);
137
137
  const normals = new Float32Array(triangles * 9);
138
138
  const colors = new Float32Array(triangles * 9);
139
+ const emissives = new Float32Array(triangles * 9);
140
+ const speculars = new Float32Array(triangles * 12);
139
141
  let o = 0;
142
+ let so = 0;
140
143
  for (const face of mesh.faces) {
141
144
  const v0 = mesh.vertices[face.indices[0]];
142
145
  const v1 = mesh.vertices[face.indices[1]];
@@ -146,6 +149,15 @@ function expandToTriangles(mesh) {
146
149
  const cr = r / 255;
147
150
  const cg = g / 255;
148
151
  const cb = b / 255;
152
+ const emissive = face.material?.emissive;
153
+ const er = emissive ? emissive[0] : 0;
154
+ const eg = emissive ? emissive[1] : 0;
155
+ const eb = emissive ? emissive[2] : 0;
156
+ const specular = face.material?.specular;
157
+ const sr = specular ? specular[0] : 0;
158
+ const sg = specular ? specular[1] : 0;
159
+ const sb = specular ? specular[2] : 0;
160
+ const sn = specular ? face.material?.shininess ?? 32 : 1;
149
161
  for (let k = 1; k < face.indices.length - 1; k++) {
150
162
  const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
151
163
  for (const index of tri) {
@@ -159,11 +171,19 @@ function expandToTriangles(mesh) {
159
171
  colors[o] = cr;
160
172
  colors[o + 1] = cg;
161
173
  colors[o + 2] = cb;
174
+ emissives[o] = er;
175
+ emissives[o + 1] = eg;
176
+ emissives[o + 2] = eb;
177
+ speculars[so] = sr;
178
+ speculars[so + 1] = sg;
179
+ speculars[so + 2] = sb;
180
+ speculars[so + 3] = sn;
162
181
  o += 3;
182
+ so += 4;
163
183
  }
164
184
  }
165
185
  }
166
- return { positions, normals, colors, count: positions.length / 3 };
186
+ return { positions, normals, colors, emissives, speculars, count: positions.length / 3 };
167
187
  }
168
188
  function midpoint(a, b) {
169
189
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
@@ -201,13 +221,42 @@ var init_geometry = __esm({
201
221
  function clamp01(value) {
202
222
  return Math.min(1, Math.max(0, value));
203
223
  }
204
- function shadeColor(normal, color, light) {
224
+ function clamp255(value) {
225
+ return Math.round(Math.min(255, Math.max(0, value)));
226
+ }
227
+ function shade(normal, color, light, surface) {
205
228
  const lambert = Math.max(0, dot(normal, light.toLight));
206
229
  const brightness = clamp01(light.ambient + light.intensity * lambert);
207
- const [r, g, b] = parseColor(color);
208
- return `rgb(${Math.round(r * brightness)}, ${Math.round(g * brightness)}, ${Math.round(
209
- b * brightness
210
- )})`;
230
+ const [baseR, baseG, baseB] = parseColor(color);
231
+ let r = baseR * brightness;
232
+ let g = baseG * brightness;
233
+ let b = baseB * brightness;
234
+ const material = surface?.material;
235
+ const specular = material?.specular;
236
+ const viewDir = surface?.viewDir;
237
+ if (specular && viewDir && lambert > 0) {
238
+ const half = normalize({
239
+ x: light.toLight.x + viewDir.x,
240
+ y: light.toLight.y + viewDir.y,
241
+ z: light.toLight.z + viewDir.z
242
+ });
243
+ const shininess = material?.shininess ?? 32;
244
+ const highlight = Math.pow(Math.max(0, dot(normal, half)), shininess) * light.intensity * 255;
245
+ r += highlight * specular[0];
246
+ g += highlight * specular[1];
247
+ b += highlight * specular[2];
248
+ }
249
+ const emissive = material?.emissive;
250
+ if (emissive) {
251
+ r += emissive[0] * 255;
252
+ g += emissive[1] * 255;
253
+ b += emissive[2] * 255;
254
+ }
255
+ return [clamp255(r), clamp255(g), clamp255(b)];
256
+ }
257
+ function shadeColor(normal, color, light, surface) {
258
+ const [r, g, b] = shade(normal, color, light, surface);
259
+ return `rgb(${r}, ${g}, ${b})`;
211
260
  }
212
261
  var DEFAULTS2, Light;
213
262
  var init_light = __esm({
@@ -230,8 +279,8 @@ var init_light = __esm({
230
279
  };
231
280
  }
232
281
  /** Convenience wrapper around {@link shadeColor} using this light. */
233
- shade(normal, color) {
234
- return shadeColor(normal, color, this.params);
282
+ shade(normal, color, surface) {
283
+ return shadeColor(normal, color, this.params, surface);
235
284
  }
236
285
  };
237
286
  }
@@ -271,28 +320,51 @@ var init_webgl = __esm({
271
320
  in vec3 aPos;
272
321
  in vec3 aNormal;
273
322
  in vec3 aColor;
323
+ in vec3 aEmissive;
324
+ in vec4 aSpecular;
274
325
  uniform mat4 uViewProj;
275
326
  uniform mat4 uModel;
276
327
  out vec3 vNormal;
277
328
  out vec3 vColor;
329
+ out vec3 vEmissive;
330
+ out vec4 vSpecular;
331
+ out vec3 vWorldPos;
278
332
  void main() {
279
333
  vNormal = mat3(uModel) * aNormal;
280
334
  vColor = aColor;
281
- gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
335
+ vEmissive = aEmissive;
336
+ vSpecular = aSpecular;
337
+ vec4 world = uModel * vec4(aPos, 1.0);
338
+ vWorldPos = world.xyz;
339
+ gl_Position = uViewProj * world;
282
340
  }`;
283
341
  FRAGMENT_SHADER = `#version 300 es
284
342
  precision mediump float;
285
343
  in vec3 vNormal;
286
344
  in vec3 vColor;
345
+ in vec3 vEmissive;
346
+ in vec4 vSpecular;
347
+ in vec3 vWorldPos;
287
348
  uniform vec3 uToLight;
349
+ uniform vec3 uEye;
288
350
  uniform float uIntensity;
289
351
  uniform float uAmbient;
290
352
  uniform float uOpacity;
291
353
  out vec4 fragColor;
292
354
  void main() {
293
- float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
355
+ vec3 normal = normalize(vNormal);
356
+ vec3 toLight = normalize(uToLight);
357
+ float lambert = max(dot(normal, toLight), 0.0);
294
358
  float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
295
- fragColor = vec4(vColor * brightness, uOpacity);
359
+ vec3 lit = vColor * brightness;
360
+ if (lambert > 0.0) {
361
+ vec3 viewDir = normalize(uEye - vWorldPos);
362
+ vec3 halfVec = normalize(toLight + viewDir);
363
+ float highlight = pow(max(dot(normal, halfVec), 0.0), vSpecular.w) * uIntensity;
364
+ lit += highlight * vSpecular.xyz;
365
+ }
366
+ lit += vEmissive;
367
+ fragColor = vec4(lit, uOpacity);
296
368
  }`;
297
369
  WebGLRenderer = class {
298
370
  constructor(options = {}) {
@@ -313,9 +385,12 @@ void main() {
313
385
  aPos: gl.getAttribLocation(this.program, "aPos"),
314
386
  aNormal: gl.getAttribLocation(this.program, "aNormal"),
315
387
  aColor: gl.getAttribLocation(this.program, "aColor"),
388
+ aEmissive: gl.getAttribLocation(this.program, "aEmissive"),
389
+ aSpecular: gl.getAttribLocation(this.program, "aSpecular"),
316
390
  uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
317
391
  uModel: gl.getUniformLocation(this.program, "uModel"),
318
392
  uToLight: gl.getUniformLocation(this.program, "uToLight"),
393
+ uEye: gl.getUniformLocation(this.program, "uEye"),
319
394
  uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
320
395
  uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
321
396
  uOpacity: gl.getUniformLocation(this.program, "uOpacity")
@@ -339,19 +414,23 @@ void main() {
339
414
  const data = expandToTriangles(mesh);
340
415
  const vao = gl.createVertexArray();
341
416
  gl.bindVertexArray(vao);
342
- const attribute = (location, array) => {
417
+ const buffers = [];
418
+ const attribute = (location, array, size = 3) => {
343
419
  if (location < 0) return;
344
420
  const buffer = gl.createBuffer();
421
+ buffers.push(buffer);
345
422
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
346
423
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
347
424
  gl.enableVertexAttribArray(location);
348
- gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
425
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
349
426
  };
350
427
  attribute(loc.aPos, data.positions);
351
428
  attribute(loc.aNormal, data.normals);
352
429
  attribute(loc.aColor, data.colors);
430
+ attribute(loc.aEmissive, data.emissives);
431
+ attribute(loc.aSpecular, data.speculars, 4);
353
432
  gl.bindVertexArray(null);
354
- const result = { vao, count: data.count };
433
+ const result = { vao, buffers, count: data.count };
355
434
  this.cache.set(mesh, result);
356
435
  return result;
357
436
  }
@@ -364,6 +443,7 @@ void main() {
364
443
  gl.useProgram(this.program);
365
444
  gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
366
445
  gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
446
+ gl.uniform3f(loc.uEye, frame.eye.x, frame.eye.y, frame.eye.z);
367
447
  gl.uniform1f(loc.uIntensity, frame.light.intensity);
368
448
  gl.uniform1f(loc.uAmbient, frame.light.ambient);
369
449
  gl.disable(gl.BLEND);
@@ -408,7 +488,10 @@ void main() {
408
488
  destroy() {
409
489
  const gl = this.gl;
410
490
  if (gl) {
411
- for (const mesh of this.cache.values()) gl.deleteVertexArray(mesh.vao);
491
+ for (const mesh of this.cache.values()) {
492
+ gl.deleteVertexArray(mesh.vao);
493
+ for (const buffer of mesh.buffers) gl.deleteBuffer(buffer);
494
+ }
412
495
  if (this.program) gl.deleteProgram(this.program);
413
496
  }
414
497
  this.cache.clear();
@@ -438,6 +521,7 @@ struct Uniforms {
438
521
  model: mat4x4<f32>,
439
522
  toLight: vec4<f32>,
440
523
  params: vec4<f32>,
524
+ eye: vec4<f32>,
441
525
  };
442
526
  @group(0) @binding(0) var<uniform> u: Uniforms;
443
527
 
@@ -445,23 +529,40 @@ struct VSOut {
445
529
  @builtin(position) position: vec4<f32>,
446
530
  @location(0) normal: vec3<f32>,
447
531
  @location(1) color: vec3<f32>,
532
+ @location(2) emissive: vec3<f32>,
533
+ @location(3) specular: vec4<f32>,
534
+ @location(4) worldPos: vec3<f32>,
448
535
  };
449
536
 
450
537
  @vertex
451
- fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
538
+ 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 {
452
539
  var out: VSOut;
453
540
  let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
454
541
  out.normal = m * normal;
455
542
  out.color = color;
456
- out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
543
+ out.emissive = emissive;
544
+ out.specular = specular;
545
+ let world = u.model * vec4<f32>(pos, 1.0);
546
+ out.worldPos = world.xyz;
547
+ out.position = u.viewProj * world;
457
548
  return out;
458
549
  }
459
550
 
460
551
  @fragment
461
552
  fn fs(in: VSOut) -> @location(0) vec4<f32> {
462
- let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
553
+ let normal = normalize(in.normal);
554
+ let toLight = normalize(u.toLight.xyz);
555
+ let lambert = max(dot(normal, toLight), 0.0);
463
556
  let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
464
- return vec4<f32>(in.color * brightness, u.params.z);
557
+ var lit = in.color * brightness;
558
+ if (lambert > 0.0) {
559
+ let viewDir = normalize(u.eye.xyz - in.worldPos);
560
+ let halfVec = normalize(toLight + viewDir);
561
+ let highlight = pow(max(dot(normal, halfVec), 0.0), in.specular.w) * u.params.x;
562
+ lit = lit + highlight * in.specular.xyz;
563
+ }
564
+ lit = lit + in.emissive;
565
+ return vec4<f32>(lit, u.params.z);
465
566
  }
466
567
  `;
467
568
  CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
@@ -502,13 +603,15 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
502
603
  {
503
604
  binding: 0,
504
605
  visibility: stage.VERTEX | stage.FRAGMENT,
505
- buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
606
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 176 }
506
607
  }
507
608
  ]
508
609
  });
509
- const vertexBuffer = (location) => ({
510
- arrayStride: 12,
511
- attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
610
+ const vertexBuffer = (location, components = 3) => ({
611
+ arrayStride: components * 4,
612
+ attributes: [
613
+ { shaderLocation: location, offset: 0, format: `float32x${components}` }
614
+ ]
512
615
  });
513
616
  const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
514
617
  const blend = {
@@ -524,7 +627,13 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
524
627
  vertex: {
525
628
  module: module2,
526
629
  entryPoint: "vs",
527
- buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
630
+ buffers: [
631
+ vertexBuffer(0),
632
+ vertexBuffer(1),
633
+ vertexBuffer(2),
634
+ vertexBuffer(3),
635
+ vertexBuffer(4, 4)
636
+ ]
528
637
  },
529
638
  fragment: {
530
639
  module: module2,
@@ -577,6 +686,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
577
686
  position: upload(data.positions),
578
687
  normal: upload(data.normals),
579
688
  color: upload(data.colors),
689
+ emissive: upload(data.emissives),
690
+ specular: upload(data.speculars),
580
691
  count: data.count
581
692
  };
582
693
  this.cache.set(mesh, result);
@@ -628,7 +739,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
628
739
  const bindGroup = this.device.createBindGroup({
629
740
  layout,
630
741
  entries: [
631
- { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
742
+ { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 176 } }
632
743
  ]
633
744
  });
634
745
  draws.forEach((draw, i) => {
@@ -637,6 +748,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
637
748
  data.set(draw.item.model, 16);
638
749
  data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
639
750
  data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
751
+ data.set([frame.eye.x, frame.eye.y, frame.eye.z, 0], 40);
640
752
  this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
641
753
  });
642
754
  const encoder = this.device.createCommandEncoder();
@@ -663,6 +775,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
663
775
  pass.setVertexBuffer(0, mesh.position);
664
776
  pass.setVertexBuffer(1, mesh.normal);
665
777
  pass.setVertexBuffer(2, mesh.color);
778
+ pass.setVertexBuffer(3, mesh.emissive);
779
+ pass.setVertexBuffer(4, mesh.specular);
666
780
  pass.draw(mesh.count);
667
781
  });
668
782
  pass.end();
@@ -674,6 +788,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
674
788
  mesh.position.destroy?.();
675
789
  mesh.normal.destroy?.();
676
790
  mesh.color.destroy?.();
791
+ mesh.emissive.destroy?.();
792
+ mesh.specular.destroy?.();
677
793
  }
678
794
  this.cache.clear();
679
795
  this.uniformBuffer?.destroy?.();
@@ -756,9 +872,30 @@ var init_canvas2d = __esm({
756
872
  depth += dot(d, d);
757
873
  }
758
874
  depth /= face.indices.length;
875
+ let surface;
876
+ const material = face.material;
877
+ if (material) {
878
+ if (material.specular) {
879
+ let cx = 0;
880
+ let cy = 0;
881
+ let cz = 0;
882
+ for (const i of face.indices) {
883
+ cx += world[i].x;
884
+ cy += world[i].y;
885
+ cz += world[i].z;
886
+ }
887
+ const inv = 1 / face.indices.length;
888
+ const viewDir = normalize(
889
+ subtract(frame.eye, { x: cx * inv, y: cy * inv, z: cz * inv })
890
+ );
891
+ surface = { material, viewDir };
892
+ } else {
893
+ surface = { material };
894
+ }
895
+ }
759
896
  polygons.push({
760
897
  points,
761
- color: shadeColor(normal, face.color, frame.light),
898
+ color: shadeColor(normal, face.color, frame.light, surface),
762
899
  depth,
763
900
  opacity: faceOpacity
764
901
  });
@@ -888,6 +1025,12 @@ init_light();
888
1025
  init_math();
889
1026
 
890
1027
  // src/engines/little-3d-engine/core/mesh.ts
1028
+ function attachMaterial(mesh, material) {
1029
+ if (material) {
1030
+ for (const face of mesh.faces) face.material = material;
1031
+ }
1032
+ return mesh;
1033
+ }
891
1034
  function transform(init) {
892
1035
  return {
893
1036
  position: init?.position ?? { x: 0, y: 0, z: 0 },
@@ -940,8 +1083,11 @@ var SEED_FACES = [
940
1083
  [8, 6, 7],
941
1084
  [9, 8, 1]
942
1085
  ];
943
- function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS) {
944
- return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
1086
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS, material) {
1087
+ return attachMaterial(
1088
+ sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors),
1089
+ material
1090
+ );
945
1091
  }
946
1092
 
947
1093
  // src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
@@ -1148,6 +1294,7 @@ var REENTER_STAGGER_MS = 45;
1148
1294
  var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
1149
1295
  var CENTER_POP_OUT_MS = 420;
1150
1296
  var PARKED = { x: 0, y: 0, z: 50 };
1297
+ var ORB_MATERIAL = { specular: [1, 1, 1], shininess: 28 };
1151
1298
  var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
1152
1299
  var MINI_COLORS = [
1153
1300
  ["#e0f2fe", "#bae6fd", "#7dd3fc"],
@@ -1176,9 +1323,9 @@ var ChargedOrbAnimation = class {
1176
1323
  backend: this.backend,
1177
1324
  camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
1178
1325
  });
1179
- this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
1326
+ this.center = engine.add(icosphere(1, 2, CENTER_COLORS, ORB_MATERIAL), { scale: 0 });
1180
1327
  for (let i = 0; i < MINIS; i++) {
1181
- const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
1328
+ const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length], ORB_MATERIAL);
1182
1329
  this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
1183
1330
  }
1184
1331
  this.engine = engine;