@seatlayer/core 0.28.4 → 0.29.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.js CHANGED
@@ -1,3 +1,12 @@
1
+ import {
2
+ CHART_UNITS_PER_METRE,
3
+ METRES_PER_CHART_UNIT,
4
+ SEATED_EYE_HEIGHT_M,
5
+ TIER_HEIGHT_M,
6
+ sectionElevationTier,
7
+ sectionGeometry
8
+ } from "./chunk-5MLADB2N.js";
9
+
1
10
  // src/core/types.ts
2
11
  var ACCESSIBILITY_TYPES = [
3
12
  { key: "wheelchair", label: "Wheelchair space", short: "Wheelchair", icon: "\u267F" },
@@ -180,50 +189,6 @@ function toRoman(value) {
180
189
  return out;
181
190
  }
182
191
 
183
- // src/core/units.ts
184
- var METRES_PER_CHART_UNIT = 0.55 / 24;
185
- var CHART_UNITS_PER_METRE = 1 / METRES_PER_CHART_UNIT;
186
- var LIFT_PER_STEP_WORLD = 58;
187
- var TIER_HEIGHT_M = LIFT_PER_STEP_WORLD * METRES_PER_CHART_UNIT;
188
- var SECTION_ELEVATION_TIER_MIN = 0;
189
- var SECTION_ELEVATION_TIER_MAX = 3;
190
- var SECTION_HEIGHT_MIN_M = 0;
191
- var SECTION_HEIGHT_MAX_M = 120;
192
- var SECTION_RAKE_MIN_DEG = 0;
193
- var SECTION_RAKE_MAX_DEG = 45;
194
- var LEGACY_SECTION_ELEVATION_TIER_MAX = 7;
195
- var SEATED_EYE_HEIGHT_M = 1.2;
196
- function finiteClamped(value, min, max, fallback) {
197
- return Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback;
198
- }
199
- function sectionElevationTier(value) {
200
- if (!Number.isFinite(value)) return SECTION_ELEVATION_TIER_MIN;
201
- return Math.max(
202
- SECTION_ELEVATION_TIER_MIN,
203
- Math.min(SECTION_ELEVATION_TIER_MAX, Math.round(value))
204
- );
205
- }
206
- function compatibleAutomaticTier(value) {
207
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) return 0;
208
- if (value <= LEGACY_SECTION_ELEVATION_TIER_MAX) return value;
209
- return SECTION_ELEVATION_TIER_MAX;
210
- }
211
- function sectionGeometry(section, context = {}) {
212
- const floorBaseHeightM = finiteClamped(
213
- context.floorBaseHeightM,
214
- SECTION_HEIGHT_MIN_M,
215
- SECTION_HEIGHT_MAX_M,
216
- 0
217
- );
218
- const automaticHeight = Math.min(
219
- SECTION_HEIGHT_MAX_M,
220
- floorBaseHeightM + compatibleAutomaticTier(section.elevation) * TIER_HEIGHT_M
221
- );
222
- const height = section.height === void 0 ? automaticHeight : finiteClamped(section.height, SECTION_HEIGHT_MIN_M, SECTION_HEIGHT_MAX_M, automaticHeight);
223
- const rake = finiteClamped(section.rake, SECTION_RAKE_MIN_DEG, SECTION_RAKE_MAX_DEG, 0);
224
- return { height, rake };
225
- }
226
-
227
192
  // src/core/layout.ts
228
193
  function overrideAccessibility(o) {
229
194
  if (!o) return [];
@@ -4359,7 +4324,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4359
4324
  this.accessRingById.set(seat.id, ring);
4360
4325
  target.add(ring);
4361
4326
  }
