@seatlayer/core 0.28.4 → 0.30.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.
package/dist/index.cjs CHANGED
@@ -411,6 +411,7 @@ function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected
411
411
  })
412
412
  };
413
413
  }
414
+ var SAGITTA_TO_CONTROL = 4 / 3;
414
415
 
415
416
  // src/core/labeling.ts
416
417
  var FULL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -503,6 +504,345 @@ function sectionGeometry(section, context = {}) {
503
504
  return { height, rake };
504
505
  }
505
506
 
507
+ // src/core/venueStructure.ts
508
+ var BLOCK_LINK_RATIO = 1.8;
509
+ function probesOf(pts) {
510
+ const n = pts.length;
511
+ if (n <= 3) return [...pts];
512
+ return [pts[Math.floor(n * 0.25)], pts[Math.floor(n * 0.5)], pts[Math.floor(n * 0.75)]];
513
+ }
514
+ function distanceToPolyline(pts, x, y) {
515
+ if (!pts.length) return Infinity;
516
+ if (pts.length === 1) return Math.hypot(x - pts[0].x, y - pts[0].y);
517
+ let best = Infinity;
518
+ for (let i = 0; i + 1 < pts.length; i++) {
519
+ const a = pts[i], b = pts[i + 1];
520
+ const vx = b.x - a.x, vy = b.y - a.y;
521
+ const len2 = vx * vx + vy * vy;
522
+ let t2 = len2 > 1e-12 ? ((x - a.x) * vx + (y - a.y) * vy) / len2 : 0;
523
+ if (t2 < 0) t2 = 0;
524
+ else if (t2 > 1) t2 = 1;
525
+ const d = Math.hypot(x - (a.x + t2 * vx), y - (a.y + t2 * vy));
526
+ if (d < best) best = d;
527
+ }
528
+ return best;
529
+ }
530
+ function boundOf(pts) {
531
+ let cx = 0, cy = 0;
532
+ for (const p of pts) {
533
+ cx += p.x;
534
+ cy += p.y;
535
+ }
536
+ const n = pts.length || 1;
537
+ cx /= n;
538
+ cy /= n;
539
+ let r = 0;
540
+ for (const p of pts) {
541
+ const d = Math.hypot(p.x - cx, p.y - cy);
542
+ if (d > r) r = d;
543
+ }
544
+ return { cx, cy, r };
545
+ }
546
+ function boundGap(a, b) {
547
+ return Math.hypot(a.cx - b.cx, a.cy - b.cy) - a.r - b.r;
548
+ }
549
+ function rowGap(a, b) {
550
+ let best = Infinity;
551
+ for (const p of probesOf(a)) {
552
+ const d = distanceToPolyline(b, p.x, p.y);
553
+ if (d < best) best = d;
554
+ }
555
+ for (const p of probesOf(b)) {
556
+ const d = distanceToPolyline(a, p.x, p.y);
557
+ if (d < best) best = d;
558
+ }
559
+ return best;
560
+ }
561
+ function orderAlongRow(pts, indices) {
562
+ if (pts.length < 3) return { pts, seatIndices: indices };
563
+ let cx = 0, cy = 0;
564
+ for (const p of pts) {
565
+ cx += p.x;
566
+ cy += p.y;
567
+ }
568
+ cx /= pts.length;
569
+ cy /= pts.length;
570
+ let far = -1, ax = pts[0];
571
+ for (const p of pts) {
572
+ const d = Math.hypot(p.x - cx, p.y - cy);
573
+ if (d > far) {
574
+ far = d;
575
+ ax = p;
576
+ }
577
+ }
578
+ let dx = ax.x - cx, dy = ax.y - cy;
579
+ const len = Math.hypot(dx, dy);
580
+ if (len > 1e-9) {
581
+ dx /= len;
582
+ dy /= len;
583
+ } else {
584
+ dx = 1;
585
+ dy = 0;
586
+ }
587
+ const order = pts.map((p, i) => ({ p, i, t: (p.x - cx) * dx + (p.y - cy) * dy })).sort((u, v) => u.t - v.t);
588
+ return { pts: order.map((o) => o.p), seatIndices: order.map((o) => indices[o.i]) };
589
+ }
590
+ function fitCircle(pts) {
591
+ const n = pts.length;
592
+ if (n < 3) return null;
593
+ let mx = 0, my = 0;
594
+ for (const p of pts) {
595
+ mx += p.x;
596
+ my += p.y;
597
+ }
598
+ mx /= n;
599
+ my /= n;
600
+ let suu = 0, suv = 0, svv = 0, suuu = 0, svvv = 0, suvv = 0, svuu = 0;
601
+ for (const p of pts) {
602
+ const u = p.x - mx, v = p.y - my;
603
+ suu += u * u;
604
+ svv += v * v;
605
+ suv += u * v;
606
+ suuu += u * u * u;
607
+ svvv += v * v * v;
608
+ suvv += u * v * v;
609
+ svuu += v * u * u;
610
+ }
611
+ const det = suu * svv - suv * suv;
612
+ if (Math.abs(det) < 1e-9) return null;
613
+ const b1 = (suuu + suvv) / 2;
614
+ const b2 = (svvv + svuu) / 2;
615
+ const uc = (b1 * svv - b2 * suv) / det;
616
+ const vc = (b2 * suu - b1 * suv) / det;
617
+ const cx = uc + mx, cy = vc + my;
618
+ if (!Number.isFinite(cx) || !Number.isFinite(cy)) return null;
619
+ return { cx, cy };
620
+ }
621
+ var CIRCLE_ROW_SPREAD_LIMIT = 0.15;
622
+ function fitBlockAxis(group) {
623
+ if (group.length < 2) return null;
624
+ const cents = group.map((r) => {
625
+ let x = 0, y = 0;
626
+ for (const p of r.pts) {
627
+ x += p.x;
628
+ y += p.y;
629
+ }
630
+ return { x: x / r.pts.length, y: y / r.pts.length };
631
+ });
632
+ let ox = 0, oy = 0;
633
+ for (const c of cents) {
634
+ ox += c.x;
635
+ oy += c.y;
636
+ }
637
+ ox /= cents.length;
638
+ oy /= cents.length;
639
+ let sxx = 0, sxy = 0, syy = 0;
640
+ for (const c of cents) {
641
+ const dx = c.x - ox, dy = c.y - oy;
642
+ sxx += dx * dx;
643
+ sxy += dx * dy;
644
+ syy += dy * dy;
645
+ }
646
+ const tr = sxx + syy;
647
+ const det = sxx * syy - sxy * sxy;
648
+ const lambda = tr / 2 + Math.sqrt(Math.max(0, tr * tr / 4 - det));
649
+ let ax, ay;
650
+ if (Math.abs(sxy) > 1e-12) {
651
+ ax = lambda - syy;
652
+ ay = sxy;
653
+ } else if (sxx >= syy) {
654
+ ax = 1;
655
+ ay = 0;
656
+ } else {
657
+ ax = 0;
658
+ ay = 1;
659
+ }
660
+ const len = Math.hypot(ax, ay);
661
+ if (!(len > 1e-12)) return null;
662
+ ax /= len;
663
+ ay /= len;
664
+ let lo = Infinity, hi = -Infinity;
665
+ const widths = [];
666
+ for (const r of group) {
667
+ let rlo = Infinity, rhi = -Infinity;
668
+ for (const p of r.pts) {
669
+ const t2 = p.x * ax + p.y * ay;
670
+ if (t2 < rlo) rlo = t2;
671
+ if (t2 > rhi) rhi = t2;
672
+ if (t2 < lo) lo = t2;
673
+ if (t2 > hi) hi = t2;
674
+ }
675
+ widths.push(rhi - rlo);
676
+ }
677
+ const range = hi - lo;
678
+ if (!(range > 1e-9)) return null;
679
+ widths.sort((a, b) => a - b);
680
+ const p90 = widths[Math.min(widths.length - 1, Math.floor(widths.length * 0.9))];
681
+ return p90 / range <= AXIS_ROW_WIDTH_LIMIT ? { x: ax, y: ay } : null;
682
+ }
683
+ var AXIS_ROW_WIDTH_LIMIT = 0.35;
684
+ function resolveSection(sectionId, seats, seatIndices, focal) {
685
+ const groups = /* @__PURE__ */ new Map();
686
+ for (const i of seatIndices) {
687
+ const s = seats[i];
688
+ const key = s.rowId || `__seat-${i}`;
689
+ let g = groups.get(key);
690
+ if (!g) {
691
+ g = { pts: [], idx: [] };
692
+ groups.set(key, g);
693
+ }
694
+ g.pts.push({ x: s.x, y: s.y });
695
+ g.idx.push(i);
696
+ }
697
+ if (!groups.size) return { sectionId, rows: [], blockCount: 0 };
698
+ const rows = [];
699
+ for (const [id, g] of groups) {
700
+ const ordered = orderAlongRow(g.pts, g.idx);
701
+ let sum = 0;
702
+ for (const p of ordered.pts) sum += Math.hypot(p.x - focal.x, p.y - focal.y);
703
+ rows.push({
704
+ id,
705
+ pts: ordered.pts,
706
+ seatIndices: ordered.seatIndices,
707
+ blockId: -1,
708
+ ordinal: 0,
709
+ blockDepth: 0,
710
+ focalDistance: sum / ordered.pts.length
711
+ });
712
+ }
713
+ const bounds2 = rows.map((r) => boundOf(r.pts));
714
+ const nearest = [];
715
+ for (let i = 0; i < rows.length; i++) {
716
+ let best = Infinity;
717
+ for (let j = 0; j < rows.length; j++) {
718
+ if (i === j) continue;
719
+ if (boundGap(bounds2[i], bounds2[j]) >= best) continue;
720
+ const d = rowGap(rows[i].pts, rows[j].pts);
721
+ if (d < best) best = d;
722
+ }
723
+ if (Number.isFinite(best)) nearest.push(best);
724
+ }
725
+ nearest.sort((a, b) => a - b);
726
+ const typicalGap = nearest.length ? nearest[Math.floor(nearest.length / 2)] : 1;
727
+ const linkDistance = Math.max(typicalGap * BLOCK_LINK_RATIO, 1e-6);
728
+ const adjacency = rows.map(() => []);
729
+ for (let i = 0; i < rows.length; i++) {
730
+ for (let j = i + 1; j < rows.length; j++) {
731
+ if (boundGap(bounds2[i], bounds2[j]) > linkDistance) continue;
732
+ if (rowGap(rows[i].pts, rows[j].pts) <= linkDistance) {
733
+ adjacency[i].push(j);
734
+ adjacency[j].push(i);
735
+ }
736
+ }
737
+ }
738
+ let blockCount = 0;
739
+ for (let i = 0; i < rows.length; i++) {
740
+ if (rows[i].blockId !== -1) continue;
741
+ const id = blockCount++;
742
+ const stack = [i];
743
+ rows[i].blockId = id;
744
+ while (stack.length) {
745
+ const k = stack.pop();
746
+ for (const n of adjacency[k]) {
747
+ if (rows[n].blockId !== -1) continue;
748
+ rows[n].blockId = id;
749
+ stack.push(n);
750
+ }
751
+ }
752
+ }
753
+ const byBlock = /* @__PURE__ */ new Map();
754
+ for (const r of rows) {
755
+ const a = byBlock.get(r.blockId) ?? [];
756
+ a.push(r);
757
+ byBlock.set(r.blockId, a);
758
+ }
759
+ const centres = [];
760
+ for (const r of rows) {
761
+ if (r.pts.length < 4) continue;
762
+ const c = fitCircle(r.pts);
763
+ if (!c) continue;
764
+ if (!Number.isFinite(c.cx) || !Number.isFinite(c.cy)) continue;
765
+ centres.push({ x: c.cx, y: c.cy });
766
+ }
767
+ const centre = centres.length >= 2 ? (() => {
768
+ const xs = centres.map((c) => c.x).sort((a, b) => a - b);
769
+ const ys = centres.map((c) => c.y).sort((a, b) => a - b);
770
+ const mid = Math.floor(centres.length / 2);
771
+ return { cx: xs[mid], cy: ys[mid] };
772
+ })() : null;
773
+ if (centre) {
774
+ const radiusOf = (p) => Math.hypot(p.x - centre.cx, p.y - centre.cy);
775
+ let lo = Infinity, hi = -Infinity;
776
+ const spreads = [];
777
+ for (const r of rows) {
778
+ let rlo = Infinity, rhi = -Infinity;
779
+ for (const p of r.pts) {
780
+ const d = radiusOf(p);
781
+ if (d < rlo) rlo = d;
782
+ if (d > rhi) rhi = d;
783
+ }
784
+ spreads.push(rhi - rlo);
785
+ if (rlo < lo) lo = rlo;
786
+ if (rhi > hi) hi = rhi;
787
+ }
788
+ const range = hi - lo;
789
+ spreads.sort((a, b) => a - b);
790
+ const p90 = spreads[Math.min(spreads.length - 1, Math.floor(spreads.length * 0.9))];
791
+ if (range > 1e-9 && p90 / range <= CIRCLE_ROW_SPREAD_LIMIT) {
792
+ const withRadius = rows.map((r) => {
793
+ let sum = 0;
794
+ for (const p of r.pts) sum += radiusOf(p);
795
+ return { row: r, radius: sum / r.pts.length };
796
+ });
797
+ let minR = Infinity;
798
+ for (const w of withRadius) if (w.radius < minR) minR = w.radius;
799
+ withRadius.sort((a, b) => a.radius - b.radius);
800
+ let ordinal = -1, lastRadius = -Infinity;
801
+ const tolerance = range / Math.max(1, withRadius.length) * 0.5;
802
+ const ordered = [];
803
+ for (const w of withRadius) {
804
+ if (w.radius - lastRadius > tolerance) {
805
+ ordinal++;
806
+ lastRadius = w.radius;
807
+ }
808
+ w.row.ordinal = ordinal;
809
+ w.row.blockDepth = w.radius - minR;
810
+ ordered.push(w.row);
811
+ }
812
+ return { sectionId, rows: ordered, blockCount };
813
+ }
814
+ }
815
+ const out = [];
816
+ for (const [, group] of byBlock) {
817
+ const axis = fitBlockAxis(group);
818
+ if (axis) {
819
+ const key = (r) => {
820
+ let sum = 0;
821
+ for (const p of r.pts) sum += p.x * axis.x + p.y * axis.y;
822
+ return sum / r.pts.length;
823
+ };
824
+ group.sort((a, b) => key(a) - key(b));
825
+ let depth = 0;
826
+ for (let i = 0; i < group.length; i++) {
827
+ if (i > 0) depth += rowGap(group[i - 1].pts, group[i].pts);
828
+ group[i].ordinal = i;
829
+ group[i].blockDepth = depth;
830
+ out.push(group[i]);
831
+ }
832
+ } else {
833
+ let front = Infinity;
834
+ for (const r of group) if (r.focalDistance < front) front = r.focalDistance;
835
+ group.sort((a, b) => a.focalDistance - b.focalDistance);
836
+ for (let i = 0; i < group.length; i++) {
837
+ group[i].ordinal = i;
838
+ group[i].blockDepth = Math.max(0, group[i].focalDistance - front);
839
+ out.push(group[i]);
840
+ }
841
+ }
842
+ }
843
+ return { sectionId, rows: out, blockCount };
844
+ }
845
+
506
846
  // src/core/layout.ts
