@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.cjs CHANGED
@@ -4638,7 +4638,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4638
4638
  this.accessRingById.set(seat.id, ring);
4639
4639
  target.add(ring);
4640
4640
  }
4641
- if (accessGlyphPath(seat.accessibility ?? [])) {
4641
+ if (seat.accessibility?.includes("wheelchair") && accessGlyphPath(seat.accessibility)) {
4642
4642
  const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
4643
4643
  this.accessGlyphById.set(seat.id, glyph);
4644
4644
  target.add(glyph);
@@ -8788,8 +8788,428 @@ function stageSightlinePitch(eyeHeightM, distM) {
8788
8788
  var W = 2048;
8789
8789
  var H = 1024;
8790
8790
  var UNIT = METRES_PER_CHART_UNIT;
8791
+ var TAU2 = Math.PI * 2;
8792
+ var PX_PER_DEG = H / 180;
8791
8793
  var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
8792
8794
  var pitchToY = (pitchDeg) => (90 - pitchDeg) / 180 * H;
8795
+ function fnv1a(str) {
8796
+ let h = 2166136261;
8797
+ for (let i = 0; i < str.length; i++) {
8798
+ h ^= str.charCodeAt(i);
8799
+ h = Math.imul(h, 16777619);
8800
+ }
8801
+ return h >>> 0;
8802
+ }
8803
+ function rng(seed) {
8804
+ let a = seed >>> 0;
8805
+ return () => {
8806
+ a |= 0;
8807
+ a = a + 1831565813 | 0;
8808
+ let t2 = Math.imul(a ^ a >>> 15, 1 | a);
8809
+ t2 = t2 + Math.imul(t2 ^ t2 >>> 7, 61 | t2) ^ t2;
8810
+ return ((t2 ^ t2 >>> 14) >>> 0) / 4294967296;
8811
+ };
8812
+ }
8813
+ var NBINS = 96;
8814
+ var BIN_DEG = 360 / NBINS;
8815
+ var FLAT_DELTA_M = 0.6;
8816
+ var HAZE_SKY = [17, 23, 42];
8817
+ function blendToSky(base, t2, a = 1) {
8818
+ const k = Math.max(0, Math.min(0.58, t2));
8819
+ const r = Math.round(base[0] + (HAZE_SKY[0] - base[0]) * k);
8820
+ const g = Math.round(base[1] + (HAZE_SKY[1] - base[1]) * k);
8821
+ const b = Math.round(base[2] + (HAZE_SKY[2] - base[2]) * k);
8822
+ return `rgba(${r},${g},${b},${a})`;
8823
+ }
8824
+ function mixRgb(a, b, t2) {
8825
+ return [
8826
+ Math.round(a[0] + (b[0] - a[0]) * t2),
8827
+ Math.round(a[1] + (b[1] - a[1]) * t2),
8828
+ Math.round(a[2] + (b[2] - a[2]) * t2)
8829
+ ];
8830
+ }
8831
+ function hslToRgb(h, s, l) {
8832
+ const c = (1 - Math.abs(2 * l - 1)) * s;
8833
+ const hp = (h % 360 + 360) % 360 / 60;
8834
+ const x = c * (1 - Math.abs(hp % 2 - 1));
8835
+ let r = 0, g = 0, b = 0;
8836
+ if (hp < 1) [r, g, b] = [c, x, 0];
8837
+ else if (hp < 2) [r, g, b] = [x, c, 0];
8838
+ else if (hp < 3) [r, g, b] = [0, c, x];
8839
+ else if (hp < 4) [r, g, b] = [0, x, c];
8840
+ else if (hp < 5) [r, g, b] = [x, 0, c];
8841
+ else [r, g, b] = [c, 0, x];
8842
+ const m = l - c / 2;
8843
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
8844
+ }
8845
+ var tintCache = /* @__PURE__ */ new Map();
8846
+ function tintFromKey(key) {
8847
+ if (!key) return null;
8848
+ const cached = tintCache.get(key);
8849
+ if (cached) return cached;
8850
+ let h = 2166136261;
8851
+ for (let i = 0; i < key.length; i++) {
8852
+ h ^= key.charCodeAt(i);
8853
+ h = Math.imul(h, 16777619);
8854
+ }
8855
+ const hue = (h >>> 0) % 360;
8856
+ const rgb2 = hslToRgb(hue, 0.3, 0.42);
8857
+ tintCache.set(key, rgb2);
8858
+ return rgb2;
8859
+ }
8860
+ function buildStands(seat, neighborSeats, stageBearing, myEyeM) {
8861
+ const bins = new Array(NBINS).fill(null);
8862
+ let maxDelta = 0;
8863
+ const own = seat.sectionId;
8864
+ for (const other of neighborSeats) {
8865
+ if (other.id === seat.id) continue;
8866
+ const os = other.sectionId;
8867
+ if (!os || own && os === own) continue;
8868
+ const ox = other.x - seat.x;
8869
+ const oy = other.y - seat.y;
8870
+ const dM = Math.hypot(ox, oy) * UNIT;
8871
+ if (dM < 1.5 || dM > 95) continue;
8872
+ const otherEye = other.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
8873
+ const dTop = otherEye - myEyeM;
8874
+ if (Math.abs(dTop) > maxDelta) maxDelta = Math.abs(dTop);
8875
+ const top = Math.atan2(dTop, dM) * 180 / Math.PI;
8876
+ const base = Math.atan2(otherEye - SEATED_EYE_HEIGHT_M - myEyeM, dM) * 180 / Math.PI;
8877
+ const bearing = Math.atan2(ox, -oy) - stageBearing;
8878
+ const yaw = (bearing * 180 / Math.PI + 540) % 360 - 180;
8879
+ const idx = Math.min(NBINS - 1, Math.max(0, Math.floor((yaw + 180) / BIN_DEG)));
8880
+ const overhead = top > 34 && dM < 16;
8881
+ const cur = bins[idx];
8882
+ if (!cur) bins[idx] = { top, base, nearM: dM, overhead, tint: tintFromKey(other.categoryKey) };
8883
+ else {
8884
+ if (top > cur.top) cur.top = top;
8885
+ if (base < cur.base) cur.base = base;
8886
+ if (dM < cur.nearM) {
8887
+ cur.nearM = dM;
8888
+ cur.tint = tintFromKey(other.categoryKey);
8889
+ }
8890
+ cur.overhead = cur.overhead || overhead;
8891
+ }
8892
+ }
8893
+ return { bins, maxDelta };
8894
+ }
8895
+ function drawStands(ctx, bins, toX, toY) {
8896
+ for (let i = 0; i < NBINS; i++) {
8897
+ const b = bins[i];
8898
+ if (!b) continue;
8899
+ const yaw0 = -180 + i * BIN_DEG;
8900
+ const x0 = toX(yaw0);
8901
+ const x1 = toX(yaw0 + BIN_DEG);
8902
+ if (!(x1 > x0)) continue;
8903
+ const top = Math.min(72, b.top);
8904
+ const bottom = Math.min(b.base, -4);
8905
+ const yTop = toY(top);
8906
+ const yBot = toY(bottom);
8907
+ if (yBot <= yTop) continue;
8908
+ const haze = b.nearM / 95;
8909
+ const tint = b.tint;
8910
+ const crest = tint ? mixRgb([44, 54, 80], tint, 0.34) : [44, 54, 80];
8911
+ const seating = tint ? mixRgb([24, 30, 48], tint, 0.28) : [24, 30, 48];
8912
+ const grad = ctx.createLinearGradient(0, yTop, 0, yBot);
8913
+ grad.addColorStop(0, blendToSky(crest, haze));
8914
+ grad.addColorStop(0.24, blendToSky(seating, haze));
8915
+ grad.addColorStop(1, blendToSky([10, 13, 22], haze));
8916
+ ctx.fillStyle = grad;
8917
+ ctx.fillRect(x0, yTop, x1 - x0 + 1, yBot - yTop);
8918
+ ctx.fillStyle = `rgba(150,165,205,${0.35 * (1 - haze * 0.55)})`;
8919
+ ctx.fillRect(x0, yTop, x1 - x0 + 1, 1.5);
8920
+ const bandBottom = Math.min(yBot, toY(0));
8921
+ const bandSpan = bandBottom - yTop;
8922
+ if (bandSpan > 8) {
8923
+ const bands = 6;
8924
+ const contrast = 1 - haze * 0.4;
8925
+ for (let r = 1; r < bands; r++) {
8926
+ const y = yTop + bandSpan * r / bands;
8927
+ ctx.fillStyle = `rgba(0,0,0,${0.16 * contrast})`;
8928
+ ctx.fillRect(x0, y, x1 - x0 + 1, 1.5);
8929
+ ctx.fillStyle = `rgba(120,134,170,${0.1 * contrast})`;
8930
+ ctx.fillRect(x0, y + 1.5, x1 - x0 + 1, 1.2);
8931
+ }
8932
+ }
8933
+ if (b.overhead) {
8934
+ const lipY = toY(Math.min(86, top + 12));
8935
+ const g2 = ctx.createLinearGradient(0, lipY, 0, yTop);
8936
+ g2.addColorStop(0, "rgba(3,4,8,0.9)");
8937
+ g2.addColorStop(1, "rgba(3,4,8,0)");
8938
+ ctx.fillStyle = g2;
8939
+ ctx.fillRect(x0, lipY, x1 - x0 + 1, yTop - lipY);
8940
+ ctx.fillStyle = "rgba(120,130,160,0.14)";
8941
+ ctx.fillRect(x0, yTop - 1.5, x1 - x0 + 1, 1.5);
8942
+ }
8943
+ }
8944
+ }
8945
+ function classifyScene(_seat, focal, neighbors) {
8946
+ const NB = 72;
8947
+ const occ = new Array(NB).fill(false);
8948
+ const innerM = new Array(NB).fill(Infinity);
8949
+ const secSet = /* @__PURE__ */ new Set();
8950
+ let n = 0;
8951
+ for (const s of neighbors) {
8952
+ n++;
8953
+ if (s.sectionId) secSet.add(s.sectionId);
8954
+ const dx = s.x - focal.x;
8955
+ const dy = s.y - focal.y;
8956
+ const dM = Math.hypot(dx, dy) * UNIT;
8957
+ if (dM < 0.5) continue;
8958
+ const bearing = Math.atan2(dx, -dy);
8959
+ let idx = Math.floor((bearing + Math.PI) / TAU2 * NB);
8960
+ if (idx < 0) idx = 0;
8961
+ else if (idx >= NB) idx = NB - 1;
8962
+ occ[idx] = true;
8963
+ if (dM < innerM[idx]) innerM[idx] = dM;
8964
+ }
8965
+ let occN = 0;
8966
+ const inner = [];
8967
+ for (let i = 0; i < NB; i++) if (occ[i]) {
8968
+ occN++;
8969
+ inner.push(innerM[i]);
8970
+ }
8971
+ const coverage = occN / NB * 360;
8972
+ inner.sort((a, b) => a - b);
8973
+ const median = inner.length ? inner[inner.length >> 1] : 0;
8974
+ const lo = inner.length ? inner[Math.floor(inner.length * 0.15)] ?? median : 0;
8975
+ const hi = inner.length ? inner[Math.floor(inner.length * 0.85)] ?? median : 0;
8976
+ const aspect = lo > 0.5 ? hi / lo : 1;
8977
+ const seed = fnv1a(`${n}|${[...secSet].sort().join(",")}|${Math.round(focal.x)},${Math.round(focal.y)}`);
8978
+ let mode;
8979
+ if (coverage >= 300 && median > 11 && aspect > 1.35) mode = "sport";
8980
+ else if (coverage >= 285) mode = "arena";
8981
+ else mode = "proscenium";
8982
+ return { mode, seed, voidRadiusM: median, aspect };
8983
+ }
8984
+ var HAIR_KINDS = 5;
8985
+ function drawHair(ctx, r, kind) {
8986
+ switch (kind) {
8987
+ case 1:
8988
+ ctx.beginPath();
8989
+ ctx.ellipse(0, -r * 0.14, r * 1.16, r * 1.16, 0, 0, TAU2);
8990
+ ctx.fill();
8991
+ break;
8992
+ case 2:
8993
+ ctx.beginPath();
8994
+ ctx.ellipse(0, -r * 1.02, r * 0.42, r * 0.42, 0, 0, TAU2);
8995
+ ctx.fill();
8996
+ ctx.beginPath();
8997
+ ctx.ellipse(0, -r * 0.2, r * 0.98, r * 1.02, 0, 0, TAU2);
8998
+ ctx.fill();
8999
+ break;
9000
+ case 3:
9001
+ ctx.beginPath();
9002
+ ctx.ellipse(0, -r * 0.36, r * 1.04, r * 0.6, 0, Math.PI, TAU2);
9003
+ ctx.fill();
9004
+ ctx.beginPath();
9005
+ ctx.ellipse(0, -r * 0.52, r * 1.22, r * 0.24, 0, 0, TAU2);
9006
+ ctx.fill();
9007
+ break;
9008
+ case 4:
9009
+ ctx.beginPath();
9010
+ ctx.ellipse(-r * 0.66, r * 0.5, r * 0.5, r * 1.35, 0, 0, TAU2);
9011
+ ctx.fill();
9012
+ ctx.beginPath();
9013
+ ctx.ellipse(r * 0.66, r * 0.5, r * 0.5, r * 1.35, 0, 0, TAU2);
9014
+ ctx.fill();
9015
+ ctx.beginPath();
9016
+ ctx.ellipse(0, -r * 0.08, r * 1.06, r * 1.08, 0, 0, TAU2);
9017
+ ctx.fill();
9018
+ break;
9019
+ default:
9020
+ break;
9021
+ }
9022
+ }
9023
+ function drawSeated(ctx, hx, hy, r, o) {
9024
+ const neckHalf = r * 0.44;
9025
+ const shHalf = r * o.shoulderK;
9026
+ const neckTopY = r * 0.66;
9027
+ const shoulderY = r * 1.68;
9028
+ const botY = r * 5.4;
9029
+ const clo = `rgba(${o.clothing[0]},${o.clothing[1]},${o.clothing[2]},${o.alpha})`;
9030
+ const skin = `rgba(${o.tone[0]},${o.tone[1]},${o.tone[2]},${o.alpha})`;
9031
+ ctx.save();
9032
+ ctx.translate(hx, hy);
9033
+ ctx.fillStyle = clo;
9034
+ ctx.beginPath();
9035
+ ctx.moveTo(-neckHalf, neckTopY);
9036
+ ctx.quadraticCurveTo(-neckHalf * 1.18, shoulderY - r * 0.6, -shHalf, shoulderY);
9037
+ ctx.quadraticCurveTo(-shHalf * 1.05, shoulderY + r * 1.2, -shHalf * 0.94, botY);
9038
+ ctx.lineTo(shHalf * 0.94, botY);
9039
+ ctx.quadraticCurveTo(shHalf * 1.05, shoulderY + r * 1.2, shHalf, shoulderY);
9040
+ ctx.quadraticCurveTo(neckHalf * 1.18, shoulderY - r * 0.6, neckHalf, neckTopY);
9041
+ ctx.closePath();
9042
+ ctx.fill();
9043
+ ctx.fillStyle = skin;
9044
+ ctx.fillRect(-neckHalf * 0.82, r * 0.5, neckHalf * 1.64, r * 0.9);
9045
+ ctx.save();
9046
+ ctx.rotate(o.tilt);
9047
+ ctx.fillStyle = skin;
9048
+ ctx.beginPath();
9049
+ ctx.ellipse(0, 0, r * 0.9, r * 1.06, 0, 0, TAU2);
9050
+ ctx.fill();
9051
+ drawHair(ctx, r, o.hair);
9052
+ if (o.rim > 0.01) {
9053
+ ctx.strokeStyle = `rgba(245,205,150,${(0.5 * o.rim).toFixed(3)})`;
9054
+ ctx.lineWidth = Math.max(1, r * 0.14);
9055
+ ctx.beginPath();
9056
+ ctx.ellipse(0, -r * 0.06, r * 0.86, r * 1, 0, Math.PI * 1.12, Math.PI * 1.9);
9057
+ ctx.stroke();
9058
+ }
9059
+ ctx.restore();
9060
+ if (o.rim > 0.01) {
9061
+ ctx.strokeStyle = `rgba(240,200,150,${(0.28 * o.rim).toFixed(3)})`;
9062
+ ctx.lineWidth = Math.max(1, r * 0.12);
9063
+ ctx.beginPath();
9064
+ ctx.moveTo(-shHalf * 0.8, shoulderY - r * 0.05);
9065
+ ctx.quadraticCurveTo(-neckHalf, shoulderY - r * 0.75, 0, shoulderY - r * 0.82);
9066
+ ctx.quadraticCurveTo(neckHalf, shoulderY - r * 0.75, shHalf * 0.8, shoulderY - r * 0.05);
9067
+ ctx.stroke();
9068
+ }
9069
+ ctx.restore();
9070
+ }
9071
+ function drawSeatedMid(ctx, hx, hy, r, clothing, tone, alpha) {
9072
+ ctx.fillStyle = `rgba(${clothing[0]},${clothing[1]},${clothing[2]},${alpha})`;
9073
+ ctx.beginPath();
9074
+ ctx.moveTo(hx - r * 1.9, hy + r * 4);
9075
+ ctx.quadraticCurveTo(hx - r * 1.95, hy + r * 1.2, hx - r * 0.5, hy + r * 0.9);
9076
+ ctx.quadraticCurveTo(hx, hy + r * 0.4, hx + r * 0.5, hy + r * 0.9);
9077
+ ctx.quadraticCurveTo(hx + r * 1.95, hy + r * 1.2, hx + r * 1.9, hy + r * 4);
9078
+ ctx.closePath();
9079
+ ctx.fill();
9080
+ ctx.fillStyle = `rgba(${tone[0]},${tone[1]},${tone[2]},${alpha})`;
9081
+ ctx.beginPath();
9082
+ ctx.ellipse(hx, hy, r * 0.92, r * 1.05, 0, 0, TAU2);
9083
+ ctx.fill();
9084
+ }
9085
+ function drawPerformer(ctx, x, footY, h, o) {
9086
+ const headR = h * 0.1;
9087
+ const headCy = footY - h * 0.9;
9088
+ const shoulderY = footY - h * 0.74;
9089
+ const hipY = footY - h * 0.46;
9090
+ const shHalf = h * 0.13;
9091
+ const hipHalf = h * 0.09;
9092
+ const skin = `rgba(${o.tone[0]},${o.tone[1]},${o.tone[2]},${o.alpha})`;
9093
+ const sh = ctx.createRadialGradient(x, footY, 1, x, footY, h * 0.24);
9094
+ sh.addColorStop(0, "rgba(0,0,0,0.4)");
9095
+ sh.addColorStop(1, "rgba(0,0,0,0)");
9096
+ ctx.save();
9097
+ ctx.scale(1, 0.28);
9098
+ ctx.fillStyle = sh;
9099
+ ctx.beginPath();
9100
+ ctx.ellipse(x, footY / 0.28, h * 0.24, h * 0.24, 0, 0, TAU2);
9101
+ ctx.fill();
9102
+ ctx.restore();
9103
+ ctx.fillStyle = skin;
9104
+ ctx.beginPath();
9105
+ ctx.moveTo(x - hipHalf, hipY);
9106
+ ctx.lineTo(x - hipHalf * 0.7, footY);
9107
+ ctx.lineTo(x - hipHalf * 0.1, footY);
9108
+ ctx.lineTo(x - hipHalf * 0.1, hipY + h * 0.02);
9109
+ ctx.lineTo(x + hipHalf * 0.1, hipY + h * 0.02);
9110
+ ctx.lineTo(x + hipHalf * 0.1, footY);
9111
+ ctx.lineTo(x + hipHalf * 0.7, footY);
9112
+ ctx.lineTo(x + hipHalf, hipY);
9113
+ ctx.closePath();
9114
+ ctx.fill();
9115
+ ctx.beginPath();
9116
+ ctx.moveTo(x - shHalf, shoulderY);
9117
+ ctx.quadraticCurveTo(x - shHalf * 0.9, hipY - h * 0.12, x - hipHalf, hipY);
9118
+ ctx.lineTo(x + hipHalf, hipY);
9119
+ ctx.quadraticCurveTo(x + shHalf * 0.9, hipY - h * 0.12, x + shHalf, shoulderY);
9120
+ ctx.quadraticCurveTo(x, shoulderY - h * 0.05, x - shHalf, shoulderY);
9121
+ ctx.closePath();
9122
+ ctx.fill();
9123
+ ctx.lineCap = "round";
9124
+ ctx.lineJoin = "round";
9125
+ ctx.strokeStyle = skin;
9126
+ ctx.lineWidth = h * 0.055;
9127
+ const armY = shoulderY + h * 0.02;
9128
+ if (o.pose === 1) {
9129
+ ctx.beginPath();
9130
+ ctx.moveTo(x - shHalf * 0.8, armY);
9131
+ ctx.lineTo(x - shHalf * 1.5, footY - h * 1.02);
9132
+ ctx.moveTo(x + shHalf * 0.8, armY);
9133
+ ctx.lineTo(x + shHalf * 1.5, footY - h * 1.02);
9134
+ ctx.stroke();
9135
+ } else if (o.pose === 2) {
9136
+ ctx.beginPath();
9137
+ ctx.moveTo(x - shHalf * 0.8, armY);
9138
+ ctx.lineTo(x + hipHalf * 1.4, hipY - h * 0.02);
9139
+ ctx.moveTo(x + shHalf * 0.8, armY);
9140
+ ctx.lineTo(x - hipHalf * 0.4, hipY - h * 0.06);
9141
+ ctx.stroke();
9142
+ ctx.fillStyle = skin;
9143
+ ctx.beginPath();
9144
+ ctx.ellipse(x + hipHalf * 1.5, hipY - h * 0.04, h * 0.07, h * 0.09, -0.5, 0, TAU2);
9145
+ ctx.fill();
9146
+ } else if (o.pose === 3) {
9147
+ ctx.beginPath();
9148
+ ctx.moveTo(x - shHalf * 0.8, armY);
9149
+ ctx.lineTo(x - shHalf * 0.2, headCy + headR * 0.6);
9150
+ ctx.moveTo(x + shHalf * 0.8, armY);
9151
+ ctx.lineTo(x + shHalf * 1.1, hipY);
9152
+ ctx.stroke();
9153
+ ctx.lineWidth = h * 0.02;
9154
+ ctx.beginPath();
9155
+ ctx.moveTo(x - shHalf * 0.2, headCy + headR * 0.9);
9156
+ ctx.lineTo(x - shHalf * 0.2, footY - h * 0.02);
9157
+ ctx.stroke();
9158
+ } else {
9159
+ ctx.beginPath();
9160
+ ctx.moveTo(x - shHalf * 0.85, armY);
9161
+ ctx.lineTo(x - shHalf * 0.95, hipY + h * 0.02);
9162
+ ctx.moveTo(x + shHalf * 0.85, armY);
9163
+ ctx.lineTo(x + shHalf * 0.95, hipY + h * 0.02);
9164
+ ctx.stroke();
9165
+ }
9166
+ ctx.fillStyle = skin;
9167
+ ctx.fillRect(x - headR * 0.34, headCy + headR * 0.6, headR * 0.68, headR * 0.9);
9168
+ ctx.beginPath();
9169
+ ctx.ellipse(x, headCy, headR * 0.86, headR * 1.05, 0, 0, TAU2);
9170
+ ctx.fill();
9171
+ if (o.rimStrength > 0.01) {
9172
+ ctx.strokeStyle = `rgba(${o.rim[0]},${o.rim[1]},${o.rim[2]},${(0.6 * o.rimStrength).toFixed(3)})`;
9173
+ ctx.lineWidth = Math.max(1, h * 0.02);
9174
+ ctx.beginPath();
9175
+ ctx.moveTo(x - shHalf, shoulderY);
9176
+ ctx.lineTo(x - headR * 0.6, headCy + headR * 0.4);
9177
+ ctx.moveTo(x - headR * 0.7, headCy);
9178
+ ctx.ellipse(x, headCy, headR * 0.84, headR * 1.02, 0, Math.PI * 1.05, Math.PI * 1.55);
9179
+ ctx.stroke();
9180
+ }
9181
+ }
9182
+ function drawBeam(ctx, x0, y0, x1, y1, halfTop, halfBot, tint, strength) {
9183
+ ctx.save();
9184
+ ctx.globalCompositeOperation = "lighter";
9185
+ const g = ctx.createLinearGradient(0, y0, 0, y1);
9186
+ g.addColorStop(0, `rgba(${tint},${strength})`);
9187
+ g.addColorStop(0.5, `rgba(${tint},${(strength * 0.4).toFixed(3)})`);
9188
+ g.addColorStop(1, `rgba(${tint},0)`);
9189
+ ctx.fillStyle = g;
9190
+ ctx.beginPath();
9191
+ ctx.moveTo(x0 - halfTop, y0);
9192
+ ctx.lineTo(x1 - halfBot, y1);
9193
+ ctx.lineTo(x1 + halfBot, y1);
9194
+ ctx.lineTo(x0 + halfTop, y0);
9195
+ ctx.closePath();
9196
+ ctx.fill();
9197
+ ctx.restore();
9198
+ }
9199
+ function drawFixture(ctx, x, y, r, tint) {
9200
+ const g = ctx.createRadialGradient(x, y, 0, x, y, r * 3);
9201
+ g.addColorStop(0, `rgba(${tint},0.9)`);
9202
+ g.addColorStop(0.4, `rgba(${tint},0.35)`);
9203
+ g.addColorStop(1, `rgba(${tint},0)`);
9204
+ ctx.fillStyle = g;
9205
+ ctx.beginPath();
9206
+ ctx.arc(x, y, r * 3, 0, TAU2);
9207
+ ctx.fill();
9208
+ ctx.fillStyle = `rgba(${tint},1)`;
9209
+ ctx.beginPath();
9210
+ ctx.arc(x, y, r, 0, TAU2);
9211
+ ctx.fill();
9212
+ }
8793
9213
  function generateSeatPanorama(seat, focalPoint, neighborSeats) {
8794
9214
  const canvas = document.createElement("canvas");
8795
9215
  canvas.width = W;
@@ -8799,102 +9219,428 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
8799
9219
  const dy = focalPoint.y - seat.y;
8800
9220
  const distU = Math.hypot(dx, dy);
8801
9221
  const distM = Math.max(2, distU * UNIT);
9222
+ const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
9223
+ const scene = classifyScene(seat, focalPoint, neighborSeats);
9224
+ const rand = rng(scene.seed);
9225
+ const baseHue = 210 + rand() * 150;
9226
+ const rigTint = hslToRgb(220 + (rand() - 0.5) * 60, 0.55, 0.82);
9227
+ const rigTintStr = `${rigTint[0]},${rigTint[1]},${rigTint[2]}`;
8802
9228
  const sky = ctx.createLinearGradient(0, 0, 0, H);
8803
- sky.addColorStop(0, "#05070c");
8804
- sky.addColorStop(0.42, "#11172a");
8805
- sky.addColorStop(0.5, "#1a2238");
8806
- sky.addColorStop(0.56, "#10141f");
8807
- sky.addColorStop(1, "#07090f");
9229
+ sky.addColorStop(0, "#080b12");
9230
+ sky.addColorStop(0.42, "#12182c");
9231
+ sky.addColorStop(0.5, "#1c2440");
9232
+ sky.addColorStop(0.56, "#151b2c");
9233
+ sky.addColorStop(0.78, "#171e30");
9234
+ sky.addColorStop(1, "#131829");
8808
9235
  ctx.fillStyle = sky;
8809
9236
  ctx.fillRect(0, 0, W, H);
8810
- const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
8811
- const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
9237
+ const floorAmb = ctx.createLinearGradient(0, pitchToY(-6), 0, H);
9238
+ floorAmb.addColorStop(0, "rgba(30, 37, 56, 0)");
9239
+ floorAmb.addColorStop(0.55, "rgba(30, 37, 56, 0.22)");
9240
+ floorAmb.addColorStop(1, "rgba(22, 28, 44, 0.12)");
9241
+ ctx.fillStyle = floorAmb;
9242
+ ctx.fillRect(0, pitchToY(-6), W, H - pitchToY(-6));
9243
+ const stageBearing = Math.atan2(dx, -dy);
9244
+ const stands = buildStands(seat, neighborSeats, stageBearing, eyeM);
9245
+ const hasRelief = stands.maxDelta >= FLAT_DELTA_M;
9246
+ if (hasRelief) drawStands(ctx, stands.bins, yawToX, pitchToY);
9247
+ const nearGlow = Math.max(0.3, Math.min(1, 1 - (distM - 6) / 60));
8812
9248
  const sightline = stageSightlinePitch(eyeM, distM);
9249
+ const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
8813
9250
  const stageTopPitch = Math.min(45, sightline.topPitch);
8814
9251
  const stageBasePitch = sightline.basePitch;
9252
+ if (scene.mode === "sport") {
9253
+ drawSportScene(ctx, distM, eyeM, scene, nearGlow, baseHue, rigTintStr, rand);
9254
+ } else if (scene.mode === "arena") {
9255
+ drawArenaScene(ctx, distM, eyeM, scene, nearGlow, rigTintStr, rand);
9256
+ } else {
9257
+ drawProsceniumScene(ctx, stageHalfYaw, stageTopPitch, stageBasePitch, nearGlow, baseHue, rigTintStr, rand);
9258
+ }
9259
+ const rigCount = scene.mode === "sport" ? 34 : 24 + Math.floor(rand() * 8);
9260
+ ctx.fillStyle = `rgba(${rigTintStr},0.8)`;
9261
+ for (let i = 0; i < rigCount; i++) {
9262
+ const x = (i + 0.5) / rigCount * W;
9263
+ const y = pitchToY(scene.mode === "sport" ? 58 : 54 + i * 7 % 3 * 6);
9264
+ ctx.beginPath();
9265
+ ctx.arc(x, y, scene.mode === "sport" ? 2.6 : 3.2, 0, TAU2);
9266
+ ctx.fill();
9267
+ }
9268
+ drawAudience(ctx, seat, neighborSeats, focalPoint, stageBearing, eyeM, dx, dy, hasRelief, scene);
9269
+ const poleFade = ctx.createLinearGradient(0, 0, 0, H);
9270
+ poleFade.addColorStop(0, "rgba(0,0,0,0.85)");
9271
+ poleFade.addColorStop(0.12, "rgba(0,0,0,0)");
9272
+ poleFade.addColorStop(0.9, "rgba(0,0,0,0)");
9273
+ poleFade.addColorStop(1, "rgba(0,0,0,0.55)");
9274
+ ctx.fillStyle = poleFade;
9275
+ ctx.fillRect(0, 0, W, H);
9276
+ return { url: canvas.toDataURL("image/jpeg", 0.82), distanceM: Math.round(distM) };
9277
+ }
9278
+ function drawProsceniumScene(ctx, stageHalfYaw, stageTopPitch, stageBasePitch, nearGlow, baseHue, rigTintStr, rand) {
8815
9279
  const sx0 = yawToX(-stageHalfYaw);
8816
9280
  const sx1 = yawToX(stageHalfYaw);
8817
9281
  const sy0 = pitchToY(stageTopPitch);
8818
9282
  const sy1 = pitchToY(stageBasePitch);
8819
- const glow = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, 10, W / 2, (sy0 + sy1) / 2, (sx1 - sx0) * 1.1);
8820
- glow.addColorStop(0, "rgba(129, 140, 248, 0.5)");
8821
- glow.addColorStop(0.5, "rgba(99, 102, 241, 0.16)");
9283
+ const sw = sx1 - sx0;
9284
+ const sh = sy1 - sy0;
9285
+ const archTop = pitchToY(Math.min(50, stageTopPitch + 10));
9286
+ const glowR = sw * (1.1 + (1 - nearGlow) * 0.5);
9287
+ const glow = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, 8, W / 2, (sy0 + sy1) / 2, glowR);
9288
+ glow.addColorStop(0, "rgba(255, 214, 150, 0.34)");
9289
+ glow.addColorStop(0.28, "rgba(150, 150, 230, 0.3)");
9290
+ glow.addColorStop(0.6, "rgba(99, 102, 241, 0.12)");
8822
9291
  glow.addColorStop(1, "rgba(99, 102, 241, 0)");
8823
9292
  ctx.fillStyle = glow;
8824
- ctx.fillRect(sx0 - (sx1 - sx0) * 0.6, sy0 - (sy1 - sy0) * 1.2, (sx1 - sx0) * 2.2, (sy1 - sy0) * 3.4);
8825
- ctx.fillStyle = "#242c44";
8826
- ctx.fillRect(sx0, sy0, sx1 - sx0, sy1 - sy0);
9293
+ ctx.fillRect(sx0 - sw * 0.6, sy0 - sh * 1.2, sw * 2.2, sh * 3.4);
9294
+ const topCol = hslToRgb(baseHue, 0.62, 0.55);
9295
+ const midCol = hslToRgb(baseHue + 40, 0.7, 0.5);
9296
+ const botCol = hslToRgb(baseHue + 70, 0.78, 0.52);
9297
+ ctx.save();
9298
+ ctx.beginPath();
9299
+ ctx.rect(sx0, sy0, sw, sh);
9300
+ ctx.clip();
8827
9301
  const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
8828
- backdrop.addColorStop(0, "#7c5cff");
8829
- backdrop.addColorStop(0.5, "#e1447a");
8830
- backdrop.addColorStop(1, "#f59e0b");
8831
- ctx.globalAlpha = 0.75;
9302
+ backdrop.addColorStop(0, `rgb(${topCol[0]},${topCol[1]},${topCol[2]})`);
9303
+ backdrop.addColorStop(0.5, `rgb(${midCol[0]},${midCol[1]},${midCol[2]})`);
9304
+ backdrop.addColorStop(1, `rgb(${botCol[0]},${botCol[1]},${botCol[2]})`);
9305
+ ctx.globalAlpha = 0.8;
8832
9306
  ctx.fillStyle = backdrop;
8833
- const inset = (sx1 - sx0) * 0.06;
8834
- ctx.fillRect(sx0 + inset, sy0 + (sy1 - sy0) * 0.12, sx1 - sx0 - inset * 2, (sy1 - sy0) * 0.62);
9307
+ ctx.fillRect(sx0, sy0, sw, sh);
8835
9308
  ctx.globalAlpha = 1;
8836
- ctx.fillStyle = "#0d1119";
8837
- ctx.fillRect(sx0, sy1 - (sy1 - sy0) * 0.1, sx1 - sx0, (sy1 - sy0) * 0.1);
8838
- const performerH = (sy1 - sy0) * 0.45;
8839
- ctx.fillStyle = "rgba(8, 10, 16, 0.85)";
8840
- for (const fx of [0.35, 0.5, 0.65]) {
8841
- const px = sx0 + (sx1 - sx0) * fx;
8842
- const py = sy1 - (sy1 - sy0) * 0.12;
8843
- ctx.beginPath();
8844
- ctx.arc(px, py - performerH * 0.82, performerH * 0.16, 0, Math.PI * 2);
8845
- ctx.fill();
8846
- ctx.fillRect(px - performerH * 0.14, py - performerH * 0.68, performerH * 0.28, performerH * 0.68);
9309
+ const fall = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, sw * 0.12, W / 2, (sy0 + sy1) / 2, sw * 0.62);
9310
+ fall.addColorStop(0, "rgba(6,8,14,0)");
9311
+ fall.addColorStop(0.7, "rgba(6,8,14,0.15)");
9312
+ fall.addColorStop(1, "rgba(5,6,11,0.92)");
9313
+ ctx.fillStyle = fall;
9314
+ ctx.fillRect(sx0, sy0, sw, sh);
9315
+ ctx.restore();
9316
+ const perfCount = 3 + Math.floor(rand() * 3);
9317
+ const perfH = sh * 0.46;
9318
+ const floorY = sy1 - sh * 0.08;
9319
+ for (let i = 0; i < perfCount; i++) {
9320
+ const t2 = (i + 1) / (perfCount + 1);
9321
+ const px = sx0 + sw * (0.22 + t2 * 0.56 + (rand() - 0.5) * 0.06);
9322
+ drawPerformer(ctx, px, floorY, perfH * (0.9 + rand() * 0.24), {
9323
+ tone: [8, 10, 16],
9324
+ alpha: 0.9,
9325
+ pose: Math.floor(rand() * 4),
9326
+ rim: hslToRgb(baseHue + 20, 0.5, 0.7),
9327
+ rimStrength: 0.7
9328
+ });
9329
+ }
9330
+ ctx.fillStyle = "#05070d";
9331
+ ctx.fillRect(sx0 - sw * 0.5, archTop, sw * 0.5, sy1 - archTop);
9332
+ ctx.fillRect(sx1, archTop, sw * 0.5, sy1 - archTop);
9333
+ const legBot = sw * 0.055 + 3;
9334
+ const legTop = legBot * 1.35;
9335
+ ctx.fillStyle = "#03050a";
9336
+ ctx.beginPath();
9337
+ ctx.moveTo(sx0, sy1);
9338
+ ctx.lineTo(sx0 - legBot, sy1);
9339
+ ctx.lineTo(sx0 - legTop, archTop);
9340
+ ctx.lineTo(sx0 + sw * 0.012, archTop);
9341
+ ctx.closePath();
9342
+ ctx.fill();
9343
+ ctx.beginPath();
9344
+ ctx.moveTo(sx1, sy1);
9345
+ ctx.lineTo(sx1 + legBot, sy1);
9346
+ ctx.lineTo(sx1 + legTop, archTop);
9347
+ ctx.lineTo(sx1 - sw * 0.012, archTop);
9348
+ ctx.closePath();
9349
+ ctx.fill();
9350
+ ctx.fillRect(sx0 - legTop, archTop, sw + legTop * 2, (sy1 - archTop) * 0.12);
9351
+ const valH = sh * 0.14;
9352
+ const valY = archTop + (sy1 - archTop) * 0.12;
9353
+ ctx.fillStyle = "rgba(4,6,12,0.96)";
9354
+ const scallops = 7;
9355
+ ctx.beginPath();
9356
+ ctx.moveTo(sx0 - legTop, valY);
9357
+ for (let i = 0; i <= scallops; i++) {
9358
+ const xx = sx0 - legTop + (sw + legTop * 2) * i / scallops;
9359
+ ctx.quadraticCurveTo(xx - (sw + legTop * 2) / scallops / 2, valY + valH, xx, valY);
9360
+ }
9361
+ ctx.lineTo(sx1 + legTop, valY);
9362
+ ctx.lineTo(sx1 + legTop, archTop);
9363
+ ctx.lineTo(sx0 - legTop, archTop);
9364
+ ctx.closePath();
9365
+ ctx.fill();
9366
+ ctx.fillStyle = `rgba(${rigTintStr},0.12)`;
9367
+ ctx.fillRect(sx0 - 1.5, valY, 1.5, sy1 - valY);
9368
+ ctx.fillRect(sx1, valY, 1.5, sy1 - valY);
9369
+ ctx.fillStyle = "#0b0f18";
9370
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.05 + 3);
9371
+ const foot = ctx.createLinearGradient(0, sy1 - sh * 0.05, 0, sy1 + sh * 0.12);
9372
+ foot.addColorStop(0, `rgba(255,206,140,${(0.5 * nearGlow).toFixed(3)})`);
9373
+ foot.addColorStop(1, "rgba(255,206,140,0)");
9374
+ ctx.fillStyle = foot;
9375
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.2);
9376
+ const beams = 3 + Math.floor(rand() * 2);
9377
+ for (let i = 0; i < beams; i++) {
9378
+ const t2 = (i + 1) / (beams + 1);
9379
+ const fx = sx0 + sw * (t2 + (rand() - 0.5) * 0.08);
9380
+ const fy = pitchToY(Math.min(62, stageTopPitch + 26));
9381
+ const skew = (rand() - 0.5) * sw * 0.16;
9382
+ drawBeam(ctx, fx, fy, fx + skew, sy1, 4, sw * 0.09, rigTintStr, 0.16 * (0.7 + nearGlow * 0.5));
9383
+ drawFixture(ctx, fx, fy, 2.4, rigTintStr);
9384
+ }
9385
+ }
9386
+ function drawArenaScene(ctx, distM, eyeM, scene, nearGlow, rigTintStr, rand) {
9387
+ const stageR = Math.min(distM * 0.6, Math.max(3, scene.voidRadiusM));
9388
+ const halfYaw = Math.min(55, Math.atan2(stageR, distM) * 180 / Math.PI);
9389
+ const cx = W / 2;
9390
+ const deckRiserM = 1.2;
9391
+ const nearDist = Math.max(distM * 0.45, distM - stageR);
9392
+ const farDist = distM + stageR;
9393
+ const nearTopPitch = Math.atan2(deckRiserM - eyeM, nearDist) * 180 / Math.PI;
9394
+ const farTopPitch = Math.atan2(deckRiserM - eyeM, farDist) * 180 / Math.PI;
9395
+ const nearBasePitch = Math.atan2(-eyeM, nearDist) * 180 / Math.PI;
9396
+ const yNearTop = pitchToY(nearTopPitch);
9397
+ const yFarTop = pitchToY(farTopPitch);
9398
+ const yBase = pitchToY(nearBasePitch);
9399
+ const halfW = yawToX(halfYaw) - cx;
9400
+ const topRy = Math.max(6, (yNearTop - yFarTop) / 2);
9401
+ const topCy = (yNearTop + yFarTop) / 2;
9402
+ const pool = ctx.createRadialGradient(cx, yBase, 4, cx, yBase, halfW * 2.6);
9403
+ pool.addColorStop(0, `rgba(255,206,150,${(0.2 * nearGlow).toFixed(3)})`);
9404
+ pool.addColorStop(0.5, "rgba(150,140,190,0.08)");
9405
+ pool.addColorStop(1, "rgba(40,44,60,0)");
9406
+ ctx.save();
9407
+ ctx.globalCompositeOperation = "lighter";
9408
+ ctx.fillStyle = pool;
9409
+ ctx.fillRect(cx - halfW * 2.6, yFarTop - topRy, halfW * 5.2, yBase - yFarTop + topRy + 40);
9410
+ ctx.restore();
9411
+ ctx.fillStyle = "#0c1019";
9412
+ ctx.beginPath();
9413
+ ctx.moveTo(cx - halfW, topCy);
9414
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, Math.PI, 0, false);
9415
+ ctx.lineTo(cx + halfW, yBase);
9416
+ ctx.ellipse(cx, yBase, halfW, topRy, 0, 0, Math.PI, false);
9417
+ ctx.closePath();
9418
+ ctx.fill();
9419
+ const riser = ctx.createLinearGradient(0, topCy, 0, yBase);
9420
+ riser.addColorStop(0, `rgba(${rigTintStr},0.14)`);
9421
+ riser.addColorStop(1, "rgba(0,0,0,0)");
9422
+ ctx.fillStyle = riser;
9423
+ ctx.fillRect(cx - halfW, topCy, halfW * 2, yBase - topCy);
9424
+ const deckTop = ctx.createLinearGradient(0, topCy - topRy, 0, topCy + topRy);
9425
+ deckTop.addColorStop(0, "#2b3350");
9426
+ deckTop.addColorStop(1, "#161b2c");
9427
+ ctx.fillStyle = deckTop;
9428
+ ctx.beginPath();
9429
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU2);
9430
+ ctx.fill();
9431
+ const deckGlow = ctx.createRadialGradient(cx, topCy, 2, cx, topCy, halfW);
9432
+ deckGlow.addColorStop(0, `rgba(255,214,160,${(0.28 * nearGlow).toFixed(3)})`);
9433
+ deckGlow.addColorStop(1, "rgba(255,214,160,0)");
9434
+ ctx.save();
9435
+ ctx.globalCompositeOperation = "lighter";
9436
+ ctx.fillStyle = deckGlow;
9437
+ ctx.beginPath();
9438
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU2);
9439
+ ctx.fill();
9440
+ ctx.restore();
9441
+ ctx.strokeStyle = `rgba(${rigTintStr},0.7)`;
9442
+ ctx.lineWidth = 2;
9443
+ ctx.beginPath();
9444
+ ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU2);
9445
+ ctx.stroke();
9446
+ const perfCount = 2 + Math.floor(rand() * 3);
9447
+ const perfH = Math.max(24, (yBase - yFarTop) * 1.3);
9448
+ for (let i = 0; i < perfCount; i++) {
9449
+ const t2 = perfCount === 1 ? 0.5 : i / (perfCount - 1);
9450
+ const px = cx + (t2 - 0.5) * 2 * halfW * 0.66;
9451
+ const footY = topCy + topRy * 0.5 * Math.cos((t2 - 0.5) * Math.PI) - topRy * 0.1;
9452
+ drawPerformer(ctx, px, footY, perfH * (0.85 + rand() * 0.3), {
9453
+ tone: [10, 12, 20],
9454
+ alpha: 0.92,
9455
+ pose: Math.floor(rand() * 4),
9456
+ rim: hslToRgb(40, 0.6, 0.72),
9457
+ rimStrength: 0.8
9458
+ });
8847
9459
  }