4362
- if (accessGlyphPath(seat.accessibility ?? [])) {
4327
+ if (seat.accessibility?.includes("wheelchair") && accessGlyphPath(seat.accessibility)) {
4363
4328
  const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
4364
4329
  this.accessGlyphById.set(seat.id, glyph);
4365
4330
  target.add(glyph);
@@ -8509,8 +8474,428 @@ function stageSightlinePitch(eyeHeightM, distM) {
8509
8474
  var W = 2048;
8510
8475
  var H = 1024;
8511
8476
  var UNIT = METRES_PER_CHART_UNIT;
8477
+ var TAU2 = Math.PI * 2;
8478
+ var PX_PER_DEG = H / 180;
8512
8479
  var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
8513
8480
  var pitchToY = (pitchDeg) => (90 - pitchDeg) / 180 * H;
8481
+ function fnv1a(str) {
8482
+ let h = 2166136261;
8483
+ for (let i = 0; i < str.length; i++) {
8484
+ h ^= str.charCodeAt(i);
8485
+ h = Math.imul(h, 16777619);
8486
+ }
8487
+ return h >>> 0;
8488
+ }
8489
+ function rng(seed) {
8490
+ let a = seed >>> 0;
8491
+ return () => {
8492
+ a |= 0;
8493
+ a = a + 1831565813 | 0;
8494
+ let t2 = Math.imul(a ^ a >>> 15, 1 | a);
8495
+ t2 = t2 + Math.imul(t2 ^ t2 >>> 7, 61 | t2) ^ t2;
8496
+ return ((t2 ^ t2 >>> 14) >>> 0) / 4294967296;
8497
+ };
8498
+ }
8499
+ var NBINS = 96;
8500
+ var BIN_DEG = 360 / NBINS;
8501
+ var FLAT_DELTA_M = 0.6;
8502
+ var HAZE_SKY = [17, 23, 42];
8503
+ function blendToSky(base, t2, a = 1) {
8504
+ const k = Math.max(0, Math.min(0.58, t2));
8505
+ const r = Math.round(base[0] + (HAZE_SKY[0] - base[0]) * k);
8506
+ const g = Math.round(base[1] + (HAZE_SKY[1] - base[1]) * k);
8507
+ const b = Math.round(base[2] + (HAZE_SKY[2] - base[2]) * k);
8508
+ return `rgba(${r},${g},${b},${a})`;
8509
+ }
8510
+ function mixRgb(a, b, t2) {
8511
+ return [
8512
+ Math.round(a[0] + (b[0] - a[0]) * t2),
8513
+ Math.round(a[1] + (b[1] - a[1]) * t2),
8514
+ Math.round(a[2] + (b[2] - a[2]) * t2)
8515
+ ];
8516
+ }
8517
+ function hslToRgb(h, s, l) {
8518
+ const c = (1 - Math.abs(2 * l - 1)) * s;
8519
+ const hp = (h % 360 + 360) % 360 / 60;
8520
+ const x = c * (1 - Math.abs(hp % 2 - 1));
8521
+ let r = 0, g = 0, b = 0;
8522
+ if (hp < 1) [r, g, b] = [c, x, 0];
8523
+ else if (hp < 2) [r, g, b] = [x, c, 0];
8524
+ else if (hp < 3) [r, g, b] = [0, c, x];
8525
+ else if (hp < 4) [r, g, b] = [0, x, c];
8526
+ else if (hp < 5) [r, g, b] = [x, 0, c];
8527
+ else [r, g, b] = [c, 0, x];
8528
+ const m = l - c / 2;
8529
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
8530
+ }
8531
+ var tintCache = /* @__PURE__ */ new Map();
8532
+ function tintFromKey(key) {
8533
+ if (!key) return null;
8534
+ const cached = tintCache.get(key);
8535
+ if (cached) return cached;
8536
+ let h = 2166136261;
8537
+ for (let i = 0; i < key.length; i++) {
8538
+ h ^= key.charCodeAt(i);
8539
+ h = Math.imul(h, 16777619);
8540
+ }
8541
+ const hue = (h >>> 0) % 360;
8542
+ const rgb2 = hslToRgb(hue, 0.3, 0.42);
8543
+ tintCache.set(key, rgb2);
8544
+ return rgb2;
8545
+ }
8546
+ function buildStands(seat, neighborSeats, stageBearing, myEyeM) {
8547
+ const bins = new Array(NBINS).fill(null);
8548
+ let maxDelta = 0;
8549
+ const own = seat.sectionId;
8550
+ for (const other of neighborSeats) {
8551
+ if (other.id === seat.id) continue;
8552
+ const os = other.sectionId;
8553
+ if (!os || own && os === own) continue;
8554
+ const ox = other.x - seat.x;
8555
+ const oy = other.y - seat.y;
8556
+ const dM = Math.hypot(ox, oy) * UNIT;
8557
+ if (dM < 1.5 || dM > 95) continue;
8558
+ const otherEye = other.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
8559
+ const dTop = otherEye - myEyeM;
8560
+ if (Math.abs(dTop) > maxDelta) maxDelta = Math.abs(dTop);
8561
+ const top = Math.atan2(dTop, dM) * 180 / Math.PI;
8562
+ const base = Math.atan2(otherEye - SEATED_EYE_HEIGHT_M - myEyeM, dM) * 180 / Math.PI;
8563
+ const bearing = Math.atan2(ox, -oy) - stageBearing;
8564
+ const yaw = (bearing * 180 / Math.PI + 540) % 360 - 180;
8565
+ const idx = Math.min(NBINS - 1, Math.max(0, Math.floor((yaw + 180) / BIN_DEG)));
8566
+ const overhead = top > 34 && dM < 16;
8567
+ const cur = bins[idx];
8568
+ if (!cur) bins[idx] = { top, base, nearM: dM, overhead, tint: tintFromKey(other.categoryKey) };
8569
+ else {
8570
+ if (top > cur.top) cur.top = top;
8571
+ if (base < cur.base) cur.base = base;
8572
+ if (dM < cur.nearM) {
8573
+ cur.nearM = dM;
8574
+ cur.tint = tintFromKey(other.categoryKey);
8575
+ }
8576
+ cur.overhead = cur.overhead || overhead;
8577
+ }
8578
+ }
8579
+ return { bins, maxDelta };
8580
+ }
8581
+ function drawStands(ctx, bins, toX, toY) {
8582
+ for (let i = 0; i < NBINS; i++) {
8583
+ const b = bins[i];
8584
+ if (!b) continue;
8585
+ const yaw0 = -180 + i * BIN_DEG;
8586
+ const x0 = toX(yaw0);
8587
+ const x1 = toX(yaw0 + BIN_DEG);
8588
+ if (!(x1 > x0)) continue;
8589
+ const top = Math.min(72, b.top);
8590
+ const bottom = Math.min(b.base, -4);
8591
+ const yTop = toY(top);
8592
+ const yBot = toY(bottom);
8593
+ if (yBot <= yTop) continue;
8594
+ const haze = b.nearM / 95;
8595
+ const tint = b.tint;
8596
+ const crest = tint ? mixRgb([44, 54, 80], tint, 0.34) : [44, 54, 80];
8597
+ const seating = tint ? mixRgb([24, 30, 48], tint, 0.28) : [24, 30, 48];
8598
+ const grad = ctx.createLinearGradient(0, yTop, 0, yBot);
8599
+ grad.addColorStop(0, blendToSky(crest, haze));
8600
+ grad.addColorStop(0.24, blendToSky(seating, haze));
8601
+ grad.addColorStop(1, blendToSky([10, 13, 22], haze));
8602
+ ctx.fillStyle = grad;
8603
+ ctx.fillRect(x0, yTop, x1 - x0 + 1, yBot - yTop);
8604
+ ctx.fillStyle = `rgba(150,165,205,${0.35 * (1 - haze * 0.55)})`;
8605
+ ctx.fillRect(x0, yTop, x1 - x0 + 1, 1.5);
8606
+ const bandBottom = Math.min(yBot, toY(0));
8607
+ const bandSpan = bandBottom - yTop;
8608
+ if (bandSpan > 8) {
8609
+ const bands = 6;
8610
+ const contrast = 1 - haze * 0.4;
8611
+ for (let r = 1; r < bands; r++) {
8612
+ const y = yTop + bandSpan * r / bands;
8613
+ ctx.fillStyle = `rgba(0,0,0,${0.16 * contrast})`;
8614
+ ctx.fillRect(x0, y, x1 - x0 + 1, 1.5);
8615
+ ctx.fillStyle = `rgba(120,134,170,${0.1 * contrast})`;
8616
+ ctx.fillRect(x0, y + 1.5, x1 - x0 + 1, 1.2);
8617
+ }
8618
+ }
8619
+ if (b.overhead) {
8620
+ const lipY = toY(Math.min(86, top + 12));
8621
+ const g2 = ctx.createLinearGradient(0, lipY, 0, yTop);
8622
+ g2.addColorStop(0, "rgba(3,4,8,0.9)");
8623
+ g2.addColorStop(1, "rgba(3,4,8,0)");
8624
+ ctx.fillStyle = g2;
8625
+ ctx.fillRect(x0, lipY, x1 - x0 + 1, yTop - lipY);
8626
+ ctx.fillStyle = "rgba(120,130,160,0.14)";
8627
+ ctx.fillRect(x0, yTop - 1.5, x1 - x0 + 1, 1.5);
8628
+ }
8629
+ }
8630
+ }
8631
+ function classifyScene(_seat, focal, neighbors) {
8632
+ const NB = 72;
8633
+ const occ = new Array(NB).fill(false);
8634
+ const innerM = new Array(NB).fill(Infinity);
8635
+ const secSet = /* @__PURE__ */ new Set();
8636
+ let n = 0;
8637
+ for (const s of neighbors) {
8638
+ n++;
8639
+ if (s.sectionId) secSet.add(s.sectionId);
8640
+ const dx = s.x - focal.x;
8641
+ const dy = s.y - focal.y;
8642
+ const dM = Math.hypot(dx, dy) * UNIT;
8643
+ if (dM < 0.5) continue;
8644
+ const bearing = Math.atan2(dx, -dy);
8645
+ let idx = Math.floor((bearing + Math.PI) / TAU2 * NB);
8646
+ if (idx < 0) idx = 0;
8647
+ else if (idx >= NB) idx = NB - 1;
8648
+ occ[idx] = true;
8649
+ if (dM < innerM[idx]) innerM[idx] = dM;
8650
+ }
8651
+ let occN = 0;
8652
+ const inner = [];
8653
+ for (let i = 0; i < NB; i++) if (occ[i]) {
8654
+ occN++;
8655
+ inner.push(innerM[i]);
8656
+ }
8657
+ const coverage = occN / NB * 360;
8658
+ inner.sort((a, b) => a - b);
8659
+ const median = inner.length ? inner[inner.length >> 1] : 0;
8660
+ const lo = inner.length ? inner[Math.floor(inner.length * 0.15)] ?? median : 0;
8661
+ const hi = inner.length ? inner[Math.floor(inner.length * 0.85)] ?? median : 0;
8662
+ const aspect = lo > 0.5 ? hi / lo : 1;
8663
+ const seed = fnv1a(`${n}|${[...secSet].sort().join(",")}|${Math.round(focal.x)},${Math.round(focal.y)}`);
8664
+ let mode;
8665
+ if (coverage >= 300 && median > 11 && aspect > 1.35) mode = "sport";
8666
+ else if (coverage >= 285) mode = "arena";
8667
+ else mode = "proscenium";
8668
+ return { mode, seed, voidRadiusM: median, aspect };
8669
+ }
8670
+ var HAIR_KINDS = 5;
8671
+ function drawHair(ctx, r, kind) {
8672
+ switch (kind) {
8673
+ case 1:
8674
+ ctx.beginPath();
8675
+ ctx.ellipse(0, -r * 0.14, r * 1.16, r * 1.16, 0, 0, TAU2);
8676
+ ctx.fill();
8677
+ break;
8678
+ case 2:
8679
+ ctx.beginPath();
8680
+ ctx.ellipse(0, -r * 1.02, r * 0.42, r * 0.42, 0, 0, TAU2);
8681
+ ctx.fill();
8682
+ ctx.beginPath();
8683
+ ctx.ellipse(0, -r * 0.2, r * 0.98, r * 1.02, 0, 0, TAU2);
8684
+ ctx.fill();
8685
+ break;
8686
+ case 3:
8687
+ ctx.beginPath();
8688
+ ctx.ellipse(0, -r * 0.36, r * 1.04, r * 0.6, 0, Math.PI, TAU2);
8689
+ ctx.fill();
8690
+ ctx.beginPath();
8691
+ ctx.ellipse(0, -r * 0.52, r * 1.22, r * 0.24, 0, 0, TAU2);
8692
+ ctx.fill();
8693
+ break;
8694
+ case 4:
8695
+ ctx.beginPath();
8696
+ ctx.ellipse(-r * 0.66, r * 0.5, r * 0.5, r * 1.35, 0, 0, TAU2);
8697
+ ctx.fill();
8698
+ ctx.beginPath();
8699
+ ctx.ellipse(r * 0.66, r * 0.5, r * 0.5, r * 1.35, 0, 0, TAU2);
8700
+ ctx.fill();
8701
+ ctx.beginPath();
8702
+ ctx.ellipse(0, -r * 0.08, r * 1.06, r * 1.08, 0, 0, TAU2);
8703
+ ctx.fill();
8704
+ break;
8705
+ default:
8706
+ break;
8707
+ }
8708
+ }
8709
+ function drawSeated(ctx, hx, hy, r, o) {
8710
+ const neckHalf = r * 0.44;
8711
+ const shHalf = r * o.shoulderK;
8712
+ const neckTopY = r * 0.66;
8713
+ const shoulderY = r * 1.68;
8714
+ const botY = r * 5.4;
8715
+ const clo = `rgba(${o.clothing[0]},${o.clothing[1]},${o.clothing[2]},${o.alpha})`;
8716
+ const skin = `rgba(${o.tone[0]},${o.tone[1]},${o.tone[2]},${o.alpha})`;
8717
+ ctx.save();
8718
+ ctx.translate(hx, hy);
8719
+ ctx.fillStyle = clo;
8720
+ ctx.beginPath();
8721
+ ctx.moveTo(-neckHalf, neckTopY);
8722
+ ctx.quadraticCurveTo(-neckHalf * 1.18, shoulderY - r * 0.6, -shHalf, shoulderY);
8723
+ ctx.quadraticCurveTo(-shHalf * 1.05, shoulderY + r * 1.2, -shHalf * 0.94, botY);
8724
+ ctx.lineTo(shHalf * 0.94, botY);
8725
+ ctx.quadraticCurveTo(shHalf * 1.05, shoulderY + r * 1.2, shHalf, shoulderY);
8726
+ ctx.quadraticCurveTo(neckHalf * 1.18, shoulderY - r * 0.6, neckHalf, neckTopY);
8727
+ ctx.closePath();
8728
+ ctx.fill();
8729
+ ctx.fillStyle = skin;
8730
+ ctx.fillRect(-neckHalf * 0.82, r * 0.5, neckHalf * 1.64, r * 0.9);
8731
+ ctx.save();
8732
+ ctx.rotate(o.tilt);
8733
+ ctx.fillStyle = skin;
8734
+ ctx.beginPath();
8735
+ ctx.ellipse(0, 0, r * 0.9, r * 1.06, 0, 0, TAU2);
8736
+ ctx.fill();
8737
+ drawHair(ctx, r, o.hair);
8738
+ if (o.rim > 0.01) {
8739
+ ctx.strokeStyle = `rgba(245,205,150,${(0.5 * o.rim).toFixed(3)})`;
8740
+ ctx.lineWidth = Math.max(1, r * 0.14);
8741
+ ctx.beginPath();
8742
+ ctx.ellipse(0, -r * 0.06, r * 0.86, r * 1, 0, Math.PI * 1.12, Math.PI * 1.9);
8743
+ ctx.stroke();
8744
+ }
8745
+ ctx.restore();
8746
+ if (o.rim > 0.01) {
8747
+ ctx.strokeStyle = `rgba(240,200,150,${(0.28 * o.rim).toFixed(3)})`;
8748
+ ctx.lineWidth = Math.max(1, r * 0.12);
8749
+ ctx.beginPath();
8750
+ ctx.moveTo(-shHalf * 0.8, shoulderY - r * 0.05);
8751
+ ctx.quadraticCurveTo(-neckHalf, shoulderY - r * 0.75, 0, shoulderY - r * 0.82);
8752
+ ctx.quadraticCurveTo(neckHalf, shoulderY - r * 0.75, shHalf * 0.8, shoulderY - r * 0.05);
8753
+ ctx.stroke();
8754
+ }
8755
+ ctx.restore();
8756
+ }
8757
+ function drawSeatedMid(ctx, hx, hy, r, clothing, tone, alpha) {
8758
+ ctx.fillStyle = `rgba(${clothing[0]},${clothing[1]},${clothing[2]},${alpha})`;
8759
+ ctx.beginPath();
8760
+ ctx.moveTo(hx - r * 1.9, hy + r * 4);
8761
+ ctx.quadraticCurveTo(hx - r * 1.95, hy + r * 1.2, hx - r * 0.5, hy + r * 0.9);
8762
+ ctx.quadraticCurveTo(hx, hy + r * 0.4, hx + r * 0.5, hy + r * 0.9);
8763
+ ctx.quadraticCurveTo(hx + r * 1.95, hy + r * 1.2, hx + r * 1.9, hy + r * 4);
8764
+ ctx.closePath();
8765
+ ctx.fill();
8766
+ ctx.fillStyle = `rgba(${tone[0]},${tone[1]},${tone[2]},${alpha})`;
8767
+ ctx.beginPath();
8768
+ ctx.ellipse(hx, hy, r * 0.92, r * 1.05, 0, 0, TAU2);
8769
+ ctx.fill();
8770
+ }
8771
+ function drawPerformer(ctx, x, footY, h, o) {
8772
+ const headR = h * 0.1;
8773
+ const headCy = footY - h * 0.9;
8774
+ const shoulderY = footY - h * 0.74;
8775
+ const hipY = footY - h * 0.46;
8776
+ const shHalf = h * 0.13;
8777
+ const hipHalf = h * 0.09;
8778
+ const skin = `rgba(${o.tone[0]},${o.tone[1]},${o.tone[2]},${o.alpha})`;
8779
+ const sh = ctx.createRadialGradient(x, footY, 1, x, footY, h * 0.24);
8780
+ sh.addColorStop(0, "rgba(0,0,0,0.4)");
8781
+ sh.addColorStop(1, "rgba(0,0,0,0)");
8782
+ ctx.save();
8783
+ ctx.scale(1, 0.28);
8784
+ ctx.fillStyle = sh;
8785
+ ctx.beginPath();
8786
+ ctx.ellipse(x, footY / 0.28, h * 0.24, h * 0.24, 0, 0, TAU2);
8787
+ ctx.fill();
8788
+ ctx.restore();
8789
+ ctx.fillStyle = skin;
8790
+ ctx.beginPath();
8791
+ ctx.moveTo(x - hipHalf, hipY);
8792
+ ctx.lineTo(x - hipHalf * 0.7, footY);
8793
+ ctx.lineTo(x - hipHalf * 0.1, footY);
8794
+ ctx.lineTo(x - hipHalf * 0.1, hipY + h * 0.02);
8795
+ ctx.lineTo(x + hipHalf * 0.1, hipY + h * 0.02);
8796
+ ctx.lineTo(x + hipHalf * 0.1, footY);
8797
+ ctx.lineTo(x + hipHalf * 0.7, footY);
8798
+ ctx.lineTo(x + hipHalf, hipY);
8799
+ ctx.closePath();
8800
+ ctx.fill();
8801
+ ctx.beginPath();
8802
+ ctx.moveTo(x - shHalf, shoulderY);
8803
+ ctx.quadraticCurveTo(x - shHalf * 0.9, hipY - h * 0.12, x - hipHalf, hipY);
8804
+ ctx.lineTo(x + hipHalf, hipY);
8805
+ ctx.quadraticCurveTo(x + shHalf * 0.9, hipY - h * 0.12, x + shHalf, shoulderY);
8806
+ ctx.quadraticCurveTo(x, shoulderY - h * 0.05, x - shHalf, shoulderY);
8807
+ ctx.closePath();
8808
+ ctx.fill();
8809
+ ctx.lineCap = "round";
8810
+ ctx.lineJoin = "round";
8811
+ ctx.strokeStyle = skin;
8812
+ ctx.lineWidth = h * 0.055;
8813
+ const armY = shoulderY + h * 0.02;
8814
+ if (o.pose === 1) {
8815
+ ctx.beginPath();
8816
+ ctx.moveTo(x - shHalf * 0.8, armY);
8817
+ ctx.lineTo(x - shHalf * 1.5, footY - h * 1.02);
8818
+ ctx.moveTo(x + shHalf * 0.8, armY);
8819
+ ctx.lineTo(x + shHalf * 1.5, footY - h * 1.02);
8820
+ ctx.stroke();
8821
+ } else if (o.pose === 2) {
8822
+ ctx.beginPath();
8823
+ ctx.moveTo(x - shHalf * 0.8, armY);
8824
+ ctx.lineTo(x + hipHalf * 1.4, hipY - h * 0.02);
8825
+ ctx.moveTo(x + shHalf * 0.8, armY);
8826
+ ctx.lineTo(x - hipHalf * 0.4, hipY - h * 0.06);
8827
+ ctx.stroke();
8828
+ ctx.fillStyle = skin;
8829
+ ctx.beginPath();
8830
+ ctx.ellipse(x + hipHalf * 1.5, hipY - h * 0.04, h * 0.07, h * 0.09, -0.5, 0, TAU2);
8831
+ ctx.fill();
8832
+ } else if (o.pose === 3) {
8833
+ ctx.beginPath();
8834
+ ctx.moveTo(x - shHalf * 0.8, armY);
8835
+ ctx.lineTo(x - shHalf * 0.2, headCy + headR * 0.6);
8836
+ ctx.moveTo(x + shHalf * 0.8, armY);
8837
+ ctx.lineTo(x + shHalf * 1.1, hipY);
8838
+ ctx.stroke();
8839
+ ctx.lineWidth = h * 0.02;
8840
+ ctx.beginPath();
8841
+ ctx.moveTo(x - shHalf * 0.2, headCy + headR * 0.9);
8842
+ ctx.lineTo(x - shHalf * 0.2, footY - h * 0.02);
8843
+ ctx.stroke();
8844
+ } else {
8845
+ ctx.beginPath();
8846
+ ctx.moveTo(x - shHalf * 0.85, armY);
8847
+ ctx.lineTo(x - shHalf * 0.95, hipY + h * 0.02);
8848
+ ctx.moveTo(x + shHalf * 0.85, armY);
8849
+ ctx.lineTo(x + shHalf * 0.95, hipY + h * 0.02);
8850
+ ctx.stroke();
8851
+ }
8852
+ ctx.fillStyle = skin;
8853
+ ctx.fillRect(x - headR * 0.34, headCy + headR * 0.6, headR * 0.68, headR * 0.9);
8854
+ ctx.beginPath();
8855
+ ctx.ellipse(x, headCy, headR * 0.86, headR * 1.05, 0, 0, TAU2);
8856
+ ctx.fill();
8857
+ if (o.rimStrength > 0.01) {
8858
+ ctx.strokeStyle = `rgba(${o.rim[0]},${o.rim[1]},${o.rim[2]},${(0.6 * o.rimStrength).toFixed(3)})`;
8859
+ ctx.lineWidth = Math.max(1, h * 0.02);
8860
+ ctx.beginPath();
8861
+ ctx.moveTo(x - shHalf, shoulderY);
8862
+ ctx.lineTo(x - headR * 0.6, headCy + headR * 0.4);
8863
+ ctx.moveTo(x - headR * 0.7, headCy);
8864
+ ctx.ellipse(x, headCy, headR * 0.84, headR * 1.02, 0, Math.PI * 1.05, Math.PI * 1.55);
8865
+ ctx.stroke();
8866
+ }
8867
+ }
8868
+ function drawBeam(ctx, x0, y0, x1, y1, halfTop, halfBot, tint, strength) {
8869
+ ctx.save();
8870
+ ctx.globalCompositeOperation = "lighter";
8871
+ const g = ctx.createLinearGradient(0, y0, 0, y1);
8872
+ g.addColorStop(0, `rgba(${tint},${strength})`);
8873
+ g.addColorStop(0.5, `rgba(${tint},${(strength * 0.4).toFixed(3)})`);
8874
+ g.addColorStop(1, `rgba(${tint},0)`);
8875
+ ctx.fillStyle = g;
8876
+ ctx.beginPath();
8877
+ ctx.moveTo(x0 - halfTop, y0);
8878
+ ctx.lineTo(x1 - halfBot, y1);
8879
+ ctx.lineTo(x1 + halfBot, y1);
8880
+ ctx.lineTo(x0 + halfTop, y0);
8881
+ ctx.closePath();
8882
+ ctx.fill();
8883
+ ctx.restore();
8884
+ }
8885
+ function drawFixture(ctx, x, y, r, tint) {
8886
+ const g = ctx.createRadialGradient(x, y, 0, x, y, r * 3);
8887
+ g.addColorStop(0, `rgba(${tint},0.9)`);
8888
+ g.addColorStop(0.4, `rgba(${tint},0.35)`);
8889
+ g.addColorStop(1, `rgba(${tint},0)`);
8890
+ ctx.fillStyle = g;
8891
+ ctx.beginPath();
8892
+ ctx.arc(x, y, r * 3, 0, TAU2);
8893
+ ctx.fill();
8894
+ ctx.fillStyle = `rgba(${tint},1)`;
8895
+ ctx.beginPath();
8896
+ ctx.arc(x, y, r, 0, TAU2);
8897
+ ctx.fill();
8898
+ }
8514
8899
  function generateSeatPanorama(seat, focalPoint, neighborSeats) {
8515
8900
  const canvas = document.createElement("canvas");
8516
8901
  canvas.width = W;
@@ -8520,102 +8905,428 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
8520
8905
  const dy = focalPoint.y - seat.y;
8521
8906
  const distU = Math.hypot(dx, dy);
8522
8907
  const distM = Math.max(2, distU * UNIT);
8908
+ const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
8909
+ const scene = classifyScene(seat, focalPoint, neighborSeats);
8910
+ const rand = rng(scene.seed);
8911
+ const baseHue = 210 + rand() * 150;
8912
+ const rigTint = hslToRgb(220 + (rand() - 0.5) * 60, 0.55, 0.82);
8913
+ const rigTintStr = `${rigTint[0]},${rigTint[1]},${rigTint[2]}`;
8523
8914
  const sky = ctx.createLinearGradient(0, 0, 0, H);
8524
- sky.addColorStop(0, "#05070c");
8525
- sky.addColorStop(0.42, "#11172a");
8526
- sky.addColorStop(0.5, "#1a2238");
8527
- sky.addColorStop(0.56, "#10141f");
8528
- sky.addColorStop(1, "#07090f");
8915
+ sky.addColorStop(0, "#080b12");
8916
+ sky.addColorStop(0.42, "#12182c");
8917
+ sky.addColorStop(0.5, "#1c2440");
8918
+ sky.addColorStop(0.56, "#151b2c");
8919
+ sky.addColorStop(0.78, "#171e30");
8920
+ sky.addColorStop(1, "#131829");
8529
8921
  ctx.fillStyle = sky;
8530
8922
  ctx.fillRect(0, 0, W, H);
8531
- const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
8532
- const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
8923
+ const floorAmb = ctx.createLinearGradient(0, pitchToY(-6), 0, H);
8924
+ floorAmb.addColorStop(0, "rgba(30, 37, 56, 0)");
8925
+ floorAmb.addColorStop(0.55, "rgba(30, 37, 56, 0.22)");
8926
+ floorAmb.addColorStop(1, "rgba(22, 28, 44, 0.12)");
8927
+ ctx.fillStyle = floorAmb;
8928
+ ctx.fillRect(0, pitchToY(-6), W, H - pitchToY(-6));
8929
+ const stageBearing = Math.atan2(dx, -dy);
8930
+ const stands = buildStands(seat, neighborSeats, stageBearing, eyeM);
8931
+ const hasRelief = stands.maxDelta >= FLAT_DELTA_M;
8932
+ if (hasRelief) drawStands(ctx, stands.bins, yawToX, pitchToY);
8933
+ const nearGlow = Math.max(0.3, Math.min(1, 1 - (distM - 6) / 60));
8533
8934
  const sightline = stageSightlinePitch(eyeM, distM);
8935
+ const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
8534
8936
  const stageTopPitch = Math.min(45, sightline.topPitch);
8535
8937
  const stageBasePitch = sightline.basePitch;
8938
+ if (scene.mode === "sport") {
8939
+ drawSportScene(ctx, distM, eyeM, scene, nearGlow, baseHue, rigTintStr, rand);
8940
+ } else if (scene.mode === "arena") {
8941
+ drawArenaScene(ctx, distM, eyeM, scene, nearGlow, rigTintStr, rand);
8942
+ } else {
8943
+ drawProsceniumScene(ctx, stageHalfYaw, stageTopPitch, stageBasePitch, nearGlow, baseHue, rigTintStr, rand);
8944
+ }
8945
+ const rigCount = scene.mode === "sport" ? 34 : 24 + Math.floor(rand() * 8);
8946
+ ctx.fillStyle = `rgba(${rigTintStr},0.8)`;
8947
+ for (let i = 0; i < rigCount; i++) {
8948
+ const x = (i + 0.5) / rigCount * W;
8949
+ const y = pitchToY(scene.mode === "sport" ? 58 : 54 + i * 7 % 3 * 6);
8950
+ ctx.beginPath();
8951
+ ctx.arc(x, y, scene.mode === "sport" ? 2.6 : 3.2, 0, TAU2);
8952
+ ctx.fill();
8953
+ }
8954
+ drawAudience(ctx, seat, neighborSeats, focalPoint, stageBearing, eyeM, dx, dy, hasRelief, scene);
8955
+ const poleFade = ctx.createLinearGradient(0, 0, 0, H);
8956
+ poleFade.addColorStop(0, "rgba(0,0,0,0.85)");
8957
+ poleFade.addColorStop(0.12, "rgba(0,0,0,0)");
8958
+ poleFade.addColorStop(0.9, "rgba(0,0,0,0)");
8959
+ poleFade.addColorStop(1, "rgba(0,0,0,0.55)");
8960
+ ctx.fillStyle = poleFade;
8961
+ ctx.fillRect(0, 0, W, H);
8962
+ return { url: canvas.toDataURL("image/jpeg", 0.82), distanceM: Math.round(distM) };
8963
+ }
8964
+ function drawProsceniumScene(ctx, stageHalfYaw, stageTopPitch, stageBasePitch, nearGlow, baseHue, rigTintStr, rand) {
8536
8965
  const sx0 = yawToX(-stageHalfYaw);
8537
8966
  const sx1 = yawToX(stageHalfYaw);
8538
8967
  const sy0 = pitchToY(stageTopPitch);
8539
8968
  const sy1 = pitchToY(stageBasePitch);
8540
- const glow = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, 10, W / 2, (sy0 + sy1) / 2, (sx1 - sx0) * 1.1);
8541
- glow.addColorStop(0, "rgba(129, 140, 248, 0.5)");
8542
- glow.addColorStop(0.5, "rgba(99, 102, 241, 0.16)");
8969
+ const sw = sx1 - sx0;
8970
+ const sh = sy1 - sy0;
8971
+ const archTop = pitchToY(Math.min(50, stageTopPitch + 10));
8972
+ const glowR = sw * (1.1 + (1 - nearGlow) * 0.5);
8973
+ const glow = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, 8, W / 2, (sy0 + sy1) / 2, glowR);
8974
+ glow.addColorStop(0, "rgba(255, 214, 150, 0.34)");
8975
+ glow.addColorStop(0.28, "rgba(150, 150, 230, 0.3)");
8976
+ glow.addColorStop(0.6, "rgba(99, 102, 241, 0.12)");
8543
8977
  glow.addColorStop(1, "rgba(99, 102, 241, 0)");
8544
8978
  ctx.fillStyle = glow;
8545
- ctx.fillRect(sx0 - (sx1 - sx0) * 0.6, sy0 - (sy1 - sy0) * 1.2, (sx1 - sx0) * 2.2, (sy1 - sy0) * 3.4);
8546
- ctx.fillStyle = "#242c44";
8547
- ctx.fillRect(sx0, sy0, sx1 - sx0, sy1 - sy0);
8979
+ ctx.fillRect(sx0 - sw * 0.6, sy0 - sh * 1.2, sw * 2.2, sh * 3.4);
8980
+ const topCol = hslToRgb(baseHue, 0.62, 0.55);
8981
+ const midCol = hslToRgb(baseHue + 40, 0.7, 0.5);
8982
+ const botCol = hslToRgb(baseHue + 70, 0.78, 0.52);
8983
+ ctx.save();
8984
+ ctx.beginPath();
8985
+ ctx.rect(sx0, sy0, sw, sh);
8986
+ ctx.clip();
8548
8987
  const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
8549
- backdrop.addColorStop(0, "#7c5cff");
8550
- backdrop.addColorStop(0.5, "#e1447a");
8551
- backdrop.addColorStop(1, "#f59e0b");
8552
- ctx.globalAlpha = 0.75;
8988
+ backdrop.addColorStop(0, `rgb(${topCol[0]},${topCol[1]},${topCol[2]})`);
8989
+ backdrop.addColorStop(0.5, `rgb(${midCol[0]},${midCol[1]},${midCol[2]})`);
8990
+ backdrop.addColorStop(1, `rgb(${botCol[0]},${botCol[1]},${botCol[2]})`);
8991
+ ctx.globalAlpha = 0.8;
8553
8992
  ctx.fillStyle = backdrop;
8554
- const inset = (sx1 - sx0) * 0.06;
8555
- ctx.fillRect(sx0 + inset, sy0 + (sy1 - sy0) * 0.12, sx1 - sx0 - inset * 2, (sy1 - sy0) * 0.62);
8993
+ ctx.fillRect(sx0, sy0, sw, sh);
8556
8994
  ctx.globalAlpha = 1;
8557
- ctx.fillStyle = "#0d1119";
8558
- ctx.fillRect(sx0, sy1 - (sy1 - sy0) * 0.1, sx1 - sx0, (sy1 - sy0) * 0.1);
8559
- const performerH = (sy1 - sy0) * 0.45;
8560
- ctx.fillStyle = "rgba(8, 10, 16, 0.85)";
8561
- for (const fx of [0.35, 0.5, 0.65]) {
8562
- const px = sx0 + (sx1 - sx0) * fx;
8563
- const py = sy1 - (sy1 - sy0) * 0.12;
8564
- ctx.beginPath();
8565
- ctx.arc(px, py - performerH * 0.82, performerH * 0.16, 0, Math.PI * 2);
8566
- ctx.fill();
8567
- ctx.fillRect(px - performerH * 0.14, py - performerH * 0.68, performerH * 0.28, performerH * 0.68);
8995
+ const fall = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, sw * 0.12, W / 2, (sy0 + sy1) / 2, sw * 0.62);
8996
+ fall.addColorStop(0, "rgba(6,8,14,0)");
8997
+ fall.addColorStop(0.7, "rgba(6,8,14,0.15)");
8998
+ fall.addColorStop(1, "rgba(5,6,11,0.92)");
8999
+ ctx.fillStyle = fall;
9000
+ ctx.fillRect(sx0, sy0, sw, sh);
9001
+ ctx.restore();
9002
+ const perfCount = 3 + Math.floor(rand() * 3);
9003
+ const perfH = sh * 0.46;
9004
+ const floorY = sy1 - sh * 0.08;
9005
+ for (let i = 0; i < perfCount; i++) {
9006
+ const t2 = (i + 1) / (perfCount + 1);
9007
+ const px = sx0 + sw * (0.22 + t2 * 0.56 + (rand() - 0.5) * 0.06);
9008
+ drawPerformer(ctx, px, floorY, perfH * (0.9 + rand() * 0.24), {
9009
+ tone: [8, 10, 16],
9010
+ alpha: 0.9,
9011
+ pose: Math.floor(rand() * 4),
9012
+ rim: hslToRgb(baseHue + 20, 0.5, 0.7),
9013
+ rimStrength: 0.7
9014
+ });
9015
+ }
9016
+ ctx.fillStyle = "#05070d";
9017
+ ctx.fillRect(sx0 - sw * 0.5, archTop, sw * 0.5, sy1 - archTop);
9018
+ ctx.fillRect(sx1, archTop, sw * 0.5, sy1 - archTop);
9019
+ const legBot = sw * 0.055 + 3;
9020
+ const legTop = legBot * 1.35;
9021
+ ctx.fillStyle = "#03050a";
9022
+ ctx.beginPath();
9023
+ ctx.moveTo(sx0, sy1);
9024
+ ctx.lineTo(sx0 - legBot, sy1);
9025
+ ctx.lineTo(sx0 - legTop, archTop);
9026
+ ctx.lineTo(sx0 + sw * 0.012, archTop);
9027
+ ctx.closePath();
9028
+ ctx.fill();
9029
+ ctx.beginPath();
9030
+ ctx.moveTo(sx1, sy1);
9031
+ ctx.lineTo(sx1 + legBot, sy1);
9032
+ ctx.lineTo(sx1 + legTop, archTop);
9033
+ ctx.lineTo(sx1 - sw * 0.012, archTop);
9034
+ ctx.closePath();
9035
+ ctx.fill();
9036
+ ctx.fillRect(sx0 - legTop, archTop, sw + legTop * 2, (sy1 - archTop) * 0.12);
9037
+ const valH = sh * 0.14;
9038
+ const valY = archTop + (sy1 - archTop) * 0.12;
9039
+ ctx.fillStyle = "rgba(4,6,12,0.96)";
9040
+ const scallops = 7;
9041
+ ctx.beginPath();
9042
+ ctx.moveTo(sx0 - legTop, valY);
9043
+ for (let i = 0; i <= scallops; i++) {
9044
+ const xx = sx0 - legTop + (sw + legTop * 2) * i / scallops;
9045
+ ctx.quadraticCurveTo(xx - (sw + legTop * 2) / scallops / 2, valY + valH, xx, valY);
9046
+ }
9047
+ ctx.lineTo(sx1 + legTop, valY);
9048
+ ctx.lineTo(sx1 + legTop, archTop);
9049
+ ctx.lineTo(sx0 - legTop, archTop);
9050
+ ctx.closePath();
9051
+ ctx.fill();
9052
+ ctx.fillStyle = `rgba(${rigTintStr},0.12)`;
9053
+ ctx.fillRect(sx0 - 1.5, valY, 1.5, sy1 - valY);
9054
+ ctx.fillRect(sx1, valY, 1.5, sy1 - valY);
9055
+ ctx.fillStyle = "#0b0f18";
9056
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.05 + 3);
9057
+ const foot = ctx.createLinearGradient(0, sy1 - sh * 0.05, 0, sy1 + sh * 0.12);
9058
+ foot.addColorStop(0, `rgba(255,206,140,${(0.5 * nearGlow).toFixed(3)})`);
9059
+ foot.addColorStop(1, "rgba(255,206,140,0)");
9060
+ ctx.fillStyle = foot;
9061
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.2);
9062
+ const beams = 3 + Math.floor(rand() * 2);
9063
+ for (let i = 0; i < beams; i++) {
9064
+ const t2 = (i + 1) / (beams + 1);
9065
+ const fx = sx0 + sw * (t2 + (rand() - 0.5) * 0.08);
9066
+ const fy = pitchToY(Math.min(62, stageTopPitch + 26));
9067
+ const skew = (rand() - 0.5) * sw * 0.16;
9068
+ drawBeam(ctx, fx, fy, fx + skew, sy1, 4, sw * 0.09, rigTintStr, 0.16 * (0.7 + nearGlow * 0.5));
9069
+ drawFixture(ctx, fx, fy, 2.4, rigTintStr);
9070
+ }
9071
+ }
9072
+ function drawArenaScene(ctx, distM, eyeM, scene, nearGlow, rigTintStr, rand) {
9073
+ const stageR = Math.min(distM * 0.6, Math.max(3, scene.voidRadiusM));
9074
+ const halfYaw = Math.min(55, Math.atan2(stageR, distM) * 180 / Math.PI);
9075
+ const cx = W / 2;
9076
+ const deckRiserM = 1.2;
9077
+ const nearDist = Math.max(distM * 0.45, distM - stageR);
9078
+ const farDist = distM + stageR;
9079
+ const nearTopPitch = Math.atan2(deckRiserM - eyeM, nearDist) * 180 / Math.PI;
9080
+ const farTopPitch = Math.atan2(deckRiserM - eyeM, farDist) * 180 / Math.PI;
9081
+ const nearBasePitch = Math.atan2(-eyeM, nearDist) * 180 / Math.PI;
9082
+ const yNearTop = pitchToY(nearTopPitch);
9083
+ const yFarTop = pitchToY(farTopPitch);
9084
+ const yBase = pitchToY(nearBasePitch);
9085
+ const halfW = yawToX(halfYaw) - cx;
9086
+ const topRy = Math.max(6, (yNearTop - yFarTop) / 2);
9087
+ const topCy = (yNearTop + yFarTop) / 2;
9088
+ const pool = ctx.createRadialGradient(cx, yBase, 4, cx, yBase, halfW * 2.6);
9089
+ pool.addColorStop(0, `rgba(255,206,150,${(0.2 * nearGlow).toFixed(3)})`);
9090
+ pool.addColorStop(0.5, "rgba(150,140,190,0.08)");
9091
+ pool.addColorStop(1, "rgba(40,44,60,0)");
9092
+ ctx.save();
9093
+ ctx.globalCompositeOperation = "lighter";
9094
+ ctx.fillStyle = pool;
9095
+ ctx.fillRect(cx - halfW * 2.6, yFarTop - topRy, halfW * 5.2, yBase - yFarTop + topRy + 40);
9096
+ ctx.restore();
9097
+ ctx.fillStyle = "#0c1019";
9098
+ ctx.beginPath();
9099
+ ctx.moveTo(cx - halfW, topCy);
9100
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, Math.PI, 0, false);
9101
+ ctx.lineTo(cx + halfW, yBase);
9102
+ ctx.ellipse(cx, yBase, halfW, topRy, 0, 0, Math.PI, false);
9103
+ ctx.closePath();
9104
+ ctx.fill();
9105
+ const riser = ctx.createLinearGradient(0, topCy, 0, yBase);
9106
+ riser.addColorStop(0, `rgba(${rigTintStr},0.14)`);
9107
+ riser.addColorStop(1, "rgba(0,0,0,0)");
9108
+ ctx.fillStyle = riser;
9109
+ ctx.fillRect(cx - halfW, topCy, halfW * 2, yBase - topCy);
9110
+ const deckTop = ctx.createLinearGradient(0, topCy - topRy, 0, topCy + topRy);
9111
+ deckTop.addColorStop(0, "#2b3350");
9112
+ deckTop.addColorStop(1, "#161b2c");
9113
+ ctx.fillStyle = deckTop;
9114
+ ctx.beginPath();
9115
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU2);
9116
+ ctx.fill();
9117
+ const deckGlow = ctx.createRadialGradient(cx, topCy, 2, cx, topCy, halfW);
9118
+ deckGlow.addColorStop(0, `rgba(255,214,160,${(0.28 * nearGlow).toFixed(3)})`);
9119
+ deckGlow.addColorStop(1, "rgba(255,214,160,0)");
9120
+ ctx.save();
9121
+ ctx.globalCompositeOperation = "lighter";
9122
+ ctx.fillStyle = deckGlow;
9123
+ ctx.beginPath();
9124
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU2);
9125
+ ctx.fill();
9126
+ ctx.restore();
9127
+ ctx.strokeStyle = `rgba(${rigTintStr},0.7)`;
9128
+ ctx.lineWidth = 2;
9129
+ ctx.beginPath();
9130
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU2);
9131
+ ctx.stroke();
9132
+ const perfCount = 2 + Math.floor(rand() * 3);
9133
+ const perfH = Math.max(24, (yBase - yFarTop) * 1.3);
9134
+ for (let i = 0; i < perfCount; i++) {
9135
+ const t2 = perfCount === 1 ? 0.5 : i / (perfCount - 1);
9136
+ const px = cx + (t2 - 0.5) * 2 * halfW * 0.66;
9137
+ const footY = topCy + topRy * 0.5 * Math.cos((t2 - 0.5) * Math.PI) - topRy * 0.1;
9138
+ drawPerformer(ctx, px, footY, perfH * (0.85 + rand() * 0.3), {
9139
+ tone: [10, 12, 20],
9140
+ alpha: 0.92,
9141
+ pose: Math.floor(rand() * 4),
9142
+ rim: hslToRgb(40, 0.6, 0.72),
9143
+ rimStrength: 0.8
9144
+ });
8568
9145
  }
