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