@seatlayer/core 0.28.3 → 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);
@@ -7406,8 +7371,12 @@ var PickerController = class {
7406
7371
  this.emitSelectionChange();
7407
7372
  return true;
7408
7373
  }
7409
- /** Atomically replace an active variable-table hold with a new guest count. */
7410
- async replaceTableQuantity(label, quantity) {
7374
+ /**
7375
+ * Atomically replace an active variable-table hold with a new guest count.
7376
+ * `ttlMs` keeps the host's checkout window across the replacement — without it
7377
+ * a guest-count change silently re-based the countdown on the server default.
7378
+ */
7379
+ async replaceTableQuantity(label, quantity, ttlMs) {
7411
7380
  const table = this.groupedTablesByLabel.get(label);
7412
7381
  const hold = this.hold_;
7413
7382
  if (!table || table.mode !== "variable" || !hold?.labels.includes(label)) return null;
@@ -7424,7 +7393,7 @@ var PickerController = class {
7424
7393
  quantity: item.label === label ? q : item.quantity
7425
7394
  }));
7426
7395
  if (!selections.some((item) => item.label === label)) return null;
7427
- const result = await this.api.hold(this.key, selections, void 0, hold.holdId);
7396
+ const result = await this.api.hold(this.key, selections, ttlMs, hold.holdId);
7428
7397
  this.tableQuantities.set(label, q);