507
847
  function overrideAccessibility(o) {
508
848
  if (!o) return [];
@@ -948,7 +1288,7 @@ function stackFloors(doc, spread = 900) {
948
1288
  });
949
1289
  return { ...doc, objects, floors: void 0 };
950
1290
  }
951
- function expandFloorObjects(objects, zones, fallbackFocal) {
1291
+ function expandFloorObjects(objects, zones, fallbackFocal, viewFallback) {
952
1292
  const out = [];
953
1293
  const segmented = /* @__PURE__ */ new Map();
954
1294
  const grouped = /* @__PURE__ */ new Map();
@@ -1018,6 +1358,7 @@ function expandFloorObjects(objects, zones, fallbackFocal) {
1018
1358
  const resolvedFocal = zone?.focalPoint ?? fallbackFocal;
1019
1359
  for (const seat of seats) {
1020
1360
  if (inheritedView) seat.viewUrl ??= inheritedView;
1361
+ if (viewFallback) seat.viewUrl ??= viewFallback;
1021
1362
  if (owner) seat.sectionId = owner.logicalSectionId ?? owner.id;
1022
1363
  if (owner?.zone) seat.zoneId = owner.zone;
1023
1364
  if (resolvedFocal) seat.focalPoint = { ...resolvedFocal };
@@ -1031,13 +1372,14 @@ function expandChart(doc, options = {}) {
1031
1372
  const out2 = [];
1032
1373
  for (const floor of doc.floors) {
1033
1374
  const floorFocal = floor.focalPoint ?? doc.focalPoint;
1034
- const seats = expandFloorObjects(floor.objects, doc.zones, floorFocal);
1375
+ const floorView = floor.viewFromSeatUrl ?? doc.viewFromSeatUrl;
1376
+ const seats = expandFloorObjects(floor.objects, doc.zones, floorFocal, floorView);
1035
1377
  assignEyeHeights(floor.objects, floor.focalPoint ?? doc.focalPoint, floor.baseHeightM ?? 0, seats);
1036
1378
  out2.push(...seats);
1037
1379
  }
1038
1380
  return out2;
1039
1381
  }
1040
- const out = expandFloorObjects(doc.objects, doc.zones, doc.focalPoint);
1382
+ const out = expandFloorObjects(doc.objects, doc.zones, doc.focalPoint, doc.viewFromSeatUrl);
1041
1383
  assignEyeHeights(doc.objects, doc.focalPoint, options.floorBaseHeightM ?? 0, out);
1042
1384
  return out;
1043
1385
  }
@@ -1050,17 +1392,28 @@ function assignEyeHeights(objects, focal, floorBaseHeightM, seats) {
1050
1392
  }
1051
1393
  const owner = new Array(seats.length);
1052
1394
  const geo = /* @__PURE__ */ new Map();
1053
- const frontDistU = /* @__PURE__ */ new Map();
1395
+ const seatsBySection = /* @__PURE__ */ new Map();
1054
1396
  for (let i = 0; i < seats.length; i++) {
1055
1397
  const seat = seats[i];
1056
1398
  const sec = sections.find((s) => pointInPolygonWithHoles({ x: seat.x, y: seat.y }, s.outline, s.holes)) ?? null;
1057
1399
  owner[i] = sec;
1058
1400
  if (!sec) continue;
1059
1401
  if (!geo.has(sec.id)) geo.set(sec.id, sectionGeometry(sec, { floorBaseHeightM }));
1060
- const seatFocal = seat.focalPoint ?? focal;
1061
- const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
1062
- const cur = frontDistU.get(sec.id);
1063
- if (cur === void 0 || d < cur) frontDistU.set(sec.id, d);
1402
+ const list = seatsBySection.get(sec.id);
1403
+ if (list) list.push(i);
1404
+ else seatsBySection.set(sec.id, [i]);
1405
+ }
1406
+ const seatLevel = new Array(seats.length).fill(void 0);
1407
+ for (const [secId, indices] of seatsBySection) {
1408
+ const g = geo.get(secId);
1409
+ if (!g) continue;
1410
+ const rakeTan = g.rake > 0 ? Math.tan(g.rake * Math.PI / 180) : 0;
1411
+ const structure = resolveSection(secId, seats, indices, focal);
1412
+ for (const row of structure.rows) {
1413
+ const riseM = row.blockDepth * METRES_PER_CHART_UNIT * rakeTan;
1414
+ const level = g.height + riseM;
1415
+ for (const si of row.seatIndices) seatLevel[si] = level;
1416
+ }
1064
1417
  }
1065
1418
  for (let i = 0; i < seats.length; i++) {
1066
1419
  const seat = seats[i];
@@ -1070,14 +1423,7 @@ function assignEyeHeights(objects, focal, floorBaseHeightM, seats) {
1070
1423
  continue;
1071
1424
  }
1072
1425
  const g = geo.get(sec.id);
1073
- let riseM = 0;
1074
- if (g.rake > 0) {
1075
- const seatFocal = seat.focalPoint ?? focal;
1076
- const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
1077
- const depthU = Math.max(0, d - (frontDistU.get(sec.id) ?? d));
1078
- riseM = depthU * METRES_PER_CHART_UNIT * Math.tan(g.rake * Math.PI / 180);
1079
- }
1080
- seat.eyeHeightM = g.height + riseM + SEATED_EYE_HEIGHT_M;
1426
+ seat.eyeHeightM = (seatLevel[i] ?? g.height) + SEATED_EYE_HEIGHT_M;
1081
1427
  }
1082
1428
  }
1083
1429
  var PAD = 40;
@@ -4638,7 +4984,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4638
4984
  this.accessRingById.set(seat.id, ring);
4639
4985
  target.add(ring);
4640
4986
  }
4641
- if (accessGlyphPath(seat.accessibility ?? [])) {
4987
+ if (seat.accessibility?.includes("wheelchair") && accessGlyphPath(seat.accessibility)) {
4642
4988
  const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
4643
4989
  this.accessGlyphById.set(seat.id, glyph);
4644
4990
  target.add(glyph);
@@ -8788,8 +9134,450 @@ function stageSightlinePitch(eyeHeightM, distM) {
8788
9134
  var W = 2048;
8789
9135
  var H = 1024;
8790
9136
  var UNIT = METRES_PER_CHART_UNIT;
9137
+ var TAU2 = Math.PI * 2;
9138
+ var PX_PER_DEG = H / 180;
8791
9139
  var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
8792
9140
  var pitchToY = (pitchDeg) => (90 - pitchDeg) / 180 * H;
9141
+ function fnv1a(str) {
9142
+ let h = 2166136261;
9143
+ for (let i = 0; i < str.length; i++) {
9144
+ h ^= str.charCodeAt(i);
9145
+ h = Math.imul(h, 16777619);
9146
+ }
9147
+ return h >>> 0;
9148
+ }
9149
+ function rng(seed) {
9150
+ let a = seed >>> 0;
9151
+ return () => {
9152
+ a |= 0;
9153
+ a = a + 1831565813 | 0;
9154
+ let t2 = Math.imul(a ^ a >>> 15, 1 | a);
9155
+ t2 = t2 + Math.imul(t2 ^ t2 >>> 7, 61 | t2) ^ t2;
9156
+ return ((t2 ^ t2 >>> 14) >>> 0) / 4294967296;
9157
+ };
9158
+ }
9159
+ var NBINS = 96;
9160
+ var BIN_DEG = 360 / NBINS;
9161
+ var FLAT_DELTA_M = 0.6;
9162
+ var HAZE_SKY = [17, 23, 42];
9163
+ function blendToSky(base, t2, a = 1) {
9164
+ const k = Math.max(0, Math.min(0.58, t2));
9165
+ const r = Math.round(base[0] + (HAZE_SKY[0] - base[0]) * k);
9166
+ const g = Math.round(base[1] + (HAZE_SKY[1] - base[1]) * k);
9167
+ const b = Math.round(base[2] + (HAZE_SKY[2] - base[2]) * k);
9168
+ return `rgba(${r},${g},${b},${a})`;
9169
+ }
9170
+ function mixRgb(a, b, t2) {
9171
+ return [
9172
+ Math.round(a[0] + (b[0] - a[0]) * t2),
9173
+ Math.round(a[1] + (b[1] - a[1]) * t2),
9174
+ Math.round(a[2] + (b[2] - a[2]) * t2)
9175
+ ];
9176
+ }
9177
+ function hslToRgb(h, s, l) {
9178
+ const c = (1 - Math.abs(2 * l - 1)) * s;
9179
+ const hp = (h % 360 + 360) % 360 / 60;
9180
+ const x = c * (1 - Math.abs(hp % 2 - 1));
9181
+ let r = 0, g = 0, b = 0;
9182
+ if (hp < 1) [r, g, b] = [c, x, 0];
9183
+ else if (hp < 2) [r, g, b] = [x, c, 0];
9184
+ else if (hp < 3) [r, g, b] = [0, c, x];
9185
+ else if (hp < 4) [r, g, b] = [0, x, c];
9186
+ else if (hp < 5) [r, g, b] = [x, 0, c];
9187
+ else [r, g, b] = [c, 0, x];
9188
+ const m = l - c / 2;
9189
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
9190
+ }
9191
+ var tintCache = /* @__PURE__ */ new Map();
9192
+ function tintFromKey(key) {
9193
+ if (!key) return null;
9194
+ const cached = tintCache.get(key);
9195
+ if (cached) return cached;
9196
+ let h = 2166136261;
9197
+ for (let i = 0; i < key.length; i++) {
9198
+ h ^= key.charCodeAt(i);
9199
+ h = Math.imul(h, 16777619);
9200
+ }
9201
+ const hue = (h >>> 0) % 360;
9202
+ const rgb2 = hslToRgb(hue, 0.3, 0.42);
9203
+ tintCache.set(key, rgb2);
9204
+ return rgb2;
9205
+ }
9206
+ function buildStands(seat, neighborSeats, stageBearing, myEyeM) {
9207
+ const bins = new Array(NBINS).fill(null);
9208
+ let maxDelta = 0;
9209
+ const own = seat.sectionId;
9210
+ for (const other of neighborSeats) {
9211
+ if (other.id === seat.id) continue;
9212
+ const os = other.sectionId;
9213
+ if (!os || own && os === own) continue;
9214
+ const ox = other.x - seat.x;
9215
+ const oy = other.y - seat.y;
9216
+ const dM = Math.hypot(ox, oy) * UNIT;
9217
+ if (dM < 1.5 || dM > 95) continue;
9218
+ const otherEye = other.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
9219
+ const dTop = otherEye - myEyeM;
9220
+ if (Math.abs(dTop) > maxDelta) maxDelta = Math.abs(dTop);
9221
+ const top = Math.atan2(dTop, dM) * 180 / Math.PI;
9222
+ const base = Math.atan2(otherEye - SEATED_EYE_HEIGHT_M - myEyeM, dM) * 180 / Math.PI;
9223
+ const bearing = Math.atan2(ox, -oy) - stageBearing;
9224
+ const yaw = (bearing * 180 / Math.PI + 540) % 360 - 180;
9225
+ const idx = Math.min(NBINS - 1, Math.max(0, Math.floor((yaw + 180) / BIN_DEG)));
9226
+ const overhead = top > 34 && dM < 16;
9227
+ const cur = bins[idx];
9228
+ if (!cur) bins[idx] = { top, base, nearM: dM, overhead, tint: tintFromKey(other.categoryKey) };
9229
+ else {
9230
+ if (top > cur.top) cur.top = top;
9231
+ if (base < cur.base) cur.base = base;
9232
+ if (dM < cur.nearM) {
9233
+ cur.nearM = dM;
9234
+ cur.tint = tintFromKey(other.categoryKey);
9235
+ }
9236
+ cur.overhead = cur.overhead || overhead;
9237
+ }
9238
+ }
9239
+ return { bins, maxDelta };
9240
+ }
9241
+ function drawStands(ctx, bins, toX, toY) {
9242
+ for (let i = 0; i < NBINS; i++) {
9243
+ const b = bins[i];
9244
+ if (!b) continue;
9245
+ const yaw0 = -180 + i * BIN_DEG;
9246
+ const x0 = toX(yaw0);
9247
+ const x1 = toX(yaw0 + BIN_DEG);
9248
+ if (!(x1 > x0)) continue;
9249
+ const top = Math.min(72, b.top);
9250
+ const bottom = Math.min(b.base, -4);
9251
+ const yTop = toY(top);
9252
+ const yBot = toY(bottom);
9253
+ if (yBot <= yTop) continue;
9254
+ const haze = b.nearM / 95;
9255
+ const tint = b.tint;
9256
+ const crest = tint ? mixRgb([44, 54, 80], tint, 0.34) : [44, 54, 80];
9257
+ const seating = tint ? mixRgb([24, 30, 48], tint, 0.28) : [24, 30, 48];
9258
+ const grad = ctx.createLinearGradient(0, yTop, 0, yBot);
9259
+ grad.addColorStop(0, blendToSky(crest, haze));
9260
+ grad.addColorStop(0.24, blendToSky(seating, haze));
9261
+ grad.addColorStop(1, blendToSky([10, 13, 22], haze));
9262
+ ctx.fillStyle = grad;
9263
+ ctx.fillRect(x0, yTop, x1 - x0 + 1, yBot - yTop);
9264
+ ctx.fillStyle = `rgba(150,165,205,${0.35 * (1 - haze * 0.55)})`;
9265
+ ctx.fillRect(x0, yTop, x1 - x0 + 1, 1.5);
9266
+ const bandBottom = Math.min(yBot, toY(0));
9267
+ const bandSpan = bandBottom - yTop;
9268
+ if (bandSpan > 8) {
9269
+ const bands = 6;
9270
+ const contrast = 1 - haze * 0.4;
9271
+ for (let r = 1; r < bands; r++) {
9272
+ const y = yTop + bandSpan * r / bands;
9273
+ ctx.fillStyle = `rgba(0,0,0,${0.16 * contrast})`;
9274
+ ctx.fillRect(x0, y, x1 - x0 + 1, 1.5);
9275
+ ctx.fillStyle = `rgba(120,134,170,${0.1 * contrast})`;
9276
+ ctx.fillRect(x0, y + 1.5, x1 - x0 + 1, 1.2);
9277
+ }
9278
+ }
9279
+ if (b.overhead) {
9280
+ const lipY = toY(Math.min(86, top + 12));
9281
+ const g2 = ctx.createLinearGradient(0, lipY, 0, yTop);
9282
+ g2.addColorStop(0, "rgba(3,4,8,0.9)");
9283
+ g2.addColorStop(1, "rgba(3,4,8,0)");
9284
+ ctx.fillStyle = g2;
9285
+ ctx.fillRect(x0, lipY, x1 - x0 + 1, yTop - lipY);
9286
+ ctx.fillStyle = "rgba(120,130,160,0.14)";
9287
+ ctx.fillRect(x0, yTop - 1.5, x1 - x0 + 1, 1.5);
9288
+ }
9289
+ }
9290
+ }
9291
+ function classifyScene(_seat, focal, neighbors) {
9292
+ const NB = 72;
9293
+ const occ = new Array(NB).fill(false);
9294
+ const innerM = new Array(NB).fill(Infinity);
9295
+ const secSet = /* @__PURE__ */ new Set();
9296
+ let n = 0;
9297
+ for (const s of neighbors) {
9298
+ n++;
9299
+ if (s.sectionId) secSet.add(s.sectionId);
9300
+ const dx = s.x - focal.x;
9301
+ const dy = s.y - focal.y;
9302
+ const dM = Math.hypot(dx, dy) * UNIT;
9303
+ if (dM < 0.5) continue;
9304
+ const bearing = Math.atan2(dx, -dy);
9305
+ let idx = Math.floor((bearing + Math.PI) / TAU2 * NB);
9306
+ if (idx < 0) idx = 0;
9307
+ else if (idx >= NB) idx = NB - 1;
9308
+ occ[idx] = true;
9309
+ if (dM < innerM[idx]) innerM[idx] = dM;
9310
+ }
9311
+ let occN = 0;
9312
+ const inner = [];
9313
+ for (let i = 0; i < NB; i++) if (occ[i]) {
9314
+ occN++;
9315
+ inner.push(innerM[i]);
9316
+ }
9317
+ const coverage = occN / NB * 360;
9318
+ inner.sort((a, b) => a - b);
9319
+ const median = inner.length ? inner[inner.length >> 1] : 0;
9320
+ const lo = inner.length ? inner[Math.floor(inner.length * 0.15)] ?? median : 0;
9321
+ const hi = inner.length ? inner[Math.floor(inner.length * 0.85)] ?? median : 0;
9322
+ const aspect = lo > 0.5 ? hi / lo : 1;
9323
+ const seed = fnv1a(`${n}|${[...secSet].sort().join(",")}|${Math.round(focal.x)},${Math.round(focal.y)}`);
9324
+ let mode;
9325
+ if (coverage >= 300 && median > 11 && aspect > 1.35) mode = "sport";
9326
+ else if (coverage >= 285) mode = "arena";
9327
+ else mode = "proscenium";
9328
+ return { mode, seed, voidRadiusM: median, aspect };
9329
+ }
9330
+ var HAIR_KINDS = 5;
9331
+ function drawHair(ctx, r, kind) {
9332
+ switch (kind) {
9333
+ case 1:
9334
+ ctx.beginPath();
9335
+ ctx.ellipse(0, -r * 0.14, r * 1.16, r * 1.16, 0, 0, TAU2);
9336
+ ctx.fill();
9337
+ break;
9338
+ case 2:
9339
+ ctx.beginPath();
9340
+ ctx.ellipse(0, -r * 1.02, r * 0.42, r * 0.42, 0, 0, TAU2);
9341
+ ctx.fill();
9342
+ ctx.beginPath();
9343
+ ctx.ellipse(0, -r * 0.2, r * 0.98, r * 1.02, 0, 0, TAU2);
9344
+ ctx.fill();
9345
+ break;
9346
+ case 3:
9347
+ ctx.beginPath();
9348
+ ctx.ellipse(0, -r * 0.36, r * 1.04, r * 0.6, 0, Math.PI, TAU2);
9349
+ ctx.fill();
9350
+ ctx.beginPath();
9351
+ ctx.ellipse(0, -r * 0.52, r * 1.22, r * 0.24, 0, 0, TAU2);
9352
+ ctx.fill();
9353
+ break;
9354
+ case 4:
9355
+ ctx.beginPath();
9356
+ ctx.ellipse(-r * 0.66, r * 0.5, r * 0.5, r * 1.35, 0, 0, TAU2);
9357
+ ctx.fill();
9358
+ ctx.beginPath();
9359
+ ctx.ellipse(r * 0.66, r * 0.5, r * 0.5, r * 1.35, 0, 0, TAU2);
9360
+ ctx.fill();
9361
+ ctx.beginPath();
9362
+ ctx.ellipse(0, -r * 0.08, r * 1.06, r * 1.08, 0, 0, TAU2);
9363
+ ctx.fill();
9364
+ break;
9365
+ default:
9366
+ break;
9367
+ }
9368
+ }
9369
+ function drawSeated(ctx, hx, hy, r, o) {
9370
+ const neckHalf = r * 0.44;
9371
+ const shHalf = r * o.shoulderK;
9372
+ const neckTopY = r * 0.66;
9373
+ const shoulderY = r * 1.68;
9374
+ const botY = r * 5.4;
9375
+ const clo = `rgba(${o.clothing[0]},${o.clothing[1]},${o.clothing[2]},${o.alpha})`;
9376
+ const skin = `rgba(${o.tone[0]},${o.tone[1]},${o.tone[2]},${o.alpha})`;
9377
+ ctx.save();
9378
+ ctx.translate(hx, hy);
9379
+ const leanPx = o.lean * r;
9380
+ ctx.fillStyle = clo;
9381
+ ctx.beginPath();
9382
+ ctx.moveTo(-neckHalf, neckTopY);
9383
+ ctx.quadraticCurveTo(-neckHalf * 1.18, shoulderY - leanPx - r * 0.6, -shHalf, shoulderY - leanPx);
9384
+ ctx.quadraticCurveTo(-shHalf * 1.05, shoulderY + r * 1.2, -shHalf * 0.94, botY);
9385
+ ctx.lineTo(shHalf * 0.94, botY);
9386
+ ctx.quadraticCurveTo(shHalf * 1.05, shoulderY + r * 1.2, shHalf, shoulderY + leanPx);
9387
+ ctx.quadraticCurveTo(neckHalf * 1.18, shoulderY + leanPx - r * 0.6, neckHalf, neckTopY);
9388
+ ctx.closePath();
9389
+ ctx.fill();
9390
+ ctx.fillStyle = skin;
9391
+ ctx.fillRect(-neckHalf * 0.82, r * 0.5, neckHalf * 1.64, r * 0.9);
9392
+ ctx.save();
9393
+ ctx.rotate(o.tilt);
9394
+ ctx.translate(o.turn * r, 0);
9395
+ ctx.fillStyle = skin;
9396
+ if (r >= 18) {
9397
+ ctx.beginPath();
9398
+ ctx.moveTo(-r * 0.9, -r * 0.15);
9399
+ ctx.quadraticCurveTo(-r * 0.96, -r * 1.12, 0, -r * 1.06);
9400
+ ctx.quadraticCurveTo(r * 0.96, -r * 1.12, r * 0.9, -r * 0.15);
9401
+ ctx.quadraticCurveTo(r * 0.86, r * 0.55, r * 0.36, r * 0.98);
9402
+ ctx.quadraticCurveTo(0, r * 1.14, -r * 0.36, r * 0.98);
9403
+ ctx.quadraticCurveTo(-r * 0.86, r * 0.55, -r * 0.9, -r * 0.15);
9404
+ ctx.closePath();
9405
+ ctx.fill();
9406
+ if (r >= 22) {
9407
+ ctx.beginPath();
9408
+ ctx.ellipse(-r * 0.9, r * 0.18, r * 0.13, r * 0.22, 0, 0, TAU2);
9409
+ ctx.ellipse(r * 0.9, r * 0.18, r * 0.13, r * 0.22, 0, 0, TAU2);
9410
+ ctx.fill();
9411
+ }
9412
+ } else {
9413
+ ctx.beginPath();
9414
+ ctx.ellipse(0, 0, r * 0.9, r * 1.06, 0, 0, TAU2);
9415
+ ctx.fill();
9416
+ }
9417
+ drawHair(ctx, r, o.hair);
9418
+ if (o.rim > 0.01) {
9419
+ ctx.strokeStyle = `rgba(245,205,150,${(0.5 * o.rim).toFixed(3)})`;
9420
+ ctx.lineWidth = Math.max(1, r * 0.14);
9421
+ ctx.beginPath();
9422
+ ctx.ellipse(0, -r * 0.06, r * 0.86, r * 1, 0, Math.PI * 1.12, Math.PI * 1.9);
9423
+ ctx.stroke();
9424
+ }
9425
+ ctx.restore();
9426
+ if (o.rim > 0.01) {
9427
+ ctx.strokeStyle = `rgba(240,200,150,${(0.28 * o.rim).toFixed(3)})`;
9428
+ ctx.lineWidth = Math.max(1, r * 0.12);
9429
+ ctx.beginPath();
9430
+ ctx.moveTo(-shHalf * 0.8, shoulderY - r * 0.05);
9431
+ ctx.quadraticCurveTo(-neckHalf, shoulderY - r * 0.75, 0, shoulderY - r * 0.82);
9432
+ ctx.quadraticCurveTo(neckHalf, shoulderY - r * 0.75, shHalf * 0.8, shoulderY - r * 0.05);
9433
+ ctx.stroke();
9434
+ }
9435
+ ctx.restore();
9436
+ }
9437
+ function drawSeatedMid(ctx, hx, hy, r, clothing, tone, alpha) {
9438
+ ctx.fillStyle = `rgba(${clothing[0]},${clothing[1]},${clothing[2]},${alpha})`;
9439
+ ctx.beginPath();
9440
+ ctx.moveTo(hx - r * 1.72, hy + r * 4);
9441
+ ctx.quadraticCurveTo(hx - r * 1.78, hy + r * 1.55, hx - r * 0.46, hy + r * 1.02);
9442
+ ctx.lineTo(hx - r * 0.4, hy + r * 0.62);
9443
+ ctx.lineTo(hx + r * 0.4, hy + r * 0.62);
9444
+ ctx.lineTo(hx + r * 0.46, hy + r * 1.02);
9445
+ ctx.quadraticCurveTo(hx + r * 1.78, hy + r * 1.55, hx + r * 1.72, hy + r * 4);
9446
+ ctx.closePath();
9447
+ ctx.fill();
9448
+ ctx.fillStyle = `rgba(${tone[0]},${tone[1]},${tone[2]},${alpha})`;
9449
+ ctx.beginPath();
9450
+ ctx.ellipse(hx, hy, r * 0.9, r * 1.05, 0, 0, TAU2);
9451
+ ctx.fill();
9452
+ }
9453
+ function drawPerformer(ctx, x, footY, h, o) {
9454
+ const headR = h * 0.1;
9455
+ const headCy = footY - h * 0.9;
9456
+ const shoulderY = footY - h * 0.74;
9457
+ const hipY = footY - h * 0.46;
9458
+ const shHalf = h * 0.13;
9459
+ const hipHalf = h * 0.09;
9460
+ const skin = `rgba(${o.tone[0]},${o.tone[1]},${o.tone[2]},${o.alpha})`;
9461
+ const sh = ctx.createRadialGradient(x, footY, 1, x, footY, h * 0.24);
9462
+ sh.addColorStop(0, "rgba(0,0,0,0.4)");
9463
+ sh.addColorStop(1, "rgba(0,0,0,0)");
9464
+ ctx.save();
9465
+ ctx.scale(1, 0.28);
9466
+ ctx.fillStyle = sh;
9467
+ ctx.beginPath();
9468
+ ctx.ellipse(x, footY / 0.28, h * 0.24, h * 0.24, 0, 0, TAU2);
9469
+ ctx.fill();
9470
+ ctx.restore();
9471
+ ctx.fillStyle = skin;
9472
+ ctx.beginPath();
9473
+ ctx.moveTo(x - hipHalf, hipY);
9474
+ ctx.lineTo(x - hipHalf * 0.7, footY);
9475
+ ctx.lineTo(x - hipHalf * 0.1, footY);
9476
+ ctx.lineTo(x - hipHalf * 0.1, hipY + h * 0.02);
9477
+ ctx.lineTo(x + hipHalf * 0.1, hipY + h * 0.02);
9478
+ ctx.lineTo(x + hipHalf * 0.1, footY);
9479
+ ctx.lineTo(x + hipHalf * 0.7, footY);
9480
+ ctx.lineTo(x + hipHalf, hipY);
9481
+ ctx.closePath();
9482
+ ctx.fill();
9483
+ ctx.beginPath();
9484
+ ctx.moveTo(x - shHalf, shoulderY);
9485
+ ctx.quadraticCurveTo(x - shHalf * 0.9, hipY - h * 0.12, x - hipHalf, hipY);
9486
+ ctx.lineTo(x + hipHalf, hipY);
9487
+ ctx.quadraticCurveTo(x + shHalf * 0.9, hipY - h * 0.12, x + shHalf, shoulderY);
9488
+ ctx.quadraticCurveTo(x, shoulderY - h * 0.05, x - shHalf, shoulderY);
9489
+ ctx.closePath();
9490
+ ctx.fill();
9491
+ ctx.lineCap = "round";
9492
+ ctx.lineJoin = "round";
9493
+ ctx.strokeStyle = skin;
9494
+ ctx.lineWidth = h * 0.055;
9495
+ const armY = shoulderY + h * 0.02;
9496
+ if (o.pose === 1) {
9497
+ ctx.beginPath();
9498
+ ctx.moveTo(x - shHalf * 0.8, armY);
9499
+ ctx.lineTo(x - shHalf * 1.5, footY - h * 1.02);
9500
+ ctx.moveTo(x + shHalf * 0.8, armY);
9501
+ ctx.lineTo(x + shHalf * 1.5, footY - h * 1.02);
9502
+ ctx.stroke();
9503
+ } else if (o.pose === 2) {
9504
+ ctx.beginPath();
9505
+ ctx.moveTo(x - shHalf * 0.8, armY);
9506
+ ctx.lineTo(x + hipHalf * 1.4, hipY - h * 0.02);
9507
+ ctx.moveTo(x + shHalf * 0.8, armY);
9508
+ ctx.lineTo(x - hipHalf * 0.4, hipY - h * 0.06);
9509
+ ctx.stroke();
9510
+ ctx.fillStyle = skin;
9511
+ ctx.beginPath();
9512
+ ctx.ellipse(x + hipHalf * 1.5, hipY - h * 0.04, h * 0.07, h * 0.09, -0.5, 0, TAU2);
9513
+ ctx.fill();
9514
+ } else if (o.pose === 3) {
9515
+ ctx.beginPath();
9516
+ ctx.moveTo(x - shHalf * 0.8, armY);
9517
+ ctx.lineTo(x - shHalf * 0.2, headCy + headR * 0.6);
9518
+ ctx.moveTo(x + shHalf * 0.8, armY);
9519
+ ctx.lineTo(x + shHalf * 1.1, hipY);
9520
+ ctx.stroke();
9521
+ ctx.lineWidth = h * 0.02;
9522
+ ctx.beginPath();
9523
+ ctx.moveTo(x - shHalf * 0.2, headCy + headR * 0.9);
9524
+ ctx.lineTo(x - shHalf * 0.2, footY - h * 0.02);
9525
+ ctx.stroke();
9526
+ } else {
9527
+ ctx.beginPath();
9528
+ ctx.moveTo(x - shHalf * 0.85, armY);
9529
+ ctx.lineTo(x - shHalf * 0.95, hipY + h * 0.02);
9530
+ ctx.moveTo(x + shHalf * 0.85, armY);
9531
+ ctx.lineTo(x + shHalf * 0.95, hipY + h * 0.02);
9532
+ ctx.stroke();
9533
+ }
9534
+ ctx.fillStyle = skin;
9535
+ ctx.fillRect(x - headR * 0.34, headCy + headR * 0.6, headR * 0.68, headR * 0.9);
9536
+ ctx.beginPath();
9537
+ ctx.ellipse(x, headCy, headR * 0.86, headR * 1.05, 0, 0, TAU2);
9538
+ ctx.fill();
9539
+ if (o.rimStrength > 0.01) {
9540
+ ctx.strokeStyle = `rgba(${o.rim[0]},${o.rim[1]},${o.rim[2]},${(0.6 * o.rimStrength).toFixed(3)})`;
9541
+ ctx.lineWidth = Math.max(1, h * 0.02);
9542
+ ctx.beginPath();
9543
+ ctx.moveTo(x - shHalf, shoulderY);
9544
+ ctx.lineTo(x - headR * 0.6, headCy + headR * 0.4);
9545
+ ctx.moveTo(x - headR * 0.7, headCy);
9546
+ ctx.ellipse(x, headCy, headR * 0.84, headR * 1.02, 0, Math.PI * 1.05, Math.PI * 1.55);
9547
+ ctx.stroke();
9548
+ }
9549
+ }
9550
+ function drawBeam(ctx, x0, y0, x1, y1, halfTop, halfBot, tint, strength) {
9551
+ ctx.save();
9552
+ ctx.globalCompositeOperation = "lighter";
9553
+ const g = ctx.createLinearGradient(0, y0, 0, y1);
9554
+ g.addColorStop(0, `rgba(${tint},${strength})`);
9555
+ g.addColorStop(0.5, `rgba(${tint},${(strength * 0.4).toFixed(3)})`);
9556
+ g.addColorStop(1, `rgba(${tint},0)`);
9557
+ ctx.fillStyle = g;
9558
+ ctx.beginPath();
9559
+ ctx.moveTo(x0 - halfTop, y0);
9560
+ ctx.lineTo(x1 - halfBot, y1);
9561
+ ctx.lineTo(x1 + halfBot, y1);
9562
+ ctx.lineTo(x0 + halfTop, y0);
9563
+ ctx.closePath();
9564
+ ctx.fill();
9565
+ ctx.restore();
9566
+ }
9567
+ function drawFixture(ctx, x, y, r, tint) {
9568
+ const g = ctx.createRadialGradient(x, y, 0, x, y, r * 3);
9569
+ g.addColorStop(0, `rgba(${tint},0.9)`);
9570
+ g.addColorStop(0.4, `rgba(${tint},0.35)`);
9571
+ g.addColorStop(1, `rgba(${tint},0)`);
9572
+ ctx.fillStyle = g;
9573
+ ctx.beginPath();
9574
+ ctx.arc(x, y, r * 3, 0, TAU2);
9575
+ ctx.fill();
9576
+ ctx.fillStyle = `rgba(${tint},1)`;
9577
+ ctx.beginPath();
9578
+ ctx.arc(x, y, r, 0, TAU2);
9579
+ ctx.fill();
9580
+ }
8793
9581
  function generateSeatPanorama(seat, focalPoint, neighborSeats) {
8794
9582
  const canvas = document.createElement("canvas");
8795
9583
  canvas.width = W;
@@ -8799,102 +9587,557 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
8799
9587
  const dy = focalPoint.y - seat.y;
8800
9588
  const distU = Math.hypot(dx, dy);
8801
9589
  const distM = Math.max(2, distU * UNIT);
9590
+ const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
9591
+ const scene = classifyScene(seat, focalPoint, neighborSeats);
9592
+ const rand = rng(scene.seed);
9593
+ const baseHue = 210 + rand() * 150;
9594
+ const rigTint = hslToRgb(220 + (rand() - 0.5) * 60, 0.55, 0.82);
9595
+ const rigTintStr = `${rigTint[0]},${rigTint[1]},${rigTint[2]}`;
8802
9596
  const sky = ctx.createLinearGradient(0, 0, 0, H);
8803
- sky.addColorStop(0, "#05070c");
8804
- sky.addColorStop(0.42, "#11172a");
8805
- sky.addColorStop(0.5, "#1a2238");
8806
- sky.addColorStop(0.56, "#10141f");
8807
- sky.addColorStop(1, "#07090f");
9597
+ sky.addColorStop(0, "#080b12");
9598
+ sky.addColorStop(0.42, "#12182c");
9599
+ sky.addColorStop(0.5, "#1c2440");
9600
+ sky.addColorStop(0.56, "#151b2c");
9601
+ sky.addColorStop(0.78, "#171e30");
9602
+ sky.addColorStop(1, "#131829");
8808
9603
  ctx.fillStyle = sky;
8809
9604
  ctx.fillRect(0, 0, W, H);
8810
- const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
8811
- const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
9605
+ const floorAmb = ctx.createLinearGradient(0, pitchToY(-6), 0, H);
9606
+ floorAmb.addColorStop(0, "rgba(30, 37, 56, 0)");
9607
+ floorAmb.addColorStop(0.55, "rgba(30, 37, 56, 0.22)");
9608
+ floorAmb.addColorStop(1, "rgba(22, 28, 44, 0.12)");
9609
+ ctx.fillStyle = floorAmb;
9610
+ ctx.fillRect(0, pitchToY(-6), W, H - pitchToY(-6));
9611
+ const stageBearing = Math.atan2(dx, -dy);
9612
+ const stands = buildStands(seat, neighborSeats, stageBearing, eyeM);
9613
+ const hasRelief = stands.maxDelta >= FLAT_DELTA_M;
9614
+ if (hasRelief) drawStands(ctx, stands.bins, yawToX, pitchToY);
9615
+ const nearGlow = Math.max(0.3, Math.min(1, 1 - (distM - 6) / 60));
8812
9616
  const sightline = stageSightlinePitch(eyeM, distM);
9617
+ const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
8813
9618
  const stageTopPitch = Math.min(45, sightline.topPitch);
8814
9619
  const stageBasePitch = sightline.basePitch;
9620
+ if (scene.mode === "sport") {
9621
+ drawSportScene(ctx, distM, eyeM, scene, nearGlow, baseHue, rigTintStr, rand);
9622
+ } else if (scene.mode === "arena") {
9623
+ drawArenaScene(ctx, distM, eyeM, scene, nearGlow, rigTintStr, rand);
9624
+ } else {
9625
+ drawProsceniumScene(ctx, stageHalfYaw, stageTopPitch, stageBasePitch, nearGlow, baseHue, rigTintStr, rand);
9626
+ }
9627
+ const rigCount = scene.mode === "sport" ? 34 : 24 + Math.floor(rand() * 8);
9628
+ ctx.fillStyle = `rgba(${rigTintStr},0.8)`;
9629
+ for (let i = 0; i < rigCount; i++) {
9630
+ const x = (i + 0.5) / rigCount * W;
9631
+ const y = pitchToY(scene.mode === "sport" ? 58 : 54 + i * 7 % 3 * 6);
9632
+ ctx.beginPath();
9633
+ ctx.arc(x, y, scene.mode === "sport" ? 2.6 : 3.2, 0, TAU2);
9634
+ ctx.fill();
9635
+ }
9636
+ drawAudience(ctx, seat, neighborSeats, focalPoint, stageBearing, eyeM, dx, dy, hasRelief, scene);
9637
+ const poleFade = ctx.createLinearGradient(0, 0, 0, H);
9638
+ poleFade.addColorStop(0, "rgba(0,0,0,0.85)");
9639
+ poleFade.addColorStop(0.12, "rgba(0,0,0,0)");
9640
+ poleFade.addColorStop(0.9, "rgba(0,0,0,0)");
9641
+ poleFade.addColorStop(1, "rgba(0,0,0,0.55)");
9642
+ ctx.fillStyle = poleFade;
9643
+ ctx.fillRect(0, 0, W, H);
9644
+ return { url: canvas.toDataURL("image/jpeg", 0.82), distanceM: Math.round(distM) };
9645
+ }
9646
+ function drawProsceniumScene(ctx, stageHalfYaw, stageTopPitch, stageBasePitch, nearGlow, baseHue, rigTintStr, rand) {
8815
9647
  const sx0 = yawToX(-stageHalfYaw);
8816
9648
  const sx1 = yawToX(stageHalfYaw);
8817
9649
  const sy0 = pitchToY(stageTopPitch);
8818
9650
  const sy1 = pitchToY(stageBasePitch);
8819
- const glow = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, 10, W / 2, (sy0 + sy1) / 2, (sx1 - sx0) * 1.1);
8820
- glow.addColorStop(0, "rgba(129, 140, 248, 0.5)");
8821
- glow.addColorStop(0.5, "rgba(99, 102, 241, 0.16)");
9651
+ const sw = sx1 - sx0;
9652
+ const sh = sy1 - sy0;
9653
+ const archTop = pitchToY(Math.min(50, stageTopPitch + 10));
9654
+ const glowR = sw * (1.1 + (1 - nearGlow) * 0.5);
9655
+ const glow = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, 8, W / 2, (sy0 + sy1) / 2, glowR);
9656
+ glow.addColorStop(0, "rgba(255, 214, 150, 0.34)");
9657
+ glow.addColorStop(0.28, "rgba(150, 150, 230, 0.3)");
9658
+ glow.addColorStop(0.6, "rgba(99, 102, 241, 0.12)");
8822
9659
  glow.addColorStop(1, "rgba(99, 102, 241, 0)");
8823
9660
  ctx.fillStyle = glow;
8824
- ctx.fillRect(sx0 - (sx1 - sx0) * 0.6, sy0 - (sy1 - sy0) * 1.2, (sx1 - sx0) * 2.2, (sy1 - sy0) * 3.4);
8825
- ctx.fillStyle = "#242c44";
8826
- ctx.fillRect(sx0, sy0, sx1 - sx0, sy1 - sy0);
9661
+ ctx.fillRect(sx0 - sw * 0.6, sy0 - sh * 1.2, sw * 2.2, sh * 3.4);
9662
+ const topCol = hslToRgb(baseHue, 0.62, 0.55);
9663
+ const midCol = hslToRgb(baseHue + 40, 0.7, 0.5);
9664
+ const botCol = hslToRgb(baseHue + 70, 0.78, 0.52);
9665
+ ctx.save();
9666
+ ctx.beginPath();
9667
+ ctx.rect(sx0, sy0, sw, sh);
9668
+ ctx.clip();
8827
9669
  const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
8828
- backdrop.addColorStop(0, "#7c5cff");
8829
- backdrop.addColorStop(0.5, "#e1447a");
8830
- backdrop.addColorStop(1, "#f59e0b");
8831
- ctx.globalAlpha = 0.75;
9670
+ backdrop.addColorStop(0, `rgb(${topCol[0]},${topCol[1]},${topCol[2]})`);
9671
+ backdrop.addColorStop(0.5, `rgb(${midCol[0]},${midCol[1]},${midCol[2]})`);
9672
+ backdrop.addColorStop(1, `rgb(${botCol[0]},${botCol[1]},${botCol[2]})`);
9673
+ ctx.globalAlpha = 0.8;
8832
9674
  ctx.fillStyle = backdrop;
8833
- const inset = (sx1 - sx0) * 0.06;
8834
- ctx.fillRect(sx0 + inset, sy0 + (sy1 - sy0) * 0.12, sx1 - sx0 - inset * 2, (sy1 - sy0) * 0.62);
9675
+ ctx.fillRect(sx0, sy0, sw, sh);
8835
9676
  ctx.globalAlpha = 1;
8836
- ctx.fillStyle = "#0d1119";
8837
- ctx.fillRect(sx0, sy1 - (sy1 - sy0) * 0.1, sx1 - sx0, (sy1 - sy0) * 0.1);
8838
- const performerH = (sy1 - sy0) * 0.45;
8839
- ctx.fillStyle = "rgba(8, 10, 16, 0.85)";
8840
- for (const fx of [0.35, 0.5, 0.65]) {
8841
- const px = sx0 + (sx1 - sx0) * fx;
8842
- const py = sy1 - (sy1 - sy0) * 0.12;
9677
+ const fall = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, sw * 0.12, W / 2, (sy0 + sy1) / 2, sw * 0.62);
9678
+ fall.addColorStop(0, "rgba(6,8,14,0)");
9679
+ fall.addColorStop(0.7, "rgba(6,8,14,0.15)");
9680
+ fall.addColorStop(1, "rgba(5,6,11,0.92)");
9681
+ ctx.fillStyle = fall;
9682
+ ctx.fillRect(sx0, sy0, sw, sh);
9683
+ ctx.restore();
9684
+ const perfCount = 3 + Math.floor(rand() * 3);
9685
+ const perfH = sh * 0.46;
9686
+ const floorY = sy1 - sh * 0.08;
9687
+ for (let i = 0; i < perfCount; i++) {
9688
+ const t2 = (i + 1) / (perfCount + 1);
9689
+ const px = sx0 + sw * (0.22 + t2 * 0.56 + (rand() - 0.5) * 0.06);
9690
+ drawPerformer(ctx, px, floorY, perfH * (0.9 + rand() * 0.24), {
9691
+ tone: [8, 10, 16],
9692
+ alpha: 0.9,
9693
+ pose: Math.floor(rand() * 4),
9694
+ rim: hslToRgb(baseHue + 20, 0.5, 0.7),
9695
+ rimStrength: 0.7
9696
+ });
9697
+ }
9698
+ ctx.fillStyle = "#05070d";
9699
+ ctx.fillRect(sx0 - sw * 0.5, archTop, sw * 0.5, sy1 - archTop);
9700
+ ctx.fillRect(sx1, archTop, sw * 0.5, sy1 - archTop);
9701
+ const legBot = sw * 0.055 + 3;
9702
+ const legTop = legBot * 1.35;
9703
+ ctx.fillStyle = "#03050a";
9704
+ ctx.beginPath();
9705
+ ctx.moveTo(sx0, sy1);
9706
+ ctx.lineTo(sx0 - legBot, sy1);
9707
+ ctx.lineTo(sx0 - legTop, archTop);
9708
+ ctx.lineTo(sx0 + sw * 0.012, archTop);
9709
+ ctx.closePath();
9710
+ ctx.fill();
9711
+ ctx.beginPath();
9712
+ ctx.moveTo(sx1, sy1);
9713
+ ctx.lineTo(sx1 + legBot, sy1);
9714
+ ctx.lineTo(sx1 + legTop, archTop);
9715
+ ctx.lineTo(sx1 - sw * 0.012, archTop);
9716
+ ctx.closePath();
9717
+ ctx.fill();
9718
+ ctx.fillRect(sx0 - legTop, archTop, sw + legTop * 2, (sy1 - archTop) * 0.12);
9719
+ const valH = sh * 0.14;
9720
+ const valY = archTop + (sy1 - archTop) * 0.12;
9721
+ ctx.fillStyle = "rgba(4,6,12,0.96)";
9722
+ const scallops = 7;
9723
+ ctx.beginPath();
9724
+ ctx.moveTo(sx0 - legTop, valY);
9725
+ for (let i = 0; i <= scallops; i++) {
9726
+ const xx = sx0 - legTop + (sw + legTop * 2) * i / scallops;
9727
+ ctx.quadraticCurveTo(xx - (sw + legTop * 2) / scallops / 2, valY + valH, xx, valY);
9728
+ }
9729
+ ctx.lineTo(sx1 + legTop, valY);
9730
+ ctx.lineTo(sx1 + legTop, archTop);
9731
+ ctx.lineTo(sx0 - legTop, archTop);
9732
+ ctx.closePath();
9733
+ ctx.fill();
9734
+ ctx.fillStyle = `rgba(${rigTintStr},0.12)`;
9735
+ ctx.fillRect(sx0 - 1.5, valY, 1.5, sy1 - valY);
9736
+ ctx.fillRect(sx1, valY, 1.5, sy1 - valY);
9737
+ ctx.fillStyle = "#0b0f18";
9738
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.05 + 3);
9739
+ const foot = ctx.createLinearGradient(0, sy1 - sh * 0.05, 0, sy1 + sh * 0.12);
9740
+ foot.addColorStop(0, `rgba(255,206,140,${(0.5 * nearGlow).toFixed(3)})`);
9741
+ foot.addColorStop(1, "rgba(255,206,140,0)");
9742
+ ctx.fillStyle = foot;
9743
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.2);
9744
+ const beams = 3 + Math.floor(rand() * 2);
9745
+ for (let i = 0; i < beams; i++) {
9746
+ const t2 = (i + 1) / (beams + 1);
9747
+ const fx = sx0 + sw * (t2 + (rand() - 0.5) * 0.08);
9748
+ const fy = pitchToY(Math.min(62, stageTopPitch + 26));
9749
+ const skew = (rand() - 0.5) * sw * 0.16;
9750
+ drawBeam(ctx, fx, fy, fx + skew, sy1, 4, sw * 0.09, rigTintStr, 0.16 * (0.7 + nearGlow * 0.5));
9751
+ drawFixture(ctx, fx, fy, 2.4, rigTintStr);
9752
+ }
9753
+ }
9754
+ function drawArenaScene(ctx, distM, eyeM, scene, nearGlow, rigTintStr, rand) {
9755
+ const stageR = Math.min(distM * 0.6, Math.max(3, scene.voidRadiusM));
9756
+ const halfYaw = Math.min(55, Math.atan2(stageR, distM) * 180 / Math.PI);
9757
+ const cx = W / 2;
9758
+ const deckRiserM = 1.2;
9759
+ const nearDist = Math.max(distM * 0.45, distM - stageR);
9760
+ const farDist = distM + stageR;
9761
+ const nearTopPitch = Math.atan2(deckRiserM - eyeM, nearDist) * 180 / Math.PI;
9762
+ const farTopPitch = Math.atan2(deckRiserM - eyeM, farDist) * 180 / Math.PI;
9763
+ const nearBasePitch = Math.atan2(-eyeM, nearDist) * 180 / Math.PI;
9764
+ const yNearTop = pitchToY(nearTopPitch);
9765
+ const yFarTop = pitchToY(farTopPitch);
9766
+ const yBase = pitchToY(nearBasePitch);
9767
+ const halfW = yawToX(halfYaw) - cx;
9768
+ const topRy = Math.max(6, (yNearTop - yFarTop) / 2);
9769
+ const topCy = (yNearTop + yFarTop) / 2;
9770
+ const pool = ctx.createRadialGradient(cx, yBase, 4, cx, yBase, halfW * 2.6);
9771
+ pool.addColorStop(0, `rgba(255,206,150,${(0.2 * nearGlow).toFixed(3)})`);
9772
+ pool.addColorStop(0.5, "rgba(150,140,190,0.08)");
9773
+ pool.addColorStop(1, "rgba(40,44,60,0)");
9774
+ ctx.save();
9775
+ ctx.globalCompositeOperation = "lighter";
9776
+ ctx.fillStyle = pool;
9777
+ ctx.fillRect(cx - halfW * 2.6, yFarTop - topRy, halfW * 5.2, yBase - yFarTop + topRy + 40);
9778
+ ctx.restore();
9779
+ ctx.fillStyle = "#0c1019";
9780
+ ctx.beginPath();
9781
+ ctx.moveTo(cx - halfW, topCy);
9782
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, Math.PI, 0, false);
9783
+ ctx.lineTo(cx + halfW, yBase);
9784
+ ctx.ellipse(cx, yBase, halfW, topRy, 0, 0, Math.PI, false);
9785
+ ctx.closePath();
9786
+ ctx.fill();
9787
+ const riser = ctx.createLinearGradient(0, topCy, 0, yBase);
9788
+ riser.addColorStop(0, `rgba(${rigTintStr},0.14)`);
9789
+ riser.addColorStop(1, "rgba(0,0,0,0)");
9790
+ ctx.fillStyle = riser;
9791
+ ctx.fillRect(cx - halfW, topCy, halfW * 2, yBase - topCy);
9792
+ const FORMATIONS = [
9793
+ // frontman + guitarist + one at the back
9794
+ [{ u: -0.24, v: 0.4, lead: true, pose: 3 }, { u: 0.34, v: -0.04, pose: 2 }, { u: -0.52, v: -0.38, pose: 0 }],
9795
+ // duo — singer forward, instrument behind the shoulder
9796
+ [{ u: 0.2, v: 0.34, lead: true, pose: 3 }, { u: -0.3, v: -0.08, pose: 2 }],
9797
+ // 4-piece — lead just off-centre, band staggered around the far half
9798
+ [{ u: -0.1, v: 0.28, lead: true, pose: 1 }, { u: 0.44, v: -0.2, pose: 2 }, { u: -0.46, v: -0.14, pose: 2 }, { u: 0.08, v: -0.46, pose: 0 }]
9799
+ ];
9800
+ const form = FORMATIONS[Math.floor(rand() * FORMATIONS.length)];
9801
+ const mirror = rand() < 0.5 ? -1 : 1;
9802
+ const spotX = (p) => cx + p.u * mirror * halfW * 0.84;
9803
+ const spotY = (p) => topCy + p.v * topRy * 0.8;
9804
+ const lead = form.find((p) => p.lead) ?? form[0];
9805
+ const leadX = spotX(lead);
9806
+ const deckTop = ctx.createLinearGradient(0, topCy - topRy, 0, topCy + topRy);
9807
+ deckTop.addColorStop(0, "#3d4a72");
9808
+ deckTop.addColorStop(1, "#232b47");
9809
+ ctx.fillStyle = deckTop;
9810
+ ctx.beginPath();
9811
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU2);
9812
+ ctx.fill();
9813
+ const deckGlow = ctx.createRadialGradient(leadX, topCy, 2, leadX, topCy, halfW);
9814
+ deckGlow.addColorStop(0, `rgba(255,214,160,${(0.5 * nearGlow).toFixed(3)})`);
9815
+ deckGlow.addColorStop(1, "rgba(255,214,160,0)");
9816
+ ctx.save();
9817
+ ctx.globalCompositeOperation = "lighter";
9818
+ ctx.fillStyle = deckGlow;
9819
+ ctx.beginPath();
9820
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU2);
9821
+ ctx.fill();
9822
+ ctx.restore();
9823
+ ctx.strokeStyle = `rgba(${rigTintStr},0.7)`;
9824
+ ctx.lineWidth = 2;
9825
+ ctx.beginPath();
9826
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU2);
9827
+ ctx.stroke();
9828
+ const perfH = Math.max(24, (yBase - yFarTop) * 1.3);
9829
+ const gearN = form.length >= 3 ? 3 : 2;
9830
+ for (let i = 0; i < gearN; i++) {
9831
+ const gu = (-0.62 + i * 0.52 + (rand() - 0.5) * 0.12) * -mirror;
9832
+ const gx = cx + gu * halfW * 0.7;
9833
+ const gy = topCy - topRy * (0.45 + rand() * 0.18);
9834
+ const gw = halfW * (0.07 + rand() * 0.04);
9835
+ const gh = perfH * (0.26 + rand() * 0.12);
9836
+ ctx.fillStyle = "#0a0e18";
9837
+ ctx.fillRect(gx - gw / 2, gy - gh, gw, gh);
9838
+ ctx.fillStyle = `rgba(${rigTintStr},0.3)`;
9839
+ ctx.fillRect(gx - gw / 2, gy - gh, gw, 1.5);
9840
+ }
9841
+ if (form.length >= 3) {
9842
+ const drumX = cx - lead.u * mirror * halfW * 0.4;
9843
+ ctx.fillStyle = "#0b0f1a";
8843
9844
  ctx.beginPath();
8844
- ctx.arc(px, py - performerH * 0.82, performerH * 0.16, 0, Math.PI * 2);
9845
+ ctx.ellipse(drumX, topCy - topRy * 0.34, halfW * 0.08, topRy * 0.1, 0, 0, TAU2);
8845
9846
  ctx.fill();
8846
- ctx.fillRect(px - performerH * 0.14, py - performerH * 0.68, performerH * 0.28, performerH * 0.68);
9847
+ ctx.strokeStyle = `rgba(${rigTintStr},0.35)`;
9848
+ ctx.lineWidth = 1;
9849
+ ctx.stroke();
9850
+ }
9851
+ for (const p of [...form].sort((a, b) => a.v - b.v)) {
9852
+ const depthScale = 0.82 + (p.v + 1) * 0.14;
9853
+ drawPerformer(ctx, spotX(p), spotY(p), perfH * depthScale * (p.lead ? 1.06 : 0.92), {
9854
+ tone: [18, 21, 34],
9855
+ alpha: 0.95,
9856
+ pose: p.pose,
9857
+ rim: hslToRgb(40, 0.72, 0.8),
9858
+ rimStrength: p.lead ? 1.4 : 0.9
9859
+ });
8847
9860
  }