8848
- ctx.globalAlpha = 0.14;
8849
- ctx.fillStyle = "#c7d2fe";
8850
- for (const fx of [0.3, 0.5, 0.7]) {
8851
- const bx = sx0 + (sx1 - sx0) * fx;
9460
+ const trussY = pitchToY(Math.min(60, farTopPitch + 34));
9461
+ const trussRy = topRy * 1.1 + 6;
9462
+ ctx.strokeStyle = "rgba(60,68,92,0.85)";
9463
+ ctx.lineWidth = 3;
9464
+ ctx.beginPath();
9465
+ ctx.ellipse(cx, trussY, halfW * 1.15, trussRy, 0, 0, TAU2);
9466
+ ctx.stroke();
9467
+ const fixtures = 4 + Math.floor(rand() * 3);
9468
+ for (let i = 0; i < fixtures; i++) {
9469
+ const a = i / fixtures * TAU2;
9470
+ const fxX = cx + Math.cos(a) * halfW * 1.15;
9471
+ const fxY = trussY + Math.sin(a) * trussRy;
9472
+ if (Math.sin(a) > -0.2) {
9473
+ const tx = cx + (rand() - 0.5) * halfW * 0.8;
9474
+ drawBeam(ctx, fxX, fxY, tx, topCy, 3, halfW * 0.32, rigTintStr, 0.14 * (0.7 + nearGlow * 0.4));
9475
+ }
9476
+ drawFixture(ctx, fxX, fxY, 2.2, rigTintStr);
9477
+ }
9478
+ }
9479
+ function drawSportScene(ctx, distM, eyeM, scene, _nearGlow, baseHue, rigTintStr, rand) {
9480
+ const cx = W / 2;
9481
+ const surfR = Math.max(10, scene.voidRadiusM);
9482
+ const nearEdge = Math.max(2, distM - surfR);
9483
+ const farEdge = distM + surfR;
9484
+ const halfYaw = Math.min(78, Math.atan2(surfR * 1.15, distM) * 180 / Math.PI);
9485
+ const nearPitch = Math.atan2(-eyeM, nearEdge) * 180 / Math.PI;
9486
+ const farPitch = Math.atan2(-eyeM, farEdge) * 180 / Math.PI;
9487
+ const yNear = pitchToY(nearPitch);
9488
+ const yFar = pitchToY(farPitch);
9489
+ const halfW = yawToX(halfYaw) - cx;
9490
+ const cy = (yNear + yFar) / 2;
9491
+ const ry = Math.max(10, (yNear - yFar) / 2);
9492
+ const surfCol = ctx.createRadialGradient(cx, cy, 4, cx, cy, halfW);
9493
+ surfCol.addColorStop(0, "#e8f0fb");
9494
+ surfCol.addColorStop(0.7, "#b9c9e2");
9495
+ surfCol.addColorStop(1, "#8fa3c4");
9496
+ ctx.save();
9497
+ ctx.beginPath();
9498
+ ctx.ellipse(cx, cy, halfW, ry, 0, 0, TAU2);
9499
+ ctx.clip();
9500
+ ctx.fillStyle = surfCol;
9501
+ ctx.fillRect(cx - halfW, cy - ry, halfW * 2, ry * 2);
9502
+ ctx.strokeStyle = "rgba(90,120,170,0.5)";
9503
+ ctx.lineWidth = 3;
9504
+ ctx.beginPath();
9505
+ ctx.moveTo(cx, cy - ry);
9506
+ ctx.lineTo(cx, cy + ry);
9507
+ ctx.moveTo(cx - halfW * 0.5, cy - ry);
9508
+ ctx.lineTo(cx - halfW * 0.5, cy + ry);
9509
+ ctx.moveTo(cx + halfW * 0.5, cy - ry);
9510
+ ctx.lineTo(cx + halfW * 0.5, cy + ry);
9511
+ ctx.stroke();
9512
+ ctx.strokeStyle = "rgba(180,90,110,0.45)";
9513
+ ctx.beginPath();
9514
+ ctx.ellipse(cx, cy, ry * 0.9, ry * 0.5, 0, 0, TAU2);
9515
+ ctx.stroke();
9516
+ ctx.restore();
9517
+ ctx.strokeStyle = "rgba(210,225,245,0.6)";
9518
+ ctx.lineWidth = 2.5;
9519
+ ctx.beginPath();
9520
+ ctx.ellipse(cx, cy, halfW, ry, 0, 0, TAU2);
9521
+ ctx.stroke();
9522
+ ctx.fillStyle = "rgba(20,26,40,0.8)";
9523
+ for (let i = 0; i < 5; i++) {
9524
+ const px = cx + (rand() - 0.5) * halfW * 1.4;
9525
+ const py = cy + (rand() - 0.5) * ry * 1.1;
9526
+ const s = Math.max(2, ry * 0.06);
8852
9527
  ctx.beginPath();
8853
- ctx.moveTo(bx - 8, pitchToY(Math.min(60, stageTopPitch + 25)));
8854
- ctx.lineTo(bx - (sx1 - sx0) * 0.09, sy1);
8855
- ctx.lineTo(bx + (sx1 - sx0) * 0.09, sy1);
8856
- ctx.lineTo(bx + 8, pitchToY(Math.min(60, stageTopPitch + 25)));
8857
- ctx.closePath();
9528
+ ctx.ellipse(px, py, s * 0.6, s, 0, 0, TAU2);
8858
9529
  ctx.fill();
8859
9530
  }
8860
- ctx.globalAlpha = 1;
8861
- ctx.fillStyle = "rgba(199, 210, 254, 0.8)";
8862
- for (let i = 0; i < 26; i++) {
8863
- const x = i / 26 * W;
8864
- const y = pitchToY(55 + i % 3 * 6);
9531
+ const wash = ctx.createRadialGradient(cx, cy, 4, cx, cy, halfW * 1.4);
9532
+ wash.addColorStop(0, "rgba(200,220,255,0.12)");
9533
+ wash.addColorStop(1, "rgba(200,220,255,0)");
9534
+ ctx.save();
9535
+ ctx.globalCompositeOperation = "lighter";
9536
+ ctx.fillStyle = wash;
9537
+ ctx.fillRect(cx - halfW * 1.4, yFar - ry, halfW * 2.8, yNear - yFar + ry * 2);
9538
+ ctx.restore();
9539
+ const sbY = pitchToY(Math.min(66, farPitch + 60));
9540
+ const sbW = Math.max(80, halfW * 0.7);
9541
+ const sbH = sbW * 0.5;
9542
+ const sbGlow = ctx.createRadialGradient(cx, sbY, 4, cx, sbY, sbW * 1.6);
9543
+ sbGlow.addColorStop(0, `rgba(${rigTintStr},0.3)`);
9544
+ sbGlow.addColorStop(1, `rgba(${rigTintStr},0)`);
9545
+ ctx.save();
9546
+ ctx.globalCompositeOperation = "lighter";
9547
+ ctx.fillStyle = sbGlow;
9548
+ ctx.fillRect(cx - sbW * 1.6, sbY - sbW * 1.2, sbW * 3.2, sbW * 2.4);
9549
+ ctx.restore();
9550
+ ctx.fillStyle = "#080b12";
9551
+ ctx.beginPath();
9552
+ ctx.moveTo(cx - sbW / 2, sbY - sbH / 2);
9553
+ ctx.lineTo(cx + sbW / 2, sbY - sbH / 2);
9554
+ ctx.lineTo(cx + sbW * 0.6, sbY);
9555
+ ctx.lineTo(cx + sbW / 2, sbY + sbH / 2);
9556
+ ctx.lineTo(cx - sbW / 2, sbY + sbH / 2);
9557
+ ctx.lineTo(cx - sbW * 0.6, sbY);
9558
+ ctx.closePath();
9559
+ ctx.fill();
9560
+ const screenHue = baseHue;
9561
+ for (let i = 0; i < 3; i++) {
9562
+ const scol = hslToRgb(screenHue + i * 30, 0.5, 0.5);
9563
+ ctx.fillStyle = `rgba(${scol[0]},${scol[1]},${scol[2]},0.75)`;
9564
+ const swid = sbW * 0.26;
9565
+ ctx.fillRect(cx - sbW * 0.42 + i * (swid + sbW * 0.06), sbY - sbH * 0.32, swid, sbH * 0.64);
9566
+ }
9567
+ ctx.fillStyle = `rgba(${rigTintStr},0.9)`;
9568
+ for (let i = 0; i < 8; i++) {
9569
+ const lx = cx - sbW * 0.6 + sbW * 1.2 * i / 7;
8865
9570
  ctx.beginPath();
8866
- ctx.arc(x, y, 3.2, 0, Math.PI * 2);
9571
+ ctx.arc(lx, sbY + sbH * 0.62, 2.4, 0, TAU2);
8867
9572
  ctx.fill();
8868
9573
  }
8869
- const stageBearing = Math.atan2(dx, -dy);
8870
- ctx.fillStyle = "rgba(16, 20, 30, 0.95)";
9574
+ }
9575
+ function drawAudience(ctx, seat, neighborSeats, _focalPoint, stageBearing, eyeM, dx, dy, hasRelief, scene) {
9576
+ const own = seat.sectionId;
9577
+ let seatAhead = false;
9578
+ const figs = [];
8871
9579
  for (const other of neighborSeats) {
8872
9580
  if (other.id === seat.id) continue;
8873
9581
  const ox = other.x - seat.x;
8874
9582
  const oy = other.y - seat.y;
8875
9583
  const d = Math.hypot(ox, oy) * UNIT;
8876
- if (d < 0.3 || d > 14) continue;
9584
+ if (own && other.sectionId === own && d < 3 && ox * dx + oy * dy > 0) seatAhead = true;
9585
+ if (d < 0.3 || d > 17) continue;
8877
9586
  const bearing = Math.atan2(ox, -oy) - stageBearing;
8878
9587
  const yaw = (bearing * 180 / Math.PI + 540) % 360 - 180;
8879
- const headPitch = -Math.atan2(0.35, d) * 180 / Math.PI;
8880
- const headR = Math.min(46, 260 / (d / UNIT / 24 + 1.2) / 4);
8881
- const hx = yawToX(yaw);
8882
- const hy = pitchToY(headPitch);
8883
- ctx.beginPath();
8884
- ctx.arc(hx, hy, headR, 0, Math.PI * 2);
8885
- ctx.fill();
8886
- ctx.beginPath();
8887
- ctx.ellipse(hx, hy + headR * 1.4, headR * 1.7, headR * 0.9, 0, Math.PI, 0, true);
8888
- ctx.fill();
9588
+ const rise = hasRelief ? (other.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - eyeM : 0;
9589
+ const headPitch = Math.atan2(rise - 0.28, d) * 180 / Math.PI;
9590
+ if (headPitch > 10 && d > 5) continue;
9591
+ let hh = 0;
9592
+ for (let k = 0; k < other.id.length; k++) hh = hh * 31 + other.id.charCodeAt(k) & 65535;
9593
+ const jitter = 0.9 + (hh & 15) / 40;
9594
+ const r = Math.min(78, Math.atan2(0.16, d) * 180 / Math.PI * PX_PER_DEG * jitter);
9595
+ if (r < 2) continue;
9596
+ figs.push({ hx: yawToX(yaw), hy: pitchToY(headPitch), r, d, hash: hh });
9597
+ }
9598
+ figs.sort((a, b) => b.d - a.d);
9599
+ const CLOTHES = [[22, 26, 40], [28, 27, 42], [24, 31, 46], [31, 30, 47], [20, 24, 36]];
9600
+ for (const f of figs) {
9601
+ const alpha = f.d < 12 ? 0.95 : Math.max(0.66, 0.95 - (f.d - 12) / 18);
9602
+ const lift = Math.round((1 - Math.min(1, f.d / 16)) * 10);
9603
+ const headTone = [14 + (f.hash >> 4 & 6) + lift, 17 + (f.hash >> 4 & 6) + lift, 25 + (f.hash >> 4 & 6) + lift];
9604
+ const clo0 = CLOTHES[f.hash % CLOTHES.length];
9605
+ const clothing = [clo0[0] + lift, clo0[1] + lift, clo0[2] + lift];
9606
+ if (f.r >= 13) {
9607
+ const rim = f.d < 9 ? 0.16 * (1 - f.d / 9) + (scene.mode === "proscenium" ? 0.06 : 0) : 0;
9608
+ drawSeated(ctx, f.hx, f.hy, f.r, {
9609
+ tone: headTone,
9610
+ clothing,
9611
+ alpha,
9612
+ tilt: ((f.hash >> 2 & 7) - 3.5) * 0.018,
9613
+ // ±~7°
9614
+ shoulderK: 1.72 + (f.hash >> 5 & 7) / 22,
9615
+ // ~1.72..2.04
9616
+ hair: f.hash % HAIR_KINDS,
9617
+ rim
9618
+ });
9619
+ } else if (f.r >= 5.5) {
9620
+ drawSeatedMid(ctx, f.hx, f.hy, f.r, clothing, headTone, alpha);
9621
+ } else {
9622
+ ctx.fillStyle = `rgba(${headTone[0]},${headTone[1]},${headTone[2]},${alpha.toFixed(3)})`;
9623
+ ctx.beginPath();
9624
+ ctx.arc(f.hx, f.hy, f.r, 0, TAU2);
9625
+ ctx.fill();
9626
+ ctx.beginPath();
9627
+ ctx.ellipse(f.hx, f.hy + f.r * 1.4, f.r * 1.85, f.r * 0.95, 0, Math.PI, 0, true);
9628
+ ctx.fill();
9629
+ }
9630
+ }
9631
+ if (seatAhead) {
9632
+ const railTop = pitchToY(-34);
9633
+ ctx.fillStyle = "rgba(6, 8, 14, 0.92)";
9634
+ ctx.fillRect(0, railTop, W, H - railTop);
9635
+ ctx.fillStyle = "rgba(24, 30, 46, 0.9)";
9636
+ const humps = 16;
9637
+ for (let i = 0; i < humps; i++) {
9638
+ const cx = (i + 0.5) / humps * W;
9639
+ ctx.beginPath();
9640
+ ctx.ellipse(cx, railTop + 6, W / humps * 0.42, 22, 0, Math.PI, 0, true);
9641
+ ctx.fill();
9642
+ }
8889
9643
  }
8890
- const poleFade = ctx.createLinearGradient(0, 0, 0, H);
8891
- poleFade.addColorStop(0, "rgba(0,0,0,0.85)");
8892
- poleFade.addColorStop(0.12, "rgba(0,0,0,0)");
8893
- poleFade.addColorStop(0.88, "rgba(0,0,0,0)");
8894
- poleFade.addColorStop(1, "rgba(0,0,0,0.85)");
8895
- ctx.fillStyle = poleFade;
8896
- ctx.fillRect(0, 0, W, H);
8897
- return { url: canvas.toDataURL("image/jpeg", 0.82), distanceM: Math.round(distM) };
8898
9644
  }
8899
9645
  var TW = 480;
8900
9646
  var TH = 270;
@@ -8910,12 +9656,16 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
8910
9656
  const dx = focalPoint.x - seat.x;
8911
9657
  const dy = focalPoint.y - seat.y;
8912
9658
  const distM = Math.max(2, Math.hypot(dx, dy) * UNIT);
9659
+ const rand = rng(fnv1a(`${seat.id}|${Math.round(focalPoint.x)},${Math.round(focalPoint.y)}`));
9660
+ const baseHue = 210 + rand() * 150;
9661
+ const rigTint = hslToRgb(220 + (rand() - 0.5) * 60, 0.55, 0.82);
9662
+ const rigTintStr = `${rigTint[0]},${rigTint[1]},${rigTint[2]}`;
8913
9663
  const sky = ctx.createLinearGradient(0, 0, 0, TH);
8914
- sky.addColorStop(0, "#05070c");
8915
- sky.addColorStop(0.44, "#141b30");
8916
- sky.addColorStop(0.5, "#1b2440");
8917
- sky.addColorStop(0.58, "#10141f");
8918
- sky.addColorStop(1, "#070910");
9664
+ sky.addColorStop(0, "#080b12");
9665
+ sky.addColorStop(0.44, "#151c32");
9666
+ sky.addColorStop(0.5, "#1d2644");
9667
+ sky.addColorStop(0.58, "#161c2e");
9668
+ sky.addColorStop(1, "#131829");
8919
9669
  ctx.fillStyle = sky;
8920
9670
  ctx.fillRect(0, 0, TW, TH);
8921
9671
  const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
@@ -8929,58 +9679,102 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
8929
9679
  const sy1 = pitchToTY(stageBasePitch);
8930
9680
  const sw = sx1 - sx0;
8931
9681
  const sh = sy1 - sy0;
8932
- const glow = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, 4, TW / 2, (sy0 + sy1) / 2, sw * 1.1);
8933
- glow.addColorStop(0, "rgba(129,140,248,0.5)");
8934
- glow.addColorStop(0.5, "rgba(99,102,241,0.15)");
9682
+ const nearGlow = Math.max(0.35, Math.min(1, 1 - (distM - 6) / 60));
9683
+ const archTop = pitchToTY(Math.min(HALF_VFOV - 1, stageTopPitch + 8));
9684
+ const glow = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, 4, TW / 2, (sy0 + sy1) / 2, sw * 1.15);
9685
+ glow.addColorStop(0, "rgba(255,214,150,0.34)");
9686
+ glow.addColorStop(0.28, "rgba(150,150,230,0.3)");
9687
+ glow.addColorStop(0.6, "rgba(99,102,241,0.12)");
8935
9688
  glow.addColorStop(1, "rgba(99,102,241,0)");