8569
- ctx.globalAlpha = 0.14;
8570
- ctx.fillStyle = "#c7d2fe";
8571
- for (const fx of [0.3, 0.5, 0.7]) {
8572
- const bx = sx0 + (sx1 - sx0) * fx;
9146
+ const trussY = pitchToY(Math.min(60, farTopPitch + 34));
9147
+ const trussRy = topRy * 1.1 + 6;
9148
+ ctx.strokeStyle = "rgba(60,68,92,0.85)";
9149
+ ctx.lineWidth = 3;
9150
+ ctx.beginPath();
9151
+ ctx.ellipse(cx, trussY, halfW * 1.15, trussRy, 0, 0, TAU2);
9152
+ ctx.stroke();
9153
+ const fixtures = 4 + Math.floor(rand() * 3);
9154
+ for (let i = 0; i < fixtures; i++) {
9155
+ const a = i / fixtures * TAU2;
9156
+ const fxX = cx + Math.cos(a) * halfW * 1.15;
9157
+ const fxY = trussY + Math.sin(a) * trussRy;
9158
+ if (Math.sin(a) > -0.2) {
9159
+ const tx = cx + (rand() - 0.5) * halfW * 0.8;
9160
+ drawBeam(ctx, fxX, fxY, tx, topCy, 3, halfW * 0.32, rigTintStr, 0.14 * (0.7 + nearGlow * 0.4));
9161
+ }
9162
+ drawFixture(ctx, fxX, fxY, 2.2, rigTintStr);
9163
+ }
9164
+ }
9165
+ function drawSportScene(ctx, distM, eyeM, scene, _nearGlow, baseHue, rigTintStr, rand) {
9166
+ const cx = W / 2;
9167
+ const surfR = Math.max(10, scene.voidRadiusM);
9168
+ const nearEdge = Math.max(2, distM - surfR);
9169
+ const farEdge = distM + surfR;
9170
+ const halfYaw = Math.min(78, Math.atan2(surfR * 1.15, distM) * 180 / Math.PI);
9171
+ const nearPitch = Math.atan2(-eyeM, nearEdge) * 180 / Math.PI;
9172
+ const farPitch = Math.atan2(-eyeM, farEdge) * 180 / Math.PI;
9173
+ const yNear = pitchToY(nearPitch);
9174
+ const yFar = pitchToY(farPitch);
9175
+ const halfW = yawToX(halfYaw) - cx;
9176
+ const cy = (yNear + yFar) / 2;
9177
+ const ry = Math.max(10, (yNear - yFar) / 2);
9178
+ const surfCol = ctx.createRadialGradient(cx, cy, 4, cx, cy, halfW);
9179
+ surfCol.addColorStop(0, "#e8f0fb");
9180
+ surfCol.addColorStop(0.7, "#b9c9e2");
9181
+ surfCol.addColorStop(1, "#8fa3c4");
9182
+ ctx.save();
9183
+ ctx.beginPath();
9184
+ ctx.ellipse(cx, cy, halfW, ry, 0, 0, TAU2);
9185
+ ctx.clip();
9186
+ ctx.fillStyle = surfCol;
9187
+ ctx.fillRect(cx - halfW, cy - ry, halfW * 2, ry * 2);
9188
+ ctx.strokeStyle = "rgba(90,120,170,0.5)";
9189
+ ctx.lineWidth = 3;
9190
+ ctx.beginPath();
9191
+ ctx.moveTo(cx, cy - ry);
9192
+ ctx.lineTo(cx, cy + ry);
9193
+ ctx.moveTo(cx - halfW * 0.5, cy - ry);
9194
+ ctx.lineTo(cx - halfW * 0.5, cy + ry);
9195
+ ctx.moveTo(cx + halfW * 0.5, cy - ry);
9196
+ ctx.lineTo(cx + halfW * 0.5, cy + ry);
9197
+ ctx.stroke();
9198
+ ctx.strokeStyle = "rgba(180,90,110,0.45)";
9199
+ ctx.beginPath();
9200
+ ctx.ellipse(cx, cy, ry * 0.9, ry * 0.5, 0, 0, TAU2);
9201
+ ctx.stroke();
9202
+ ctx.restore();
9203
+ ctx.strokeStyle = "rgba(210,225,245,0.6)";
9204
+ ctx.lineWidth = 2.5;
9205
+ ctx.beginPath();
9206
+ ctx.ellipse(cx, cy, halfW, ry, 0, 0, TAU2);
9207
+ ctx.stroke();
9208
+ ctx.fillStyle = "rgba(20,26,40,0.8)";
9209
+ for (let i = 0; i < 5; i++) {
9210
+ const px = cx + (rand() - 0.5) * halfW * 1.4;
9211
+ const py = cy + (rand() - 0.5) * ry * 1.1;
9212
+ const s = Math.max(2, ry * 0.06);
8573
9213
  ctx.beginPath();
8574
- ctx.moveTo(bx - 8, pitchToY(Math.min(60, stageTopPitch + 25)));
8575
- ctx.lineTo(bx - (sx1 - sx0) * 0.09, sy1);
8576
- ctx.lineTo(bx + (sx1 - sx0) * 0.09, sy1);
8577
- ctx.lineTo(bx + 8, pitchToY(Math.min(60, stageTopPitch + 25)));
8578
- ctx.closePath();
9214
+ ctx.ellipse(px, py, s * 0.6, s, 0, 0, TAU2);
8579
9215
  ctx.fill();
8580
9216
  }
8581
- ctx.globalAlpha = 1;
8582
- ctx.fillStyle = "rgba(199, 210, 254, 0.8)";
8583
- for (let i = 0; i < 26; i++) {
8584
- const x = i / 26 * W;
8585
- const y = pitchToY(55 + i % 3 * 6);
9217
+ const wash = ctx.createRadialGradient(cx, cy, 4, cx, cy, halfW * 1.4);
9218
+ wash.addColorStop(0, "rgba(200,220,255,0.12)");
9219
+ wash.addColorStop(1, "rgba(200,220,255,0)");
9220
+ ctx.save();
9221
+ ctx.globalCompositeOperation = "lighter";
9222
+ ctx.fillStyle = wash;
9223
+ ctx.fillRect(cx - halfW * 1.4, yFar - ry, halfW * 2.8, yNear - yFar + ry * 2);
9224
+ ctx.restore();
9225
+ const sbY = pitchToY(Math.min(66, farPitch + 60));
9226
+ const sbW = Math.max(80, halfW * 0.7);
9227
+ const sbH = sbW * 0.5;
9228
+ const sbGlow = ctx.createRadialGradient(cx, sbY, 4, cx, sbY, sbW * 1.6);
9229
+ sbGlow.addColorStop(0, `rgba(${rigTintStr},0.3)`);
9230
+ sbGlow.addColorStop(1, `rgba(${rigTintStr},0)`);
9231
+ ctx.save();
9232
+ ctx.globalCompositeOperation = "lighter";
9233
+ ctx.fillStyle = sbGlow;
9234
+ ctx.fillRect(cx - sbW * 1.6, sbY - sbW * 1.2, sbW * 3.2, sbW * 2.4);
9235
+ ctx.restore();
9236
+ ctx.fillStyle = "#080b12";
9237
+ ctx.beginPath();
9238
+ ctx.moveTo(cx - sbW / 2, sbY - sbH / 2);
9239
+ ctx.lineTo(cx + sbW / 2, sbY - sbH / 2);
9240
+ ctx.lineTo(cx + sbW * 0.6, sbY);
9241
+ ctx.lineTo(cx + sbW / 2, sbY + sbH / 2);
9242
+ ctx.lineTo(cx - sbW / 2, sbY + sbH / 2);
9243
+ ctx.lineTo(cx - sbW * 0.6, sbY);
9244
+ ctx.closePath();
9245
+ ctx.fill();
9246
+ const screenHue = baseHue;
9247
+ for (let i = 0; i < 3; i++) {
9248
+ const scol = hslToRgb(screenHue + i * 30, 0.5, 0.5);
9249
+ ctx.fillStyle = `rgba(${scol[0]},${scol[1]},${scol[2]},0.75)`;
9250
+ const swid = sbW * 0.26;
9251
+ ctx.fillRect(cx - sbW * 0.42 + i * (swid + sbW * 0.06), sbY - sbH * 0.32, swid, sbH * 0.64);
9252
+ }
9253
+ ctx.fillStyle = `rgba(${rigTintStr},0.9)`;
9254
+ for (let i = 0; i < 8; i++) {
9255
+ const lx = cx - sbW * 0.6 + sbW * 1.2 * i / 7;
8586
9256
  ctx.beginPath();
8587
- ctx.arc(x, y, 3.2, 0, Math.PI * 2);
9257
+ ctx.arc(lx, sbY + sbH * 0.62, 2.4, 0, TAU2);
8588
9258
  ctx.fill();
8589
9259
  }
8590
- const stageBearing = Math.atan2(dx, -dy);
8591
- ctx.fillStyle = "rgba(16, 20, 30, 0.95)";
9260
+ }
9261
+ function drawAudience(ctx, seat, neighborSeats, _focalPoint, stageBearing, eyeM, dx, dy, hasRelief, scene) {
9262
+ const own = seat.sectionId;
9263
+ let seatAhead = false;
9264
+ const figs = [];
8592
9265
  for (const other of neighborSeats) {
8593
9266
  if (other.id === seat.id) continue;
8594
9267
  const ox = other.x - seat.x;
8595
9268
  const oy = other.y - seat.y;
8596
9269
  const d = Math.hypot(ox, oy) * UNIT;
8597
- if (d < 0.3 || d > 14) continue;
9270
+ if (own && other.sectionId === own && d < 3 && ox * dx + oy * dy > 0) seatAhead = true;
9271
+ if (d < 0.3 || d > 17) continue;
8598
9272
  const bearing = Math.atan2(ox, -oy) - stageBearing;
8599
9273
  const yaw = (bearing * 180 / Math.PI + 540) % 360 - 180;
8600
- const headPitch = -Math.atan2(0.35, d) * 180 / Math.PI;
8601
- const headR = Math.min(46, 260 / (d / UNIT / 24 + 1.2) / 4);
8602
- const hx = yawToX(yaw);
8603
- const hy = pitchToY(headPitch);
8604
- ctx.beginPath();
8605
- ctx.arc(hx, hy, headR, 0, Math.PI * 2);
8606
- ctx.fill();
8607
- ctx.beginPath();
8608
- ctx.ellipse(hx, hy + headR * 1.4, headR * 1.7, headR * 0.9, 0, Math.PI, 0, true);
8609
- ctx.fill();
9274
+ const rise = hasRelief ? (other.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - eyeM : 0;
9275
+ const headPitch = Math.atan2(rise - 0.28, d) * 180 / Math.PI;
9276
+ if (headPitch > 10 && d > 5) continue;
9277
+ let hh = 0;
9278
+ for (let k = 0; k < other.id.length; k++) hh = hh * 31 + other.id.charCodeAt(k) & 65535;
9279
+ const jitter = 0.9 + (hh & 15) / 40;
9280
+ const r = Math.min(78, Math.atan2(0.16, d) * 180 / Math.PI * PX_PER_DEG * jitter);
9281
+ if (r < 2) continue;
9282
+ figs.push({ hx: yawToX(yaw), hy: pitchToY(headPitch), r, d, hash: hh });
9283
+ }
9284
+ figs.sort((a, b) => b.d - a.d);
9285
+ const CLOTHES = [[22, 26, 40], [28, 27, 42], [24, 31, 46], [31, 30, 47], [20, 24, 36]];
9286
+ for (const f of figs) {
9287
+ const alpha = f.d < 12 ? 0.95 : Math.max(0.66, 0.95 - (f.d - 12) / 18);
9288
+ const lift = Math.round((1 - Math.min(1, f.d / 16)) * 10);
9289
+ const headTone = [14 + (f.hash >> 4 & 6) + lift, 17 + (f.hash >> 4 & 6) + lift, 25 + (f.hash >> 4 & 6) + lift];
9290
+ const clo0 = CLOTHES[f.hash % CLOTHES.length];
9291
+ const clothing = [clo0[0] + lift, clo0[1] + lift, clo0[2] + lift];
9292
+ if (f.r >= 13) {
9293
+ const rim = f.d < 9 ? 0.16 * (1 - f.d / 9) + (scene.mode === "proscenium" ? 0.06 : 0) : 0;
9294
+ drawSeated(ctx, f.hx, f.hy, f.r, {
9295
+ tone: headTone,
9296
+ clothing,
9297
+ alpha,
9298
+ tilt: ((f.hash >> 2 & 7) - 3.5) * 0.018,
9299
+ // ±~7°
9300
+ shoulderK: 1.72 + (f.hash >> 5 & 7) / 22,
9301
+ // ~1.72..2.04
9302
+ hair: f.hash % HAIR_KINDS,
9303
+ rim
9304
+ });
9305
+ } else if (f.r >= 5.5) {
9306
+ drawSeatedMid(ctx, f.hx, f.hy, f.r, clothing, headTone, alpha);
9307
+ } else {
9308
+ ctx.fillStyle = `rgba(${headTone[0]},${headTone[1]},${headTone[2]},${alpha.toFixed(3)})`;
9309
+ ctx.beginPath();
9310
+ ctx.arc(f.hx, f.hy, f.r, 0, TAU2);
9311
+ ctx.fill();
9312
+ ctx.beginPath();
9313
+ ctx.ellipse(f.hx, f.hy + f.r * 1.4, f.r * 1.85, f.r * 0.95, 0, Math.PI, 0, true);
9314
+ ctx.fill();
9315
+ }
9316
+ }
9317
+ if (seatAhead) {
9318
+ const railTop = pitchToY(-34);
9319
+ ctx.fillStyle = "rgba(6, 8, 14, 0.92)";
9320
+ ctx.fillRect(0, railTop, W, H - railTop);
9321
+ ctx.fillStyle = "rgba(24, 30, 46, 0.9)";
9322
+ const humps = 16;
9323
+ for (let i = 0; i < humps; i++) {
9324
+ const cx = (i + 0.5) / humps * W;
9325
+ ctx.beginPath();
9326
+ ctx.ellipse(cx, railTop + 6, W / humps * 0.42, 22, 0, Math.PI, 0, true);
9327
+ ctx.fill();
9328
+ }
8610
9329
  }
8611
- const poleFade = ctx.createLinearGradient(0, 0, 0, H);
8612
- poleFade.addColorStop(0, "rgba(0,0,0,0.85)");
8613
- poleFade.addColorStop(0.12, "rgba(0,0,0,0)");
8614
- poleFade.addColorStop(0.88, "rgba(0,0,0,0)");
8615
- poleFade.addColorStop(1, "rgba(0,0,0,0.85)");
8616
- ctx.fillStyle = poleFade;
8617
- ctx.fillRect(0, 0, W, H);
8618
- return { url: canvas.toDataURL("image/jpeg", 0.82), distanceM: Math.round(distM) };
8619
9330
  }
8620
9331
  var TW = 480;
8621
9332
  var TH = 270;
@@ -8631,12 +9342,16 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
8631
9342
  const dx = focalPoint.x - seat.x;
8632
9343
  const dy = focalPoint.y - seat.y;
8633
9344
  const distM = Math.max(2, Math.hypot(dx, dy) * UNIT);
9345
+ const rand = rng(fnv1a(`${seat.id}|${Math.round(focalPoint.x)},${Math.round(focalPoint.y)}`));
9346
+ const baseHue = 210 + rand() * 150;
9347
+ const rigTint = hslToRgb(220 + (rand() - 0.5) * 60, 0.55, 0.82);
9348
+ const rigTintStr = `${rigTint[0]},${rigTint[1]},${rigTint[2]}`;
8634
9349
  const sky = ctx.createLinearGradient(0, 0, 0, TH);
8635
- sky.addColorStop(0, "#05070c");
8636
- sky.addColorStop(0.44, "#141b30");
8637
- sky.addColorStop(0.5, "#1b2440");
8638
- sky.addColorStop(0.58, "#10141f");
8639
- sky.addColorStop(1, "#070910");
9350
+ sky.addColorStop(0, "#080b12");
9351
+ sky.addColorStop(0.44, "#151c32");
9352
+ sky.addColorStop(0.5, "#1d2644");
9353
+ sky.addColorStop(0.58, "#161c2e");
9354
+ sky.addColorStop(1, "#131829");
8640
9355
  ctx.fillStyle = sky;
8641
9356
  ctx.fillRect(0, 0, TW, TH);
8642
9357
  const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
@@ -8650,58 +9365,102 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
8650
9365
  const sy1 = pitchToTY(stageBasePitch);
8651
9366
  const sw = sx1 - sx0;
8652
9367
  const sh = sy1 - sy0;
8653
- const glow = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, 4, TW / 2, (sy0 + sy1) / 2, sw * 1.1);
8654
- glow.addColorStop(0, "rgba(129,140,248,0.5)");
8655
- glow.addColorStop(0.5, "rgba(99,102,241,0.15)");
9368
+ const nearGlow = Math.max(0.35, Math.min(1, 1 - (distM - 6) / 60));
9369
+ const archTop = pitchToTY(Math.min(HALF_VFOV - 1, stageTopPitch + 8));
9370
+ const glow = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, 4, TW / 2, (sy0 + sy1) / 2, sw * 1.15);
9371
+ glow.addColorStop(0, "rgba(255,214,150,0.34)");
9372
+ glow.addColorStop(0.28, "rgba(150,150,230,0.3)");
9373
+ glow.addColorStop(0.6, "rgba(99,102,241,0.12)");
8656
9374
  glow.addColorStop(1, "rgba(99,102,241,0)");
