vizcore 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ import { describeSvgArc, svgArcPoint, svgArcSegmentCount } from "./svg-arc.js";
2
+
1
3
  const BASE_VERTICES = [
2
4
  [-1.0, -1.0, -1.0],
3
5
  [1.0, -1.0, -1.0],
@@ -66,6 +68,11 @@ const MESH_PRESETS = {
66
68
  octahedron: { vertices: OCTAHEDRON_VERTICES, edges: OCTAHEDRON_EDGES },
67
69
  icosahedron: { vertices: ICOSAHEDRON_VERTICES, edges: ICOSAHEDRON_EDGES }
68
70
  };
71
+ const SHAPE_HALF_WIDTH = 640;
72
+ const SHAPE_HALF_HEIGHT = 360;
73
+ const PATH_DEFAULT_MAX_SEGMENTS = 4096;
74
+ const PATH_HARD_MAX_SEGMENTS = 65536;
75
+ const PATH_MAX_RECURSION = 12;
69
76
 
70
77
  export const buildWireframeLines = ({ rotationY, rotationX, deform }) => {
71
78
  const amount = clamp(Number(deform || 0), 0, 1);
@@ -185,14 +192,23 @@ export const buildWaveformLines = ({ time = 0, params = {}, audio = {} } = {}) =
185
192
 
186
193
  export const buildShapeLines = ({ params = {} } = {}) => {
187
194
  const shapes = Array.isArray(params.shapes) ? params.shapes : [];
195
+ const context = shapeCoordinateContext(params);
188
196
  const points = [];
189
197
 
190
198
  shapes.forEach((shape) => {
191
199
  const kind = String(shape?.kind || shape?.type || "").toLowerCase();
192
200
  if (kind === "circle") {
193
- appendCircleShape(points, shape);
201
+ appendCircleShape(points, shape, context);
194
202
  } else if (kind === "line") {
195
- appendLineShape(points, shape);
203
+ appendLineShape(points, shape, context);
204
+ } else if (kind === "rect") {
205
+ appendRectShape(points, shape, context);
206
+ } else if (kind === "polygon" || kind === "polyline") {
207
+ appendPolygonShape(points, shape, context, kind === "polygon");
208
+ } else if (kind === "path") {
209
+ appendPathShape(points, shape, context);
210
+ } else if (kind === "star") {
211
+ appendStarShape(points, shape, context);
196
212
  }
197
213
  });
198
214
 
@@ -223,56 +239,438 @@ const appendLineSegments = (points, samples) => {
223
239
  }
224
240
  };
225
241
 
226
- const appendCircleShape = (points, shape) => {
242
+ const appendCircleShape = (points, shape, context) => {
227
243
  const count = clampInt(shape.count || 1, 1, 64);
228
244
  const segments = clampInt(shape.segments || 96, 12, 256);
229
- const radius = normalizeShapeRadius(shape.radius ?? 100);
230
- const x = normalizeShapeCoordinate(shape.x ?? 0, "x");
231
- const y = normalizeShapeCoordinate(shape.y ?? 0, "y");
245
+ const radius = normalizeShapeLength(shape.radius ?? 100, context, "radius");
246
+ const center = normalizeShapePoint(shape.x ?? 0, shape.y ?? 0, context);
232
247
 
233
248
  for (let ring = 0; ring < count; ring += 1) {
234
249
  const ringRadius = radius * ((ring + 1) / count);
235
250
  for (let index = 0; index < segments; index += 1) {
236
- appendCirclePoint(points, x, y, ringRadius, index, segments);
237
- appendCirclePoint(points, x, y, ringRadius, index + 1, segments);
251
+ appendSegment(
252
+ points,
253
+ circlePoint(center, ringRadius, index, segments),
254
+ circlePoint(center, ringRadius, index + 1, segments),
255
+ shape,
256
+ context
257
+ );
238
258
  }
239
259
  }
240
260
  };
241
261
 
242
- const appendCirclePoint = (points, x, y, radius, index, segments) => {
262
+ const circlePoint = (center, radius, index, segments) => {
243
263
  const angle = (index / segments) * Math.PI * 2;
244
- points.push(
245
- clamp(x + Math.cos(angle) * radius, -1.2, 1.2),
246
- clamp(y + Math.sin(angle) * radius, -1.2, 1.2)
247
- );
264
+ return [center[0] + Math.cos(angle) * radius, center[1] + Math.sin(angle) * radius];
248
265
  };
249
266
 
250
- const appendLineShape = (points, shape) => {
251
- points.push(
252
- normalizeShapeCoordinate(shape.x1 ?? -0.8, "x"),
253
- normalizeShapeCoordinate(shape.y1 ?? 0, "y"),
254
- normalizeShapeCoordinate(shape.x2 ?? 0.8, "x"),
255
- normalizeShapeCoordinate(shape.y2 ?? 0, "y")
267
+ const appendLineShape = (points, shape, context) => {
268
+ const [x1, y1, x2, y2] = lineDefaults(context);
269
+ appendSegment(
270
+ points,
271
+ normalizeShapePoint(shape.x1 ?? x1, shape.y1 ?? y1, context),
272
+ normalizeShapePoint(shape.x2 ?? x2, shape.y2 ?? y2, context),
273
+ shape,
274
+ context
256
275
  );
257
276
  };
258
277
 
259
- const normalizeShapeRadius = (value) => {
260
- const numeric = finiteNumber(value, 100);
261
- const radius = Math.abs(numeric) <= 2 ? Math.abs(numeric) : Math.abs(numeric) / 360;
262
- return clamp(radius, 0.005, 1.4);
278
+ const appendRectShape = (points, shape, context) => {
279
+ const center = normalizeShapePoint(shape.x ?? 0, shape.y ?? 0, context);
280
+ const halfWidth = normalizeShapeLength(shape.width ?? 100, context, "x") / 2;
281
+ const halfHeight = normalizeShapeLength(shape.height ?? 100, context, "y") / 2;
282
+ const vertices = [
283
+ [center[0] - halfWidth, center[1] - halfHeight],
284
+ [center[0] + halfWidth, center[1] - halfHeight],
285
+ [center[0] + halfWidth, center[1] + halfHeight],
286
+ [center[0] - halfWidth, center[1] + halfHeight]
287
+ ];
288
+
289
+ appendPolylineSegments(points, vertices, shape, context, true);
290
+ };
291
+
292
+ const appendPolygonShape = (points, shape, context, defaultClosed) => {
293
+ const vertices = normalizeShapePoints(shape.points, context);
294
+ if (vertices.length < (defaultClosed ? 3 : 2)) {
295
+ return;
296
+ }
297
+
298
+ appendPolylineSegments(points, vertices, shape, context, shape.closed ?? defaultClosed);
299
+ };
300
+
301
+ const appendStarShape = (points, shape, context) => {
302
+ const tips = clampInt(shape.points || 5, 3, 128);
303
+ const center = normalizeShapePoint(shape.x ?? 0, shape.y ?? 0, context);
304
+ const radius = normalizeShapeLength(shape.radius ?? 100, context, "radius");
305
+ const innerRadius = normalizeShapeLength(shape.inner_radius ?? radiusToRawHalf(shape.radius ?? 100), context, "radius");
306
+ const rotation = (finiteNumber(shape.rotation, -90) / 180) * Math.PI;
307
+ const vertices = [];
308
+
309
+ for (let index = 0; index < tips * 2; index += 1) {
310
+ const angle = rotation + (index / (tips * 2)) * Math.PI * 2;
311
+ const pointRadius = index % 2 === 0 ? radius : innerRadius;
312
+ vertices.push([center[0] + Math.cos(angle) * pointRadius, center[1] + Math.sin(angle) * pointRadius]);
313
+ }
314
+
315
+ appendPolylineSegments(points, vertices, shape, context, true);
316
+ };
317
+
318
+ const appendPathShape = (points, shape, context) => {
319
+ const commands = Array.isArray(shape.commands) ? shape.commands : [];
320
+ const detail = clampInt(shape.detail || 32, 4, 128);
321
+ const tolerance = pathTolerance(shape);
322
+ const budget = pathSegmentBudget(shape);
323
+ let current = null;
324
+ let subpathStart = null;
325
+
326
+ commands.forEach((entry) => {
327
+ const command = Array.isArray(entry) ? String(entry[0] || "").toUpperCase() : "";
328
+ const values = Array.isArray(entry) ? entry.slice(1).map((value) => finiteNumber(value, 0)) : [];
329
+
330
+ if (command === "M" && values.length >= 2) {
331
+ current = [values[0], values[1]];
332
+ subpathStart = current;
333
+ } else if (command === "L" && current && values.length >= 2) {
334
+ const next = [values[0], values[1]];
335
+ appendRawSegment(points, current, next, shape, context, budget);
336
+ current = next;
337
+ } else if (command === "H" && current && values.length >= 1) {
338
+ const next = [values[0], current[1]];
339
+ appendRawSegment(points, current, next, shape, context, budget);
340
+ current = next;
341
+ } else if (command === "V" && current && values.length >= 1) {
342
+ const next = [current[0], values[0]];
343
+ appendRawSegment(points, current, next, shape, context, budget);
344
+ current = next;
345
+ } else if (command === "Q" && current && values.length >= 4) {
346
+ current = appendQuadraticPath(points, current, values, detail, tolerance, shape, context, budget);
347
+ } else if (command === "C" && current && values.length >= 6) {
348
+ current = appendCubicPath(points, current, values, detail, tolerance, shape, context, budget);
349
+ } else if (command === "A" && current && values.length >= 7) {
350
+ current = appendArcPath(points, current, values, detail, shape, context, budget);
351
+ } else if (command === "Z" && current && subpathStart) {
352
+ appendRawSegment(points, current, subpathStart, shape, context, budget);
353
+ current = subpathStart;
354
+ }
355
+ });
356
+ };
357
+
358
+ const appendQuadraticPath = (points, current, values, detail, tolerance, shape, context, budget) => {
359
+ let previous = current;
360
+ const control = [values[0], values[1]];
361
+ const end = [values[2], values[3]];
362
+
363
+ if (tolerance !== null) {
364
+ appendAdaptiveQuadraticPath(points, current, control, end, tolerance, shape, context, budget);
365
+ return end;
366
+ }
367
+
368
+ for (let step = 1; step <= detail; step += 1) {
369
+ const t = step / detail;
370
+ const next = [
371
+ quadraticPoint(current[0], control[0], end[0], t),
372
+ quadraticPoint(current[1], control[1], end[1], t)
373
+ ];
374
+ if (!appendRawSegment(points, previous, next, shape, context, budget)) break;
375
+ previous = next;
376
+ }
377
+
378
+ return end;
379
+ };
380
+
381
+ const appendCubicPath = (points, current, values, detail, tolerance, shape, context, budget) => {
382
+ let previous = current;
383
+ const c1 = [values[0], values[1]];
384
+ const c2 = [values[2], values[3]];
385
+ const end = [values[4], values[5]];
386
+
387
+ if (tolerance !== null) {
388
+ appendAdaptiveCubicPath(points, current, c1, c2, end, tolerance, shape, context, budget);
389
+ return end;
390
+ }
391
+
392
+ for (let step = 1; step <= detail; step += 1) {
393
+ const t = step / detail;
394
+ const next = [
395
+ cubicPoint(current[0], c1[0], c2[0], end[0], t),
396
+ cubicPoint(current[1], c1[1], c2[1], end[1], t)
397
+ ];
398
+ if (!appendRawSegment(points, previous, next, shape, context, budget)) break;
399
+ previous = next;
400
+ }
401
+
402
+ return end;
403
+ };
404
+
405
+ const appendArcPath = (points, current, values, detail, shape, context, budget) => {
406
+ const end = [values[5], values[6]];
407
+ const arc = describeSvgArc({
408
+ from: current,
409
+ to: end,
410
+ rx: values[0],
411
+ ry: values[1],
412
+ xAxisRotation: values[2],
413
+ largeArc: !!values[3],
414
+ sweep: !!values[4]
415
+ });
416
+
417
+ if (!arc) {
418
+ appendRawSegment(points, current, end, shape, context, budget);
419
+ return end;
420
+ }
421
+
422
+ let previous = current;
423
+ const segments = svgArcSegmentCount(arc, detail);
424
+ for (let step = 1; step <= segments; step += 1) {
425
+ const next = svgArcPoint(arc, step / segments);
426
+ if (!appendRawSegment(points, previous, next, shape, context, budget)) break;
427
+ previous = next;
428
+ }
429
+
430
+ return end;
431
+ };
432
+
433
+ const appendRawSegment = (points, from, to, shape, context, budget) => {
434
+ if (budget && budget.remaining <= 0) {
435
+ return false;
436
+ }
437
+
438
+ appendSegment(points, normalizeShapePoint(from[0], from[1], context), normalizeShapePoint(to[0], to[1], context), shape, context);
439
+ if (budget) {
440
+ budget.remaining -= 1;
441
+ }
442
+ return true;
263
443
  };
264
444
 
265
- const normalizeShapeCoordinate = (value, axis) => {
445
+ const appendAdaptiveQuadraticPath = (points, from, control, to, tolerance, shape, context, budget, depth = 0) => {
446
+ if (budget && budget.remaining <= 0) return;
447
+
448
+ if (depth >= PATH_MAX_RECURSION || pointLineDistance(control, from, to) <= tolerance) {
449
+ appendRawSegment(points, from, to, shape, context, budget);
450
+ return;
451
+ }
452
+
453
+ const leftControl = midpoint(from, control);
454
+ const rightControl = midpoint(control, to);
455
+ const center = midpoint(leftControl, rightControl);
456
+ appendAdaptiveQuadraticPath(points, from, leftControl, center, tolerance, shape, context, budget, depth + 1);
457
+ appendAdaptiveQuadraticPath(points, center, rightControl, to, tolerance, shape, context, budget, depth + 1);
458
+ };
459
+
460
+ const appendAdaptiveCubicPath = (points, from, c1, c2, to, tolerance, shape, context, budget, depth = 0) => {
461
+ if (budget && budget.remaining <= 0) return;
462
+
463
+ const flatness = Math.max(pointLineDistance(c1, from, to), pointLineDistance(c2, from, to));
464
+ if (depth >= PATH_MAX_RECURSION || flatness <= tolerance) {
465
+ appendRawSegment(points, from, to, shape, context, budget);
466
+ return;
467
+ }
468
+
469
+ const p01 = midpoint(from, c1);
470
+ const p12 = midpoint(c1, c2);
471
+ const p23 = midpoint(c2, to);
472
+ const p012 = midpoint(p01, p12);
473
+ const p123 = midpoint(p12, p23);
474
+ const center = midpoint(p012, p123);
475
+ appendAdaptiveCubicPath(points, from, p01, p012, center, tolerance, shape, context, budget, depth + 1);
476
+ appendAdaptiveCubicPath(points, center, p123, p23, to, tolerance, shape, context, budget, depth + 1);
477
+ };
478
+
479
+ const appendPolylineSegments = (points, vertices, shape, context, closed) => {
480
+ for (let index = 1; index < vertices.length; index += 1) {
481
+ appendSegment(points, vertices[index - 1], vertices[index], shape, context);
482
+ }
483
+
484
+ if (closed && vertices.length > 2) {
485
+ appendSegment(points, vertices[vertices.length - 1], vertices[0], shape, context);
486
+ }
487
+ };
488
+
489
+ const appendSegment = (points, from, to, shape, context) => {
490
+ const start = applyShapeTransform(from, shape, context);
491
+ const end = applyShapeTransform(to, shape, context);
492
+ points.push(start[0], start[1], end[0], end[1]);
493
+ };
494
+
495
+ const normalizeShapePoints = (value, context) => {
496
+ if (!Array.isArray(value)) {
497
+ return [];
498
+ }
499
+
500
+ return value
501
+ .filter((point) => Array.isArray(point) && point.length >= 2)
502
+ .map((point) => normalizeShapePoint(point[0], point[1], context));
503
+ };
504
+
505
+ const shapeCoordinateContext = (params) => {
506
+ const requestedUnits = String(params.units || "").trim().toLowerCase();
507
+ if (requestedUnits) {
508
+ return { units: requestedUnits };
509
+ }
510
+
511
+ const version = Number(params.shape_schema_version ?? params.shapeSchemaVersion ?? 1);
512
+ return { units: version >= 2 ? "logical" : "legacy" };
513
+ };
514
+
515
+ const normalizeShapePoint = (x, y, context) => {
516
+ return [
517
+ normalizeShapeCoordinate(x, context, "x"),
518
+ normalizeShapeCoordinate(y, context, "y")
519
+ ];
520
+ };
521
+
522
+ const normalizeShapeCoordinate = (value, context, axis) => {
266
523
  const numeric = finiteNumber(value, 0);
524
+ if (context.units === "ndc") {
525
+ return clamp(numeric, -1.2, 1.2);
526
+ }
527
+
528
+ if (logicalShapeUnits(context.units)) {
529
+ return clamp(numeric / shapeAxisHalf(axis), -1.2, 1.2);
530
+ }
531
+
532
+ if (screenShapeUnits(context.units)) {
533
+ return axis === "y"
534
+ ? clamp(1 - numeric / SHAPE_HALF_HEIGHT, -1.2, 1.2)
535
+ : clamp(numeric / SHAPE_HALF_WIDTH - 1, -1.2, 1.2);
536
+ }
537
+
538
+ return normalizeLegacyShapeCoordinate(numeric, axis);
539
+ };
540
+
541
+ const normalizeLegacyShapeCoordinate = (numeric, axis) => {
267
542
  if (Math.abs(numeric) <= 1.5) {
268
543
  return clamp(numeric, -1.2, 1.2);
269
544
  }
270
545
 
271
- if (axis === "y") {
272
- return clamp(1 - numeric / 360, -1.2, 1.2);
546
+ return axis === "y"
547
+ ? clamp(1 - numeric / SHAPE_HALF_HEIGHT, -1.2, 1.2)
548
+ : clamp(numeric / SHAPE_HALF_WIDTH - 1, -1.2, 1.2);
549
+ };
550
+
551
+ const normalizeShapeLength = (value, context, axis) => {
552
+ const numeric = Math.abs(finiteNumber(value, 0));
553
+ if (context.units === "ndc" || Math.abs(numeric) <= 2) {
554
+ return clamp(numeric, 0.005, 1.4);
555
+ }
556
+
557
+ return clamp(numeric / shapeAxisHalf(axis === "radius" ? "y" : axis), 0.005, 1.4);
558
+ };
559
+
560
+ const normalizeShapeVector = (value, context, axis) => {
561
+ const numeric = finiteNumber(value, 0);
562
+ if (context.units === "ndc") {
563
+ return clamp(numeric, -2.0, 2.0);
273
564
  }
274
565
 
275
- return clamp(numeric / 640 - 1, -1.2, 1.2);
566
+ return clamp(numeric / shapeAxisHalf(axis), -2.0, 2.0);
567
+ };
568
+
569
+ const applyShapeTransform = (point, shape, context) => {
570
+ const transform = shapeTransform(shape, context);
571
+ const shiftedX = (point[0] - transform.origin.x) * transform.scale.x;
572
+ const shiftedY = (point[1] - transform.origin.y) * transform.scale.y;
573
+ const radians = (transform.rotate / 180) * Math.PI;
574
+ const cos = Math.cos(radians);
575
+ const sin = Math.sin(radians);
576
+ const rotatedX = shiftedX * cos - shiftedY * sin;
577
+ const rotatedY = shiftedX * sin + shiftedY * cos;
578
+
579
+ return [
580
+ clamp(rotatedX + transform.origin.x + transform.translate.x, -1.2, 1.2),
581
+ clamp(rotatedY + transform.origin.y + transform.translate.y, -1.2, 1.2)
582
+ ];
583
+ };
584
+
585
+ const shapeTransform = (shape, context) => {
586
+ const transform = shape?.transform || {};
587
+ return {
588
+ translate: normalizeShapeVectorObject(transform.translate || shape.translate, context, { x: 0, y: 0 }),
589
+ origin: normalizeShapeVectorObject(transform.origin || shape.origin, context, { x: 0, y: 0 }),
590
+ rotate: finiteNumber(transform.rotate ?? shape.rotate ?? shape.rotation, 0),
591
+ scale: normalizeShapeScale(transform.scale ?? shape.scale)
592
+ };
593
+ };
594
+
595
+ const normalizeShapeVectorObject = (value, context, fallback) => {
596
+ if (Array.isArray(value)) {
597
+ return {
598
+ x: normalizeShapeVector(value[0] ?? fallback.x, context, "x"),
599
+ y: normalizeShapeVector(value[1] ?? fallback.y, context, "y")
600
+ };
601
+ }
602
+
603
+ if (value && typeof value === "object") {
604
+ return {
605
+ x: normalizeShapeVector(value.x ?? fallback.x, context, "x"),
606
+ y: normalizeShapeVector(value.y ?? fallback.y, context, "y")
607
+ };
608
+ }
609
+
610
+ return fallback;
611
+ };
612
+
613
+ const normalizeShapeScale = (value) => {
614
+ if (value && typeof value === "object") {
615
+ return {
616
+ x: clamp(finiteNumber(value.x, 1), -8, 8),
617
+ y: clamp(finiteNumber(value.y, 1), -8, 8)
618
+ };
619
+ }
620
+
621
+ const scale = clamp(finiteNumber(value, 1), -8, 8);
622
+ return { x: scale, y: scale };
623
+ };
624
+
625
+ const lineDefaults = (context) => {
626
+ if (context.units === "legacy" || context.units === "ndc") {
627
+ return [-0.8, 0, 0.8, 0];
628
+ }
629
+
630
+ return [-100, 0, 100, 0];
631
+ };
632
+
633
+ const shapeAxisHalf = (axis) => axis === "x" ? SHAPE_HALF_WIDTH : SHAPE_HALF_HEIGHT;
634
+
635
+ const logicalShapeUnits = (value) => ["logical", "center", "center_origin", "px"].includes(value);
636
+
637
+ const screenShapeUnits = (value) => ["screen", "canvas", "viewport"].includes(value);
638
+
639
+ const radiusToRawHalf = (value) => finiteNumber(value, 100) * 0.5;
640
+
641
+ const pathSegmentBudget = (shape) => {
642
+ const value = shape.max_segments ?? shape.maxSegments ?? PATH_DEFAULT_MAX_SEGMENTS;
643
+ return { remaining: clampInt(value, 1, PATH_HARD_MAX_SEGMENTS) };
644
+ };
645
+
646
+ const pathTolerance = (shape) => {
647
+ if (shape.tolerance === undefined && shape.tolerancePx === undefined) return null;
648
+
649
+ const numeric = Number(shape.tolerance ?? shape.tolerancePx);
650
+ return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
651
+ };
652
+
653
+ const midpoint = (from, to) => [
654
+ (from[0] + to[0]) * 0.5,
655
+ (from[1] + to[1]) * 0.5
656
+ ];
657
+
658
+ const pointLineDistance = (point, from, to) => {
659
+ const dx = to[0] - from[0];
660
+ const dy = to[1] - from[1];
661
+ const length = Math.hypot(dx, dy);
662
+ if (length <= 0) return Math.hypot(point[0] - from[0], point[1] - from[1]);
663
+ return Math.abs(dy * point[0] - dx * point[1] + to[0] * from[1] - to[1] * from[0]) / length;
664
+ };
665
+
666
+ const quadraticPoint = (from, control, to, t) => {
667
+ const inv = 1 - t;
668
+ return inv * inv * from + 2 * inv * t * control + t * t * to;
669
+ };
670
+
671
+ const cubicPoint = (from, c1, c2, to, t) => {
672
+ const inv = 1 - t;
673
+ return inv * inv * inv * from + 3 * inv * inv * t * c1 + 3 * inv * t * t * c2 + t * t * t * to;
276
674
  };
277
675
 
278
676
  const sampleSpectrum = (spectrum, progress) => {