8936
9689
  ctx.fillStyle = glow;
8937
9690
  ctx.fillRect(sx0 - sw * 0.6, sy0 - sh * 1.2, sw * 2.2, sh * 3.4);
8938
- ctx.fillStyle = "#242c44";
8939
- ctx.fillRect(sx0, sy0, sw, sh);
9691
+ const topCol = hslToRgb(baseHue, 0.62, 0.55);
9692
+ const midCol = hslToRgb(baseHue + 40, 0.7, 0.5);
9693
+ const botCol = hslToRgb(baseHue + 70, 0.78, 0.52);
9694
+ ctx.save();
9695
+ ctx.beginPath();
9696
+ ctx.rect(sx0, sy0, sw, sh);
9697
+ ctx.clip();
8940
9698
  const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
8941
- backdrop.addColorStop(0, "#7c5cff");
8942
- backdrop.addColorStop(0.5, "#e1447a");
8943
- backdrop.addColorStop(1, "#f59e0b");
8944
- ctx.globalAlpha = 0.78;
9699
+ backdrop.addColorStop(0, `rgb(${topCol[0]},${topCol[1]},${topCol[2]})`);
9700
+ backdrop.addColorStop(0.5, `rgb(${midCol[0]},${midCol[1]},${midCol[2]})`);
9701
+ backdrop.addColorStop(1, `rgb(${botCol[0]},${botCol[1]},${botCol[2]})`);
9702
+ ctx.globalAlpha = 0.8;
8945
9703
  ctx.fillStyle = backdrop;