8848
- ctx.globalAlpha = 0.14;
8849
- ctx.fillStyle = "#c7d2fe";
8850
- for (const fx of [0.3, 0.5, 0.7]) {
8851
- const bx = sx0 + (sx1 - sx0) * fx;
9861
+ const trussY = pitchToY(Math.min(60, farTopPitch + 34));
9862
+ const trussRy = topRy * 1.1 + 6;
9863
+ ctx.strokeStyle = "rgba(60,68,92,0.85)";
9864
+ ctx.lineWidth = 3;
9865
+ ctx.beginPath();
9866
+ ctx.ellipse(cx, trussY, halfW * 1.15, trussRy, 0, 0, TAU2);
9867
+ ctx.stroke();
9868
+ const fixtures = 4 + Math.floor(rand() * 3);
9869
+ for (let i = 0; i < fixtures; i++) {
9870
+ const a = i / fixtures * TAU2;
9871
+ const fxX = cx + Math.cos(a) * halfW * 1.15;
9872
+ const fxY = trussY + Math.sin(a) * trussRy;
9873
+ if (Math.sin(a) > -0.2) {
9874
+ const tx = cx + (rand() - 0.5) * halfW * 0.8;
9875
+ drawBeam(ctx, fxX, fxY, tx, topCy, 3, halfW * 0.32, rigTintStr, 0.2 * (0.7 + nearGlow * 0.4));
9876
+ }
9877
+ drawFixture(ctx, fxX, fxY, 3, rigTintStr);
9878
+ }
9879
+ const scoreY = pitchToY(Math.min(52, farTopPitch + 24));
9880
+ const scoreW = Math.max(60, halfW * 0.4);
9881
+ const scoreH = Math.max(24, scoreW * 0.34);
9882
+ ctx.strokeStyle = "rgba(70,78,102,0.6)";
9883
+ ctx.lineWidth = 1.5;
9884
+ ctx.beginPath();
9885
+ ctx.moveTo(cx - scoreW * 0.3, scoreY - scoreH / 2);
9886
+ ctx.lineTo(cx - scoreW * 0.16, scoreY - scoreH * 2.2);
9887
+ ctx.moveTo(cx + scoreW * 0.3, scoreY - scoreH / 2);
9888
+ ctx.lineTo(cx + scoreW * 0.16, scoreY - scoreH * 2.2);
9889
+ ctx.stroke();
9890
+ ctx.fillStyle = "#0d1220";
9891
+ ctx.fillRect(cx - scoreW / 2, scoreY - scoreH / 2, scoreW, scoreH);
9892
+ const screen = ctx.createLinearGradient(0, scoreY - scoreH * 0.3, 0, scoreY + scoreH * 0.42);
9893
+ screen.addColorStop(0, `rgba(${rigTintStr},0.5)`);
9894
+ screen.addColorStop(1, "rgba(255,214,160,0.32)");
9895
+ ctx.fillStyle = screen;
9896
+ ctx.fillRect(cx - scoreW * 0.42, scoreY - scoreH * 0.3, scoreW * 0.84, scoreH * 0.72);
9897
+ ctx.fillStyle = "rgba(255,214,160,0.55)";
9898
+ ctx.fillRect(cx - scoreW / 2, scoreY + scoreH / 2 - 2, scoreW, 2);
9899
+ const scoreGlow = ctx.createRadialGradient(cx, scoreY, 4, cx, scoreY, scoreW * 1.1);
9900
+ scoreGlow.addColorStop(0, `rgba(${rigTintStr},0.2)`);
9901
+ scoreGlow.addColorStop(1, "rgba(0,0,0,0)");
9902
+ ctx.save();
9903
+ ctx.globalCompositeOperation = "lighter";
9904
+ ctx.fillStyle = scoreGlow;
9905
+ ctx.fillRect(cx - scoreW * 1.1, scoreY - scoreW * 0.8, scoreW * 2.2, scoreW * 1.6);
9906
+ const airGlow = ctx.createRadialGradient(cx, (trussY + topCy) / 2, 6, cx, (trussY + topCy) / 2, halfW * 1.5);
9907
+ airGlow.addColorStop(0, `rgba(${rigTintStr},${(0.1 * (0.6 + nearGlow * 0.5)).toFixed(3)})`);
9908
+ airGlow.addColorStop(1, "rgba(0,0,0,0)");
9909
+ ctx.fillStyle = airGlow;
9910
+ ctx.fillRect(cx - halfW * 1.5, trussY - 40, halfW * 3, topCy - trussY + 80);
9911
+ ctx.restore();
9912
+ const keyX = cx + Math.sign(leadX - cx || 1) * halfW * 0.5;
9913
+ drawBeam(ctx, keyX, trussY - trussRy * 0.4, leadX, spotY(lead), 2.5, halfW * 0.16, "255,224,178", 0.42 * (0.7 + nearGlow * 0.4));
9914
+ const pool2 = ctx.createRadialGradient(leadX, spotY(lead), 1, leadX, spotY(lead), halfW * 0.2);
9915
+ pool2.addColorStop(0, `rgba(255,228,185,${(0.4 * nearGlow).toFixed(3)})`);
9916
+ pool2.addColorStop(1, "rgba(255,228,185,0)");
9917
+ ctx.save();
9918
+ ctx.globalCompositeOperation = "lighter";
9919
+ ctx.fillStyle = pool2;
9920
+ ctx.beginPath();
9921
+ ctx.ellipse(leadX, spotY(lead), halfW * 0.2, topRy * 0.24, 0, 0, TAU2);
9922
+ ctx.fill();
9923
+ ctx.restore();
9924
+ }
9925
+ function drawSportScene(ctx, distM, eyeM, scene, _nearGlow, baseHue, rigTintStr, rand) {
9926
+ const cx = W / 2;
9927
+ const surfR = Math.max(10, scene.voidRadiusM);
9928
+ const nearEdge = Math.max(2, distM - surfR);
9929
+ const farEdge = distM + surfR;
9930
+ const halfYaw = Math.min(78, Math.atan2(surfR * 1.15, distM) * 180 / Math.PI);
9931
+ const nearPitch = Math.atan2(-eyeM, nearEdge) * 180 / Math.PI;
9932
+ const farPitch = Math.atan2(-eyeM, farEdge) * 180 / Math.PI;
9933
+ const yNear = pitchToY(nearPitch);
9934
+ const yFar = pitchToY(farPitch);
9935
+ const halfW = yawToX(halfYaw) - cx;
9936
+ const cy = (yNear + yFar) / 2;
9937
+ const ry = Math.max(10, (yNear - yFar) / 2);
9938
+ const surfCol = ctx.createRadialGradient(cx, cy, 4, cx, cy, halfW);
9939
+ surfCol.addColorStop(0, "#e8f0fb");
9940
+ surfCol.addColorStop(0.7, "#b9c9e2");
9941
+ surfCol.addColorStop(1, "#8fa3c4");
9942
+ ctx.save();
9943
+ ctx.beginPath();
9944
+ ctx.ellipse(cx, cy, halfW, ry, 0, 0, TAU2);
9945
+ ctx.clip();
9946
+ ctx.fillStyle = surfCol;
9947
+ ctx.fillRect(cx - halfW, cy - ry, halfW * 2, ry * 2);
9948
+ ctx.strokeStyle = "rgba(90,120,170,0.5)";
9949
+ ctx.lineWidth = 3;
9950
+ ctx.beginPath();
9951
+ ctx.moveTo(cx, cy - ry);
9952
+ ctx.lineTo(cx, cy + ry);
9953
+ ctx.moveTo(cx - halfW * 0.5, cy - ry);
9954
+ ctx.lineTo(cx - halfW * 0.5, cy + ry);
9955
+ ctx.moveTo(cx + halfW * 0.5, cy - ry);
9956
+ ctx.lineTo(cx + halfW * 0.5, cy + ry);
9957
+ ctx.stroke();
9958
+ ctx.strokeStyle = "rgba(180,90,110,0.45)";
9959
+ ctx.beginPath();
9960
+ ctx.ellipse(cx, cy, ry * 0.9, ry * 0.5, 0, 0, TAU2);
9961
+ ctx.stroke();
9962
+ ctx.restore();
9963
+ ctx.strokeStyle = "rgba(210,225,245,0.6)";
9964
+ ctx.lineWidth = 2.5;
9965
+ ctx.beginPath();
9966
+ ctx.ellipse(cx, cy, halfW, ry, 0, 0, TAU2);
9967
+ ctx.stroke();
9968
+ ctx.fillStyle = "rgba(20,26,40,0.8)";
9969
+ for (let i = 0; i < 5; i++) {
9970
+ const px = cx + (rand() - 0.5) * halfW * 1.4;
9971
+ const py = cy + (rand() - 0.5) * ry * 1.1;
9972
+ const s = Math.max(2, ry * 0.06);
8852
9973
  ctx.beginPath();
8853
- ctx.moveTo(bx - 8, pitchToY(Math.min(60, stageTopPitch + 25)));
8854
- ctx.lineTo(bx - (sx1 - sx0) * 0.09, sy1);
8855
- ctx.lineTo(bx + (sx1 - sx0) * 0.09, sy1);
8856
- ctx.lineTo(bx + 8, pitchToY(Math.min(60, stageTopPitch + 25)));
8857
- ctx.closePath();
9974
+ ctx.ellipse(px, py, s * 0.6, s, 0, 0, TAU2);
8858
9975
  ctx.fill();
8859
9976
  }
8860
- ctx.globalAlpha = 1;
8861
- ctx.fillStyle = "rgba(199, 210, 254, 0.8)";
8862
- for (let i = 0; i < 26; i++) {
8863
- const x = i / 26 * W;
8864
- const y = pitchToY(55 + i % 3 * 6);
9977
+ const wash = ctx.createRadialGradient(cx, cy, 4, cx, cy, halfW * 1.4);
9978
+ wash.addColorStop(0, "rgba(200,220,255,0.12)");
9979
+ wash.addColorStop(1, "rgba(200,220,255,0)");
9980
+ ctx.save();
9981
+ ctx.globalCompositeOperation = "lighter";
9982
+ ctx.fillStyle = wash;
9983
+ ctx.fillRect(cx - halfW * 1.4, yFar - ry, halfW * 2.8, yNear - yFar + ry * 2);
9984
+ ctx.restore();
9985
+ const sbY = pitchToY(Math.min(66, farPitch + 60));
9986
+ const sbW = Math.max(80, halfW * 0.7);
9987
+ const sbH = sbW * 0.5;
9988
+ const sbGlow = ctx.createRadialGradient(cx, sbY, 4, cx, sbY, sbW * 1.6);
9989
+ sbGlow.addColorStop(0, `rgba(${rigTintStr},0.3)`);
9990
+ sbGlow.addColorStop(1, `rgba(${rigTintStr},0)`);
9991
+ ctx.save();
9992
+ ctx.globalCompositeOperation = "lighter";
9993
+ ctx.fillStyle = sbGlow;
9994
+ ctx.fillRect(cx - sbW * 1.6, sbY - sbW * 1.2, sbW * 3.2, sbW * 2.4);
9995
+ ctx.restore();
9996
+ ctx.fillStyle = "#080b12";
9997
+ ctx.beginPath();
9998
+ ctx.moveTo(cx - sbW / 2, sbY - sbH / 2);
9999
+ ctx.lineTo(cx + sbW / 2, sbY - sbH / 2);
10000
+ ctx.lineTo(cx + sbW * 0.6, sbY);
10001
+ ctx.lineTo(cx + sbW / 2, sbY + sbH / 2);
10002
+ ctx.lineTo(cx - sbW / 2, sbY + sbH / 2);
10003
+ ctx.lineTo(cx - sbW * 0.6, sbY);
10004
+ ctx.closePath();
10005
+ ctx.fill();
10006
+ const screenHue = baseHue;
10007
+ for (let i = 0; i < 3; i++) {
10008
+ const scol = hslToRgb(screenHue + i * 30, 0.5, 0.5);
10009
+ ctx.fillStyle = `rgba(${scol[0]},${scol[1]},${scol[2]},0.75)`;
10010
+ const swid = sbW * 0.26;
10011
+ ctx.fillRect(cx - sbW * 0.42 + i * (swid + sbW * 0.06), sbY - sbH * 0.32, swid, sbH * 0.64);
10012
+ }
10013
+ ctx.fillStyle = `rgba(${rigTintStr},0.9)`;
10014
+ for (let i = 0; i < 8; i++) {
10015
+ const lx = cx - sbW * 0.6 + sbW * 1.2 * i / 7;
8865
10016
  ctx.beginPath();
8866
- ctx.arc(x, y, 3.2, 0, Math.PI * 2);
10017
+ ctx.arc(lx, sbY + sbH * 0.62, 2.4, 0, TAU2);
8867
10018
  ctx.fill();
8868
10019
  }
8869
- const stageBearing = Math.atan2(dx, -dy);
8870
- ctx.fillStyle = "rgba(16, 20, 30, 0.95)";
10020
+ }
10021
+ function drawAudience(ctx, seat, neighborSeats, _focalPoint, stageBearing, eyeM, dx, dy, hasRelief, scene) {
10022
+ const own = seat.sectionId;
10023
+ let seatAhead = false;
10024
+ const figs = [];
8871
10025
  for (const other of neighborSeats) {
8872
10026
  if (other.id === seat.id) continue;
8873
10027
  const ox = other.x - seat.x;
8874
10028
  const oy = other.y - seat.y;
8875
10029
  const d = Math.hypot(ox, oy) * UNIT;
8876
- if (d < 0.3 || d > 14) continue;
10030
+ if (own && other.sectionId === own && d < 3 && ox * dx + oy * dy > 0) seatAhead = true;
10031
+ if (d < 0.3 || d > 32) continue;
8877
10032
  const bearing = Math.atan2(ox, -oy) - stageBearing;
8878
10033
  const yaw = (bearing * 180 / Math.PI + 540) % 360 - 180;
8879
- const headPitch = -Math.atan2(0.35, d) * 180 / Math.PI;
8880
- const headR = Math.min(46, 260 / (d / UNIT / 24 + 1.2) / 4);
8881
- const hx = yawToX(yaw);
8882
- const hy = pitchToY(headPitch);
8883
- ctx.beginPath();
8884
- ctx.arc(hx, hy, headR, 0, Math.PI * 2);
8885
- ctx.fill();
8886
- ctx.beginPath();
8887
- ctx.ellipse(hx, hy + headR * 1.4, headR * 1.7, headR * 0.9, 0, Math.PI, 0, true);
8888
- ctx.fill();
10034
+ const rise = hasRelief ? (other.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - eyeM : 0;
10035
+ const headPitch = Math.atan2(rise - 0.28, d) * 180 / Math.PI;
10036
+ if (headPitch > 10 && d > 5) continue;
10037
+ let hh = 0;
10038
+ for (let k = 0; k < other.id.length; k++) hh = hh * 31 + other.id.charCodeAt(k) & 65535;
10039
+ const jitter = 0.9 + (hh & 15) / 40;
10040
+ const r = Math.min(78, Math.atan2(0.16, d) * 180 / Math.PI * PX_PER_DEG * jitter);
10041
+ if (r < 1.2) continue;
10042
+ figs.push({ hx: yawToX(yaw), hy: pitchToY(headPitch), r, d, hash: hh });
10043
+ }
10044
+ figs.sort((a, b) => b.d - a.d);
10045
+ const figRows = [];
10046
+ {
10047
+ const byY = [...figs].sort((a, b) => a.hy - b.hy);
10048
+ let cur = [];
10049
+ const flush = () => {
10050
+ if (!cur.length) return;
10051
+ const d = cur.reduce((s, f) => s + f.d, 0) / cur.length;
10052
+ const hy = cur.reduce((s, f) => s + f.hy, 0) / cur.length;
10053
+ const r = cur.reduce((s, f) => s + f.r, 0) / cur.length;
10054
+ figRows.push({ figs: cur.sort((a, b) => b.d - a.d), d, hy, r });
10055
+ cur = [];
10056
+ };
10057
+ for (const f of byY) {
10058
+ const last = cur[cur.length - 1];
10059
+ if (last && Math.abs(f.hy - last.hy) > Math.max(5, (last.r + f.r) / 2 * 1.7)) flush();
10060
+ cur.push(f);
10061
+ }
10062
+ flush();
10063
+ }
10064
+ figRows.sort((a, b) => b.d - a.d);
10065
+ const CLOTHES = [[22, 26, 40], [28, 27, 42], [24, 31, 46], [31, 30, 47], [20, 24, 36]];
10066
+ for (const row of figRows) {
10067
+ if (row.figs.length >= 3 && row.r >= 1.6 && row.r < 26) {
10068
+ const runs = [];
10069
+ const byX = [...row.figs].sort((a, b) => a.hx - b.hx);
10070
+ let run = [];
10071
+ for (const f of byX) {
10072
+ const last = run[run.length - 1];
10073
+ if (last && f.hx - last.hx > Math.max(14, f.r * 7)) {
10074
+ if (run.length >= 3) runs.push(run);
10075
+ run = [];
10076
+ }
10077
+ run.push(f);
10078
+ }
10079
+ if (run.length >= 3) runs.push(run);
10080
+ const stripAlpha = Math.min(0.42, 0.5 * (1 - row.d / 42));
10081
+ if (stripAlpha > 0.05) {
10082
+ ctx.fillStyle = `rgba(15, 19, 31, ${stripAlpha.toFixed(3)})`;
10083
+ for (const seg of runs) {
10084
+ const x0 = seg[0].hx - row.r * 1.6;
10085
+ const x1 = seg[seg.length - 1].hx + row.r * 1.6;
10086
+ ctx.beginPath();
10087
+ ctx.roundRect(x0, row.hy + row.r * 0.8, x1 - x0, row.r * 2.4, row.r * 0.8);
10088
+ ctx.fill();
10089
+ }
10090
+ }
10091
+ }
10092
+ for (const f of row.figs) {
10093
+ const alpha = f.d < 12 ? 0.95 : f.d < 17 ? Math.max(0.66, 0.95 - (f.d - 12) / 18) : Math.max(0.3, 0.68 - (f.d - 17) / 34);
10094
+ const lift = Math.round((1 - Math.min(1, f.d / 16)) * 10);
10095
+ const headTone = [14 + (f.hash >> 4 & 6) + lift, 17 + (f.hash >> 4 & 6) + lift, 25 + (f.hash >> 4 & 6) + lift];
10096
+ const clo0 = CLOTHES[f.hash % CLOTHES.length];
10097
+ const clothing = [clo0[0] + lift, clo0[1] + lift, clo0[2] + lift];
10098
+ if (f.r >= 13) {
10099
+ const rim = f.d < 9 ? 0.16 * (1 - f.d / 9) + (scene.mode === "proscenium" ? 0.06 : 0) : 0;
10100
+ drawSeated(ctx, f.hx, f.hy, f.r, {
10101
+ tone: headTone,
10102
+ clothing,
10103
+ alpha,
10104
+ tilt: ((f.hash >> 2 & 7) - 3.5) * 0.018,
10105
+ // ±~7°
10106
+ shoulderK: 1.72 + (f.hash >> 5 & 7) / 22,
10107
+ // ~1.72..2.04
10108
+ hair: f.hash % HAIR_KINDS,
10109
+ rim,
10110
+ turn: ((f.hash >> 8 & 7) - 3.5) * 0.034,
10111
+ // ±~0.12 head-radii sideways
10112
+ lean: ((f.hash >> 10 & 3) - 1.5) * 0.09
10113
+ // one shoulder rides higher
10114
+ });
10115
+ } else if (f.r >= 5.5) {
10116
+ drawSeatedMid(ctx, f.hx, f.hy, f.r, clothing, headTone, alpha);
10117
+ } else {
10118
+ ctx.fillStyle = `rgba(${headTone[0]},${headTone[1]},${headTone[2]},${alpha.toFixed(3)})`;
10119
+ ctx.beginPath();
10120
+ ctx.arc(f.hx, f.hy, f.r, 0, TAU2);
10121
+ ctx.fill();
10122
+ ctx.beginPath();
10123
+ ctx.ellipse(f.hx, f.hy + f.r * 1.4, f.r * 1.85, f.r * 0.95, 0, Math.PI, 0, true);
10124
+ ctx.fill();
10125
+ }
10126
+ }
10127
+ }
10128
+ if (seatAhead) {
10129
+ const railTop = pitchToY(-34);
10130
+ ctx.fillStyle = "rgba(6, 8, 14, 0.92)";
10131
+ ctx.fillRect(0, railTop, W, H - railTop);
10132
+ ctx.fillStyle = "rgba(24, 30, 46, 0.9)";
10133
+ const humps = 16;
10134
+ for (let i = 0; i < humps; i++) {
10135
+ const cx = (i + 0.5) / humps * W;
10136
+ ctx.beginPath();
10137
+ ctx.ellipse(cx, railTop + 6, W / humps * 0.42, 22, 0, Math.PI, 0, true);
10138
+ ctx.fill();
10139
+ }
8889
10140
  }
8890
- const poleFade = ctx.createLinearGradient(0, 0, 0, H);
8891
- poleFade.addColorStop(0, "rgba(0,0,0,0.85)");
8892
- poleFade.addColorStop(0.12, "rgba(0,0,0,0)");
8893
- poleFade.addColorStop(0.88, "rgba(0,0,0,0)");
8894
- poleFade.addColorStop(1, "rgba(0,0,0,0.85)");
8895
- ctx.fillStyle = poleFade;
8896
- ctx.fillRect(0, 0, W, H);
8897
- return { url: canvas.toDataURL("image/jpeg", 0.82), distanceM: Math.round(distM) };
8898
10141
  }
8899
10142
  var TW = 480;
8900
10143
  var TH = 270;
@@ -8910,12 +10153,16 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
8910
10153
  const dx = focalPoint.x - seat.x;
8911
10154
  const dy = focalPoint.y - seat.y;
8912
10155
  const distM = Math.max(2, Math.hypot(dx, dy) * UNIT);
10156
+ const rand = rng(fnv1a(`${seat.id}|${Math.round(focalPoint.x)},${Math.round(focalPoint.y)}`));
10157
+ const baseHue = 210 + rand() * 150;
10158
+ const rigTint = hslToRgb(220 + (rand() - 0.5) * 60, 0.55, 0.82);
10159
+ const rigTintStr = `${rigTint[0]},${rigTint[1]},${rigTint[2]}`;
8913
10160
  const sky = ctx.createLinearGradient(0, 0, 0, TH);
8914
- sky.addColorStop(0, "#05070c");
8915
- sky.addColorStop(0.44, "#141b30");
8916
- sky.addColorStop(0.5, "#1b2440");
8917
- sky.addColorStop(0.58, "#10141f");
8918
- sky.addColorStop(1, "#070910");
10161
+ sky.addColorStop(0, "#080b12");
10162
+ sky.addColorStop(0.44, "#151c32");
10163
+ sky.addColorStop(0.5, "#1d2644");
10164
+ sky.addColorStop(0.58, "#161c2e");
10165
+ sky.addColorStop(1, "#131829");
8919
10166
  ctx.fillStyle = sky;
8920
10167
  ctx.fillRect(0, 0, TW, TH);
8921
10168
  const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
@@ -8929,58 +10176,102 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
8929
10176
  const sy1 = pitchToTY(stageBasePitch);
8930
10177
  const sw = sx1 - sx0;
8931
10178
  const sh = sy1 - sy0;
8932
- const glow = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, 4, TW / 2, (sy0 + sy1) / 2, sw * 1.1);
8933
- glow.addColorStop(0, "rgba(129,140,248,0.5)");
8934
- glow.addColorStop(0.5, "rgba(99,102,241,0.15)");
10179
+ const nearGlow = Math.max(0.35, Math.min(1, 1 - (distM - 6) / 60));
10180
+ const archTop = pitchToTY(Math.min(HALF_VFOV - 1, stageTopPitch + 8));
10181
+ const glow = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, 4, TW / 2, (sy0 + sy1) / 2, sw * 1.15);
10182
+ glow.addColorStop(0, "rgba(255,214,150,0.34)");
10183
+ glow.addColorStop(0.28, "rgba(150,150,230,0.3)");
10184
+ glow.addColorStop(0.6, "rgba(99,102,241,0.12)");
8935
10185
  glow.addColorStop(1, "rgba(99,102,241,0)");