8657
9375
  ctx.fillStyle = glow;
8658
9376
  ctx.fillRect(sx0 - sw * 0.6, sy0 - sh * 1.2, sw * 2.2, sh * 3.4);
8659
- ctx.fillStyle = "#242c44";
8660
- ctx.fillRect(sx0, sy0, sw, sh);
9377
+ const topCol = hslToRgb(baseHue, 0.62, 0.55);
9378
+ const midCol = hslToRgb(baseHue + 40, 0.7, 0.5);
9379
+ const botCol = hslToRgb(baseHue + 70, 0.78, 0.52);
9380
+ ctx.save();
9381
+ ctx.beginPath();
9382
+ ctx.rect(sx0, sy0, sw, sh);
9383
+ ctx.clip();
8661
9384
  const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
8662
- backdrop.addColorStop(0, "#7c5cff");
8663
- backdrop.addColorStop(0.5, "#e1447a");
8664
- backdrop.addColorStop(1, "#f59e0b");
8665
- ctx.globalAlpha = 0.78;
9385
+ backdrop.addColorStop(0, `rgb(${topCol[0]},${topCol[1]},${topCol[2]})`);
9386
+ backdrop.addColorStop(0.5, `rgb(${midCol[0]},${midCol[1]},${midCol[2]})`);
9387
+ backdrop.addColorStop(1, `rgb(${botCol[0]},${botCol[1]},${botCol[2]})`);
9388
+ ctx.globalAlpha = 0.8;
8666
9389
  ctx.fillStyle = backdrop;