8946
- const inset = sw * 0.06;
8947
- ctx.fillRect(sx0 + inset, sy0 + sh * 0.12, sw - inset * 2, sh * 0.62);
9704
+ ctx.fillRect(sx0, sy0, sw, sh);
8948
9705
  ctx.globalAlpha = 1;
8949
- ctx.fillStyle = "#0d1119";
8950
- ctx.fillRect(sx0, sy1 - sh * 0.1, sw, sh * 0.1);
8951
- const ph = sh * 0.45;
8952
- ctx.fillStyle = "rgba(8,10,16,0.85)";
8953
- for (const fx of [0.35, 0.5, 0.65]) {
8954
- const px = sx0 + sw * fx;
8955
- const py = sy1 - sh * 0.12;
8956
- ctx.beginPath();
8957
- ctx.arc(px, py - ph * 0.82, ph * 0.16, 0, Math.PI * 2);
8958
- ctx.fill();
8959
- ctx.fillRect(px - ph * 0.14, py - ph * 0.68, ph * 0.28, ph * 0.68);
8960
- }
8961
- ctx.globalAlpha = 0.15;
8962
- ctx.fillStyle = "#c7d2fe";
8963
- for (const fx of [0.3, 0.5, 0.7]) {
8964
- const bx = sx0 + sw * fx;
8965
- ctx.beginPath();
8966
- ctx.moveTo(bx - 5, pitchToTY(Math.min(HALF_VFOV, stageTopPitch + 18)));
8967
- ctx.lineTo(bx - sw * 0.09, sy1);
8968
- ctx.lineTo(bx + sw * 0.09, sy1);
8969
- ctx.lineTo(bx + 5, pitchToTY(Math.min(HALF_VFOV, stageTopPitch + 18)));
8970
- ctx.closePath();
8971
- ctx.fill();
9706
+ const fall = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, sw * 0.12, TW / 2, (sy0 + sy1) / 2, sw * 0.6);
9707
+ fall.addColorStop(0, "rgba(6,8,14,0)");
9708
+ fall.addColorStop(0.7, "rgba(6,8,14,0.15)");
9709
+ fall.addColorStop(1, "rgba(5,6,11,0.9)");
9710
+ ctx.fillStyle = fall;
9711
+ ctx.fillRect(sx0, sy0, sw, sh);
9712
+ ctx.restore();
9713
+ const perfCount = 3;
9714
+ const floorY = sy1 - sh * 0.08;
9715
+ for (let i = 0; i < perfCount; i++) {
9716
+ const t2 = (i + 1) / (perfCount + 1);
9717
+ drawPerformer(ctx, sx0 + sw * (0.24 + t2 * 0.52), floorY, sh * 0.46 * (0.9 + rand() * 0.24), {
9718
+ tone: [8, 10, 16],
9719
+ alpha: 0.9,
9720
+ pose: Math.floor(rand() * 4),
9721
+ rim: hslToRgb(baseHue + 20, 0.5, 0.7),
9722
+ rimStrength: 0.7
9723
+ });
8972
9724
  }
