@seatlayer/core 0.28.4 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -22
- package/dist/chunk-FGS67GT3.js +1252 -0
- package/dist/chunk-FGS67GT3.js.map +1 -0
- package/dist/index.cjs +1415 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -1459
- package/dist/index.d.ts +36 -1459
- package/dist/index.js +1091 -972
- package/dist/index.js.map +1 -1
- package/dist/types-CG2pEDoI.d.cts +1608 -0
- package/dist/types-CG2pEDoI.d.ts +1608 -0
- package/dist/view3d/index.cjs +4188 -0
- package/dist/view3d/index.cjs.map +1 -0
- package/dist/view3d/index.d.cts +385 -0
- package/dist/view3d/index.d.ts +385 -0
- package/dist/view3d/index.js +3720 -0
- package/dist/view3d/index.js.map +1 -0
- package/package.json +25 -3
package/dist/index.js
CHANGED
|
@@ -1,867 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"sound",
|
|
40
|
-
"concession",
|
|
41
|
-
"coat",
|
|
42
|
-
"wall"
|
|
43
|
-
];
|
|
44
|
-
var SURROUNDINGS_SHAPE_ROLE_SET = new Set(SURROUNDINGS_SHAPE_ROLES);
|
|
45
|
-
function layerOf(obj) {
|
|
46
|
-
switch (obj.type) {
|
|
47
|
-
case "row":
|
|
48
|
-
case "table":
|
|
49
|
-
case "gaArea":
|
|
50
|
-
case "booth":
|
|
51
|
-
case "section":
|
|
52
|
-
return "interactive";
|
|
53
|
-
// Raster décor follows its explicit authored z-layer. Absence retains the
|
|
54
|
-
// legacy Background default; bitmap content is never guessed semantically.
|
|
55
|
-
case "decorImage":
|
|
56
|
-
return obj.layer === "foreground" ? "foreground" : "background";
|
|
57
|
-
// Source-backed focal geometry and curated venue landmarks help an author
|
|
58
|
-
// orient around the sellable plan. A stage and an ordinary authored shape
|
|
59
|
-
// remain Background even when they carry an arbitrary/custom role.
|
|
60
|
-
case "shape":
|
|
61
|
-
return obj.role && SURROUNDINGS_SHAPE_ROLE_SET.has(obj.role) ? "surroundings" : "background";
|
|
62
|
-
// Free text — including semantic icon text — is background furniture.
|
|
63
|
-
case "text":
|
|
64
|
-
return "background";
|
|
65
|
-
default:
|
|
66
|
-
return "interactive";
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
var CHART_STORAGE_KEY = "seatmap.chart";
|
|
70
|
-
|
|
71
|
-
// src/core/complexGeometry.ts
|
|
72
|
-
function cubicPoint(path, t2) {
|
|
73
|
-
const u = 1 - t2;
|
|
74
|
-
const a = u * u * u;
|
|
75
|
-
const b = 3 * u * u * t2;
|
|
76
|
-
const c = 3 * u * t2 * t2;
|
|
77
|
-
const d = t2 * t2 * t2;
|
|
78
|
-
return {
|
|
79
|
-
x: a * path.start.x + b * path.control1.x + c * path.control2.x + d * path.end.x,
|
|
80
|
-
y: a * path.start.y + b * path.control1.y + c * path.control2.y + d * path.end.y
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function distributeAlongCubic(path, count, resolution = 192) {
|
|
84
|
-
if (!Number.isInteger(count) || count < 1) throw new Error("Path point count must be a positive integer");
|
|
85
|
-
if (count === 1) return [cubicPoint(path, 0.5)];
|
|
86
|
-
const samples = Array.from({ length: resolution + 1 }, (_, index) => cubicPoint(path, index / resolution));
|
|
87
|
-
const lengths = new Float64Array(samples.length);
|
|
88
|
-
for (let index = 1; index < samples.length; index += 1) {
|
|
89
|
-
const dx = samples[index].x - samples[index - 1].x;
|
|
90
|
-
const dy = samples[index].y - samples[index - 1].y;
|
|
91
|
-
lengths[index] = lengths[index - 1] + Math.hypot(dx, dy);
|
|
92
|
-
}
|
|
93
|
-
const total = lengths[lengths.length - 1];
|
|
94
|
-
if (total <= 1e-9) return Array.from({ length: count }, () => ({ ...path.start }));
|
|
95
|
-
const output = [];
|
|
96
|
-
let segment = 1;
|
|
97
|
-
for (let index = 0; index < count; index += 1) {
|
|
98
|
-
const target = total * index / (count - 1);
|
|
99
|
-
while (segment < lengths.length - 1 && lengths[segment] < target) segment += 1;
|
|
100
|
-
const before = lengths[segment - 1];
|
|
101
|
-
const after = lengths[segment];
|
|
102
|
-
const ratio = after === before ? 0 : (target - before) / (after - before);
|
|
103
|
-
output.push({
|
|
104
|
-
x: samples[segment - 1].x + (samples[segment].x - samples[segment - 1].x) * ratio,
|
|
105
|
-
y: samples[segment - 1].y + (samples[segment].y - samples[segment - 1].y) * ratio
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
return output;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// src/core/sectionPath.ts
|
|
112
|
-
var TAU = Math.PI * 2;
|
|
113
|
-
function translateSectionOutlinePath(path, dx, dy) {
|
|
114
|
-
const translate = (point) => ({ x: point.x + dx, y: point.y + dy });
|
|
115
|
-
return transformSectionOutlinePath(path, translate);
|
|
116
|
-
}
|
|
117
|
-
function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected = false) {
|
|
118
|
-
return {
|
|
119
|
-
...path,
|
|
120
|
-
start: transform(path.start),
|
|
121
|
-
segments: path.segments.map((segment) => segment.kind === "line" ? { ...segment, end: transform(segment.end) } : segment.kind === "arc" ? {
|
|
122
|
-
...segment,
|
|
123
|
-
center: transform(segment.center),
|
|
124
|
-
radius: segment.radius * Math.abs(radiusScale),
|
|
125
|
-
clockwise: reflected ? !segment.clockwise : segment.clockwise,
|
|
126
|
-
end: transform(segment.end)
|
|
127
|
-
} : {
|
|
128
|
-
...segment,
|
|
129
|
-
control1: transform(segment.control1),
|
|
130
|
-
control2: transform(segment.control2),
|
|
131
|
-
end: transform(segment.end)
|
|
132
|
-
})
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// src/core/labeling.ts
|
|
137
|
-
var FULL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
138
|
-
var LOWER_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
|
|
139
|
-
function toBijectiveBase(index, alphabet) {
|
|
140
|
-
const base = alphabet.length;
|
|
141
|
-
let n = index + 1;
|
|
142
|
-
let out = "";
|
|
143
|
-
while (n > 0) {
|
|
144
|
-
n -= 1;
|
|
145
|
-
const rem = n % base;
|
|
146
|
-
out = alphabet[rem] + out;
|
|
147
|
-
n = Math.floor(n / base);
|
|
148
|
-
}
|
|
149
|
-
return out;
|
|
150
|
-
}
|
|
151
|
-
function toLetters(value, lower = false) {
|
|
152
|
-
const alphabet = lower ? LOWER_ALPHABET : FULL_ALPHABET;
|
|
153
|
-
return toBijectiveBase(Math.max(0, Math.floor(value) - 1), alphabet);
|
|
154
|
-
}
|
|
155
|
-
var ROMAN_TABLE = [
|
|
156
|
-
[1e3, "M"],
|
|
157
|
-
[900, "CM"],
|
|
158
|
-
[500, "D"],
|
|
159
|
-
[400, "CD"],
|
|
160
|
-
[100, "C"],
|
|
161
|
-
[90, "XC"],
|
|
162
|
-
[50, "L"],
|
|
163
|
-
[40, "XL"],
|
|
164
|
-
[10, "X"],
|
|
165
|
-
[9, "IX"],
|
|
166
|
-
[5, "V"],
|
|
167
|
-
[4, "IV"],
|
|
168
|
-
[1, "I"]
|
|
169
|
-
];
|
|
170
|
-
function toRoman(value) {
|
|
171
|
-
if (!Number.isFinite(value) || value <= 0) return String(value);
|
|
172
|
-
let remaining = Math.floor(value);
|
|
173
|
-
let out = "";
|
|
174
|
-
for (const [n, sym] of ROMAN_TABLE) {
|
|
175
|
-
while (remaining >= n) {
|
|
176
|
-
out += sym;
|
|
177
|
-
remaining -= n;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return out;
|
|
181
|
-
}
|
|
182
|
-
|
|
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
|
-
// src/core/layout.ts
|
|
228
|
-
function overrideAccessibility(o) {
|
|
229
|
-
if (!o) return [];
|
|
230
|
-
if (o.accessibility && o.accessibility.length) {
|
|
231
|
-
return o.wheelchairSpaceType && !o.accessibility.includes("wheelchair") ? ["wheelchair", ...o.accessibility] : o.accessibility;
|
|
232
|
-
}
|
|
233
|
-
if (o.wheelchairSpaceType) return ["wheelchair"];
|
|
234
|
-
return o.accessible ? ["wheelchair"] : [];
|
|
235
|
-
}
|
|
236
|
-
var DEG = Math.PI / 180;
|
|
237
|
-
var SEAT_R = 9;
|
|
238
|
-
var TABLE_SEAT_OFFSET = 16;
|
|
239
|
-
function place(lx, ly, deg, origin) {
|
|
240
|
-
const a = deg * DEG;
|
|
241
|
-
const cos = Math.cos(a);
|
|
242
|
-
const sin = Math.sin(a);
|
|
243
|
-
return {
|
|
244
|
-
x: origin.x + lx * cos - ly * sin,
|
|
245
|
-
y: origin.y + lx * sin + ly * cos
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
function rowSeatPositions(row) {
|
|
249
|
-
const { seatCount, seatSpacing, curve, rotation, origin } = row;
|
|
250
|
-
const out = [];
|
|
251
|
-
if (row.path) return distributeAlongCubic(row.path, seatCount);
|
|
252
|
-
if (seatCount <= 1) {
|
|
253
|
-
if (seatCount === 1) out.push({ x: origin.x, y: origin.y });
|
|
254
|
-
return out;
|
|
255
|
-
}
|
|
256
|
-
if (curve === 0) {
|
|
257
|
-
for (let i = 0; i < seatCount; i++) out.push(place(i * seatSpacing, 0, rotation, origin));
|
|
258
|
-
return out;
|
|
259
|
-
}
|
|
260
|
-
const arcStep = curve / (seatCount - 1) * DEG;
|
|
261
|
-
const radius = seatSpacing / (2 * Math.sin(Math.abs(arcStep) / 2));
|
|
262
|
-
for (let i = 0; i < seatCount; i++) {
|
|
263
|
-
const phi = i * arcStep;
|
|
264
|
-
const lx = radius * Math.sin(phi);
|
|
265
|
-
const ly = -radius + radius * Math.cos(phi);
|
|
266
|
-
out.push(place(lx, ly, rotation, origin));
|
|
267
|
-
}
|
|
268
|
-
return out;
|
|
269
|
-
}
|
|
270
|
-
function overrideMap(row) {
|
|
271
|
-
const m = /* @__PURE__ */ new Map();
|
|
272
|
-
if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
|
|
273
|
-
return m;
|
|
274
|
-
}
|
|
275
|
-
function rowInventoryCount(row) {
|
|
276
|
-
const skipped = new Set((row.overrides ?? []).filter((override) => override.skip && Number.isInteger(override.index) && override.index >= 0 && override.index < row.seatCount).map((override) => override.index));
|
|
277
|
-
return Math.max(0, row.seatCount - skipped.size);
|
|
278
|
-
}
|
|
279
|
-
function centerRank(n) {
|
|
280
|
-
const rank = new Array(n);
|
|
281
|
-
Array.from({ length: n }, (_, i) => i).sort((a, b) => Math.abs(2 * a - (n - 1)) - Math.abs(2 * b - (n - 1)) || a - b).forEach((idx, k) => rank[idx] = k);
|
|
282
|
-
return rank;
|
|
283
|
-
}
|
|
284
|
-
function seatLabelPart(row, i) {
|
|
285
|
-
const rawStart = row.seatLabelStart ?? 1;
|
|
286
|
-
const dir = row.seatNumbering?.direction ?? "ltr";
|
|
287
|
-
const step = row.seatNumbering?.step ?? 1;
|
|
288
|
-
const scheme = row.seatNumbering?.scheme ?? "decimal";
|
|
289
|
-
const prefix = row.seatNumbering?.prefix ?? "";
|
|
290
|
-
const endAt = row.seatNumbering?.endAt;
|
|
291
|
-
const n = row.seatCount;
|
|
292
|
-
if (scheme === "updown" || scheme === "updown-descending") {
|
|
293
|
-
const half = Math.ceil(n / 2);
|
|
294
|
-
const core2 = scheme === "updown" ? i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i) : i < half ? rawStart + 2 * (half - 1 - i) : rawStart + 1 + 2 * (i - half);
|
|
295
|
-
return `${prefix}${core2}`;
|
|
296
|
-
}
|
|
297
|
-
const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
|
|
298
|
-
const start = endAt != null && Number.isFinite(endAt) ? endAt - (n - 1) * effStep : rawStart;
|
|
299
|
-
const p = dir === "center" ? centerRank(n)[i] : dir === "rtl" ? n - 1 - i : i;
|
|
300
|
-
let core;
|
|
301
|
-
switch (scheme) {
|
|
302
|
-
case "odd": {
|
|
303
|
-
const firstOdd = start % 2 === 1 ? start : start + 1;
|
|
304
|
-
core = String(firstOdd + p * 2);
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
case "even": {
|
|
308
|
-
const firstEven = start % 2 === 0 ? start : start + 1;
|
|
309
|
-
core = String(firstEven + p * 2);
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
case "roman":
|
|
313
|
-
core = toRoman(start + p * step);
|
|
314
|
-
break;
|
|
315
|
-
case "letters-upper":
|
|
316
|
-
core = toLetters(start + p * step, false);
|
|
317
|
-
break;
|
|
318
|
-
case "letters-lower":
|
|
319
|
-
core = toLetters(start + p * step, true);
|
|
320
|
-
break;
|
|
321
|
-
case "decimal":
|
|
322
|
-
default:
|
|
323
|
-
core = String(start + p * step);
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
return `${prefix}${core}`;
|
|
327
|
-
}
|
|
328
|
-
function expandRowSlots(row) {
|
|
329
|
-
const ov = overrideMap(row);
|
|
330
|
-
return rowSeatPositions(row).map((p, i) => {
|
|
331
|
-
const o = ov.get(i);
|
|
332
|
-
const accessibility = overrideAccessibility(o);
|
|
333
|
-
const part = seatLabelPart(row, i);
|
|
334
|
-
const inventoryLabel = o?.label ?? `${row.label}-${part}`;
|
|
335
|
-
const displayPrefix = row.displayLabel ?? row.label;
|
|
336
|
-
const commercial = { ...row.commercial, ...o?.commercial };
|
|
337
|
-
return {
|
|
338
|
-
index: i,
|
|
339
|
-
x: p.x + (o?.dx ?? 0),
|
|
340
|
-
y: p.y + (o?.dy ?? 0),
|
|
341
|
-
label: inventoryLabel,
|
|
342
|
-
displayLabel: o?.displayLabel ?? `${displayPrefix}-${part}`,
|
|
343
|
-
categoryKey: o?.categoryKey ?? row.categoryKey,
|
|
344
|
-
skipped: !!o?.skip,
|
|
345
|
-
accessible: accessibility.length > 0,
|
|
346
|
-
accessibility,
|
|
347
|
-
wheelchairSpaceType: o?.wheelchairSpaceType,
|
|
348
|
-
commercial: Object.values(commercial).some((value) => value !== void 0 && value !== false && value !== "") ? commercial : void 0,
|
|
349
|
-
viewUrl: o?.viewFromSeatUrl ?? row.viewFromSeatUrl,
|
|
350
|
-
labelStyle: o?.labelStyle
|
|
351
|
-
};
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
function expandRow(row) {
|
|
355
|
-
const seats = [];
|
|
356
|
-
for (const slot of expandRowSlots(row)) {
|
|
357
|
-
if (slot.skipped) continue;
|
|
358
|
-
seats.push({
|
|
359
|
-
id: `${row.id}:${slot.index}`,
|
|
360
|
-
label: slot.label,
|
|
361
|
-
displayLabel: slot.displayLabel === slot.label ? void 0 : slot.displayLabel,
|
|
362
|
-
x: slot.x,
|
|
363
|
-
y: slot.y,
|
|
364
|
-
rowId: row.id,
|
|
365
|
-
categoryKey: slot.categoryKey,
|
|
366
|
-
accessible: slot.accessible || void 0,
|
|
367
|
-
accessibility: slot.accessibility.length ? slot.accessibility : void 0,
|
|
368
|
-
wheelchairSpaceType: slot.wheelchairSpaceType,
|
|
369
|
-
commercial: slot.commercial,
|
|
370
|
-
viewUrl: slot.viewUrl,
|
|
371
|
-
labelStyle: slot.labelStyle
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
return seats;
|
|
375
|
-
}
|
|
376
|
-
function tableSeatCountsBySide(t2) {
|
|
377
|
-
if (t2.seatCountsBySide) return { ...t2.seatCountsBySide };
|
|
378
|
-
const enabled = t2.sides && t2.sides.length ? t2.sides : ["top", "bottom"];
|
|
379
|
-
const order = ["top", "bottom", "left", "right"].filter((side) => enabled.includes(side));
|
|
380
|
-
const counts = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
381
|
-
if (!order.length) return counts;
|
|
382
|
-
const n = Math.max(0, Math.round(t2.seatCount));
|
|
383
|
-
for (let index = 0; index < n; index++) counts[order[index % order.length]] += 1;
|
|
384
|
-
return counts;
|
|
385
|
-
}
|
|
386
|
-
function expandTableSlots(t2) {
|
|
387
|
-
const seats = [];
|
|
388
|
-
const n = Math.max(0, Math.round(t2.seatCount));
|
|
389
|
-
if (n === 0) return seats;
|
|
390
|
-
const overrides2 = new Map((t2.overrides ?? []).map((override) => [override.index, override]));
|
|
391
|
-
const mk = (index, x, y, side) => {
|
|
392
|
-
const override = overrides2.get(index);
|
|
393
|
-
const accessibility = overrideAccessibility(override);
|
|
394
|
-
const label = override?.label ?? `${t2.label}-${index + 1}`;
|
|
395
|
-
const displayPrefix = t2.displayLabel ?? t2.label;
|
|
396
|
-
return {
|
|
397
|
-
index,
|
|
398
|
-
label,
|
|
399
|
-
displayLabel: override?.displayLabel ?? `${displayPrefix}-${index + 1}`,
|
|
400
|
-
x: x + (override?.dx ?? 0),
|
|
401
|
-
y: y + (override?.dy ?? 0),
|
|
402
|
-
categoryKey: override?.categoryKey ?? t2.categoryKey,
|
|
403
|
-
skipped: !!override?.skip,
|
|
404
|
-
accessible: accessibility.length > 0,
|
|
405
|
-
accessibility,
|
|
406
|
-
wheelchairSpaceType: override?.wheelchairSpaceType,
|
|
407
|
-
commercial: override?.commercial,
|
|
408
|
-
viewUrl: override?.viewFromSeatUrl,
|
|
409
|
-
labelStyle: override?.labelStyle,
|
|
410
|
-
...side ? { side } : {}
|
|
411
|
-
};
|
|
412
|
-
};
|
|
413
|
-
if (t2.shape === "round") {
|
|
414
|
-
const R = (t2.radius ?? 40) + TABLE_SEAT_OFFSET;
|
|
415
|
-
const base = t2.rotation * DEG;
|
|
416
|
-
const arc = Math.max(0, Math.min(360, t2.seatArc ?? 360));
|
|
417
|
-
if (arc >= 360 || n === 1) {
|
|
418
|
-
for (let i = 0; i < n; i++) {
|
|
419
|
-
const a = base + i / n * 2 * Math.PI;
|
|
420
|
-
seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
|
|
421
|
-
}
|
|
422
|
-
return seats;
|
|
423
|
-
}
|
|
424
|
-
const arcRad = arc * DEG;
|
|
425
|
-
const halfGap = (2 * Math.PI - arcRad) / 2;
|
|
426
|
-
const start = base + halfGap;
|
|
427
|
-
for (let i = 0; i < n; i++) {
|
|
428
|
-
const a = start + i / (n - 1) * arcRad;
|
|
429
|
-
seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
|
|
430
|
-
}
|
|
431
|
-
return seats;
|
|
432
|
-
}
|
|
433
|
-
const w = t2.width ?? 80;
|
|
434
|
-
const h = t2.height ?? 50;
|
|
435
|
-
const counts = tableSeatCountsBySide(t2);
|
|
436
|
-
const order = ["top", "bottom", "left", "right"];
|
|
437
|
-
let idx = 0;
|
|
438
|
-
for (const side of order) {
|
|
439
|
-
const count = counts[side];
|
|
440
|
-
for (let j = 0; j < count; j++) {
|
|
441
|
-
let localX;
|
|
442
|
-
let localY;
|
|
443
|
-
if (side === "top") {
|
|
444
|
-
localX = -w / 2 + (j + 0.5) * w / count;
|
|
445
|
-
localY = -h / 2 - TABLE_SEAT_OFFSET;
|
|
446
|
-
} else if (side === "bottom") {
|
|
447
|
-
localX = -w / 2 + (j + 0.5) * w / count;
|
|
448
|
-
localY = h / 2 + TABLE_SEAT_OFFSET;
|
|
449
|
-
} else if (side === "left") {
|
|
450
|
-
localX = -w / 2 - TABLE_SEAT_OFFSET;
|
|
451
|
-
localY = -h / 2 + (j + 0.5) * h / count;
|
|
452
|
-
} else {
|
|
453
|
-
localX = w / 2 + TABLE_SEAT_OFFSET;
|
|
454
|
-
localY = -h / 2 + (j + 0.5) * h / count;
|
|
455
|
-
}
|
|
456
|
-
const point = place(localX, localY, t2.rotation, t2.center);
|
|
457
|
-
seats.push(mk(idx++, point.x, point.y, side));
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return seats;
|
|
461
|
-
}
|
|
462
|
-
function tableInventoryCount(t2) {
|
|
463
|
-
if (t2.bookAsWhole || t2.variableOccupancy) return Math.max(0, Math.round(t2.seatCount));
|
|
464
|
-
return expandTableSlots(t2).filter((slot) => !slot.skipped).length;
|
|
465
|
-
}
|
|
466
|
-
function expandTable(t2) {
|
|
467
|
-
return expandTableSlots(t2).filter((slot) => !slot.skipped).map((slot) => ({
|
|
468
|
-
id: `${t2.id}:${slot.index}`,
|
|
469
|
-
label: slot.label,
|
|
470
|
-
...slot.displayLabel !== slot.label ? { displayLabel: slot.displayLabel } : {},
|
|
471
|
-
x: slot.x,
|
|
472
|
-
y: slot.y,
|
|
473
|
-
rowId: t2.id,
|
|
474
|
-
categoryKey: slot.categoryKey,
|
|
475
|
-
...slot.accessible ? { accessible: true } : {},
|
|
476
|
-
...slot.accessibility.length ? { accessibility: slot.accessibility } : {},
|
|
477
|
-
...slot.wheelchairSpaceType ? { wheelchairSpaceType: slot.wheelchairSpaceType } : {},
|
|
478
|
-
...slot.commercial ? { commercial: slot.commercial } : {},
|
|
479
|
-
...slot.viewUrl ? { viewUrl: slot.viewUrl } : {},
|
|
480
|
-
...slot.labelStyle ? { labelStyle: slot.labelStyle } : {}
|
|
481
|
-
}));
|
|
482
|
-
}
|
|
483
|
-
function expandBooth(b) {
|
|
484
|
-
return [
|
|
485
|
-
{
|
|
486
|
-
id: `${b.id}:0`,
|
|
487
|
-
label: b.label,
|
|
488
|
-
...b.displayLabel && b.displayLabel !== b.label ? { displayLabel: b.displayLabel } : {},
|
|
489
|
-
x: b.center.x,
|
|
490
|
-
y: b.center.y,
|
|
491
|
-
rowId: b.id,
|
|
492
|
-
categoryKey: b.categoryKey,
|
|
493
|
-
kind: "booth"
|
|
494
|
-
}
|
|
495
|
-
];
|
|
496
|
-
}
|
|
497
|
-
function pointInPolygon(p, poly) {
|
|
498
|
-
let inside = false;
|
|
499
|
-
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
500
|
-
const xi = poly[i].x;
|
|
501
|
-
const yi = poly[i].y;
|
|
502
|
-
const xj = poly[j].x;
|
|
503
|
-
const yj = poly[j].y;
|
|
504
|
-
const hit = yi > p.y !== yj > p.y && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi;
|
|
505
|
-
if (hit) inside = !inside;
|
|
506
|
-
}
|
|
507
|
-
return inside;
|
|
508
|
-
}
|
|
509
|
-
function pointOnPolygonBoundary(p, poly) {
|
|
510
|
-
return poly.some((start, index) => {
|
|
511
|
-
const end = poly[(index + 1) % poly.length];
|
|
512
|
-
const cross = (p.y - start.y) * (end.x - start.x) - (p.x - start.x) * (end.y - start.y);
|
|
513
|
-
if (Math.abs(cross) > 1e-7) return false;
|
|
514
|
-
const dot = (p.x - start.x) * (end.x - start.x) + (p.y - start.y) * (end.y - start.y);
|
|
515
|
-
const lengthSquared = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
|
|
516
|
-
return dot >= -1e-7 && dot <= lengthSquared + 1e-7;
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
function pointInPolygonWithHoles(p, outer, holes) {
|
|
520
|
-
return pointInPolygon(p, outer) && !(holes ?? []).some((hole) => pointInPolygon(p, hole) || pointOnPolygonBoundary(p, hole));
|
|
521
|
-
}
|
|
522
|
-
function polygonLabelPoint(outer, holes) {
|
|
523
|
-
if (!outer.length) return { x: 0, y: 0 };
|
|
524
|
-
const xs = outer.map((point) => point.x);
|
|
525
|
-
const ys = outer.map((point) => point.y);
|
|
526
|
-
const bounds2 = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
527
|
-
const centroid2 = polygonCentroid(outer);
|
|
528
|
-
if (pointInPolygonWithHoles(centroid2, outer, holes)) return centroid2;
|
|
529
|
-
let best = outer[0];
|
|
530
|
-
let bestScore = -Infinity;
|
|
531
|
-
const rings = [outer, ...holes ?? []];
|
|
532
|
-
for (let row = 1; row < 24; row += 1) {
|
|
533
|
-
for (let column = 1; column < 24; column += 1) {
|
|
534
|
-
const point = {
|
|
535
|
-
x: bounds2.minX + (bounds2.maxX - bounds2.minX) * column / 24,
|
|
536
|
-
y: bounds2.minY + (bounds2.maxY - bounds2.minY) * row / 24
|
|
537
|
-
};
|
|
538
|
-
if (!pointInPolygonWithHoles(point, outer, holes)) continue;
|
|
539
|
-
const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
|
|
540
|
-
const end = ring[(index + 1) % ring.length];
|
|
541
|
-
const dx = end.x - start.x;
|
|
542
|
-
const dy = end.y - start.y;
|
|
543
|
-
const denominator = dx * dx + dy * dy;
|
|
544
|
-
const projection = denominator ? ((point.x - start.x) * dx + (point.y - start.y) * dy) / denominator : 0;
|
|
545
|
-
const t2 = Math.max(0, Math.min(1, projection));
|
|
546
|
-
return Math.hypot(point.x - (start.x + t2 * dx), point.y - (start.y + t2 * dy));
|
|
547
|
-
})));
|
|
548
|
-
if (score > bestScore) {
|
|
549
|
-
best = point;
|
|
550
|
-
bestScore = score;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return best;
|
|
555
|
-
}
|
|
556
|
-
function polygonCentroid(pts) {
|
|
557
|
-
if (!pts.length) return { x: 0, y: 0 };
|
|
558
|
-
let x = 0;
|
|
559
|
-
let y = 0;
|
|
560
|
-
for (const p of pts) {
|
|
561
|
-
x += p.x;
|
|
562
|
-
y += p.y;
|
|
563
|
-
}
|
|
564
|
-
return { x: x / pts.length, y: y / pts.length };
|
|
565
|
-
}
|
|
566
|
-
function objectCenter(o) {
|
|
567
|
-
switch (o.type) {
|
|
568
|
-
case "row": {
|
|
569
|
-
const seats = expandRow(o);
|
|
570
|
-
if (!seats.length) return { x: o.origin.x, y: o.origin.y };
|
|
571
|
-
let x = 0;
|
|
572
|
-
let y = 0;
|
|
573
|
-
for (const s of seats) {
|
|
574
|
-
x += s.x;
|
|
575
|
-
y += s.y;
|
|
576
|
-
}
|
|
577
|
-
return { x: x / seats.length, y: y / seats.length };
|
|
578
|
-
}
|
|
579
|
-
case "table":
|
|
580
|
-
case "booth":
|
|
581
|
-
return { x: o.center.x, y: o.center.y };
|
|
582
|
-
case "gaArea":
|
|
583
|
-
return polygonCentroid(o.points);
|
|
584
|
-
case "section":
|
|
585
|
-
return polygonCentroid(o.outline);
|
|
586
|
-
case "text":
|
|
587
|
-
return { x: o.position.x, y: o.position.y };
|
|
588
|
-
case "shape":
|
|
589
|
-
if (o.points && o.points.length) return polygonCentroid(o.points);
|
|
590
|
-
if (o.x != null && o.y != null && o.width != null && o.height != null) {
|
|
591
|
-
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
592
|
-
}
|
|
593
|
-
return { x: o.x ?? 0, y: o.y ?? 0 };
|
|
594
|
-
case "decorImage":
|
|
595
|
-
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
function samePoints(left, right) {
|
|
599
|
-
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
600
|
-
}
|
|
601
|
-
function sameGASurfaceAsSection(object, section) {
|
|
602
|
-
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
603
|
-
const objectHoles = object.holes ?? [];
|
|
604
|
-
const sectionHoles = section.holes ?? [];
|
|
605
|
-
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
606
|
-
}
|
|
607
|
-
function owningSectionForObject(objects, object) {
|
|
608
|
-
const sections = objects.filter((candidate) => candidate.type === "section");
|
|
609
|
-
const referencedLogicalId = "referenceInventorySource" in object ? object.referenceInventorySource?.logicalSectionId : void 0;
|
|
610
|
-
const center = objectCenter(object);
|
|
611
|
-
const referencedOwner = referencedLogicalId ? sections.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(object, section) || pointInPolygonWithHoles(center, section.outline, section.holes))) : void 0;
|
|
612
|
-
return referencedOwner ?? sections.find((section) => pointInPolygonWithHoles(center, section.outline, section.holes));
|
|
613
|
-
}
|
|
614
|
-
function floorsOf(doc) {
|
|
615
|
-
if (doc.floors && doc.floors.length) return doc.floors;
|
|
616
|
-
return [{
|
|
617
|
-
id: "floor-0",
|
|
618
|
-
name: "Main",
|
|
619
|
-
objects: doc.objects,
|
|
620
|
-
focalPoint: doc.focalPoint,
|
|
621
|
-
referenceImage: doc.referenceImage,
|
|
622
|
-
backgroundImage: doc.backgroundImage
|
|
623
|
-
}];
|
|
624
|
-
}
|
|
625
|
-
function floorObjects(doc, floorId) {
|
|
626
|
-
const floors = floorsOf(doc);
|
|
627
|
-
return (floorId ? floors.find((f) => f.id === floorId) : floors[0])?.objects ?? [];
|
|
628
|
-
}
|
|
629
|
-
function allObjects(doc) {
|
|
630
|
-
return doc.floors && doc.floors.length ? doc.floors.flatMap((f) => f.objects) : doc.objects;
|
|
631
|
-
}
|
|
632
|
-
function translateObject(o, dx, dy) {
|
|
633
|
-
const p = (pt) => ({ x: pt.x + dx, y: pt.y + dy });
|
|
634
|
-
const pts = (a) => a.map(p);
|
|
635
|
-
switch (o.type) {
|
|
636
|
-
case "row":
|
|
637
|
-
return { ...o, origin: p(o.origin) };
|
|
638
|
-
case "table":
|
|
639
|
-
case "booth":
|
|
640
|
-
return { ...o, center: p(o.center) };
|
|
641
|
-
case "gaArea":
|
|
642
|
-
return { ...o, points: pts(o.points), ...o.holes ? { holes: o.holes.map(pts) } : {} };
|
|
643
|
-
case "section":
|
|
644
|
-
return {
|
|
645
|
-
...o,
|
|
646
|
-
outline: pts(o.outline),
|
|
647
|
-
...o.outlinePath ? { outlinePath: translateSectionOutlinePath(o.outlinePath, dx, dy) } : {},
|
|
648
|
-
...o.holes ? { holes: o.holes.map(pts) } : {}
|
|
649
|
-
};
|
|
650
|
-
case "text":
|
|
651
|
-
return { ...o, position: p(o.position) };
|
|
652
|
-
case "shape":
|
|
653
|
-
return {
|
|
654
|
-
...o,
|
|
655
|
-
...o.points ? { points: pts(o.points) } : {},
|
|
656
|
-
...o.x != null ? { x: o.x + dx } : {},
|
|
657
|
-
...o.y != null ? { y: o.y + dy } : {}
|
|
658
|
-
};
|
|
659
|
-
case "decorImage":
|
|
660
|
-
return { ...o, x: o.x + dx, y: o.y + dy };
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
function stackFloors(doc, spread = 900) {
|
|
664
|
-
if (!doc.floors || doc.floors.length < 2) return doc;
|
|
665
|
-
const objects = [];
|
|
666
|
-
doc.floors.forEach((f, i) => {
|
|
667
|
-
const dy = -i * spread;
|
|
668
|
-
for (const o of f.objects) objects.push(dy === 0 ? o : translateObject(o, 0, dy));
|
|
669
|
-
});
|
|
670
|
-
return { ...doc, objects, floors: void 0 };
|
|
671
|
-
}
|
|
672
|
-
function expandFloorObjects(objects, zones, fallbackFocal) {
|
|
673
|
-
const out = [];
|
|
674
|
-
const segmented = /* @__PURE__ */ new Map();
|
|
675
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
676
|
-
for (const object of objects) {
|
|
677
|
-
if (object.type !== "row" || !object.segmentedRow) continue;
|
|
678
|
-
const list = grouped.get(object.segmentedRow.groupId) ?? [];
|
|
679
|
-
list.push(object);
|
|
680
|
-
grouped.set(object.segmentedRow.groupId, list);
|
|
681
|
-
}
|
|
682
|
-
for (const [groupId, members] of grouped) {
|
|
683
|
-
const ordered = members.slice().sort((left, right) => left.segmentedRow.componentIndex - right.segmentedRow.componentIndex);
|
|
684
|
-
const expectedCount = ordered[0]?.segmentedRow?.componentCount ?? 0;
|
|
685
|
-
const first = ordered[0]?.segmentedRow;
|
|
686
|
-
if (!first) continue;
|
|
687
|
-
const valid = expectedCount >= 2 && ordered.length === expectedCount && first?.boundaryBefore === "start" && ordered.every((row, index) => row.segmentedRow?.kind === "segmented-row-v1" && row.segmentedRow.groupId === groupId && row.segmentedRow.componentCount === expectedCount && row.segmentedRow.componentIndex === index && (index === 0 ? row.segmentedRow.boundaryBefore === "start" : row.segmentedRow.boundaryBefore !== "start") && row.segmentedRow.displayLabel === first.displayLabel);
|
|
688
|
-
if (!valid) continue;
|
|
689
|
-
const totalSeats = ordered.reduce((sum, row) => sum + row.seatCount, 0);
|
|
690
|
-
let adjacencyOffset = 0;
|
|
691
|
-
let displayOffset = 0;
|
|
692
|
-
for (const row of ordered) {
|
|
693
|
-
if (row.segmentedRow.boundaryBefore === "break") adjacencyOffset += 1;
|
|
694
|
-
segmented.set(row.id, {
|
|
695
|
-
groupId,
|
|
696
|
-
adjacencyOffset,
|
|
697
|
-
displayOffset,
|
|
698
|
-
displayLabel: first.displayLabel,
|
|
699
|
-
totalSeats,
|
|
700
|
-
canonical: ordered[0],
|
|
701
|
-
viewFromSeatUrl: first.viewFromSeatUrl
|
|
702
|
-
});
|
|
703
|
-
adjacencyOffset += row.seatCount;
|
|
704
|
-
displayOffset += row.seatCount;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
for (const obj of objects) {
|
|
708
|
-
let seats = [];
|
|
709
|
-
if (obj.type === "row") seats = expandRow(obj);
|
|
710
|
-
else if (obj.type === "table") seats = expandTable(obj);
|
|
711
|
-
else if (obj.type === "booth") seats = expandBooth(obj);
|
|
712
|
-
if (!seats.length) continue;
|
|
713
|
-
if (obj.type === "row") {
|
|
714
|
-
const logical = segmented.get(obj.id);
|
|
715
|
-
if (logical) {
|
|
716
|
-
const overrides2 = new Map((obj.overrides ?? []).map((override) => [override.index, override]));
|
|
717
|
-
for (const seat of seats) {
|
|
718
|
-
const physicalIndex = Number(seat.id.slice(seat.id.lastIndexOf(":") + 1));
|
|
719
|
-
if (!Number.isInteger(physicalIndex)) continue;
|
|
720
|
-
const displayOrdinal = logical.displayOffset + physicalIndex;
|
|
721
|
-
seat.logicalRowId = logical.groupId;
|
|
722
|
-
seat.logicalSeatIndex = logical.adjacencyOffset + physicalIndex;
|
|
723
|
-
if (!overrides2.get(physicalIndex)?.displayLabel) {
|
|
724
|
-
const numberingRow = {
|
|
725
|
-
...logical.canonical,
|
|
726
|
-
seatCount: logical.totalSeats,
|
|
727
|
-
label: logical.displayLabel,
|
|
728
|
-
displayLabel: logical.displayLabel
|
|
729
|
-
};
|
|
730
|
-
seat.displayLabel = `${logical.displayLabel}-${seatLabelPart(numberingRow, displayOrdinal)}`;
|
|
731
|
-
}
|
|
732
|
-
seat.viewUrl ??= logical.viewFromSeatUrl;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
const owner = owningSectionForObject(objects, obj);
|
|
737
|
-
const inheritedView = owner?.viewFromSeatUrl;
|
|
738
|
-
const zone = owner?.zone ? zones?.find((candidate) => candidate.id === owner.zone) : void 0;
|
|
739
|
-
const resolvedFocal = zone?.focalPoint ?? fallbackFocal;
|
|
740
|
-
for (const seat of seats) {
|
|
741
|
-
if (inheritedView) seat.viewUrl ??= inheritedView;
|
|
742
|
-
if (owner) seat.sectionId = owner.logicalSectionId ?? owner.id;
|
|
743
|
-
if (owner?.zone) seat.zoneId = owner.zone;
|
|
744
|
-
if (resolvedFocal) seat.focalPoint = { ...resolvedFocal };
|
|
745
|
-
}
|
|
746
|
-
out.push(...seats);
|
|
747
|
-
}
|
|
748
|
-
return out;
|
|
749
|
-
}
|
|
750
|
-
function expandChart(doc, options = {}) {
|
|
751
|
-
if (doc.floors?.length) {
|
|
752
|
-
const out2 = [];
|
|
753
|
-
for (const floor of doc.floors) {
|
|
754
|
-
const floorFocal = floor.focalPoint ?? doc.focalPoint;
|
|
755
|
-
const seats = expandFloorObjects(floor.objects, doc.zones, floorFocal);
|
|
756
|
-
assignEyeHeights(floor.objects, floor.focalPoint ?? doc.focalPoint, floor.baseHeightM ?? 0, seats);
|
|
757
|
-
out2.push(...seats);
|
|
758
|
-
}
|
|
759
|
-
return out2;
|
|
760
|
-
}
|
|
761
|
-
const out = expandFloorObjects(doc.objects, doc.zones, doc.focalPoint);
|
|
762
|
-
assignEyeHeights(doc.objects, doc.focalPoint, options.floorBaseHeightM ?? 0, out);
|
|
763
|
-
return out;
|
|
764
|
-
}
|
|
765
|
-
function assignEyeHeights(objects, focal, floorBaseHeightM, seats) {
|
|
766
|
-
const sections = objects.filter((o) => o.type === "section");
|
|
767
|
-
const hasGeometry = floorBaseHeightM > 0 || sections.some((s) => s.height !== void 0 || s.rake !== void 0 || (s.elevation ?? 0) > 0);
|
|
768
|
-
if (!sections.length || !hasGeometry || !focal) {
|
|
769
|
-
for (const seat of seats) seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
const owner = new Array(seats.length);
|
|
773
|
-
const geo = /* @__PURE__ */ new Map();
|
|
774
|
-
const frontDistU = /* @__PURE__ */ new Map();
|
|
775
|
-
for (let i = 0; i < seats.length; i++) {
|
|
776
|
-
const seat = seats[i];
|
|
777
|
-
const sec = sections.find((s) => pointInPolygonWithHoles({ x: seat.x, y: seat.y }, s.outline, s.holes)) ?? null;
|
|
778
|
-
owner[i] = sec;
|
|
779
|
-
if (!sec) continue;
|
|
780
|
-
if (!geo.has(sec.id)) geo.set(sec.id, sectionGeometry(sec, { floorBaseHeightM }));
|
|
781
|
-
const seatFocal = seat.focalPoint ?? focal;
|
|
782
|
-
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
783
|
-
const cur = frontDistU.get(sec.id);
|
|
784
|
-
if (cur === void 0 || d < cur) frontDistU.set(sec.id, d);
|
|
785
|
-
}
|
|
786
|
-
for (let i = 0; i < seats.length; i++) {
|
|
787
|
-
const seat = seats[i];
|
|
788
|
-
const sec = owner[i];
|
|
789
|
-
if (!sec) {
|
|
790
|
-
seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
const g = geo.get(sec.id);
|
|
794
|
-
let riseM = 0;
|
|
795
|
-
if (g.rake > 0) {
|
|
796
|
-
const seatFocal = seat.focalPoint ?? focal;
|
|
797
|
-
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
798
|
-
const depthU = Math.max(0, d - (frontDistU.get(sec.id) ?? d));
|
|
799
|
-
riseM = depthU * METRES_PER_CHART_UNIT * Math.tan(g.rake * Math.PI / 180);
|
|
800
|
-
}
|
|
801
|
-
seat.eyeHeightM = g.height + riseM + SEATED_EYE_HEIGHT_M;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
var PAD = 40;
|
|
805
|
-
function chartBounds(doc) {
|
|
806
|
-
let minX = Infinity;
|
|
807
|
-
let minY = Infinity;
|
|
808
|
-
let maxX = -Infinity;
|
|
809
|
-
let maxY = -Infinity;
|
|
810
|
-
const acc = (x, y) => {
|
|
811
|
-
if (x < minX) minX = x;
|
|
812
|
-
if (y < minY) minY = y;
|
|
813
|
-
if (x > maxX) maxX = x;
|
|
814
|
-
if (y > maxY) maxY = y;
|
|
815
|
-
};
|
|
816
|
-
for (const s of expandChart(doc)) acc(s.x, s.y);
|
|
817
|
-
for (const obj of allObjects(doc)) {
|
|
818
|
-
if (obj.type === "gaArea") {
|
|
819
|
-
for (const p of obj.points) acc(p.x, p.y);
|
|
820
|
-
} else if (obj.type === "section") {
|
|
821
|
-
for (const p of obj.outline) acc(p.x, p.y);
|
|
822
|
-
} else if (obj.type === "decorImage") {
|
|
823
|
-
acc(obj.x, obj.y);
|
|
824
|
-
acc(obj.x + obj.width, obj.y + obj.height);
|
|
825
|
-
} else if (obj.type === "shape") {
|
|
826
|
-
if (obj.points && obj.points.length) {
|
|
827
|
-
for (const p of obj.points) acc(p.x, p.y);
|
|
828
|
-
} else if (obj.x != null && obj.y != null && obj.width != null && obj.height != null) {
|
|
829
|
-
acc(obj.x, obj.y);
|
|
830
|
-
acc(obj.x + obj.width, obj.y + obj.height);
|
|
831
|
-
}
|
|
832
|
-
} else if (obj.type === "table") {
|
|
833
|
-
const off = TABLE_SEAT_OFFSET + SEAT_R;
|
|
834
|
-
const ext = obj.shape === "round" ? (obj.radius ?? 40) + off : Math.max((obj.width ?? 80) / 2, (obj.height ?? 50) / 2) + off;
|
|
835
|
-
acc(obj.center.x - ext, obj.center.y - ext);
|
|
836
|
-
acc(obj.center.x + ext, obj.center.y + ext);
|
|
837
|
-
} else if (obj.type === "booth") {
|
|
838
|
-
const ext = Math.max(obj.width, obj.height) / 2;
|
|
839
|
-
acc(obj.center.x - ext, obj.center.y - ext);
|
|
840
|
-
acc(obj.center.x + ext, obj.center.y + ext);
|
|
841
|
-
} else if (obj.type === "text") {
|
|
842
|
-
const w = obj.fontSize * obj.text.length * 0.6;
|
|
843
|
-
acc(obj.position.x, obj.position.y);
|
|
844
|
-
acc(obj.position.x + w, obj.position.y + obj.fontSize);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
for (const image of [doc.referenceImage, doc.backgroundImage]) {
|
|
848
|
-
if (!image) continue;
|
|
849
|
-
const { center, width } = image;
|
|
850
|
-
const bh = width * 3 / 4;
|
|
851
|
-
acc(center.x - width / 2, center.y - bh / 2);
|
|
852
|
-
acc(center.x + width / 2, center.y + bh / 2);
|
|
853
|
-
}
|
|
854
|
-
if (!isFinite(minX)) {
|
|
855
|
-
const f = doc.focalPoint ?? { x: 0, y: 0 };
|
|
856
|
-
return { x: f.x - 200, y: f.y - 200, width: 400, height: 400 };
|
|
857
|
-
}
|
|
858
|
-
return {
|
|
859
|
-
x: minX - PAD,
|
|
860
|
-
y: minY - PAD,
|
|
861
|
-
width: maxX - minX + PAD * 2,
|
|
862
|
-
height: maxY - minY + PAD * 2
|
|
863
|
-
};
|
|
864
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
ACCESSIBILITY_RING_COLOR,
|
|
3
|
+
ACCESSIBILITY_TYPES,
|
|
4
|
+
CHART_STORAGE_KEY,
|
|
5
|
+
CHART_UNITS_PER_METRE,
|
|
6
|
+
LABEL_STYLE_MAX_SIZE,
|
|
7
|
+
LABEL_STYLE_MIN_SIZE,
|
|
8
|
+
METRES_PER_CHART_UNIT,
|
|
9
|
+
SEATED_EYE_HEIGHT_M,
|
|
10
|
+
SURROUNDINGS_SHAPE_ROLES,
|
|
11
|
+
TIER_HEIGHT_M,
|
|
12
|
+
accessibilityMeta,
|
|
13
|
+
accessibilityRingColor,
|
|
14
|
+
allObjects,
|
|
15
|
+
chartBounds,
|
|
16
|
+
expandBooth,
|
|
17
|
+
expandChart,
|
|
18
|
+
expandRow,
|
|
19
|
+
expandRowSlots,
|
|
20
|
+
expandTable,
|
|
21
|
+
expandTableSlots,
|
|
22
|
+
floorObjects,
|
|
23
|
+
floorsOf,
|
|
24
|
+
layerOf,
|
|
25
|
+
objectCenter,
|
|
26
|
+
owningSectionForObject,
|
|
27
|
+
pointInPolygon,
|
|
28
|
+
pointInPolygonWithHoles,
|
|
29
|
+
polygonLabelPoint,
|
|
30
|
+
rowInventoryCount,
|
|
31
|
+
rowSeatPositions,
|
|
32
|
+
seatLabelPart,
|
|
33
|
+
sectionElevationTier,
|
|
34
|
+
sectionGeometry,
|
|
35
|
+
stackFloors,
|
|
36
|
+
tableInventoryCount,
|
|
37
|
+
tableSeatCountsBySide
|
|
38
|
+
} from "./chunk-FGS67GT3.js";
|
|
865
39
|
|
|
866
40
|
// src/core/ga.ts
|
|
867
41
|
var PREFIX = "__sl_ga__";
|
|
@@ -4359,7 +3533,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4359
3533
|
this.accessRingById.set(seat.id, ring);
|
|
4360
3534
|
target.add(ring);
|
|
4361
3535
|
}
|
|
4362
|
-
if (accessGlyphPath(seat.accessibility
|
|
3536
|
+
if (seat.accessibility?.includes("wheelchair") && accessGlyphPath(seat.accessibility)) {
|
|
4363
3537
|
const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
|
|
4364
3538
|
this.accessGlyphById.set(seat.id, glyph);
|
|
4365
3539
|
target.add(glyph);
|
|
@@ -8509,8 +7683,450 @@ function stageSightlinePitch(eyeHeightM, distM) {
|
|
|
8509
7683
|
var W = 2048;
|
|
8510
7684
|
var H = 1024;
|
|
8511
7685
|
var UNIT = METRES_PER_CHART_UNIT;
|
|
7686
|
+
var TAU = Math.PI * 2;
|
|
7687
|
+
var PX_PER_DEG = H / 180;
|
|
8512
7688
|
var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
|
|
8513
7689
|
var pitchToY = (pitchDeg) => (90 - pitchDeg) / 180 * H;
|
|
7690
|
+
function fnv1a(str) {
|
|
7691
|
+
let h = 2166136261;
|
|
7692
|
+
for (let i = 0; i < str.length; i++) {
|
|
7693
|
+
h ^= str.charCodeAt(i);
|
|
7694
|
+
h = Math.imul(h, 16777619);
|
|
7695
|
+
}
|
|
7696
|
+
return h >>> 0;
|
|
7697
|
+
}
|
|
7698
|
+
function rng(seed) {
|
|
7699
|
+
let a = seed >>> 0;
|
|
7700
|
+
return () => {
|
|
7701
|
+
a |= 0;
|
|
7702
|
+
a = a + 1831565813 | 0;
|
|
7703
|
+
let t2 = Math.imul(a ^ a >>> 15, 1 | a);
|
|
7704
|
+
t2 = t2 + Math.imul(t2 ^ t2 >>> 7, 61 | t2) ^ t2;
|
|
7705
|
+
return ((t2 ^ t2 >>> 14) >>> 0) / 4294967296;
|
|
7706
|
+
};
|
|
7707
|
+
}
|
|
7708
|
+
var NBINS = 96;
|
|
7709
|
+
var BIN_DEG = 360 / NBINS;
|
|
7710
|
+
var FLAT_DELTA_M = 0.6;
|
|
7711
|
+
var HAZE_SKY = [17, 23, 42];
|
|
7712
|
+
function blendToSky(base, t2, a = 1) {
|
|
7713
|
+
const k = Math.max(0, Math.min(0.58, t2));
|
|
7714
|
+
const r = Math.round(base[0] + (HAZE_SKY[0] - base[0]) * k);
|
|
7715
|
+
const g = Math.round(base[1] + (HAZE_SKY[1] - base[1]) * k);
|
|
7716
|
+
const b = Math.round(base[2] + (HAZE_SKY[2] - base[2]) * k);
|
|
7717
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
7718
|
+
}
|
|
7719
|
+
function mixRgb(a, b, t2) {
|
|
7720
|
+
return [
|
|
7721
|
+
Math.round(a[0] + (b[0] - a[0]) * t2),
|
|
7722
|
+
Math.round(a[1] + (b[1] - a[1]) * t2),
|
|
7723
|
+
Math.round(a[2] + (b[2] - a[2]) * t2)
|
|
7724
|
+
];
|
|
7725
|
+
}
|
|
7726
|
+
function hslToRgb(h, s, l) {
|
|
7727
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
7728
|
+
const hp = (h % 360 + 360) % 360 / 60;
|
|
7729
|
+
const x = c * (1 - Math.abs(hp % 2 - 1));
|
|
7730
|
+
let r = 0, g = 0, b = 0;
|
|
7731
|
+
if (hp < 1) [r, g, b] = [c, x, 0];
|
|
7732
|
+
else if (hp < 2) [r, g, b] = [x, c, 0];
|
|
7733
|
+
else if (hp < 3) [r, g, b] = [0, c, x];
|
|
7734
|
+
else if (hp < 4) [r, g, b] = [0, x, c];
|
|
7735
|
+
else if (hp < 5) [r, g, b] = [x, 0, c];
|
|
7736
|
+
else [r, g, b] = [c, 0, x];
|
|
7737
|
+
const m = l - c / 2;
|
|
7738
|
+
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
|
|
7739
|
+
}
|
|
7740
|
+
var tintCache = /* @__PURE__ */ new Map();
|
|
7741
|
+
function tintFromKey(key) {
|
|
7742
|
+
if (!key) return null;
|
|
7743
|
+
const cached = tintCache.get(key);
|
|
7744
|
+
if (cached) return cached;
|
|
7745
|
+
let h = 2166136261;
|
|
7746
|
+
for (let i = 0; i < key.length; i++) {
|
|
7747
|
+
h ^= key.charCodeAt(i);
|
|
7748
|
+
h = Math.imul(h, 16777619);
|
|
7749
|
+
}
|
|
7750
|
+
const hue = (h >>> 0) % 360;
|
|
7751
|
+
const rgb2 = hslToRgb(hue, 0.3, 0.42);
|
|
7752
|
+
tintCache.set(key, rgb2);
|
|
7753
|
+
return rgb2;
|
|
7754
|
+
}
|
|
7755
|
+
function buildStands(seat, neighborSeats, stageBearing, myEyeM) {
|
|
7756
|
+
const bins = new Array(NBINS).fill(null);
|
|
7757
|
+
let maxDelta = 0;
|
|
7758
|
+
const own = seat.sectionId;
|
|
7759
|
+
for (const other of neighborSeats) {
|
|
7760
|
+
if (other.id === seat.id) continue;
|
|
7761
|
+
const os = other.sectionId;
|
|
7762
|
+
if (!os || own && os === own) continue;
|
|
7763
|
+
const ox = other.x - seat.x;
|
|
7764
|
+
const oy = other.y - seat.y;
|
|
7765
|
+
const dM = Math.hypot(ox, oy) * UNIT;
|
|
7766
|
+
if (dM < 1.5 || dM > 95) continue;
|
|
7767
|
+
const otherEye = other.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
7768
|
+
const dTop = otherEye - myEyeM;
|
|
7769
|
+
if (Math.abs(dTop) > maxDelta) maxDelta = Math.abs(dTop);
|
|
7770
|
+
const top = Math.atan2(dTop, dM) * 180 / Math.PI;
|
|
7771
|
+
const base = Math.atan2(otherEye - SEATED_EYE_HEIGHT_M - myEyeM, dM) * 180 / Math.PI;
|
|
7772
|
+
const bearing = Math.atan2(ox, -oy) - stageBearing;
|
|
7773
|
+
const yaw = (bearing * 180 / Math.PI + 540) % 360 - 180;
|
|
7774
|
+
const idx = Math.min(NBINS - 1, Math.max(0, Math.floor((yaw + 180) / BIN_DEG)));
|
|
7775
|
+
const overhead = top > 34 && dM < 16;
|
|
7776
|
+
const cur = bins[idx];
|
|
7777
|
+
if (!cur) bins[idx] = { top, base, nearM: dM, overhead, tint: tintFromKey(other.categoryKey) };
|
|
7778
|
+
else {
|
|
7779
|
+
if (top > cur.top) cur.top = top;
|
|
7780
|
+
if (base < cur.base) cur.base = base;
|
|
7781
|
+
if (dM < cur.nearM) {
|
|
7782
|
+
cur.nearM = dM;
|
|
7783
|
+
cur.tint = tintFromKey(other.categoryKey);
|
|
7784
|
+
}
|
|
7785
|
+
cur.overhead = cur.overhead || overhead;
|
|
7786
|
+
}
|
|
7787
|
+
}
|
|
7788
|
+
return { bins, maxDelta };
|
|
7789
|
+
}
|
|
7790
|
+
function drawStands(ctx, bins, toX, toY) {
|
|
7791
|
+
for (let i = 0; i < NBINS; i++) {
|
|
7792
|
+
const b = bins[i];
|
|
7793
|
+
if (!b) continue;
|
|
7794
|
+
const yaw0 = -180 + i * BIN_DEG;
|
|
7795
|
+
const x0 = toX(yaw0);
|
|
7796
|
+
const x1 = toX(yaw0 + BIN_DEG);
|
|
7797
|
+
if (!(x1 > x0)) continue;
|
|
7798
|
+
const top = Math.min(72, b.top);
|
|
7799
|
+
const bottom = Math.min(b.base, -4);
|
|
7800
|
+
const yTop = toY(top);
|
|
7801
|
+
const yBot = toY(bottom);
|
|
7802
|
+
if (yBot <= yTop) continue;
|
|
7803
|
+
const haze = b.nearM / 95;
|
|
7804
|
+
const tint = b.tint;
|
|
7805
|
+
const crest = tint ? mixRgb([44, 54, 80], tint, 0.34) : [44, 54, 80];
|
|
7806
|
+
const seating = tint ? mixRgb([24, 30, 48], tint, 0.28) : [24, 30, 48];
|
|
7807
|
+
const grad = ctx.createLinearGradient(0, yTop, 0, yBot);
|
|
7808
|
+
grad.addColorStop(0, blendToSky(crest, haze));
|
|
7809
|
+
grad.addColorStop(0.24, blendToSky(seating, haze));
|
|
7810
|
+
grad.addColorStop(1, blendToSky([10, 13, 22], haze));
|
|
7811
|
+
ctx.fillStyle = grad;
|
|
7812
|
+
ctx.fillRect(x0, yTop, x1 - x0 + 1, yBot - yTop);
|
|
7813
|
+
ctx.fillStyle = `rgba(150,165,205,${0.35 * (1 - haze * 0.55)})`;
|
|
7814
|
+
ctx.fillRect(x0, yTop, x1 - x0 + 1, 1.5);
|
|
7815
|
+
const bandBottom = Math.min(yBot, toY(0));
|
|
7816
|
+
const bandSpan = bandBottom - yTop;
|
|
7817
|
+
if (bandSpan > 8) {
|
|
7818
|
+
const bands = 6;
|
|
7819
|
+
const contrast = 1 - haze * 0.4;
|
|
7820
|
+
for (let r = 1; r < bands; r++) {
|
|
7821
|
+
const y = yTop + bandSpan * r / bands;
|
|
7822
|
+
ctx.fillStyle = `rgba(0,0,0,${0.16 * contrast})`;
|
|
7823
|
+
ctx.fillRect(x0, y, x1 - x0 + 1, 1.5);
|
|
7824
|
+
ctx.fillStyle = `rgba(120,134,170,${0.1 * contrast})`;
|
|
7825
|
+
ctx.fillRect(x0, y + 1.5, x1 - x0 + 1, 1.2);
|
|
7826
|
+
}
|
|
7827
|
+
}
|
|
7828
|
+
if (b.overhead) {
|
|
7829
|
+
const lipY = toY(Math.min(86, top + 12));
|
|
7830
|
+
const g2 = ctx.createLinearGradient(0, lipY, 0, yTop);
|
|
7831
|
+
g2.addColorStop(0, "rgba(3,4,8,0.9)");
|
|
7832
|
+
g2.addColorStop(1, "rgba(3,4,8,0)");
|
|
7833
|
+
ctx.fillStyle = g2;
|
|
7834
|
+
ctx.fillRect(x0, lipY, x1 - x0 + 1, yTop - lipY);
|
|
7835
|
+
ctx.fillStyle = "rgba(120,130,160,0.14)";
|
|
7836
|
+
ctx.fillRect(x0, yTop - 1.5, x1 - x0 + 1, 1.5);
|
|
7837
|
+
}
|
|
7838
|
+
}
|
|
7839
|
+
}
|
|
7840
|
+
function classifyScene(_seat, focal, neighbors) {
|
|
7841
|
+
const NB = 72;
|
|
7842
|
+
const occ = new Array(NB).fill(false);
|
|
7843
|
+
const innerM = new Array(NB).fill(Infinity);
|
|
7844
|
+
const secSet = /* @__PURE__ */ new Set();
|
|
7845
|
+
let n = 0;
|
|
7846
|
+
for (const s of neighbors) {
|
|
7847
|
+
n++;
|
|
7848
|
+
if (s.sectionId) secSet.add(s.sectionId);
|
|
7849
|
+
const dx = s.x - focal.x;
|
|
7850
|
+
const dy = s.y - focal.y;
|
|
7851
|
+
const dM = Math.hypot(dx, dy) * UNIT;
|
|
7852
|
+
if (dM < 0.5) continue;
|
|
7853
|
+
const bearing = Math.atan2(dx, -dy);
|
|
7854
|
+
let idx = Math.floor((bearing + Math.PI) / TAU * NB);
|
|
7855
|
+
if (idx < 0) idx = 0;
|
|
7856
|
+
else if (idx >= NB) idx = NB - 1;
|
|
7857
|
+
occ[idx] = true;
|
|
7858
|
+
if (dM < innerM[idx]) innerM[idx] = dM;
|
|
7859
|
+
}
|
|
7860
|
+
let occN = 0;
|
|
7861
|
+
const inner = [];
|
|
7862
|
+
for (let i = 0; i < NB; i++) if (occ[i]) {
|
|
7863
|
+
occN++;
|
|
7864
|
+
inner.push(innerM[i]);
|
|
7865
|
+
}
|
|
7866
|
+
const coverage = occN / NB * 360;
|
|
7867
|
+
inner.sort((a, b) => a - b);
|
|
7868
|
+
const median = inner.length ? inner[inner.length >> 1] : 0;
|
|
7869
|
+
const lo = inner.length ? inner[Math.floor(inner.length * 0.15)] ?? median : 0;
|
|
7870
|
+
const hi = inner.length ? inner[Math.floor(inner.length * 0.85)] ?? median : 0;
|
|
7871
|
+
const aspect = lo > 0.5 ? hi / lo : 1;
|
|
7872
|
+
const seed = fnv1a(`${n}|${[...secSet].sort().join(",")}|${Math.round(focal.x)},${Math.round(focal.y)}`);
|
|
7873
|
+
let mode;
|
|
7874
|
+
if (coverage >= 300 && median > 11 && aspect > 1.35) mode = "sport";
|
|
7875
|
+
else if (coverage >= 285) mode = "arena";
|
|
7876
|
+
else mode = "proscenium";
|
|
7877
|
+
return { mode, seed, voidRadiusM: median, aspect };
|
|
7878
|
+
}
|
|
7879
|
+
var HAIR_KINDS = 5;
|
|
7880
|
+
function drawHair(ctx, r, kind) {
|
|
7881
|
+
switch (kind) {
|
|
7882
|
+
case 1:
|
|
7883
|
+
ctx.beginPath();
|
|
7884
|
+
ctx.ellipse(0, -r * 0.14, r * 1.16, r * 1.16, 0, 0, TAU);
|
|
7885
|
+
ctx.fill();
|
|
7886
|
+
break;
|
|
7887
|
+
case 2:
|
|
7888
|
+
ctx.beginPath();
|
|
7889
|
+
ctx.ellipse(0, -r * 1.02, r * 0.42, r * 0.42, 0, 0, TAU);
|
|
7890
|
+
ctx.fill();
|
|
7891
|
+
ctx.beginPath();
|
|
7892
|
+
ctx.ellipse(0, -r * 0.2, r * 0.98, r * 1.02, 0, 0, TAU);
|
|
7893
|
+
ctx.fill();
|
|
7894
|
+
break;
|
|
7895
|
+
case 3:
|
|
7896
|
+
ctx.beginPath();
|
|
7897
|
+
ctx.ellipse(0, -r * 0.36, r * 1.04, r * 0.6, 0, Math.PI, TAU);
|
|
7898
|
+
ctx.fill();
|
|
7899
|
+
ctx.beginPath();
|
|
7900
|
+
ctx.ellipse(0, -r * 0.52, r * 1.22, r * 0.24, 0, 0, TAU);
|
|
7901
|
+
ctx.fill();
|
|
7902
|
+
break;
|
|
7903
|
+
case 4:
|
|
7904
|
+
ctx.beginPath();
|
|
7905
|
+
ctx.ellipse(-r * 0.66, r * 0.5, r * 0.5, r * 1.35, 0, 0, TAU);
|
|
7906
|
+
ctx.fill();
|
|
7907
|
+
ctx.beginPath();
|
|
7908
|
+
ctx.ellipse(r * 0.66, r * 0.5, r * 0.5, r * 1.35, 0, 0, TAU);
|
|
7909
|
+
ctx.fill();
|
|
7910
|
+
ctx.beginPath();
|
|
7911
|
+
ctx.ellipse(0, -r * 0.08, r * 1.06, r * 1.08, 0, 0, TAU);
|
|
7912
|
+
ctx.fill();
|
|
7913
|
+
break;
|
|
7914
|
+
default:
|
|
7915
|
+
break;
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7918
|
+
function drawSeated(ctx, hx, hy, r, o) {
|
|
7919
|
+
const neckHalf = r * 0.44;
|
|
7920
|
+
const shHalf = r * o.shoulderK;
|
|
7921
|
+
const neckTopY = r * 0.66;
|
|
7922
|
+
const shoulderY = r * 1.68;
|
|
7923
|
+
const botY = r * 5.4;
|
|
7924
|
+
const clo = `rgba(${o.clothing[0]},${o.clothing[1]},${o.clothing[2]},${o.alpha})`;
|
|
7925
|
+
const skin = `rgba(${o.tone[0]},${o.tone[1]},${o.tone[2]},${o.alpha})`;
|
|
7926
|
+
ctx.save();
|
|
7927
|
+
ctx.translate(hx, hy);
|
|
7928
|
+
const leanPx = o.lean * r;
|
|
7929
|
+
ctx.fillStyle = clo;
|
|
7930
|
+
ctx.beginPath();
|
|
7931
|
+
ctx.moveTo(-neckHalf, neckTopY);
|
|
7932
|
+
ctx.quadraticCurveTo(-neckHalf * 1.18, shoulderY - leanPx - r * 0.6, -shHalf, shoulderY - leanPx);
|
|
7933
|
+
ctx.quadraticCurveTo(-shHalf * 1.05, shoulderY + r * 1.2, -shHalf * 0.94, botY);
|
|
7934
|
+
ctx.lineTo(shHalf * 0.94, botY);
|
|
7935
|
+
ctx.quadraticCurveTo(shHalf * 1.05, shoulderY + r * 1.2, shHalf, shoulderY + leanPx);
|
|
7936
|
+
ctx.quadraticCurveTo(neckHalf * 1.18, shoulderY + leanPx - r * 0.6, neckHalf, neckTopY);
|
|
7937
|
+
ctx.closePath();
|
|
7938
|
+
ctx.fill();
|
|
7939
|
+
ctx.fillStyle = skin;
|
|
7940
|
+
ctx.fillRect(-neckHalf * 0.82, r * 0.5, neckHalf * 1.64, r * 0.9);
|
|
7941
|
+
ctx.save();
|
|
7942
|
+
ctx.rotate(o.tilt);
|
|
7943
|
+
ctx.translate(o.turn * r, 0);
|
|
7944
|
+
ctx.fillStyle = skin;
|
|
7945
|
+
if (r >= 18) {
|
|
7946
|
+
ctx.beginPath();
|
|
7947
|
+
ctx.moveTo(-r * 0.9, -r * 0.15);
|
|
7948
|
+
ctx.quadraticCurveTo(-r * 0.96, -r * 1.12, 0, -r * 1.06);
|
|
7949
|
+
ctx.quadraticCurveTo(r * 0.96, -r * 1.12, r * 0.9, -r * 0.15);
|
|
7950
|
+
ctx.quadraticCurveTo(r * 0.86, r * 0.55, r * 0.36, r * 0.98);
|
|
7951
|
+
ctx.quadraticCurveTo(0, r * 1.14, -r * 0.36, r * 0.98);
|
|
7952
|
+
ctx.quadraticCurveTo(-r * 0.86, r * 0.55, -r * 0.9, -r * 0.15);
|
|
7953
|
+
ctx.closePath();
|
|
7954
|
+
ctx.fill();
|
|
7955
|
+
if (r >= 22) {
|
|
7956
|
+
ctx.beginPath();
|
|
7957
|
+
ctx.ellipse(-r * 0.9, r * 0.18, r * 0.13, r * 0.22, 0, 0, TAU);
|
|
7958
|
+
ctx.ellipse(r * 0.9, r * 0.18, r * 0.13, r * 0.22, 0, 0, TAU);
|
|
7959
|
+
ctx.fill();
|
|
7960
|
+
}
|
|
7961
|
+
} else {
|
|
7962
|
+
ctx.beginPath();
|
|
7963
|
+
ctx.ellipse(0, 0, r * 0.9, r * 1.06, 0, 0, TAU);
|
|
7964
|
+
ctx.fill();
|
|
7965
|
+
}
|
|
7966
|
+
drawHair(ctx, r, o.hair);
|
|
7967
|
+
if (o.rim > 0.01) {
|
|
7968
|
+
ctx.strokeStyle = `rgba(245,205,150,${(0.5 * o.rim).toFixed(3)})`;
|
|
7969
|
+
ctx.lineWidth = Math.max(1, r * 0.14);
|
|
7970
|
+
ctx.beginPath();
|
|
7971
|
+
ctx.ellipse(0, -r * 0.06, r * 0.86, r * 1, 0, Math.PI * 1.12, Math.PI * 1.9);
|
|
7972
|
+
ctx.stroke();
|
|
7973
|
+
}
|
|
7974
|
+
ctx.restore();
|
|
7975
|
+
if (o.rim > 0.01) {
|
|
7976
|
+
ctx.strokeStyle = `rgba(240,200,150,${(0.28 * o.rim).toFixed(3)})`;
|
|
7977
|
+
ctx.lineWidth = Math.max(1, r * 0.12);
|
|
7978
|
+
ctx.beginPath();
|
|
7979
|
+
ctx.moveTo(-shHalf * 0.8, shoulderY - r * 0.05);
|
|
7980
|
+
ctx.quadraticCurveTo(-neckHalf, shoulderY - r * 0.75, 0, shoulderY - r * 0.82);
|
|
7981
|
+
ctx.quadraticCurveTo(neckHalf, shoulderY - r * 0.75, shHalf * 0.8, shoulderY - r * 0.05);
|
|
7982
|
+
ctx.stroke();
|
|
7983
|
+
}
|
|
7984
|
+
ctx.restore();
|
|
7985
|
+
}
|
|
7986
|
+
function drawSeatedMid(ctx, hx, hy, r, clothing, tone, alpha) {
|
|
7987
|
+
ctx.fillStyle = `rgba(${clothing[0]},${clothing[1]},${clothing[2]},${alpha})`;
|
|
7988
|
+
ctx.beginPath();
|
|
7989
|
+
ctx.moveTo(hx - r * 1.72, hy + r * 4);
|
|
7990
|
+
ctx.quadraticCurveTo(hx - r * 1.78, hy + r * 1.55, hx - r * 0.46, hy + r * 1.02);
|
|
7991
|
+
ctx.lineTo(hx - r * 0.4, hy + r * 0.62);
|
|
7992
|
+
ctx.lineTo(hx + r * 0.4, hy + r * 0.62);
|
|
7993
|
+
ctx.lineTo(hx + r * 0.46, hy + r * 1.02);
|
|
7994
|
+
ctx.quadraticCurveTo(hx + r * 1.78, hy + r * 1.55, hx + r * 1.72, hy + r * 4);
|
|
7995
|
+
ctx.closePath();
|
|
7996
|
+
ctx.fill();
|
|
7997
|
+
ctx.fillStyle = `rgba(${tone[0]},${tone[1]},${tone[2]},${alpha})`;
|
|
7998
|
+
ctx.beginPath();
|
|
7999
|
+
ctx.ellipse(hx, hy, r * 0.9, r * 1.05, 0, 0, TAU);
|
|
8000
|
+
ctx.fill();
|
|
8001
|
+
}
|
|
8002
|
+
function drawPerformer(ctx, x, footY, h, o) {
|
|
8003
|
+
const headR = h * 0.1;
|
|
8004
|
+
const headCy = footY - h * 0.9;
|
|
8005
|
+
const shoulderY = footY - h * 0.74;
|
|
8006
|
+
const hipY = footY - h * 0.46;
|
|
8007
|
+
const shHalf = h * 0.13;
|
|
8008
|
+
const hipHalf = h * 0.09;
|
|
8009
|
+
const skin = `rgba(${o.tone[0]},${o.tone[1]},${o.tone[2]},${o.alpha})`;
|
|
8010
|
+
const sh = ctx.createRadialGradient(x, footY, 1, x, footY, h * 0.24);
|
|
8011
|
+
sh.addColorStop(0, "rgba(0,0,0,0.4)");
|
|
8012
|
+
sh.addColorStop(1, "rgba(0,0,0,0)");
|
|
8013
|
+
ctx.save();
|
|
8014
|
+
ctx.scale(1, 0.28);
|
|
8015
|
+
ctx.fillStyle = sh;
|
|
8016
|
+
ctx.beginPath();
|
|
8017
|
+
ctx.ellipse(x, footY / 0.28, h * 0.24, h * 0.24, 0, 0, TAU);
|
|
8018
|
+
ctx.fill();
|
|
8019
|
+
ctx.restore();
|
|
8020
|
+
ctx.fillStyle = skin;
|
|
8021
|
+
ctx.beginPath();
|
|
8022
|
+
ctx.moveTo(x - hipHalf, hipY);
|
|
8023
|
+
ctx.lineTo(x - hipHalf * 0.7, footY);
|
|
8024
|
+
ctx.lineTo(x - hipHalf * 0.1, footY);
|
|
8025
|
+
ctx.lineTo(x - hipHalf * 0.1, hipY + h * 0.02);
|
|
8026
|
+
ctx.lineTo(x + hipHalf * 0.1, hipY + h * 0.02);
|
|
8027
|
+
ctx.lineTo(x + hipHalf * 0.1, footY);
|
|
8028
|
+
ctx.lineTo(x + hipHalf * 0.7, footY);
|
|
8029
|
+
ctx.lineTo(x + hipHalf, hipY);
|
|
8030
|
+
ctx.closePath();
|
|
8031
|
+
ctx.fill();
|
|
8032
|
+
ctx.beginPath();
|
|
8033
|
+
ctx.moveTo(x - shHalf, shoulderY);
|
|
8034
|
+
ctx.quadraticCurveTo(x - shHalf * 0.9, hipY - h * 0.12, x - hipHalf, hipY);
|
|
8035
|
+
ctx.lineTo(x + hipHalf, hipY);
|
|
8036
|
+
ctx.quadraticCurveTo(x + shHalf * 0.9, hipY - h * 0.12, x + shHalf, shoulderY);
|
|
8037
|
+
ctx.quadraticCurveTo(x, shoulderY - h * 0.05, x - shHalf, shoulderY);
|
|
8038
|
+
ctx.closePath();
|
|
8039
|
+
ctx.fill();
|
|
8040
|
+
ctx.lineCap = "round";
|
|
8041
|
+
ctx.lineJoin = "round";
|
|
8042
|
+
ctx.strokeStyle = skin;
|
|
8043
|
+
ctx.lineWidth = h * 0.055;
|
|
8044
|
+
const armY = shoulderY + h * 0.02;
|
|
8045
|
+
if (o.pose === 1) {
|
|
8046
|
+
ctx.beginPath();
|
|
8047
|
+
ctx.moveTo(x - shHalf * 0.8, armY);
|
|
8048
|
+
ctx.lineTo(x - shHalf * 1.5, footY - h * 1.02);
|
|
8049
|
+
ctx.moveTo(x + shHalf * 0.8, armY);
|
|
8050
|
+
ctx.lineTo(x + shHalf * 1.5, footY - h * 1.02);
|
|
8051
|
+
ctx.stroke();
|
|
8052
|
+
} else if (o.pose === 2) {
|
|
8053
|
+
ctx.beginPath();
|
|
8054
|
+
ctx.moveTo(x - shHalf * 0.8, armY);
|
|
8055
|
+
ctx.lineTo(x + hipHalf * 1.4, hipY - h * 0.02);
|
|
8056
|
+
ctx.moveTo(x + shHalf * 0.8, armY);
|
|
8057
|
+
ctx.lineTo(x - hipHalf * 0.4, hipY - h * 0.06);
|
|
8058
|
+
ctx.stroke();
|
|
8059
|
+
ctx.fillStyle = skin;
|
|
8060
|
+
ctx.beginPath();
|
|
8061
|
+
ctx.ellipse(x + hipHalf * 1.5, hipY - h * 0.04, h * 0.07, h * 0.09, -0.5, 0, TAU);
|
|
8062
|
+
ctx.fill();
|
|
8063
|
+
} else if (o.pose === 3) {
|
|
8064
|
+
ctx.beginPath();
|
|
8065
|
+
ctx.moveTo(x - shHalf * 0.8, armY);
|
|
8066
|
+
ctx.lineTo(x - shHalf * 0.2, headCy + headR * 0.6);
|
|
8067
|
+
ctx.moveTo(x + shHalf * 0.8, armY);
|
|
8068
|
+
ctx.lineTo(x + shHalf * 1.1, hipY);
|
|
8069
|
+
ctx.stroke();
|
|
8070
|
+
ctx.lineWidth = h * 0.02;
|
|
8071
|
+
ctx.beginPath();
|
|
8072
|
+
ctx.moveTo(x - shHalf * 0.2, headCy + headR * 0.9);
|
|
8073
|
+
ctx.lineTo(x - shHalf * 0.2, footY - h * 0.02);
|
|
8074
|
+
ctx.stroke();
|
|
8075
|
+
} else {
|
|
8076
|
+
ctx.beginPath();
|
|
8077
|
+
ctx.moveTo(x - shHalf * 0.85, armY);
|
|
8078
|
+
ctx.lineTo(x - shHalf * 0.95, hipY + h * 0.02);
|
|
8079
|
+
ctx.moveTo(x + shHalf * 0.85, armY);
|
|
8080
|
+
ctx.lineTo(x + shHalf * 0.95, hipY + h * 0.02);
|
|
8081
|
+
ctx.stroke();
|
|
8082
|
+
}
|
|
8083
|
+
ctx.fillStyle = skin;
|
|
8084
|
+
ctx.fillRect(x - headR * 0.34, headCy + headR * 0.6, headR * 0.68, headR * 0.9);
|
|
8085
|
+
ctx.beginPath();
|
|
8086
|
+
ctx.ellipse(x, headCy, headR * 0.86, headR * 1.05, 0, 0, TAU);
|
|
8087
|
+
ctx.fill();
|
|
8088
|
+
if (o.rimStrength > 0.01) {
|
|
8089
|
+
ctx.strokeStyle = `rgba(${o.rim[0]},${o.rim[1]},${o.rim[2]},${(0.6 * o.rimStrength).toFixed(3)})`;
|
|
8090
|
+
ctx.lineWidth = Math.max(1, h * 0.02);
|
|
8091
|
+
ctx.beginPath();
|
|
8092
|
+
ctx.moveTo(x - shHalf, shoulderY);
|
|
8093
|
+
ctx.lineTo(x - headR * 0.6, headCy + headR * 0.4);
|
|
8094
|
+
ctx.moveTo(x - headR * 0.7, headCy);
|
|
8095
|
+
ctx.ellipse(x, headCy, headR * 0.84, headR * 1.02, 0, Math.PI * 1.05, Math.PI * 1.55);
|
|
8096
|
+
ctx.stroke();
|
|
8097
|
+
}
|
|
8098
|
+
}
|
|
8099
|
+
function drawBeam(ctx, x0, y0, x1, y1, halfTop, halfBot, tint, strength) {
|
|
8100
|
+
ctx.save();
|
|
8101
|
+
ctx.globalCompositeOperation = "lighter";
|
|
8102
|
+
const g = ctx.createLinearGradient(0, y0, 0, y1);
|
|
8103
|
+
g.addColorStop(0, `rgba(${tint},${strength})`);
|
|
8104
|
+
g.addColorStop(0.5, `rgba(${tint},${(strength * 0.4).toFixed(3)})`);
|
|
8105
|
+
g.addColorStop(1, `rgba(${tint},0)`);
|
|
8106
|
+
ctx.fillStyle = g;
|
|
8107
|
+
ctx.beginPath();
|
|
8108
|
+
ctx.moveTo(x0 - halfTop, y0);
|
|
8109
|
+
ctx.lineTo(x1 - halfBot, y1);
|
|
8110
|
+
ctx.lineTo(x1 + halfBot, y1);
|
|
8111
|
+
ctx.lineTo(x0 + halfTop, y0);
|
|
8112
|
+
ctx.closePath();
|
|
8113
|
+
ctx.fill();
|
|
8114
|
+
ctx.restore();
|
|
8115
|
+
}
|
|
8116
|
+
function drawFixture(ctx, x, y, r, tint) {
|
|
8117
|
+
const g = ctx.createRadialGradient(x, y, 0, x, y, r * 3);
|
|
8118
|
+
g.addColorStop(0, `rgba(${tint},0.9)`);
|
|
8119
|
+
g.addColorStop(0.4, `rgba(${tint},0.35)`);
|
|
8120
|
+
g.addColorStop(1, `rgba(${tint},0)`);
|
|
8121
|
+
ctx.fillStyle = g;
|
|
8122
|
+
ctx.beginPath();
|
|
8123
|
+
ctx.arc(x, y, r * 3, 0, TAU);
|
|
8124
|
+
ctx.fill();
|
|
8125
|
+
ctx.fillStyle = `rgba(${tint},1)`;
|
|
8126
|
+
ctx.beginPath();
|
|
8127
|
+
ctx.arc(x, y, r, 0, TAU);
|
|
8128
|
+
ctx.fill();
|
|
8129
|
+
}
|
|
8514
8130
|
function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
8515
8131
|
const canvas = document.createElement("canvas");
|
|
8516
8132
|
canvas.width = W;
|
|
@@ -8520,102 +8136,557 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
|
8520
8136
|
const dy = focalPoint.y - seat.y;
|
|
8521
8137
|
const distU = Math.hypot(dx, dy);
|
|
8522
8138
|
const distM = Math.max(2, distU * UNIT);
|
|
8139
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
8140
|
+
const scene = classifyScene(seat, focalPoint, neighborSeats);
|
|
8141
|
+
const rand = rng(scene.seed);
|
|
8142
|
+
const baseHue = 210 + rand() * 150;
|
|
8143
|
+
const rigTint = hslToRgb(220 + (rand() - 0.5) * 60, 0.55, 0.82);
|
|
8144
|
+
const rigTintStr = `${rigTint[0]},${rigTint[1]},${rigTint[2]}`;
|
|
8523
8145
|
const sky = ctx.createLinearGradient(0, 0, 0, H);
|
|
8524
|
-
sky.addColorStop(0, "#
|
|
8525
|
-
sky.addColorStop(0.42, "#
|
|
8526
|
-
sky.addColorStop(0.5, "#
|
|
8527
|
-
sky.addColorStop(0.56, "#
|
|
8528
|
-
sky.addColorStop(
|
|
8146
|
+
sky.addColorStop(0, "#080b12");
|
|
8147
|
+
sky.addColorStop(0.42, "#12182c");
|
|
8148
|
+
sky.addColorStop(0.5, "#1c2440");
|
|
8149
|
+
sky.addColorStop(0.56, "#151b2c");
|
|
8150
|
+
sky.addColorStop(0.78, "#171e30");
|
|
8151
|
+
sky.addColorStop(1, "#131829");
|
|
8529
8152
|
ctx.fillStyle = sky;
|
|
8530
8153
|
ctx.fillRect(0, 0, W, H);
|
|
8531
|
-
const
|
|
8532
|
-
|
|
8154
|
+
const floorAmb = ctx.createLinearGradient(0, pitchToY(-6), 0, H);
|
|
8155
|
+
floorAmb.addColorStop(0, "rgba(30, 37, 56, 0)");
|
|
8156
|
+
floorAmb.addColorStop(0.55, "rgba(30, 37, 56, 0.22)");
|
|
8157
|
+
floorAmb.addColorStop(1, "rgba(22, 28, 44, 0.12)");
|
|
8158
|
+
ctx.fillStyle = floorAmb;
|
|
8159
|
+
ctx.fillRect(0, pitchToY(-6), W, H - pitchToY(-6));
|
|
8160
|
+
const stageBearing = Math.atan2(dx, -dy);
|
|
8161
|
+
const stands = buildStands(seat, neighborSeats, stageBearing, eyeM);
|
|
8162
|
+
const hasRelief = stands.maxDelta >= FLAT_DELTA_M;
|
|
8163
|
+
if (hasRelief) drawStands(ctx, stands.bins, yawToX, pitchToY);
|
|
8164
|
+
const nearGlow = Math.max(0.3, Math.min(1, 1 - (distM - 6) / 60));
|
|
8533
8165
|
const sightline = stageSightlinePitch(eyeM, distM);
|
|
8166
|
+
const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
|
|
8534
8167
|
const stageTopPitch = Math.min(45, sightline.topPitch);
|
|
8535
8168
|
const stageBasePitch = sightline.basePitch;
|
|
8169
|
+
if (scene.mode === "sport") {
|
|
8170
|
+
drawSportScene(ctx, distM, eyeM, scene, nearGlow, baseHue, rigTintStr, rand);
|
|
8171
|
+
} else if (scene.mode === "arena") {
|
|
8172
|
+
drawArenaScene(ctx, distM, eyeM, scene, nearGlow, rigTintStr, rand);
|
|
8173
|
+
} else {
|
|
8174
|
+
drawProsceniumScene(ctx, stageHalfYaw, stageTopPitch, stageBasePitch, nearGlow, baseHue, rigTintStr, rand);
|
|
8175
|
+
}
|
|
8176
|
+
const rigCount = scene.mode === "sport" ? 34 : 24 + Math.floor(rand() * 8);
|
|
8177
|
+
ctx.fillStyle = `rgba(${rigTintStr},0.8)`;
|
|
8178
|
+
for (let i = 0; i < rigCount; i++) {
|
|
8179
|
+
const x = (i + 0.5) / rigCount * W;
|
|
8180
|
+
const y = pitchToY(scene.mode === "sport" ? 58 : 54 + i * 7 % 3 * 6);
|
|
8181
|
+
ctx.beginPath();
|
|
8182
|
+
ctx.arc(x, y, scene.mode === "sport" ? 2.6 : 3.2, 0, TAU);
|
|
8183
|
+
ctx.fill();
|
|
8184
|
+
}
|
|
8185
|
+
drawAudience(ctx, seat, neighborSeats, focalPoint, stageBearing, eyeM, dx, dy, hasRelief, scene);
|
|
8186
|
+
const poleFade = ctx.createLinearGradient(0, 0, 0, H);
|
|
8187
|
+
poleFade.addColorStop(0, "rgba(0,0,0,0.85)");
|
|
8188
|
+
poleFade.addColorStop(0.12, "rgba(0,0,0,0)");
|
|
8189
|
+
poleFade.addColorStop(0.9, "rgba(0,0,0,0)");
|
|
8190
|
+
poleFade.addColorStop(1, "rgba(0,0,0,0.55)");
|
|
8191
|
+
ctx.fillStyle = poleFade;
|
|
8192
|
+
ctx.fillRect(0, 0, W, H);
|
|
8193
|
+
return { url: canvas.toDataURL("image/jpeg", 0.82), distanceM: Math.round(distM) };
|
|
8194
|
+
}
|
|
8195
|
+
function drawProsceniumScene(ctx, stageHalfYaw, stageTopPitch, stageBasePitch, nearGlow, baseHue, rigTintStr, rand) {
|
|
8536
8196
|
const sx0 = yawToX(-stageHalfYaw);
|
|
8537
8197
|
const sx1 = yawToX(stageHalfYaw);
|
|
8538
8198
|
const sy0 = pitchToY(stageTopPitch);
|
|
8539
8199
|
const sy1 = pitchToY(stageBasePitch);
|
|
8540
|
-
const
|
|
8541
|
-
|
|
8542
|
-
|
|
8200
|
+
const sw = sx1 - sx0;
|
|
8201
|
+
const sh = sy1 - sy0;
|
|
8202
|
+
const archTop = pitchToY(Math.min(50, stageTopPitch + 10));
|
|
8203
|
+
const glowR = sw * (1.1 + (1 - nearGlow) * 0.5);
|
|
8204
|
+
const glow = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, 8, W / 2, (sy0 + sy1) / 2, glowR);
|
|
8205
|
+
glow.addColorStop(0, "rgba(255, 214, 150, 0.34)");
|
|
8206
|
+
glow.addColorStop(0.28, "rgba(150, 150, 230, 0.3)");
|
|
8207
|
+
glow.addColorStop(0.6, "rgba(99, 102, 241, 0.12)");
|
|
8543
8208
|
glow.addColorStop(1, "rgba(99, 102, 241, 0)");
|
|
8544
8209
|
ctx.fillStyle = glow;
|
|
8545
|
-
ctx.fillRect(sx0 -
|
|
8546
|
-
|
|
8547
|
-
|
|
8210
|
+
ctx.fillRect(sx0 - sw * 0.6, sy0 - sh * 1.2, sw * 2.2, sh * 3.4);
|
|
8211
|
+
const topCol = hslToRgb(baseHue, 0.62, 0.55);
|
|
8212
|
+
const midCol = hslToRgb(baseHue + 40, 0.7, 0.5);
|
|
8213
|
+
const botCol = hslToRgb(baseHue + 70, 0.78, 0.52);
|
|
8214
|
+
ctx.save();
|
|
8215
|
+
ctx.beginPath();
|
|
8216
|
+
ctx.rect(sx0, sy0, sw, sh);
|
|
8217
|
+
ctx.clip();
|
|
8548
8218
|
const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
|
|
8549
|
-
backdrop.addColorStop(0,
|
|
8550
|
-
backdrop.addColorStop(0.5,
|
|
8551
|
-
backdrop.addColorStop(1,
|
|
8552
|
-
ctx.globalAlpha = 0.
|
|
8219
|
+
backdrop.addColorStop(0, `rgb(${topCol[0]},${topCol[1]},${topCol[2]})`);
|
|
8220
|
+
backdrop.addColorStop(0.5, `rgb(${midCol[0]},${midCol[1]},${midCol[2]})`);
|
|
8221
|
+
backdrop.addColorStop(1, `rgb(${botCol[0]},${botCol[1]},${botCol[2]})`);
|
|
8222
|
+
ctx.globalAlpha = 0.8;
|
|
8553
8223
|
ctx.fillStyle = backdrop;
|
|
8554
|
-
|
|
8555
|
-
ctx.fillRect(sx0 + inset, sy0 + (sy1 - sy0) * 0.12, sx1 - sx0 - inset * 2, (sy1 - sy0) * 0.62);
|
|
8224
|
+
ctx.fillRect(sx0, sy0, sw, sh);
|
|
8556
8225
|
ctx.globalAlpha = 1;
|
|
8557
|
-
ctx.
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8226
|
+
const fall = ctx.createRadialGradient(W / 2, (sy0 + sy1) / 2, sw * 0.12, W / 2, (sy0 + sy1) / 2, sw * 0.62);
|
|
8227
|
+
fall.addColorStop(0, "rgba(6,8,14,0)");
|
|
8228
|
+
fall.addColorStop(0.7, "rgba(6,8,14,0.15)");
|
|
8229
|
+
fall.addColorStop(1, "rgba(5,6,11,0.92)");
|
|
8230
|
+
ctx.fillStyle = fall;
|
|
8231
|
+
ctx.fillRect(sx0, sy0, sw, sh);
|
|
8232
|
+
ctx.restore();
|
|
8233
|
+
const perfCount = 3 + Math.floor(rand() * 3);
|
|
8234
|
+
const perfH = sh * 0.46;
|
|
8235
|
+
const floorY = sy1 - sh * 0.08;
|
|
8236
|
+
for (let i = 0; i < perfCount; i++) {
|
|
8237
|
+
const t2 = (i + 1) / (perfCount + 1);
|
|
8238
|
+
const px = sx0 + sw * (0.22 + t2 * 0.56 + (rand() - 0.5) * 0.06);
|
|
8239
|
+
drawPerformer(ctx, px, floorY, perfH * (0.9 + rand() * 0.24), {
|
|
8240
|
+
tone: [8, 10, 16],
|
|
8241
|
+
alpha: 0.9,
|
|
8242
|
+
pose: Math.floor(rand() * 4),
|
|
8243
|
+
rim: hslToRgb(baseHue + 20, 0.5, 0.7),
|
|
8244
|
+
rimStrength: 0.7
|
|
8245
|
+
});
|
|
8246
|
+
}
|
|
8247
|
+
ctx.fillStyle = "#05070d";
|
|
8248
|
+
ctx.fillRect(sx0 - sw * 0.5, archTop, sw * 0.5, sy1 - archTop);
|
|
8249
|
+
ctx.fillRect(sx1, archTop, sw * 0.5, sy1 - archTop);
|
|
8250
|
+
const legBot = sw * 0.055 + 3;
|
|
8251
|
+
const legTop = legBot * 1.35;
|
|
8252
|
+
ctx.fillStyle = "#03050a";
|
|
8253
|
+
ctx.beginPath();
|
|
8254
|
+
ctx.moveTo(sx0, sy1);
|
|
8255
|
+
ctx.lineTo(sx0 - legBot, sy1);
|
|
8256
|
+
ctx.lineTo(sx0 - legTop, archTop);
|
|
8257
|
+
ctx.lineTo(sx0 + sw * 0.012, archTop);
|
|
8258
|
+
ctx.closePath();
|
|
8259
|
+
ctx.fill();
|
|
8260
|
+
ctx.beginPath();
|
|
8261
|
+
ctx.moveTo(sx1, sy1);
|
|
8262
|
+
ctx.lineTo(sx1 + legBot, sy1);
|
|
8263
|
+
ctx.lineTo(sx1 + legTop, archTop);
|
|
8264
|
+
ctx.lineTo(sx1 - sw * 0.012, archTop);
|
|
8265
|
+
ctx.closePath();
|
|
8266
|
+
ctx.fill();
|
|
8267
|
+
ctx.fillRect(sx0 - legTop, archTop, sw + legTop * 2, (sy1 - archTop) * 0.12);
|
|
8268
|
+
const valH = sh * 0.14;
|
|
8269
|
+
const valY = archTop + (sy1 - archTop) * 0.12;
|
|
8270
|
+
ctx.fillStyle = "rgba(4,6,12,0.96)";
|
|
8271
|
+
const scallops = 7;
|
|
8272
|
+
ctx.beginPath();
|
|
8273
|
+
ctx.moveTo(sx0 - legTop, valY);
|
|
8274
|
+
for (let i = 0; i <= scallops; i++) {
|
|
8275
|
+
const xx = sx0 - legTop + (sw + legTop * 2) * i / scallops;
|
|
8276
|
+
ctx.quadraticCurveTo(xx - (sw + legTop * 2) / scallops / 2, valY + valH, xx, valY);
|
|
8277
|
+
}
|
|
8278
|
+
ctx.lineTo(sx1 + legTop, valY);
|
|
8279
|
+
ctx.lineTo(sx1 + legTop, archTop);
|
|
8280
|
+
ctx.lineTo(sx0 - legTop, archTop);
|
|
8281
|
+
ctx.closePath();
|
|
8282
|
+
ctx.fill();
|
|
8283
|
+
ctx.fillStyle = `rgba(${rigTintStr},0.12)`;
|
|
8284
|
+
ctx.fillRect(sx0 - 1.5, valY, 1.5, sy1 - valY);
|
|
8285
|
+
ctx.fillRect(sx1, valY, 1.5, sy1 - valY);
|
|
8286
|
+
ctx.fillStyle = "#0b0f18";
|
|
8287
|
+
ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.05 + 3);
|
|
8288
|
+
const foot = ctx.createLinearGradient(0, sy1 - sh * 0.05, 0, sy1 + sh * 0.12);
|
|
8289
|
+
foot.addColorStop(0, `rgba(255,206,140,${(0.5 * nearGlow).toFixed(3)})`);
|
|
8290
|
+
foot.addColorStop(1, "rgba(255,206,140,0)");
|
|
8291
|
+
ctx.fillStyle = foot;
|
|
8292
|
+
ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.2);
|
|
8293
|
+
const beams = 3 + Math.floor(rand() * 2);
|
|
8294
|
+
for (let i = 0; i < beams; i++) {
|
|
8295
|
+
const t2 = (i + 1) / (beams + 1);
|
|
8296
|
+
const fx = sx0 + sw * (t2 + (rand() - 0.5) * 0.08);
|
|
8297
|
+
const fy = pitchToY(Math.min(62, stageTopPitch + 26));
|
|
8298
|
+
const skew = (rand() - 0.5) * sw * 0.16;
|
|
8299
|
+
drawBeam(ctx, fx, fy, fx + skew, sy1, 4, sw * 0.09, rigTintStr, 0.16 * (0.7 + nearGlow * 0.5));
|
|
8300
|
+
drawFixture(ctx, fx, fy, 2.4, rigTintStr);
|
|
8301
|
+
}
|
|
8302
|
+
}
|
|
8303
|
+
function drawArenaScene(ctx, distM, eyeM, scene, nearGlow, rigTintStr, rand) {
|
|
8304
|
+
const stageR = Math.min(distM * 0.6, Math.max(3, scene.voidRadiusM));
|
|
8305
|
+
const halfYaw = Math.min(55, Math.atan2(stageR, distM) * 180 / Math.PI);
|
|
8306
|
+
const cx = W / 2;
|
|
8307
|
+
const deckRiserM = 1.2;
|
|
8308
|
+
const nearDist = Math.max(distM * 0.45, distM - stageR);
|
|
8309
|
+
const farDist = distM + stageR;
|
|
8310
|
+
const nearTopPitch = Math.atan2(deckRiserM - eyeM, nearDist) * 180 / Math.PI;
|
|
8311
|
+
const farTopPitch = Math.atan2(deckRiserM - eyeM, farDist) * 180 / Math.PI;
|
|
8312
|
+
const nearBasePitch = Math.atan2(-eyeM, nearDist) * 180 / Math.PI;
|
|
8313
|
+
const yNearTop = pitchToY(nearTopPitch);
|
|
8314
|
+
const yFarTop = pitchToY(farTopPitch);
|
|
8315
|
+
const yBase = pitchToY(nearBasePitch);
|
|
8316
|
+
const halfW = yawToX(halfYaw) - cx;
|
|
8317
|
+
const topRy = Math.max(6, (yNearTop - yFarTop) / 2);
|
|
8318
|
+
const topCy = (yNearTop + yFarTop) / 2;
|
|
8319
|
+
const pool = ctx.createRadialGradient(cx, yBase, 4, cx, yBase, halfW * 2.6);
|
|
8320
|
+
pool.addColorStop(0, `rgba(255,206,150,${(0.2 * nearGlow).toFixed(3)})`);
|
|
8321
|
+
pool.addColorStop(0.5, "rgba(150,140,190,0.08)");
|
|
8322
|
+
pool.addColorStop(1, "rgba(40,44,60,0)");
|
|
8323
|
+
ctx.save();
|
|
8324
|
+
ctx.globalCompositeOperation = "lighter";
|
|
8325
|
+
ctx.fillStyle = pool;
|
|
8326
|
+
ctx.fillRect(cx - halfW * 2.6, yFarTop - topRy, halfW * 5.2, yBase - yFarTop + topRy + 40);
|
|
8327
|
+
ctx.restore();
|
|
8328
|
+
ctx.fillStyle = "#0c1019";
|
|
8329
|
+
ctx.beginPath();
|
|
8330
|
+
ctx.moveTo(cx - halfW, topCy);
|
|
8331
|
+
ctx.ellipse(cx, topCy, halfW, topRy, 0, Math.PI, 0, false);
|
|
8332
|
+
ctx.lineTo(cx + halfW, yBase);
|
|
8333
|
+
ctx.ellipse(cx, yBase, halfW, topRy, 0, 0, Math.PI, false);
|
|
8334
|
+
ctx.closePath();
|
|
8335
|
+
ctx.fill();
|
|
8336
|
+
const riser = ctx.createLinearGradient(0, topCy, 0, yBase);
|
|
8337
|
+
riser.addColorStop(0, `rgba(${rigTintStr},0.14)`);
|
|
8338
|
+
riser.addColorStop(1, "rgba(0,0,0,0)");
|
|
8339
|
+
ctx.fillStyle = riser;
|
|
8340
|
+
ctx.fillRect(cx - halfW, topCy, halfW * 2, yBase - topCy);
|
|
8341
|
+
const FORMATIONS = [
|
|
8342
|
+
// frontman + guitarist + one at the back
|
|
8343
|
+
[{ u: -0.24, v: 0.4, lead: true, pose: 3 }, { u: 0.34, v: -0.04, pose: 2 }, { u: -0.52, v: -0.38, pose: 0 }],
|
|
8344
|
+
// duo — singer forward, instrument behind the shoulder
|
|
8345
|
+
[{ u: 0.2, v: 0.34, lead: true, pose: 3 }, { u: -0.3, v: -0.08, pose: 2 }],
|
|
8346
|
+
// 4-piece — lead just off-centre, band staggered around the far half
|
|
8347
|
+
[{ u: -0.1, v: 0.28, lead: true, pose: 1 }, { u: 0.44, v: -0.2, pose: 2 }, { u: -0.46, v: -0.14, pose: 2 }, { u: 0.08, v: -0.46, pose: 0 }]
|
|
8348
|
+
];
|
|
8349
|
+
const form = FORMATIONS[Math.floor(rand() * FORMATIONS.length)];
|
|
8350
|
+
const mirror = rand() < 0.5 ? -1 : 1;
|
|
8351
|
+
const spotX = (p) => cx + p.u * mirror * halfW * 0.84;
|
|
8352
|
+
const spotY = (p) => topCy + p.v * topRy * 0.8;
|
|
8353
|
+
const lead = form.find((p) => p.lead) ?? form[0];
|
|
8354
|
+
const leadX = spotX(lead);
|
|
8355
|
+
const deckTop = ctx.createLinearGradient(0, topCy - topRy, 0, topCy + topRy);
|
|
8356
|
+
deckTop.addColorStop(0, "#3d4a72");
|
|
8357
|
+
deckTop.addColorStop(1, "#232b47");
|
|
8358
|
+
ctx.fillStyle = deckTop;
|
|
8359
|
+
ctx.beginPath();
|
|
8360
|
+
ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU);
|
|
8361
|
+
ctx.fill();
|
|
8362
|
+
const deckGlow = ctx.createRadialGradient(leadX, topCy, 2, leadX, topCy, halfW);
|
|
8363
|
+
deckGlow.addColorStop(0, `rgba(255,214,160,${(0.5 * nearGlow).toFixed(3)})`);
|
|
8364
|
+
deckGlow.addColorStop(1, "rgba(255,214,160,0)");
|
|
8365
|
+
ctx.save();
|
|
8366
|
+
ctx.globalCompositeOperation = "lighter";
|
|
8367
|
+
ctx.fillStyle = deckGlow;
|
|
8368
|
+
ctx.beginPath();
|
|
8369
|
+
ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU);
|
|
8370
|
+
ctx.fill();
|
|
8371
|
+
ctx.restore();
|
|
8372
|
+
ctx.strokeStyle = `rgba(${rigTintStr},0.7)`;
|
|
8373
|
+
ctx.lineWidth = 2;
|
|
8374
|
+
ctx.beginPath();
|
|
8375
|
+
ctx.ellipse(cx, topCy, halfW, topRy, 0, 0, TAU);
|
|
8376
|
+
ctx.stroke();
|
|
8377
|
+
const perfH = Math.max(24, (yBase - yFarTop) * 1.3);
|
|
8378
|
+
const gearN = form.length >= 3 ? 3 : 2;
|
|
8379
|
+
for (let i = 0; i < gearN; i++) {
|
|
8380
|
+
const gu = (-0.62 + i * 0.52 + (rand() - 0.5) * 0.12) * -mirror;
|
|
8381
|
+
const gx = cx + gu * halfW * 0.7;
|
|
8382
|
+
const gy = topCy - topRy * (0.45 + rand() * 0.18);
|
|
8383
|
+
const gw = halfW * (0.07 + rand() * 0.04);
|
|
8384
|
+
const gh = perfH * (0.26 + rand() * 0.12);
|
|
8385
|
+
ctx.fillStyle = "#0a0e18";
|
|
8386
|
+
ctx.fillRect(gx - gw / 2, gy - gh, gw, gh);
|
|
8387
|
+
ctx.fillStyle = `rgba(${rigTintStr},0.3)`;
|
|
8388
|
+
ctx.fillRect(gx - gw / 2, gy - gh, gw, 1.5);
|
|
8389
|
+
}
|
|
8390
|
+
if (form.length >= 3) {
|
|
8391
|
+
const drumX = cx - lead.u * mirror * halfW * 0.4;
|
|
8392
|
+
ctx.fillStyle = "#0b0f1a";
|
|
8564
8393
|
ctx.beginPath();
|
|
8565
|
-
ctx.
|
|
8394
|
+
ctx.ellipse(drumX, topCy - topRy * 0.34, halfW * 0.08, topRy * 0.1, 0, 0, TAU);
|
|
8566
8395
|
ctx.fill();
|
|
8567
|
-
ctx.
|
|
8396
|
+
ctx.strokeStyle = `rgba(${rigTintStr},0.35)`;
|
|
8397
|
+
ctx.lineWidth = 1;
|
|
8398
|
+
ctx.stroke();
|
|
8399
|
+
}
|
|
8400
|
+
for (const p of [...form].sort((a, b) => a.v - b.v)) {
|
|
8401
|
+
const depthScale = 0.82 + (p.v + 1) * 0.14;
|
|
8402
|
+
drawPerformer(ctx, spotX(p), spotY(p), perfH * depthScale * (p.lead ? 1.06 : 0.92), {
|
|
8403
|
+
tone: [18, 21, 34],
|
|
8404
|
+
alpha: 0.95,
|
|
8405
|
+
pose: p.pose,
|
|
8406
|
+
rim: hslToRgb(40, 0.72, 0.8),
|
|
8407
|
+
rimStrength: p.lead ? 1.4 : 0.9
|
|
8408
|
+
});
|
|
8568
8409
|
}
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8410
|
+
const trussY = pitchToY(Math.min(60, farTopPitch + 34));
|
|
8411
|
+
const trussRy = topRy * 1.1 + 6;
|
|
8412
|
+
ctx.strokeStyle = "rgba(60,68,92,0.85)";
|
|
8413
|
+
ctx.lineWidth = 3;
|
|
8414
|
+
ctx.beginPath();
|
|
8415
|
+
ctx.ellipse(cx, trussY, halfW * 1.15, trussRy, 0, 0, TAU);
|
|
8416
|
+
ctx.stroke();
|
|
8417
|
+
const fixtures = 4 + Math.floor(rand() * 3);
|
|
8418
|
+
for (let i = 0; i < fixtures; i++) {
|
|
8419
|
+
const a = i / fixtures * TAU;
|
|
8420
|
+
const fxX = cx + Math.cos(a) * halfW * 1.15;
|
|
8421
|
+
const fxY = trussY + Math.sin(a) * trussRy;
|
|
8422
|
+
if (Math.sin(a) > -0.2) {
|
|
8423
|
+
const tx = cx + (rand() - 0.5) * halfW * 0.8;
|
|
8424
|
+
drawBeam(ctx, fxX, fxY, tx, topCy, 3, halfW * 0.32, rigTintStr, 0.2 * (0.7 + nearGlow * 0.4));
|
|
8425
|
+
}
|
|
8426
|
+
drawFixture(ctx, fxX, fxY, 3, rigTintStr);
|
|
8427
|
+
}
|
|
8428
|
+
const scoreY = pitchToY(Math.min(52, farTopPitch + 24));
|
|
8429
|
+
const scoreW = Math.max(60, halfW * 0.4);
|
|
8430
|
+
const scoreH = Math.max(24, scoreW * 0.34);
|
|
8431
|
+
ctx.strokeStyle = "rgba(70,78,102,0.6)";
|
|
8432
|
+
ctx.lineWidth = 1.5;
|
|
8433
|
+
ctx.beginPath();
|
|
8434
|
+
ctx.moveTo(cx - scoreW * 0.3, scoreY - scoreH / 2);
|
|
8435
|
+
ctx.lineTo(cx - scoreW * 0.16, scoreY - scoreH * 2.2);
|
|
8436
|
+
ctx.moveTo(cx + scoreW * 0.3, scoreY - scoreH / 2);
|
|
8437
|
+
ctx.lineTo(cx + scoreW * 0.16, scoreY - scoreH * 2.2);
|
|
8438
|
+
ctx.stroke();
|
|
8439
|
+
ctx.fillStyle = "#0d1220";
|
|
8440
|
+
ctx.fillRect(cx - scoreW / 2, scoreY - scoreH / 2, scoreW, scoreH);
|
|
8441
|
+
const screen = ctx.createLinearGradient(0, scoreY - scoreH * 0.3, 0, scoreY + scoreH * 0.42);
|
|
8442
|
+
screen.addColorStop(0, `rgba(${rigTintStr},0.5)`);
|
|
8443
|
+
screen.addColorStop(1, "rgba(255,214,160,0.32)");
|
|
8444
|
+
ctx.fillStyle = screen;
|
|
8445
|
+
ctx.fillRect(cx - scoreW * 0.42, scoreY - scoreH * 0.3, scoreW * 0.84, scoreH * 0.72);
|
|
8446
|
+
ctx.fillStyle = "rgba(255,214,160,0.55)";
|
|
8447
|
+
ctx.fillRect(cx - scoreW / 2, scoreY + scoreH / 2 - 2, scoreW, 2);
|
|
8448
|
+
const scoreGlow = ctx.createRadialGradient(cx, scoreY, 4, cx, scoreY, scoreW * 1.1);
|
|
8449
|
+
scoreGlow.addColorStop(0, `rgba(${rigTintStr},0.2)`);
|
|
8450
|
+
scoreGlow.addColorStop(1, "rgba(0,0,0,0)");
|
|
8451
|
+
ctx.save();
|
|
8452
|
+
ctx.globalCompositeOperation = "lighter";
|
|
8453
|
+
ctx.fillStyle = scoreGlow;
|
|
8454
|
+
ctx.fillRect(cx - scoreW * 1.1, scoreY - scoreW * 0.8, scoreW * 2.2, scoreW * 1.6);
|
|
8455
|
+
const airGlow = ctx.createRadialGradient(cx, (trussY + topCy) / 2, 6, cx, (trussY + topCy) / 2, halfW * 1.5);
|
|
8456
|
+
airGlow.addColorStop(0, `rgba(${rigTintStr},${(0.1 * (0.6 + nearGlow * 0.5)).toFixed(3)})`);
|
|
8457
|
+
airGlow.addColorStop(1, "rgba(0,0,0,0)");
|
|
8458
|
+
ctx.fillStyle = airGlow;
|
|
8459
|
+
ctx.fillRect(cx - halfW * 1.5, trussY - 40, halfW * 3, topCy - trussY + 80);
|
|
8460
|
+
ctx.restore();
|
|
8461
|
+
const keyX = cx + Math.sign(leadX - cx || 1) * halfW * 0.5;
|
|
8462
|
+
drawBeam(ctx, keyX, trussY - trussRy * 0.4, leadX, spotY(lead), 2.5, halfW * 0.16, "255,224,178", 0.42 * (0.7 + nearGlow * 0.4));
|
|
8463
|
+
const pool2 = ctx.createRadialGradient(leadX, spotY(lead), 1, leadX, spotY(lead), halfW * 0.2);
|
|
8464
|
+
pool2.addColorStop(0, `rgba(255,228,185,${(0.4 * nearGlow).toFixed(3)})`);
|
|
8465
|
+
pool2.addColorStop(1, "rgba(255,228,185,0)");
|
|
8466
|
+
ctx.save();
|
|
8467
|
+
ctx.globalCompositeOperation = "lighter";
|
|
8468
|
+
ctx.fillStyle = pool2;
|
|
8469
|
+
ctx.beginPath();
|
|
8470
|
+
ctx.ellipse(leadX, spotY(lead), halfW * 0.2, topRy * 0.24, 0, 0, TAU);
|
|
8471
|
+
ctx.fill();
|
|
8472
|
+
ctx.restore();
|
|
8473
|
+
}
|
|
8474
|
+
function drawSportScene(ctx, distM, eyeM, scene, _nearGlow, baseHue, rigTintStr, rand) {
|
|
8475
|
+
const cx = W / 2;
|
|
8476
|
+
const surfR = Math.max(10, scene.voidRadiusM);
|
|
8477
|
+
const nearEdge = Math.max(2, distM - surfR);
|
|
8478
|
+
const farEdge = distM + surfR;
|
|
8479
|
+
const halfYaw = Math.min(78, Math.atan2(surfR * 1.15, distM) * 180 / Math.PI);
|
|
8480
|
+
const nearPitch = Math.atan2(-eyeM, nearEdge) * 180 / Math.PI;
|
|
8481
|
+
const farPitch = Math.atan2(-eyeM, farEdge) * 180 / Math.PI;
|
|
8482
|
+
const yNear = pitchToY(nearPitch);
|
|
8483
|
+
const yFar = pitchToY(farPitch);
|
|
8484
|
+
const halfW = yawToX(halfYaw) - cx;
|
|
8485
|
+
const cy = (yNear + yFar) / 2;
|
|
8486
|
+
const ry = Math.max(10, (yNear - yFar) / 2);
|
|
8487
|
+
const surfCol = ctx.createRadialGradient(cx, cy, 4, cx, cy, halfW);
|
|
8488
|
+
surfCol.addColorStop(0, "#e8f0fb");
|
|
8489
|
+
surfCol.addColorStop(0.7, "#b9c9e2");
|
|
8490
|
+
surfCol.addColorStop(1, "#8fa3c4");
|
|
8491
|
+
ctx.save();
|
|
8492
|
+
ctx.beginPath();
|
|
8493
|
+
ctx.ellipse(cx, cy, halfW, ry, 0, 0, TAU);
|
|
8494
|
+
ctx.clip();
|
|
8495
|
+
ctx.fillStyle = surfCol;
|
|
8496
|
+
ctx.fillRect(cx - halfW, cy - ry, halfW * 2, ry * 2);
|
|
8497
|
+
ctx.strokeStyle = "rgba(90,120,170,0.5)";
|
|
8498
|
+
ctx.lineWidth = 3;
|
|
8499
|
+
ctx.beginPath();
|
|
8500
|
+
ctx.moveTo(cx, cy - ry);
|
|
8501
|
+
ctx.lineTo(cx, cy + ry);
|
|
8502
|
+
ctx.moveTo(cx - halfW * 0.5, cy - ry);
|
|
8503
|
+
ctx.lineTo(cx - halfW * 0.5, cy + ry);
|
|
8504
|
+
ctx.moveTo(cx + halfW * 0.5, cy - ry);
|
|
8505
|
+
ctx.lineTo(cx + halfW * 0.5, cy + ry);
|
|
8506
|
+
ctx.stroke();
|
|
8507
|
+
ctx.strokeStyle = "rgba(180,90,110,0.45)";
|
|
8508
|
+
ctx.beginPath();
|
|
8509
|
+
ctx.ellipse(cx, cy, ry * 0.9, ry * 0.5, 0, 0, TAU);
|
|
8510
|
+
ctx.stroke();
|
|
8511
|
+
ctx.restore();
|
|
8512
|
+
ctx.strokeStyle = "rgba(210,225,245,0.6)";
|
|
8513
|
+
ctx.lineWidth = 2.5;
|
|
8514
|
+
ctx.beginPath();
|
|
8515
|
+
ctx.ellipse(cx, cy, halfW, ry, 0, 0, TAU);
|
|
8516
|
+
ctx.stroke();
|
|
8517
|
+
ctx.fillStyle = "rgba(20,26,40,0.8)";
|
|
8518
|
+
for (let i = 0; i < 5; i++) {
|
|
8519
|
+
const px = cx + (rand() - 0.5) * halfW * 1.4;
|
|
8520
|
+
const py = cy + (rand() - 0.5) * ry * 1.1;
|
|
8521
|
+
const s = Math.max(2, ry * 0.06);
|
|
8573
8522
|
ctx.beginPath();
|
|
8574
|
-
ctx.
|
|
8575
|
-
ctx.lineTo(bx - (sx1 - sx0) * 0.09, sy1);
|
|
8576
|
-
ctx.lineTo(bx + (sx1 - sx0) * 0.09, sy1);
|
|
8577
|
-
ctx.lineTo(bx + 8, pitchToY(Math.min(60, stageTopPitch + 25)));
|
|
8578
|
-
ctx.closePath();
|
|
8523
|
+
ctx.ellipse(px, py, s * 0.6, s, 0, 0, TAU);
|
|
8579
8524
|
ctx.fill();
|
|
8580
8525
|
}
|
|
8581
|
-
ctx.
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8526
|
+
const wash = ctx.createRadialGradient(cx, cy, 4, cx, cy, halfW * 1.4);
|
|
8527
|
+
wash.addColorStop(0, "rgba(200,220,255,0.12)");
|
|
8528
|
+
wash.addColorStop(1, "rgba(200,220,255,0)");
|
|
8529
|
+
ctx.save();
|
|
8530
|
+
ctx.globalCompositeOperation = "lighter";
|
|
8531
|
+
ctx.fillStyle = wash;
|
|
8532
|
+
ctx.fillRect(cx - halfW * 1.4, yFar - ry, halfW * 2.8, yNear - yFar + ry * 2);
|
|
8533
|
+
ctx.restore();
|
|
8534
|
+
const sbY = pitchToY(Math.min(66, farPitch + 60));
|
|
8535
|
+
const sbW = Math.max(80, halfW * 0.7);
|
|
8536
|
+
const sbH = sbW * 0.5;
|
|
8537
|
+
const sbGlow = ctx.createRadialGradient(cx, sbY, 4, cx, sbY, sbW * 1.6);
|
|
8538
|
+
sbGlow.addColorStop(0, `rgba(${rigTintStr},0.3)`);
|
|
8539
|
+
sbGlow.addColorStop(1, `rgba(${rigTintStr},0)`);
|
|
8540
|
+
ctx.save();
|
|
8541
|
+
ctx.globalCompositeOperation = "lighter";
|
|
8542
|
+
ctx.fillStyle = sbGlow;
|
|
8543
|
+
ctx.fillRect(cx - sbW * 1.6, sbY - sbW * 1.2, sbW * 3.2, sbW * 2.4);
|
|
8544
|
+
ctx.restore();
|
|
8545
|
+
ctx.fillStyle = "#080b12";
|
|
8546
|
+
ctx.beginPath();
|
|
8547
|
+
ctx.moveTo(cx - sbW / 2, sbY - sbH / 2);
|
|
8548
|
+
ctx.lineTo(cx + sbW / 2, sbY - sbH / 2);
|
|
8549
|
+
ctx.lineTo(cx + sbW * 0.6, sbY);
|
|
8550
|
+
ctx.lineTo(cx + sbW / 2, sbY + sbH / 2);
|
|
8551
|
+
ctx.lineTo(cx - sbW / 2, sbY + sbH / 2);
|
|
8552
|
+
ctx.lineTo(cx - sbW * 0.6, sbY);
|
|
8553
|
+
ctx.closePath();
|
|
8554
|
+
ctx.fill();
|
|
8555
|
+
const screenHue = baseHue;
|
|
8556
|
+
for (let i = 0; i < 3; i++) {
|
|
8557
|
+
const scol = hslToRgb(screenHue + i * 30, 0.5, 0.5);
|
|
8558
|
+
ctx.fillStyle = `rgba(${scol[0]},${scol[1]},${scol[2]},0.75)`;
|
|
8559
|
+
const swid = sbW * 0.26;
|
|
8560
|
+
ctx.fillRect(cx - sbW * 0.42 + i * (swid + sbW * 0.06), sbY - sbH * 0.32, swid, sbH * 0.64);
|
|
8561
|
+
}
|
|
8562
|
+
ctx.fillStyle = `rgba(${rigTintStr},0.9)`;
|
|
8563
|
+
for (let i = 0; i < 8; i++) {
|
|
8564
|
+
const lx = cx - sbW * 0.6 + sbW * 1.2 * i / 7;
|
|
8586
8565
|
ctx.beginPath();
|
|
8587
|
-
ctx.arc(
|
|
8566
|
+
ctx.arc(lx, sbY + sbH * 0.62, 2.4, 0, TAU);
|
|
8588
8567
|
ctx.fill();
|
|
8589
8568
|
}
|
|
8590
|
-
|
|
8591
|
-
|
|
8569
|
+
}
|
|
8570
|
+
function drawAudience(ctx, seat, neighborSeats, _focalPoint, stageBearing, eyeM, dx, dy, hasRelief, scene) {
|
|
8571
|
+
const own = seat.sectionId;
|
|
8572
|
+
let seatAhead = false;
|
|
8573
|
+
const figs = [];
|
|
8592
8574
|
for (const other of neighborSeats) {
|
|
8593
8575
|
if (other.id === seat.id) continue;
|
|
8594
8576
|
const ox = other.x - seat.x;
|
|
8595
8577
|
const oy = other.y - seat.y;
|
|
8596
8578
|
const d = Math.hypot(ox, oy) * UNIT;
|
|
8597
|
-
if (d <
|
|
8579
|
+
if (own && other.sectionId === own && d < 3 && ox * dx + oy * dy > 0) seatAhead = true;
|
|
8580
|
+
if (d < 0.3 || d > 32) continue;
|
|
8598
8581
|
const bearing = Math.atan2(ox, -oy) - stageBearing;
|
|
8599
8582
|
const yaw = (bearing * 180 / Math.PI + 540) % 360 - 180;
|
|
8600
|
-
const
|
|
8601
|
-
const
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8583
|
+
const rise = hasRelief ? (other.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - eyeM : 0;
|
|
8584
|
+
const headPitch = Math.atan2(rise - 0.28, d) * 180 / Math.PI;
|
|
8585
|
+
if (headPitch > 10 && d > 5) continue;
|
|
8586
|
+
let hh = 0;
|
|
8587
|
+
for (let k = 0; k < other.id.length; k++) hh = hh * 31 + other.id.charCodeAt(k) & 65535;
|
|
8588
|
+
const jitter = 0.9 + (hh & 15) / 40;
|
|
8589
|
+
const r = Math.min(78, Math.atan2(0.16, d) * 180 / Math.PI * PX_PER_DEG * jitter);
|
|
8590
|
+
if (r < 1.2) continue;
|
|
8591
|
+
figs.push({ hx: yawToX(yaw), hy: pitchToY(headPitch), r, d, hash: hh });
|
|
8592
|
+
}
|
|
8593
|
+
figs.sort((a, b) => b.d - a.d);
|
|
8594
|
+
const figRows = [];
|
|
8595
|
+
{
|
|
8596
|
+
const byY = [...figs].sort((a, b) => a.hy - b.hy);
|
|
8597
|
+
let cur = [];
|
|
8598
|
+
const flush = () => {
|
|
8599
|
+
if (!cur.length) return;
|
|
8600
|
+
const d = cur.reduce((s, f) => s + f.d, 0) / cur.length;
|
|
8601
|
+
const hy = cur.reduce((s, f) => s + f.hy, 0) / cur.length;
|
|
8602
|
+
const r = cur.reduce((s, f) => s + f.r, 0) / cur.length;
|
|
8603
|
+
figRows.push({ figs: cur.sort((a, b) => b.d - a.d), d, hy, r });
|
|
8604
|
+
cur = [];
|
|
8605
|
+
};
|
|
8606
|
+
for (const f of byY) {
|
|
8607
|
+
const last = cur[cur.length - 1];
|
|
8608
|
+
if (last && Math.abs(f.hy - last.hy) > Math.max(5, (last.r + f.r) / 2 * 1.7)) flush();
|
|
8609
|
+
cur.push(f);
|
|
8610
|
+
}
|
|
8611
|
+
flush();
|
|
8612
|
+
}
|
|
8613
|
+
figRows.sort((a, b) => b.d - a.d);
|
|
8614
|
+
const CLOTHES = [[22, 26, 40], [28, 27, 42], [24, 31, 46], [31, 30, 47], [20, 24, 36]];
|
|
8615
|
+
for (const row of figRows) {
|
|
8616
|
+
if (row.figs.length >= 3 && row.r >= 1.6 && row.r < 26) {
|
|
8617
|
+
const runs = [];
|
|
8618
|
+
const byX = [...row.figs].sort((a, b) => a.hx - b.hx);
|
|
8619
|
+
let run = [];
|
|
8620
|
+
for (const f of byX) {
|
|
8621
|
+
const last = run[run.length - 1];
|
|
8622
|
+
if (last && f.hx - last.hx > Math.max(14, f.r * 7)) {
|
|
8623
|
+
if (run.length >= 3) runs.push(run);
|
|
8624
|
+
run = [];
|
|
8625
|
+
}
|
|
8626
|
+
run.push(f);
|
|
8627
|
+
}
|
|
8628
|
+
if (run.length >= 3) runs.push(run);
|
|
8629
|
+
const stripAlpha = Math.min(0.42, 0.5 * (1 - row.d / 42));
|
|
8630
|
+
if (stripAlpha > 0.05) {
|
|
8631
|
+
ctx.fillStyle = `rgba(15, 19, 31, ${stripAlpha.toFixed(3)})`;
|
|
8632
|
+
for (const seg of runs) {
|
|
8633
|
+
const x0 = seg[0].hx - row.r * 1.6;
|
|
8634
|
+
const x1 = seg[seg.length - 1].hx + row.r * 1.6;
|
|
8635
|
+
ctx.beginPath();
|
|
8636
|
+
ctx.roundRect(x0, row.hy + row.r * 0.8, x1 - x0, row.r * 2.4, row.r * 0.8);
|
|
8637
|
+
ctx.fill();
|
|
8638
|
+
}
|
|
8639
|
+
}
|
|
8640
|
+
}
|
|
8641
|
+
for (const f of row.figs) {
|
|
8642
|
+
const alpha = f.d < 12 ? 0.95 : f.d < 17 ? Math.max(0.66, 0.95 - (f.d - 12) / 18) : Math.max(0.3, 0.68 - (f.d - 17) / 34);
|
|
8643
|
+
const lift = Math.round((1 - Math.min(1, f.d / 16)) * 10);
|
|
8644
|
+
const headTone = [14 + (f.hash >> 4 & 6) + lift, 17 + (f.hash >> 4 & 6) + lift, 25 + (f.hash >> 4 & 6) + lift];
|
|
8645
|
+
const clo0 = CLOTHES[f.hash % CLOTHES.length];
|
|
8646
|
+
const clothing = [clo0[0] + lift, clo0[1] + lift, clo0[2] + lift];
|
|
8647
|
+
if (f.r >= 13) {
|
|
8648
|
+
const rim = f.d < 9 ? 0.16 * (1 - f.d / 9) + (scene.mode === "proscenium" ? 0.06 : 0) : 0;
|
|
8649
|
+
drawSeated(ctx, f.hx, f.hy, f.r, {
|
|
8650
|
+
tone: headTone,
|
|
8651
|
+
clothing,
|
|
8652
|
+
alpha,
|
|
8653
|
+
tilt: ((f.hash >> 2 & 7) - 3.5) * 0.018,
|
|
8654
|
+
// ±~7°
|
|
8655
|
+
shoulderK: 1.72 + (f.hash >> 5 & 7) / 22,
|
|
8656
|
+
// ~1.72..2.04
|
|
8657
|
+
hair: f.hash % HAIR_KINDS,
|
|
8658
|
+
rim,
|
|
8659
|
+
turn: ((f.hash >> 8 & 7) - 3.5) * 0.034,
|
|
8660
|
+
// ±~0.12 head-radii sideways
|
|
8661
|
+
lean: ((f.hash >> 10 & 3) - 1.5) * 0.09
|
|
8662
|
+
// one shoulder rides higher
|
|
8663
|
+
});
|
|
8664
|
+
} else if (f.r >= 5.5) {
|
|
8665
|
+
drawSeatedMid(ctx, f.hx, f.hy, f.r, clothing, headTone, alpha);
|
|
8666
|
+
} else {
|
|
8667
|
+
ctx.fillStyle = `rgba(${headTone[0]},${headTone[1]},${headTone[2]},${alpha.toFixed(3)})`;
|
|
8668
|
+
ctx.beginPath();
|
|
8669
|
+
ctx.arc(f.hx, f.hy, f.r, 0, TAU);
|
|
8670
|
+
ctx.fill();
|
|
8671
|
+
ctx.beginPath();
|
|
8672
|
+
ctx.ellipse(f.hx, f.hy + f.r * 1.4, f.r * 1.85, f.r * 0.95, 0, Math.PI, 0, true);
|
|
8673
|
+
ctx.fill();
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
8676
|
+
}
|
|
8677
|
+
if (seatAhead) {
|
|
8678
|
+
const railTop = pitchToY(-34);
|
|
8679
|
+
ctx.fillStyle = "rgba(6, 8, 14, 0.92)";
|
|
8680
|
+
ctx.fillRect(0, railTop, W, H - railTop);
|
|
8681
|
+
ctx.fillStyle = "rgba(24, 30, 46, 0.9)";
|
|
8682
|
+
const humps = 16;
|
|
8683
|
+
for (let i = 0; i < humps; i++) {
|
|
8684
|
+
const cx = (i + 0.5) / humps * W;
|
|
8685
|
+
ctx.beginPath();
|
|
8686
|
+
ctx.ellipse(cx, railTop + 6, W / humps * 0.42, 22, 0, Math.PI, 0, true);
|
|
8687
|
+
ctx.fill();
|
|
8688
|
+
}
|
|
8610
8689
|
}
|
|
8611
|
-
const poleFade = ctx.createLinearGradient(0, 0, 0, H);
|
|
8612
|
-
poleFade.addColorStop(0, "rgba(0,0,0,0.85)");
|
|
8613
|
-
poleFade.addColorStop(0.12, "rgba(0,0,0,0)");
|
|
8614
|
-
poleFade.addColorStop(0.88, "rgba(0,0,0,0)");
|
|
8615
|
-
poleFade.addColorStop(1, "rgba(0,0,0,0.85)");
|
|
8616
|
-
ctx.fillStyle = poleFade;
|
|
8617
|
-
ctx.fillRect(0, 0, W, H);
|
|
8618
|
-
return { url: canvas.toDataURL("image/jpeg", 0.82), distanceM: Math.round(distM) };
|
|
8619
8690
|
}
|
|
8620
8691
|
var TW = 480;
|
|
8621
8692
|
var TH = 270;
|
|
@@ -8631,12 +8702,16 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
8631
8702
|
const dx = focalPoint.x - seat.x;
|
|
8632
8703
|
const dy = focalPoint.y - seat.y;
|
|
8633
8704
|
const distM = Math.max(2, Math.hypot(dx, dy) * UNIT);
|
|
8705
|
+
const rand = rng(fnv1a(`${seat.id}|${Math.round(focalPoint.x)},${Math.round(focalPoint.y)}`));
|
|
8706
|
+
const baseHue = 210 + rand() * 150;
|
|
8707
|
+
const rigTint = hslToRgb(220 + (rand() - 0.5) * 60, 0.55, 0.82);
|
|
8708
|
+
const rigTintStr = `${rigTint[0]},${rigTint[1]},${rigTint[2]}`;
|
|
8634
8709
|
const sky = ctx.createLinearGradient(0, 0, 0, TH);
|
|
8635
|
-
sky.addColorStop(0, "#
|
|
8636
|
-
sky.addColorStop(0.44, "#
|
|
8637
|
-
sky.addColorStop(0.5, "#
|
|
8638
|
-
sky.addColorStop(0.58, "#
|
|
8639
|
-
sky.addColorStop(1, "#
|
|
8710
|
+
sky.addColorStop(0, "#080b12");
|
|
8711
|
+
sky.addColorStop(0.44, "#151c32");
|
|
8712
|
+
sky.addColorStop(0.5, "#1d2644");
|
|
8713
|
+
sky.addColorStop(0.58, "#161c2e");
|
|
8714
|
+
sky.addColorStop(1, "#131829");
|
|
8640
8715
|
ctx.fillStyle = sky;
|
|
8641
8716
|
ctx.fillRect(0, 0, TW, TH);
|
|
8642
8717
|
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
@@ -8650,58 +8725,102 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
8650
8725
|
const sy1 = pitchToTY(stageBasePitch);
|
|
8651
8726
|
const sw = sx1 - sx0;
|
|
8652
8727
|
const sh = sy1 - sy0;
|
|
8653
|
-
const
|
|
8654
|
-
|
|
8655
|
-
glow.
|
|
8728
|
+
const nearGlow = Math.max(0.35, Math.min(1, 1 - (distM - 6) / 60));
|
|
8729
|
+
const archTop = pitchToTY(Math.min(HALF_VFOV - 1, stageTopPitch + 8));
|
|
8730
|
+
const glow = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, 4, TW / 2, (sy0 + sy1) / 2, sw * 1.15);
|
|
8731
|
+
glow.addColorStop(0, "rgba(255,214,150,0.34)");
|
|
8732
|
+
glow.addColorStop(0.28, "rgba(150,150,230,0.3)");
|
|
8733
|
+
glow.addColorStop(0.6, "rgba(99,102,241,0.12)");
|
|
8656
8734
|
glow.addColorStop(1, "rgba(99,102,241,0)");
|
|
8657
8735
|
ctx.fillStyle = glow;
|
|
8658
8736
|
ctx.fillRect(sx0 - sw * 0.6, sy0 - sh * 1.2, sw * 2.2, sh * 3.4);
|
|
8659
|
-
|
|
8660
|
-
|
|
8737
|
+
const topCol = hslToRgb(baseHue, 0.62, 0.55);
|
|
8738
|
+
const midCol = hslToRgb(baseHue + 40, 0.7, 0.5);
|
|
8739
|
+
const botCol = hslToRgb(baseHue + 70, 0.78, 0.52);
|
|
8740
|
+
ctx.save();
|
|
8741
|
+
ctx.beginPath();
|
|
8742
|
+
ctx.rect(sx0, sy0, sw, sh);
|
|
8743
|
+
ctx.clip();
|
|
8661
8744
|
const backdrop = ctx.createLinearGradient(0, sy0, 0, sy1);
|
|
8662
|
-
backdrop.addColorStop(0,
|
|
8663
|
-
backdrop.addColorStop(0.5,
|
|
8664
|
-
backdrop.addColorStop(1,
|
|
8665
|
-
ctx.globalAlpha = 0.
|
|
8745
|
+
backdrop.addColorStop(0, `rgb(${topCol[0]},${topCol[1]},${topCol[2]})`);
|
|
8746
|
+
backdrop.addColorStop(0.5, `rgb(${midCol[0]},${midCol[1]},${midCol[2]})`);
|
|
8747
|
+
backdrop.addColorStop(1, `rgb(${botCol[0]},${botCol[1]},${botCol[2]})`);
|
|
8748
|
+
ctx.globalAlpha = 0.8;
|
|
8666
8749
|
ctx.fillStyle = backdrop;
|
|
8667
|
-
|
|
8668
|
-
ctx.fillRect(sx0 + inset, sy0 + sh * 0.12, sw - inset * 2, sh * 0.62);
|
|
8750
|
+
ctx.fillRect(sx0, sy0, sw, sh);
|
|
8669
8751
|
ctx.globalAlpha = 1;
|
|
8670
|
-
ctx.
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
ctx.lineTo(bx - sw * 0.09, sy1);
|
|
8689
|
-
ctx.lineTo(bx + sw * 0.09, sy1);
|
|
8690
|
-
ctx.lineTo(bx + 5, pitchToTY(Math.min(HALF_VFOV, stageTopPitch + 18)));
|
|
8691
|
-
ctx.closePath();
|
|
8692
|
-
ctx.fill();
|
|
8752
|
+
const fall = ctx.createRadialGradient(TW / 2, (sy0 + sy1) / 2, sw * 0.12, TW / 2, (sy0 + sy1) / 2, sw * 0.6);
|
|
8753
|
+
fall.addColorStop(0, "rgba(6,8,14,0)");
|
|
8754
|
+
fall.addColorStop(0.7, "rgba(6,8,14,0.15)");
|
|
8755
|
+
fall.addColorStop(1, "rgba(5,6,11,0.9)");
|
|
8756
|
+
ctx.fillStyle = fall;
|
|
8757
|
+
ctx.fillRect(sx0, sy0, sw, sh);
|
|
8758
|
+
ctx.restore();
|
|
8759
|
+
const perfCount = 3;
|
|
8760
|
+
const floorY = sy1 - sh * 0.08;
|
|
8761
|
+
for (let i = 0; i < perfCount; i++) {
|
|
8762
|
+
const t2 = (i + 1) / (perfCount + 1);
|
|
8763
|
+
drawPerformer(ctx, sx0 + sw * (0.24 + t2 * 0.52), floorY, sh * 0.46 * (0.9 + rand() * 0.24), {
|
|
8764
|
+
tone: [8, 10, 16],
|
|
8765
|
+
alpha: 0.9,
|
|
8766
|
+
pose: Math.floor(rand() * 4),
|
|
8767
|
+
rim: hslToRgb(baseHue + 20, 0.5, 0.7),
|
|
8768
|
+
rimStrength: 0.7
|
|
8769
|
+
});
|
|
8693
8770
|
}
|
|
8694
|
-
ctx.
|
|
8695
|
-
ctx.
|
|
8771
|
+
ctx.fillStyle = "#05070d";
|
|
8772
|
+
ctx.fillRect(sx0 - sw * 0.5, archTop, sw * 0.5, sy1 - archTop);
|
|
8773
|
+
ctx.fillRect(sx1, archTop, sw * 0.5, sy1 - archTop);
|
|
8774
|
+
const legBot = sw * 0.05 + 2;
|
|
8775
|
+
const legTop = legBot * 1.35;
|
|
8776
|
+
ctx.fillStyle = "#03050a";
|
|
8777
|
+
ctx.beginPath();
|
|
8778
|
+
ctx.moveTo(sx0, sy1);
|
|
8779
|
+
ctx.lineTo(sx0 - legBot, sy1);
|
|
8780
|
+
ctx.lineTo(sx0 - legTop, archTop);
|
|
8781
|
+
ctx.lineTo(sx0 + sw * 0.012, archTop);
|
|
8782
|
+
ctx.closePath();
|
|
8783
|
+
ctx.fill();
|
|
8784
|
+
ctx.beginPath();
|
|
8785
|
+
ctx.moveTo(sx1, sy1);
|
|
8786
|
+
ctx.lineTo(sx1 + legBot, sy1);
|
|
8787
|
+
ctx.lineTo(sx1 + legTop, archTop);
|
|
8788
|
+
ctx.lineTo(sx1 - sw * 0.012, archTop);
|
|
8789
|
+
ctx.closePath();
|
|
8790
|
+
ctx.fill();
|
|
8791
|
+
ctx.fillRect(sx0 - legTop, archTop, sw + legTop * 2, (sy1 - archTop) * 0.13);
|
|
8792
|
+
ctx.fillStyle = `rgba(${rigTintStr},0.12)`;
|
|
8793
|
+
ctx.fillRect(sx0 - 1.2, archTop, 1.2, sy1 - archTop);
|
|
8794
|
+
ctx.fillRect(sx1, archTop, 1.2, sy1 - archTop);
|
|
8795
|
+
ctx.fillStyle = "#0b0f18";
|
|
8796
|
+
ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.05 + 2);
|
|
8797
|
+
const foot = ctx.createLinearGradient(0, sy1 - sh * 0.05, 0, sy1 + sh * 0.14);
|
|
8798
|
+
foot.addColorStop(0, `rgba(255,206,140,${(0.5 * nearGlow).toFixed(3)})`);
|
|
8799
|
+
foot.addColorStop(1, "rgba(255,206,140,0)");
|
|
8800
|
+
ctx.fillStyle = foot;
|
|
8801
|
+
ctx.fillRect(sx0 - legBot, sy1 - sh * 0.05, sw + legBot * 2, sh * 0.22);
|
|
8802
|
+
for (let i = 0; i < 3; i++) {
|
|
8803
|
+
const t2 = (i + 1) / 4;
|
|
8804
|
+
const fx = sx0 + sw * t2;
|
|
8805
|
+
const fy = pitchToTY(Math.min(HALF_VFOV - 1, stageTopPitch + 20));
|
|
8806
|
+
drawBeam(ctx, fx, fy, fx + (rand() - 0.5) * sw * 0.14, sy1, 3, sw * 0.09, rigTintStr, 0.17 * nearGlow);
|
|
8807
|
+
drawFixture(ctx, fx, fy, 1.8, rigTintStr);
|
|
8808
|
+
}
|
|
8809
|
+
ctx.fillStyle = `rgba(${rigTintStr},0.75)`;
|
|
8696
8810
|
for (let i = 0; i < 8; i++) {
|
|
8697
8811
|
ctx.beginPath();
|
|
8698
|
-
ctx.arc((i + 0.5) / 8 * TW, pitchToTY(HALF_VFOV - 4 - i % 2 * 3), 2.2, 0,
|
|
8812
|
+
ctx.arc((i + 0.5) / 8 * TW, pitchToTY(HALF_VFOV - 4 - i % 2 * 3), 2.2, 0, TAU);
|
|
8699
8813
|
ctx.fill();
|
|
8700
8814
|
}
|
|
8701
8815
|
ctx.fillStyle = "rgba(10,13,20,0.9)";
|
|
8702
8816
|
ctx.fillRect(0, TH - 26, TW, 26);
|
|
8703
8817
|
ctx.fillStyle = "rgba(30,37,54,0.9)";
|
|
8704
|
-
for (let i = 0; i < 9; i++)
|
|
8818
|
+
for (let i = 0; i < 9; i++) {
|
|
8819
|
+
const bx = i * (TW / 9) + TW / 18;
|
|
8820
|
+
ctx.beginPath();
|
|
8821
|
+
ctx.ellipse(bx, TH - 22, TW / 9 * 0.4, 15, 0, Math.PI, 0, true);
|
|
8822
|
+
ctx.fill();
|
|
8823
|
+
}
|
|
8705
8824
|
return { url: canvas.toDataURL("image/jpeg", 0.8), distanceM: Math.round(distM) };
|
|
8706
8825
|
}
|
|
8707
8826
|
|