8667
- const inset = sw * 0.06;
8668
- ctx.fillRect(sx0 + inset, sy0 + sh * 0.12, sw - inset * 2, sh * 0.62);
9390
+ ctx.fillRect(sx0, sy0, sw, sh);
8669
9391
  ctx.globalAlpha = 1;
8670
- ctx.fillStyle = "#0d1119";
8671
- ctx.fillRect(sx0, sy1 - sh * 0.1, sw, sh * 0.1);
8672
- const ph = sh * 0.45;
8673
- ctx.fillStyle = "rgba(8,10,16,0.85)";
8674
- for (const fx of [0.35, 0.5, 0.65]) {
8675
- const px = sx0 + sw * fx;
8676
- const py = sy1 - sh * 0.12;
8677
- ctx.beginPath();
8678
- ctx.arc(px, py - ph * 0.82, ph * 0.16, 0, Math.PI * 2);
8679
- ctx.fill();
8680
- ctx.fillRect(px - ph * 0.14, py - ph * 0.68, ph * 0.28, ph * 0.68);
8681
- }
8682
- ctx.globalAlpha = 0.15;
8683
- ctx.fillStyle = "#c7d2fe";
8684
- for (const fx of [0.3, 0.5, 0.7]) {
8685
- const bx = sx0 + sw * fx;
8686
- ctx.beginPath();
8687
- ctx.moveTo(bx - 5, pitchToTY(Math.min(HALF_VFOV, stageTopPitch + 18)));
8688
- ctx.lineTo(bx - sw * 0.09, sy1);
8689
- ctx.lineTo(bx + sw * 0.09, sy1);
8690
- ctx.lineTo(bx + 5, pitchToTY(Math.min(HALF_VFOV, stageTopPitch + 18)));
8691
- ctx.closePath();
8692
- ctx.fill();
9392
+ const fall = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, sw * 0.12, TW / 2, (sy0 + sy1) / 2, sw * 0.6);
9393
+ fall.addColorStop(0, "rgba(6,8,14,0)");
9394
+ fall.addColorStop(0.7, "rgba(6,8,14,0.15)");
9395
+ fall.addColorStop(1, "rgba(5,6,11,0.9)");
9396
+ ctx.fillStyle = fall;
9397
+ ctx.fillRect(sx0, sy0, sw, sh);
9398
+ ctx.restore();
9399
+ const perfCount = 3;
9400
+ const floorY = sy1 - sh * 0.08;
9401
+ for (let i = 0; i < perfCount; i++) {
9402
+ const t2 = (i + 1) / (perfCount + 1);
9403
+ drawPerformer(ctx, sx0 + sw * (0.24 + t2 * 0.52), floorY, sh * 0.46 * (0.9 + rand() * 0.24), {
9404
+ tone: [8, 10, 16],
9405
+ alpha: 0.9,
9406
+ pose: Math.floor(rand() * 4),
9407
+ rim: hslToRgb(baseHue + 20, 0.5, 0.7),
9408
+ rimStrength: 0.7
9409
+ });
8693
9410
  }