8936
10186
  ctx.fillStyle = glow;
8937
10187
  ctx.fillRect(sx0 - sw * 0.6, sy0 - sh * 1.2, sw * 2.2, sh * 3.4);
8938
- ctx.fillStyle = "#242c44";
8939
- ctx.fillRect(sx0, sy0, sw, sh);
10188
+ const topCol = hslToRgb(baseHue, 0.62, 0.55);
10189
+ const midCol = hslToRgb(baseHue + 40, 0.7, 0.5);
10190
+ const botCol = hslToRgb(baseHue + 70, 0.78, 0.52);
10191
+ ctx.save();
10192
+ ctx.beginPath();
10193
+ ctx.rect(sx0, sy0, sw, sh);
10194
+ ctx.clip();
8940
10195
  const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
8941
- backdrop.addColorStop(0, "#7c5cff");
8942
- backdrop.addColorStop(0.5, "#e1447a");
8943
- backdrop.addColorStop(1, "#f59e0b");
8944
- ctx.globalAlpha = 0.78;
10196
+ backdrop.addColorStop(0, `rgb(${topCol[0]},${topCol[1]},${topCol[2]})`);
10197
+ backdrop.addColorStop(0.5, `rgb(${midCol[0]},${midCol[1]},${midCol[2]})`);
10198
+ backdrop.addColorStop(1, `rgb(${botCol[0]},${botCol[1]},${botCol[2]})`);
10199
+ ctx.globalAlpha = 0.8;
8945
10200
  ctx.fillStyle = backdrop;