8973
- ctx.globalAlpha = 1;
8974
- ctx.fillStyle = "rgba(199,210,254,0.75)";
9725
+ ctx.fillStyle = "#05070d";
9726
+ ctx.fillRect(sx0 - sw * 0.5, archTop, sw * 0.5, sy1 - archTop);
9727
+ ctx.fillRect(sx1, archTop, sw * 0.5, sy1 - archTop);
9728
+ const legBot = sw * 0.05 + 2;
9729
+ const legTop = legBot * 1.35;
9730
+ ctx.fillStyle = "#03050a";
9731
+ ctx.beginPath();
9732
+ ctx.moveTo(sx0, sy1);
9733
+ ctx.lineTo(sx0 - legBot, sy1);
9734
+ ctx.lineTo(sx0 - legTop, archTop);
9735
+ ctx.lineTo(sx0 + sw * 0.012, archTop);
9736
+ ctx.closePath();
9737
+ ctx.fill();
9738
+ ctx.beginPath();
9739
+ ctx.moveTo(sx1, sy1);
9740
+ ctx.lineTo(sx1 + legBot, sy1);
9741
+ ctx.lineTo(sx1 + legTop, archTop);
9742
+ ctx.lineTo(sx1 - sw * 0.012, archTop);
9743
+ ctx.closePath();
9744
+ ctx.fill();
9745
+ ctx.fillRect(sx0 - legTop, archTop, sw + legTop * 2, (sy1 - archTop) * 0.13);
9746
+ ctx.fillStyle = `rgba(${rigTintStr},0.12)`;
9747
+ ctx.fillRect(sx0 - 1.2, archTop, 1.2, sy1 - archTop);
9748
+ ctx.fillRect(sx1, archTop, 1.2, sy1 - archTop);
9749
+ ctx.fillStyle = "#0b0f18";
9750
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.05 + 2);
9751
+ const foot = ctx.createLinearGradient(0, sy1 - sh * 0.05, 0, sy1 + sh * 0.14);
9752
+ foot.addColorStop(0, `rgba(255,206,140,${(0.5 * nearGlow).toFixed(3)})`);
9753
+ foot.addColorStop(1, "rgba(255,206,140,0)");
9754
+ ctx.fillStyle = foot;
9755
+ ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.22);
9756
+ for (let i = 0; i < 3; i++) {
9757
+ const t2 = (i + 1) / 4;
9758
+ const fx = sx0 + sw * t2;
9759
+ const fy = pitchToTY(Math.min(HALF_VFOV - 1, stageTopPitch + 20));
9760
+ drawBeam(ctx, fx, fy, fx + (rand() - 0.5) * sw * 0.14, sy1, 3, sw * 0.09, rigTintStr, 0.17 * nearGlow);
9761
+ drawFixture(ctx, fx, fy, 1.8, rigTintStr);
9762
+ }
9763
+ ctx.fillStyle = `rgba(${rigTintStr},0.75)`;
8975
9764
  for (let i = 0; i < 8; i++) {
8976
9765
  ctx.beginPath();
8977
- ctx.arc((i + 0.5) / 8 * TW, pitchToTY(HALF_VFOV - 4 - i % 2 * 3), 2.2, 0, Math.PI * 2);
9766
+ ctx.arc((i + 0.5) / 8 * TW, pitchToTY(HALF_VFOV - 4 - i % 2 * 3), 2.2, 0, TAU2);
8978
9767
  ctx.fill();
8979
9768
  }
8980
9769
  ctx.fillStyle = "rgba(10,13,20,0.9)";
8981
9770
  ctx.fillRect(0, TH - 26, TW, 26);
8982
9771
  ctx.fillStyle = "rgba(30,37,54,0.9)";
8983
- for (let i = 0; i < 9; i++) ctx.fillRect(i * (TW / 9) + 6, TH - 22, TW / 9 - 12, 14);
9772
+ for (let i = 0; i < 9; i++) {
9773
+ const bx = i * (TW / 9) + TW / 18;
9774
+ ctx.beginPath();
9775
+ ctx.ellipse(bx, TH - 22, TW / 9 * 0.4, 15, 0, Math.PI, 0, true);
9776
+ ctx.fill();
9777
+ }
8984
9778
  return { url: canvas.toDataURL("image/jpeg", 0.8), distanceM: Math.round(distM) };
8985
9779
  }
8986
9780