7429
7398
  this.setHold({
7430
7399
  holdId: result.holdId,
@@ -7708,6 +7677,10 @@ var PickerController = class {
7708
7677
  * failure. With `preferPremium`, first tries to hold the best PREMIUM block
7709
7678
  * locally; when none matches the requested quantity it falls back to the
7710
7679
  * normal server pick (the widget surfaces a subtle note on that fallback).
7680
+ *
7681
+ * `ttlMs` is the host's checkout window and MUST reach both branches: a hold
7682
+ * born here is the same buyer commitment as one made by clicking seats, so
7683
+ * omitting it silently handed the buyer the server default instead.
7711
7684
  */
7712
7685
  async bestAvailable(qty, categoryKey, opts = {}) {
7713
7686
  const r = this.renderer;
@@ -7722,7 +7695,7 @@ var PickerController = class {
7722
7695
  const resolved = seat ? this.toSeat(seat) : null;
7723
7696
  return { label, ...resolved?.tierId ? { tierId: resolved.tierId } : {} };
7724
7697
  });
7725
- const result = await this.api.hold(this.key, selection);
7698
+ const result = await this.api.hold(this.key, selection, opts.ttlMs);
7726
7699
  r.clearSelection();
7727
7700
  const ids = this.idsForLabels(block);
7728
7701
  if (ids.length) r.setStatus(ids, "held");
@@ -7741,7 +7714,7 @@ var PickerController = class {
7741
7714
  }
7742
7715
  if (this.hold_ && !await this.release()) return null;
7743
7716
  try {
7744
- const result = await this.api.bestAvailable(this.key, qty, categoryKey, opts.zoneId);
7717
+ const result = await this.api.bestAvailable(this.key, qty, categoryKey, opts.zoneId, opts.ttlMs);
7745
7718
  r.clearSelection();
7746
7719
  const ids = this.idsForLabels(result.labels);
7747
7720
  if (ids.length) r.setStatus(ids, "held");
@@ -7762,8 +7735,10 @@ var PickerController = class {
7762
7735
  * cart can hold best-available seats that aren't in the renderer selection);
7763
7736
  * defaults to the renderer selection. Reuses an existing exact-match hold, else
7764
7737
  * holds first. Returns the labels booked, or null on failure (conflicts painted).
7738
+ * `ttlMs` only matters on the hold-first path: if the book call then fails, the
7739
+ * seats sit held for the host's window rather than the longer server default.
7765
7740
  */
7766
- async book(bookingRef, labelsArg) {
7741
+ async book(bookingRef, labelsArg, ttlMs) {
7767
7742
  const r = this.renderer;
7768
7743
  if (!r) return null;
7769
7744
  if (!this.api.book) throw new Error("picker: transport has no book() \u2014 hold-only mode");
@@ -7786,7 +7761,7 @@ var PickerController = class {
7786
7761
  ...this.tableQuantityRequest(resolved)
7787
7762
  };
7788
7763
  }),
7789
- void 0,
7764
+ ttlMs,
7790
7765
  replaceHoldId
7791
7766
  );
7792
7767
  holdId = h.holdId;
@@ -8499,8 +8474,428 @@ function stageSightlinePitch(eyeHeightM, distM) {
8499
8474
  var W = 2048;
8500
8475
  var H = 1024;
8501
8476
  var UNIT = METRES_PER_CHART_UNIT;
8477
+ var TAU2 = Math.PI * 2;
8478
+ var PX_PER_DEG = H / 180;
8502
8479
  var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
8503
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
+ }
8504
8899
  function generateSeatPanorama(seat, focalPoint, neighborSeats) {
8505
8900
  const canvas = document.createElement("canvas");
8506
8901
  canvas.width = W;
@@ -8510,102 +8905,428 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
8510
8905
  const dy = focalPoint.y - seat.y;
8511
8906
  const distU = Math.hypot(dx, dy);
8512
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]}`;
8513
8914
  const sky = ctx.createLinearGradient(0, 0, 0, H);
8514
- sky.addColorStop(0, "#05070c");
8515
- sky.addColorStop(0.42, "#11172a");
8516
- sky.addColorStop(0.5, "#1a2238");
8517
- sky.addColorStop(0.56, "#10141f");
8518
- 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");
8519
8921
  ctx.fillStyle = sky;
8520
8922
  ctx.fillRect(0, 0, W, H);
8521
- const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
8522
- 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));
8523
8934
  const sightline = stageSightlinePitch(eyeM, distM);
8935
+ const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
8524
8936
  const stageTopPitch = Math.min(45, sightline.topPitch);
8525
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) {
8526
8965
  const sx0 = yawToX(-stageHalfYaw);
8527
8966
  const sx1 = yawToX(stageHalfYaw);
8528
8967
  const sy0 = pitchToY(stageTopPitch);
8529
8968
  const sy1 = pitchToY(stageBasePitch);
8530
- const glow = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, 10, W / 2, (sy0 + sy1) / 2, (sx1 - sx0) * 1.1);
8531
- glow.addColorStop(0, "rgba(129, 140, 248, 0.5)");
8532
- 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)");
8533
8977
  glow.addColorStop(1, "rgba(99, 102, 241, 0)");
8534
8978
  ctx.fillStyle = glow;
8535
- ctx.fillRect(sx0 - (sx1 - sx0) * 0.6, sy0 - (sy1 - sy0) * 1.2, (sx1 - sx0) * 2.2, (sy1 - sy0) * 3.4);
8536
- ctx.fillStyle = "#242c44";
8537
- 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();
8538
8987
  const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
8539
- backdrop.addColorStop(0, "#7c5cff");
8540
- backdrop.addColorStop(0.5, "#e1447a");
8541
- backdrop.addColorStop(1, "#f59e0b");
8542
- 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;
8543
8992
  ctx.fillStyle = backdrop;
8544
- const inset = (sx1 - sx0) * 0.06;
8545
- ctx.fillRect(sx0 + inset, sy0 + (sy1 - sy0) * 0.12, sx1 - sx0 - inset * 2, (sy1 - sy0) * 0.62);
8993
+ ctx.fillRect(sx0, sy0, sw, sh);
8546
8994
  ctx.globalAlpha = 1;
8547
- ctx.fillStyle = "#0d1119";
8548
- ctx.fillRect(sx0, sy1 - (sy1 - sy0) * 0.1, sx1 - sx0, (sy1 - sy0) * 0.1);
8549
- const performerH = (sy1 - sy0) * 0.45;
8550
- ctx.fillStyle = "rgba(8, 10, 16, 0.85)";
8551
- for (const fx of [0.35, 0.5, 0.65]) {
8552
- const px = sx0 + (sx1 - sx0) * fx;
8553
- const py = sy1 - (sy1 - sy0) * 0.12;
8554
- ctx.beginPath();
8555
- ctx.arc(px, py - performerH * 0.82, performerH * 0.16, 0, Math.PI * 2);
8556
- ctx.fill();
8557
- 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
+ });
8558
9015
  }
8559
- ctx.globalAlpha = 0.14;
8560
- ctx.fillStyle = "#c7d2fe";
8561
- for (const fx of [0.3, 0.5, 0.7]) {
8562
- const bx = sx0 + (sx1 - sx0) * fx;
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
+ });
9145
+ }
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);
8563
9213
  ctx.beginPath();
8564
- ctx.moveTo(bx - 8, pitchToY(Math.min(60, stageTopPitch + 25)));
8565
- ctx.lineTo(bx - (sx1 - sx0) * 0.09, sy1);
8566
- ctx.lineTo(bx + (sx1 - sx0) * 0.09, sy1);
8567
- ctx.lineTo(bx + 8, pitchToY(Math.min(60, stageTopPitch + 25)));
8568
- ctx.closePath();
9214
+ ctx.ellipse(px, py, s * 0.6, s, 0, 0, TAU2);
8569
9215
  ctx.fill();
8570
9216
  }
8571
- ctx.globalAlpha = 1;
8572
- ctx.fillStyle = "rgba(199, 210, 254, 0.8)";
8573
- for (let i = 0; i < 26; i++) {
8574
- const x = i / 26 * W;
8575
- 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;
8576
9256
  ctx.beginPath();
8577
- ctx.arc(x, y, 3.2, 0, Math.PI * 2);
9257
+ ctx.arc(lx, sbY + sbH * 0.62, 2.4, 0, TAU2);
8578
9258
  ctx.fill();
8579
9259
  }
8580
- const stageBearing = Math.atan2(dx, -dy);
8581
- 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 = [];
8582
9265
  for (const other of neighborSeats) {
8583
9266
  if (other.id === seat.id) continue;
8584
9267
  const ox = other.x - seat.x;
8585
9268
  const oy = other.y - seat.y;
8586
9269
  const d = Math.hypot(ox, oy) * UNIT;
8587
- 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;
8588
9272
  const bearing = Math.atan2(ox, -oy) - stageBearing;
8589
9273
  const yaw = (bearing * 180 / Math.PI + 540) % 360 - 180;
8590
- const headPitch = -Math.atan2(0.35, d) * 180 / Math.PI;
8591
- const headR = Math.min(46, 260 / (d / UNIT / 24 + 1.2) / 4);
8592
- const hx = yawToX(yaw);
8593
- const hy = pitchToY(headPitch);
8594
- ctx.beginPath();
8595
- ctx.arc(hx, hy, headR, 0, Math.PI * 2);
8596
- ctx.fill();
8597
- ctx.beginPath();
8598
- ctx.ellipse(hx, hy + headR * 1.4, headR * 1.7, headR * 0.9, 0, Math.PI, 0, true);
8599
- 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
+ }
8600
9329
  }
8601
- const poleFade = ctx.createLinearGradient(0, 0, 0, H);
8602
- poleFade.addColorStop(0, "rgba(0,0,0,0.85)");
8603
- poleFade.addColorStop(0.12, "rgba(0,0,0,0)");
8604
- poleFade.addColorStop(0.88, "rgba(0,0,0,0)");
8605
- poleFade.addColorStop(1, "rgba(0,0,0,0.85)");
8606
- ctx.fillStyle = poleFade;
8607
- ctx.fillRect(0, 0, W, H);
8608
- return { url: canvas.toDataURL("image/jpeg", 0.82), distanceM: Math.round(distM) };
8609
9330
  }
8610
9331
  var TW = 480;
8611
9332
  var TH = 270;
@@ -8621,12 +9342,16 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
8621
9342
  const dx = focalPoint.x - seat.x;
8622
9343
  const dy = focalPoint.y - seat.y;
8623
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]}`;
8624
9349
  const sky = ctx.createLinearGradient(0, 0, 0, TH);
8625
- sky.addColorStop(0, "#05070c");
8626
- sky.addColorStop(0.44, "#141b30");
8627
- sky.addColorStop(0.5, "#1b2440");
8628
- sky.addColorStop(0.58, "#10141f");
8629
- 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");
8630
9355
  ctx.fillStyle = sky;
8631
9356
  ctx.fillRect(0, 0, TW, TH);
8632
9357
  const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
@@ -8640,58 +9365,102 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
8640
9365
  const sy1 = pitchToTY(stageBasePitch);
8641
9366
  const sw = sx1 - sx0;
8642
9367
  const sh = sy1 - sy0;
8643
- const glow = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, 4, TW / 2, (sy0 + sy1) / 2, sw * 1.1);
8644
- glow.addColorStop(0, "rgba(129,140,248,0.5)");
8645
- 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)");
8646
9374
  glow.addColorStop(1, "rgba(99,102,241,0)");
8647
9375
  ctx.fillStyle = glow;
8648
9376
  ctx.fillRect(sx0 - sw * 0.6, sy0 - sh * 1.2, sw * 2.2, sh * 3.4);
8649
- ctx.fillStyle = "#242c44";
8650
- 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();
8651
9384
  const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
8652
- backdrop.addColorStop(0, "#7c5cff");
8653
- backdrop.addColorStop(0.5, "#e1447a");
8654
- backdrop.addColorStop(1, "#f59e0b");
8655
- 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;
8656
9389
  ctx.fillStyle = backdrop;
8657
- const inset = sw * 0.06;
8658
- ctx.fillRect(sx0 + inset, sy0 + sh * 0.12, sw - inset * 2, sh * 0.62);
9390
+ ctx.fillRect(sx0, sy0, sw, sh);
8659
9391
  ctx.globalAlpha = 1;
8660
- ctx.fillStyle = "#0d1119";
8661
- ctx.fillRect(sx0, sy1 - sh * 0.1, sw, sh * 0.1);
8662
- const ph = sh * 0.45;
8663
- ctx.fillStyle = "rgba(8,10,16,0.85)";
8664
- for (const fx of [0.35, 0.5, 0.65]) {
8665
- const px = sx0 + sw * fx;
8666
- const py = sy1 - sh * 0.12;
8667
- ctx.beginPath();
8668
- ctx.arc(px, py - ph * 0.82, ph * 0.16, 0, Math.PI * 2);
8669
- ctx.fill();
8670
- ctx.fillRect(px - ph * 0.14, py - ph * 0.68, ph * 0.28, ph * 0.68);
8671
- }
8672
- ctx.globalAlpha = 0.15;
8673
- ctx.fillStyle = "#c7d2fe";
8674
- for (const fx of [0.3, 0.5, 0.7]) {
8675
- const bx = sx0 + sw * fx;
8676
- ctx.beginPath();
8677
- ctx.moveTo(bx - 5, pitchToTY(Math.min(HALF_VFOV, stageTopPitch + 18)));
8678
- ctx.lineTo(bx - sw * 0.09, sy1);
8679
- ctx.lineTo(bx + sw * 0.09, sy1);
8680
- ctx.lineTo(bx + 5, pitchToTY(Math.min(HALF_VFOV, stageTopPitch + 18)));
8681
- ctx.closePath();
8682
- 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
+ });
8683
9410
  }
8684
- ctx.globalAlpha = 1;
8685
- 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)`;
8686
9450
  for (let i = 0; i < 8; i++) {
8687
9451
  ctx.beginPath();
8688
- 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);
8689
9453
  ctx.fill();
8690
9454
  }
8691
9455
  ctx.fillStyle = "rgba(10,13,20,0.9)";
8692
9456
  ctx.fillRect(0, TH - 26, TW, 26);
8693
9457
  ctx.fillStyle = "rgba(30,37,54,0.9)";
8694
- 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
+ }
8695
9464
  return { url: canvas.toDataURL("image/jpeg", 0.8), distanceM: Math.round(distM) };
8696
9465
  }
8697
9466