8946
- const inset = sw * 0.06;
8947
- ctx.fillRect(sx0 + inset, sy0 + sh * 0.12, sw - inset * 2, sh * 0.62);
10201
+ ctx.fillRect(sx0, sy0, sw, sh);
8948
10202
  ctx.globalAlpha = 1;
8949
- ctx.fillStyle = "#0d1119";
8950
- ctx.fillRect(sx0, sy1 - sh * 0.1, sw, sh * 0.1);
8951
- const ph = sh * 0.45;
8952
- ctx.fillStyle = "rgba(8,10,16,0.85)";
8953
- for (const fx of [0.35, 0.5, 0.65]) {
8954
- const px = sx0 + sw * fx;
8955
- const py = sy1 - sh * 0.12;
8956
- ctx.beginPath();
8957
- ctx.arc(px, py - ph * 0.82, ph * 0.16, 0, Math.PI * 2);
8958
- ctx.fill();
8959
- ctx.fillRect(px - ph * 0.14, py - ph * 0.68, ph * 0.28, ph * 0.68);
8960
- }
8961
- ctx.globalAlpha = 0.15;
8962
- ctx.fillStyle = "#c7d2fe";
8963
- for (const fx of [0.3, 0.5, 0.7]) {
8964
- const bx = sx0 + sw * fx;
8965
- ctx.beginPath();
8966
- ctx.moveTo(bx - 5, pitchToTY(Math.min(HALF_VFOV, stageTopPitch + 18)));
8967
- ctx.lineTo(bx - sw * 0.09, sy1);
8968
- ctx.lineTo(bx + sw * 0.09, sy1);
8969
- ctx.lineTo(bx + 5, pitchToTY(Math.min(HALF_VFOV, stageTopPitch + 18)));
8970
- ctx.closePath();
8971
- ctx.fill();
10203
+ const fall = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, sw * 0.12, TW / 2, (sy0 + sy1) / 2, sw * 0.6);
10204
+ fall.addColorStop(0, "rgba(6,8,14,0)");
10205
+ fall.addColorStop(0.7, "rgba(6,8,14,0.15)");
10206
+ fall.addColorStop(1, "rgba(5,6,11,0.9)");
10207
+ ctx.fillStyle = fall;
10208
+ ctx.fillRect(sx0, sy0, sw, sh);
10209
+ ctx.restore();
10210
+ const perfCount = 3;
10211
+ const floorY = sy1 - sh * 0.08;
10212
+ for (let i = 0; i < perfCount; i++) {
10213
+ const t2 = (i + 1) / (perfCount + 1);
10214
+ drawPerformer(ctx, sx0 + sw * (0.24 + t2 * 0.52), floorY, sh * 0.46 * (0.9 + rand() * 0.24), {
10215
+ tone: [8, 10, 16],
10216
+ alpha: 0.9,
10217
+ pose: Math.floor(rand() * 4),
10218
+ rim: hslToRgb(baseHue + 20, 0.5, 0.7),
10219
+ rimStrength: 0.7
10220
+ });
8972
10221
  }