8694
- ctx.globalAlpha = 1;
8695
- ctx.fillStyle = "rgba(199,210,254,0.75)";
9411
+ ctx.fillStyle = "#05070d";
9412
+ ctx.fillRect(sx0 - sw * 0.5, archTop, sw * 0.5, sy1 - archTop);
9413
+ ctx.fillRect(sx1, archTop, sw * 0.5, sy1 - archTop);
9414
+ const legBot = sw * 0.05 + 2;
9415
+ const legTop = legBot * 1.35;
9416
+ ctx.fillStyle = "#03050a";
9417
+ ctx.beginPath();
9418
+ ctx.moveTo(sx0, sy1);
9419
+ ctx.lineTo(sx0 - legBot, sy1);
9420
+ ctx.lineTo(sx0 - legTop, archTop);
9421
+ ctx.lineTo(sx0 + sw * 0.012, archTop);
9422
+ ctx.closePath();
9423
+ ctx.fill();
9424
+ ctx.beginPath();
9425
+ ctx.moveTo(sx1, sy1);
9426
+ ctx.lineTo(sx1 + legBot, sy1);
9427
+ ctx.lineTo(sx1 + legTop, archTop);
9428
+ ctx.lineTo(sx1 - sw * 0.012, archTop);
9429
+ ctx.closePath();
9430
+ ctx.fill();
9431
+ ctx.fillRect(sx0 - legTop, archTop, sw + legTop * 2, (sy1 - archTop) * 0.13);
9432
+ ctx.fillStyle = `rgba(${rigTintStr},0.12)`;
9433
+ ctx.fillRect(sx0 - 1.2, archTop, 1.2, sy1 - archTop);
9434
+ ctx.fillRect(sx1, archTop, 1.2, sy1 - archTop);
9435
+ ctx.fillStyle = "#0b0f18";
9436
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.05 + 2);
9437
+ const foot = ctx.createLinearGradient(0, sy1 - sh * 0.05, 0, sy1 + sh * 0.14);
9438
+ foot.addColorStop(0, `rgba(255,206,140,${(0.5 * nearGlow).toFixed(3)})`);
9439
+ foot.addColorStop(1, "rgba(255,206,140,0)");
9440
+ ctx.fillStyle = foot;
9441
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.22);
9442
+ for (let i = 0; i < 3; i++) {
9443
+ const t2 = (i + 1) / 4;
9444
+ const fx = sx0 + sw * t2;
9445
+ const fy = pitchToTY(Math.min(HALF_VFOV - 1, stageTopPitch + 20));
9446
+ drawBeam(ctx, fx, fy, fx + (rand() - 0.5) * sw * 0.14, sy1, 3, sw * 0.09, rigTintStr, 0.17 * nearGlow);
9447
+ drawFixture(ctx, fx, fy, 1.8, rigTintStr);
9448
+ }
9449
+ ctx.fillStyle = `rgba(${rigTintStr},0.75)`;
8696
9450
  for (let i = 0; i < 8; i++) {
8697
9451
  ctx.beginPath();
8698
- ctx.arc((i + 0.5) / 8 * TW, pitchToTY(HALF_VFOV - 4 - i % 2 * 3), 2.2, 0, Math.PI * 2);
9452
+ ctx.arc((i + 0.5) / 8 * TW, pitchToTY(HALF_VFOV - 4 - i % 2 * 3), 2.2, 0, TAU2);
8699
9453
  ctx.fill();
8700
9454
  }
8701
9455
  ctx.fillStyle = "rgba(10,13,20,0.9)";
8702
9456
  ctx.fillRect(0, TH - 26, TW, 26);
8703
9457
  ctx.fillStyle = "rgba(30,37,54,0.9)";
8704
- for (let i = 0; i < 9; i++) ctx.fillRect(i * (TW / 9) + 6, TH - 22, TW / 9 - 12, 14);
9458
+ for (let i = 0; i < 9; i++) {
9459
+ const bx = i * (TW / 9) + TW / 18;
9460
+ ctx.beginPath();
9461
+ ctx.ellipse(bx, TH - 22, TW / 9 * 0.4, 15, 0, Math.PI, 0, true);
9462
+ ctx.fill();
9463
+ }
8705
9464
  return { url: canvas.toDataURL("image/jpeg", 0.8), distanceM: Math.round(distM) };
8706
9465
  }
8707
9466