8973
- ctx.globalAlpha = 1;
8974
- ctx.fillStyle = "rgba(199,210,254,0.75)";
10222
+ ctx.fillStyle = "#05070d";
10223
+ ctx.fillRect(sx0 - sw * 0.5, archTop, sw * 0.5, sy1 - archTop);
10224
+ ctx.fillRect(sx1, archTop, sw * 0.5, sy1 - archTop);
10225
+ const legBot = sw * 0.05 + 2;
10226
+ const legTop = legBot * 1.35;
10227
+ ctx.fillStyle = "#03050a";
10228
+ ctx.beginPath();
10229
+ ctx.moveTo(sx0, sy1);
10230
+ ctx.lineTo(sx0 - legBot, sy1);
10231
+ ctx.lineTo(sx0 - legTop, archTop);
10232
+ ctx.lineTo(sx0 + sw * 0.012, archTop);
10233
+ ctx.closePath();
10234
+ ctx.fill();
10235
+ ctx.beginPath();
10236
+ ctx.moveTo(sx1, sy1);
10237
+ ctx.lineTo(sx1 + legBot, sy1);
10238
+ ctx.lineTo(sx1 + legTop, archTop);
10239
+ ctx.lineTo(sx1 - sw * 0.012, archTop);
10240
+ ctx.closePath();
10241
+ ctx.fill();
10242
+ ctx.fillRect(sx0 - legTop, archTop, sw + legTop * 2, (sy1 - archTop) * 0.13);
10243
+ ctx.fillStyle = `rgba(${rigTintStr},0.12)`;
10244
+ ctx.fillRect(sx0 - 1.2, archTop, 1.2, sy1 - archTop);
10245
+ ctx.fillRect(sx1, archTop, 1.2, sy1 - archTop);
10246
+ ctx.fillStyle = "#0b0f18";
10247
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.05 + 2);
10248
+ const foot = ctx.createLinearGradient(0, sy1 - sh * 0.05, 0, sy1 + sh * 0.14);
10249
+ foot.addColorStop(0, `rgba(255,206,140,${(0.5 * nearGlow).toFixed(3)})`);
10250
+ foot.addColorStop(1, "rgba(255,206,140,0)");
10251
+ ctx.fillStyle = foot;
10252
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.22);
10253
+ for (let i = 0; i < 3; i++) {
10254
+ const t2 = (i + 1) / 4;
10255
+ const fx = sx0 + sw * t2;
10256
+ const fy = pitchToTY(Math.min(HALF_VFOV - 1, stageTopPitch + 20));
10257
+ drawBeam(ctx, fx, fy, fx + (rand() - 0.5) * sw * 0.14, sy1, 3, sw * 0.09, rigTintStr, 0.17 * nearGlow);
10258
+ drawFixture(ctx, fx, fy, 1.8, rigTintStr);
10259
+ }
10260
+ ctx.fillStyle = `rgba(${rigTintStr},0.75)`;
8975
10261
  for (let i = 0; i < 8; i++) {
8976
10262
  ctx.beginPath();
8977
- ctx.arc((i + 0.5) / 8 * TW, pitchToTY(HALF_VFOV - 4 - i % 2 * 3), 2.2, 0, Math.PI * 2);
10263
+ ctx.arc((i + 0.5) / 8 * TW, pitchToTY(HALF_VFOV - 4 - i % 2 * 3), 2.2, 0, TAU2);
8978
10264
  ctx.fill();
8979
10265
  }
8980
10266
  ctx.fillStyle = "rgba(10,13,20,0.9)";
8981
10267
  ctx.fillRect(0, TH - 26, TW, 26);
8982
10268
  ctx.fillStyle = "rgba(30,37,54,0.9)";
8983
- for (let i = 0; i < 9; i++) ctx.fillRect(i * (TW / 9) + 6, TH - 22, TW / 9 - 12, 14);
10269
+ for (let i = 0; i < 9; i++) {
10270
+ const bx = i * (TW / 9) + TW / 18;
10271
+ ctx.beginPath();
10272
+ ctx.ellipse(bx, TH - 22, TW / 9 * 0.4, 15, 0, Math.PI, 0, true);
10273
+ ctx.fill();
10274
+ }
8984
10275
  return { url: canvas.toDataURL("image/jpeg", 0.8), distanceM: Math.round(distM) };
8985
10276
  }
8986
10277