craft-library 0.4.0 → 0.5.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 +30 -5
- package/dist/card.cdn.js +1 -1
- package/dist/card.js +1 -1
- package/dist/cdn.js +4 -4
- package/dist/craft.js +749 -30
- package/dist/elements.js +23 -2
- package/dist/glassnav.js +1 -1
- package/dist/index.js +16 -1
- package/dist/like.js +2 -2
- package/dist/magnet.js +2 -2
- package/dist/panorama/poster.webp +0 -0
- package/dist/panorama/reel.mp4 +0 -0
- package/dist/panorama/t/s01.webp +0 -0
- package/dist/panorama/t/s02.webp +0 -0
- package/dist/panorama/t/s03.webp +0 -0
- package/dist/panorama/t/s05.webp +0 -0
- package/dist/panorama/t/s06.webp +0 -0
- package/dist/panorama/t/s07.webp +0 -0
- package/dist/panorama/t/s08.webp +0 -0
- package/dist/panorama/t/s09.webp +0 -0
- package/dist/panorama/t/s10.webp +0 -0
- package/dist/panorama/t/s11.webp +0 -0
- package/dist/panorama/t/s12.webp +0 -0
- package/dist/panorama/t/s13.webp +0 -0
- package/dist/panorama/t/s14.webp +0 -0
- package/dist/panorama/t/s15.webp +0 -0
- package/dist/panorama/t/s16.webp +0 -0
- package/dist/panorama-assets.js +2 -0
- package/dist/panorama-mount.js +22 -0
- package/dist/panorama.js +496 -0
- package/dist/piggy.cdn.js +1 -1
- package/dist/piggy.js +1 -1
- package/dist/thinking.js +2 -2
- package/package.json +9 -4
package/dist/craft.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* Sara Lobo Library · liquid glass nav bar, bursting heart, magnetic icon, iridescent card, glass piggy bank, magic sphere, sweep, panorama · <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere>, <sweep-sphere>, <panorama-reel> */
|
|
2
2
|
(() => {
|
|
3
3
|
// dist/style.js
|
|
4
4
|
var done = /* @__PURE__ */ new Set();
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// dist/like.js
|
|
16
|
-
var CSS = `/* Bursting heart \xB7
|
|
16
|
+
var CSS = `/* Bursting heart \xB7 Sara Lobo Library
|
|
17
17
|
The heart eases out of sight, a disc grows and hollows into a ring, 14 dots and 2 or 3 small hearts fly out,
|
|
18
18
|
and the heart springs back full. Every motion runs on its own property with its own curve; where two curves
|
|
19
19
|
meet, both are at rest there, so nothing hitches. Unliking only takes the colour away. Pairs with like.js. */
|
|
@@ -424,7 +424,7 @@
|
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
// dist/magnet.js
|
|
427
|
-
var CSS2 = "/* Magnetic icon \xB7
|
|
427
|
+
var CSS2 = "/* Magnetic icon \xB7 Sara Lobo Library\n A dark disc with a brushed-metal rim and a search glyph (Figma: 40\xD740). Near the cursor the rim acts like metal\n near a strong magnet: the disc is drawn toward the pointer, the strained rim creases and trembles, its tip cracks\n into shards, and small voxels break off and fly to the pointer. Pairs with magnet.js.\n Everything is drawn in a 40-unit box, so --mag-size scales the whole piece, the pull included. */\n\n.mag {\n all: unset;\n position: relative;\n display: inline-grid;\n place-items: center;\n width: var(--mag-size, 40px);\n height: var(--mag-size, 40px);\n border-radius: 50%;\n color: #fff;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n transition: transform .2s cubic-bezier(.2, .8, .2, 1);\n}\n.mag:active {\n transform: scale(.94);\n}\n.mag:focus-visible {\n outline: 2px solid #9DB8FF;\n outline-offset: 4px;\n}\n/* the disc and its rim, redrawn by magnet.js; the stretch reaches past the button */\n.mag-ring {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n overflow: visible;\n pointer-events: none;\n}\n/* the shards and voxels: 46 units of room on every side of the 40-unit button (46 / 40 = 115%) */\n.mag-dust {\n position: absolute;\n left: -115%;\n top: -115%;\n width: 330%;\n height: 330%;\n pointer-events: none;\n}\n.mag-icon {\n position: relative;\n display: block;\n width: 50%;\n height: 50%;\n}\n";
|
|
428
428
|
var MARKUP = '<svg class="mag-ring" viewBox="0 0 40 40" aria-hidden="true"><defs><linearGradient id="magMetal" x1="32" y1="3.5" x2="20" y2="40" gradientUnits="userSpaceOnUse"><stop stop-color="#999"></stop><stop offset=".452" stop-color="#fff"></stop><stop offset="1" stop-color="#A2A2A2"></stop></linearGradient><radialGradient id="magFade" cx="20" cy="20" r="40" gradientUnits="userSpaceOnUse"><stop offset=".506" stop-color="#fff"></stop><stop offset=".507" stop-color="#000"></stop></radialGradient><mask id="magMask" maskUnits="userSpaceOnUse" x="-30" y="-30" width="100" height="100"><rect x="-30" y="-30" width="100" height="100" fill="url(#magFade)"></rect></mask></defs><path class="mag-rim" mask="url(#magMask)" d="M39.25 20C39.25 30.6315 30.6315 39.25 20 39.25C9.36852 39.25 .75 30.6315 .75 20C.75 9.36852 9.36852 .75 20 .75C30.6315 .75 39.25 9.36852 39.25 20Z" fill="#fff" fill-opacity=".2" stroke="url(#magMetal)" stroke-width="1.5"></path></svg><svg class="mag-icon" viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M17.5 17.5L13.8807 13.8807M13.8807 13.8807C15.0871 12.6743 15.8333 11.0076 15.8333 9.16667C15.8333 5.48477 12.8486 2.5 9.16667 2.5C5.48477 2.5 2.5 5.48477 2.5 9.16667C2.5 12.8486 5.48477 15.8333 9.16667 15.8333C11.0076 15.8333 12.6743 15.0871 13.8807 13.8807Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
|
|
429
429
|
var api2;
|
|
430
430
|
function engine2() {
|
|
@@ -446,7 +446,7 @@
|
|
|
446
446
|
};
|
|
447
447
|
return;
|
|
448
448
|
}
|
|
449
|
-
function
|
|
449
|
+
function clamp2(x, a, b) {
|
|
450
450
|
return Math.max(a, Math.min(b, x));
|
|
451
451
|
}
|
|
452
452
|
function wrap(a) {
|
|
@@ -471,17 +471,17 @@
|
|
|
471
471
|
return hash(i, k) * (1 - u) + hash(i, k + 1) * u - 0.5;
|
|
472
472
|
}
|
|
473
473
|
function width(m) {
|
|
474
|
-
return 0.55 - 0.25 *
|
|
474
|
+
return 0.55 - 0.25 * clamp2(m.d / PULL, 0, 1);
|
|
475
475
|
}
|
|
476
476
|
function radius(m, t) {
|
|
477
477
|
var dt = wrap(t - m.a), d = m.d;
|
|
478
478
|
return R + (d > 0 ? d * (spike(dt, width(m)) + BODY * bell(dt, 1)) : d * bell(dt, 0.7)) - 0.12 * d * bell(wrap(dt - Math.PI), 1.3);
|
|
479
479
|
}
|
|
480
480
|
function strainOf(m) {
|
|
481
|
-
return
|
|
481
|
+
return clamp2((Math.abs(m.d) + 3 * m.heat) / PULL, 0, 1);
|
|
482
482
|
}
|
|
483
483
|
function damage(m, t) {
|
|
484
|
-
return
|
|
484
|
+
return clamp2((Math.abs(m.d) + 3 * m.heat) / 9, 0, 1) * bell(wrap(t - m.a), 0.7);
|
|
485
485
|
}
|
|
486
486
|
function rimPath(m, now) {
|
|
487
487
|
var pts = [], strain = strainOf(m), i, t, r, env, sharp, fray, n, p, a, b, c, e;
|
|
@@ -490,7 +490,7 @@
|
|
|
490
490
|
r = radius(m, t);
|
|
491
491
|
env = bell(wrap(t - m.a), 0.6) * strain;
|
|
492
492
|
sharp = env > 0.12;
|
|
493
|
-
fray = sharp ? 0 :
|
|
493
|
+
fray = sharp ? 0 : clamp2((damage(m, t) - 0.15) / 0.85, 0, 1);
|
|
494
494
|
if (sharp && i % 3 || fray > 0 && i % 2) continue;
|
|
495
495
|
if (sharp) r += 2 * JAG * PULL * env * tremble(i, now);
|
|
496
496
|
else if (fray > 0) r += FRAY * fray * (hash(i, 21) - 0.62 + 0.3 * tremble(i + 500, now));
|
|
@@ -514,8 +514,8 @@
|
|
|
514
514
|
var x = (p.x - m.box.left) * k - C, y = (p.y - m.box.top) * k - C;
|
|
515
515
|
var dist = Math.hypot(x, y), edge = dist - R, near = 1 - Math.max(edge, 0) / REACH;
|
|
516
516
|
if (edge > REACH) return null;
|
|
517
|
-
var d = edge >= 0 ? Math.min(edge * 0.9 / (1 + BODY), PULL * Math.pow(1 - edge / REACH, FALL)) : -Math.min(-edge * 0.85, DENT *
|
|
518
|
-
return { d, a: Math.atan2(y, x), x, y, dist, near, heat: edge >= 0 ? near * near :
|
|
517
|
+
var d = edge >= 0 ? Math.min(edge * 0.9 / (1 + BODY), PULL * Math.pow(1 - edge / REACH, FALL)) : -Math.min(-edge * 0.85, DENT * clamp2(dist / R, 0, 1));
|
|
518
|
+
return { d, a: Math.atan2(y, x), x, y, dist, near, heat: edge >= 0 ? near * near : clamp2(dist / R, 0, 1) };
|
|
519
519
|
}
|
|
520
520
|
function step(m, dt) {
|
|
521
521
|
var t = m.t = aim(m), to = t ? t.d : 0, heat = t ? t.heat : 0, turn = 0, k, c, ix = 0, iy = 0, e;
|
|
@@ -531,7 +531,7 @@
|
|
|
531
531
|
m.heat += (heat - m.heat) * (1 - Math.exp(-dt * 6));
|
|
532
532
|
if (t) {
|
|
533
533
|
turn = wrap(t.a - m.a);
|
|
534
|
-
m.a += turn * (1 - Math.exp(-dt * 22 *
|
|
534
|
+
m.a += turn * (1 - Math.exp(-dt * 22 * clamp2(t.dist / 8, 0, 1)));
|
|
535
535
|
e = 1.4 * t.near * t.near / Math.max(t.dist, 1);
|
|
536
536
|
ix = t.x * e;
|
|
537
537
|
iy = t.y * e;
|
|
@@ -550,13 +550,13 @@
|
|
|
550
550
|
m.acc = 0;
|
|
551
551
|
return;
|
|
552
552
|
}
|
|
553
|
-
pull = m.d > 0 ?
|
|
554
|
-
m.acc += dt * RATE * Math.pow(pull, 1.4) * (0.35 + 0.65 *
|
|
553
|
+
pull = m.d > 0 ? clamp2(m.d / PULL, 0, 1) : 0.5 * clamp2(-m.d / DENT, 0, 1);
|
|
554
|
+
m.acc += dt * RATE * Math.pow(pull, 1.4) * (0.35 + 0.65 * clamp2(m.pace / 160, 0, 1));
|
|
555
555
|
for (; m.acc >= 1; m.acc--) {
|
|
556
556
|
if (m.dust.length >= MAXDUST) continue;
|
|
557
557
|
th = m.a + (Math.random() + Math.random() - 1) * width(m) * 1.4;
|
|
558
558
|
r = radius(m, th) - Math.random() * 1.2;
|
|
559
|
-
if (m.d > 0) r += 0.35 * m.d *
|
|
559
|
+
if (m.d > 0) r += 0.35 * m.d * clamp2((r - R) / (m.d * (1 + BODY)), 0, 1);
|
|
560
560
|
x = m.cx + r * Math.cos(th);
|
|
561
561
|
y = m.cy + r * Math.sin(th);
|
|
562
562
|
dx = C + t.x - x;
|
|
@@ -595,12 +595,12 @@
|
|
|
595
595
|
dy = ty - p.y;
|
|
596
596
|
dist = Math.hypot(dx, dy) || 1e-3;
|
|
597
597
|
sp = (home ? 18 : 30) + 420 / (dist + 6);
|
|
598
|
-
sw = p.spin * sp * 0.4 *
|
|
598
|
+
sw = p.spin * sp * 0.4 * clamp2(dist / 20, 0, 1);
|
|
599
599
|
p.vx += (dx / dist * sp - dy / dist * sw - p.vx) * e;
|
|
600
600
|
p.vy += (dy / dist * sp + dx / dist * sw - p.vy) * e;
|
|
601
601
|
p.x += p.vx * dt;
|
|
602
602
|
p.y += p.vy * dt;
|
|
603
|
-
p.k = 0.45 + 0.55 *
|
|
603
|
+
p.k = 0.45 + 0.55 * clamp2(dist / 12, 0, 1);
|
|
604
604
|
if (dist < (home ? 1 : 2)) p.age = Math.max(p.age, p.life - 0.1);
|
|
605
605
|
if (p.age >= p.life) m.dust.splice(k, 1);
|
|
606
606
|
}
|
|
@@ -631,7 +631,7 @@
|
|
|
631
631
|
r0 = radius(m, m.a + t0);
|
|
632
632
|
r1 = radius(m, m.a + t1);
|
|
633
633
|
ro = radius(m, m.a + tc);
|
|
634
|
-
q =
|
|
634
|
+
q = clamp2((ro - f) / top, 0, 1);
|
|
635
635
|
if (ro - f > 6) {
|
|
636
636
|
rm = f + (ro - f) * (0.4 + 0.2 * hash(i, 7));
|
|
637
637
|
shard(m, g, i * 2, q * 0.5, [at(t0, f), at(tc, f), at(t1, f), at(t1, Math.min(rm, r1)), at(t0, Math.min(rm, r0))], 0);
|
|
@@ -684,7 +684,7 @@
|
|
|
684
684
|
function chips(m, now) {
|
|
685
685
|
var t = m.t, f = crack(m)[0];
|
|
686
686
|
m.chips.forEach(function(c, k) {
|
|
687
|
-
var o =
|
|
687
|
+
var o = clamp2((damage(m, c.a) + 0.08 * tremble(300 + k, now) - c.at) / 0.12, 0, 1), z, r, x, y, dx, dy, l;
|
|
688
688
|
if (o < 0.02) o = 0;
|
|
689
689
|
if (!o && !c.open) return;
|
|
690
690
|
c.open = o;
|
|
@@ -888,7 +888,7 @@
|
|
|
888
888
|
}
|
|
889
889
|
|
|
890
890
|
// dist/glassnav.js
|
|
891
|
-
var CSS3 = `/* Liquid glass nav bar \xB7
|
|
891
|
+
var CSS3 = `/* Liquid glass nav bar \xB7 Sara Lobo Library
|
|
892
892
|
Figma "Liquid Glass Button - Dark", 291\xD760, for a dark app. Paint order follows the Figma layer: background blur \u2192
|
|
893
893
|
fills \u2192 lenses \u2192 capsule and icons \u2192 inner shadows \u2192 strokes. The bar stays out of any stacking context (no transform,
|
|
894
894
|
opacity or isolation on it), so every blend mode mixes with what is behind it: keep it on a solid background. */
|
|
@@ -1010,7 +1010,7 @@
|
|
|
1010
1010
|
function still(s) {
|
|
1011
1011
|
return Math.abs(s.x - s.t) < 0.02 && Math.abs(s.v) < 0.05;
|
|
1012
1012
|
}
|
|
1013
|
-
function
|
|
1013
|
+
function clamp2(v, a, b) {
|
|
1014
1014
|
return Math.max(a, Math.min(b, v));
|
|
1015
1015
|
}
|
|
1016
1016
|
var running = /* @__PURE__ */ new Set(), raf = 0, last = 0, uid = 0;
|
|
@@ -1072,7 +1072,7 @@
|
|
|
1072
1072
|
pill.style.left = l + "px";
|
|
1073
1073
|
pill.style.width = r - l + "px";
|
|
1074
1074
|
dark.style.clipPath = "inset(3px " + (291 - r) + "px 3px " + l + "px round 27px)";
|
|
1075
|
-
var fl =
|
|
1075
|
+
var fl = clamp2((43 - l) / 40, 0, 1), fr = clamp2((r - 248) / 40, 0, 1);
|
|
1076
1076
|
rim.classList.toggle("lgn-fade", fl + fr > 1e-3);
|
|
1077
1077
|
rim.style.setProperty("--fl", fl.toFixed(3));
|
|
1078
1078
|
rim.style.setProperty("--fr", fr.toFixed(3));
|
|
@@ -1270,7 +1270,7 @@
|
|
|
1270
1270
|
if (to === void 0) return;
|
|
1271
1271
|
e.preventDefault();
|
|
1272
1272
|
user();
|
|
1273
|
-
to =
|
|
1273
|
+
to = clamp2(to, 0, 4);
|
|
1274
1274
|
select(to);
|
|
1275
1275
|
hits[to].focus();
|
|
1276
1276
|
});
|
|
@@ -1393,7 +1393,7 @@
|
|
|
1393
1393
|
}
|
|
1394
1394
|
|
|
1395
1395
|
// dist/thinking.js
|
|
1396
|
-
var CSS4 = `/* Thinking orbs \xB7
|
|
1396
|
+
var CSS4 = `/* Thinking orbs \xB7 Sara Lobo Library
|
|
1397
1397
|
A ball of dots and the word beside it, for the wait while an answer is being written. thinking.js draws the ball
|
|
1398
1398
|
and, for the Sweep, carries the ball's wave on through the word. Made for dark backgrounds: on a light one, set
|
|
1399
1399
|
--thinking-ink (the dots and the light in the word) and --thinking-dim (the word at rest). */
|
|
@@ -1472,7 +1472,7 @@
|
|
|
1472
1472
|
var t = clamp01((x - a) / (b - a));
|
|
1473
1473
|
return t * t * (3 - 2 * t);
|
|
1474
1474
|
}
|
|
1475
|
-
function
|
|
1475
|
+
function ease2(t) {
|
|
1476
1476
|
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
1477
1477
|
}
|
|
1478
1478
|
function painter() {
|
|
@@ -1536,7 +1536,7 @@
|
|
|
1536
1536
|
q[c] = qb * sn + qc * cs;
|
|
1537
1537
|
}
|
|
1538
1538
|
function sphere(small) {
|
|
1539
|
-
var per = small ? 2 : 4, COUNT = 5, SLOT = 0.5, DUR = 0.42,
|
|
1539
|
+
var per = small ? 2 : 4, COUNT = 5, SLOT = 0.5, DUR = 0.42, HOLD2 = 1.1, R0 = 0.8, D = [], v = [0, 0, 0], o = [0, 0, 0, 0, 0, 0];
|
|
1540
1540
|
for (var a = 0; a < 3; a++) for (var sg = 1; sg >= -1; sg -= 2) {
|
|
1541
1541
|
var s = TONES[a * 2 + (sg > 0 ? 0 : 1)], b = (a + 1) % 3, c = (a + 2) % 3;
|
|
1542
1542
|
for (var su = -1; su <= 1; su++) for (var sv = -1; sv <= 1; sv++) for (var j = 0; j < per; j++) for (var k = 0; k < per; k++) {
|
|
@@ -1583,17 +1583,17 @@
|
|
|
1583
1583
|
mi++;
|
|
1584
1584
|
mt -= SLOT;
|
|
1585
1585
|
}
|
|
1586
|
-
} else if (mt >=
|
|
1586
|
+
} else if (mt >= HOLD2) {
|
|
1587
1587
|
plan();
|
|
1588
1588
|
mi = 0;
|
|
1589
|
-
mt -=
|
|
1589
|
+
mt -= HOLD2;
|
|
1590
1590
|
}
|
|
1591
1591
|
},
|
|
1592
1592
|
draw: function(p, u) {
|
|
1593
1593
|
var V = view(0.72 + 0.2 * Math.sin(t * TAU / 15), 0.5 + 0.07 * Math.sin(t * TAU / 21));
|
|
1594
1594
|
var m = mi < seq.length ? seq[mi] : null;
|
|
1595
|
-
var th = m ? m.dir * Math.PI / 2 *
|
|
1596
|
-
var glow = m ? 0 : Math.sin(Math.PI * clamp01(mt /
|
|
1595
|
+
var th = m ? m.dir * Math.PI / 2 * ease2(clamp01(mt / DUR)) : 0, cs = Math.cos(th), sn = Math.sin(th);
|
|
1596
|
+
var glow = m ? 0 : Math.sin(Math.PI * clamp01(mt / HOLD2)), rb = R0 * (1 + 0.03 * glow);
|
|
1597
1597
|
for (var i = 0; i < D.length; i++) {
|
|
1598
1598
|
var d = D[i];
|
|
1599
1599
|
v[0] = d.pos[0];
|
|
@@ -1775,6 +1775,700 @@
|
|
|
1775
1775
|
} };
|
|
1776
1776
|
}
|
|
1777
1777
|
|
|
1778
|
+
// dist/panorama.js
|
|
1779
|
+
var DEFAULTS = {
|
|
1780
|
+
title: "Lorem ipsum",
|
|
1781
|
+
text: "Dolor sit amet,",
|
|
1782
|
+
textItalic: "consectetur adipiscing.",
|
|
1783
|
+
// set in the display face, after text
|
|
1784
|
+
video: null,
|
|
1785
|
+
// { src, poster, title, year }: the opening clip, then the screen in the middle of the dome
|
|
1786
|
+
stills: [],
|
|
1787
|
+
// [{ src, title, year }]: the other screens, repeated around the dome
|
|
1788
|
+
labels: true,
|
|
1789
|
+
// "CAM 07" on each screen
|
|
1790
|
+
intro: true,
|
|
1791
|
+
// the opening plays the first time the dome comes on screen; false starts it settled
|
|
1792
|
+
cursor: true,
|
|
1793
|
+
// the dot-and-ring pointer (only for a mouse or trackpad)
|
|
1794
|
+
fonts: true
|
|
1795
|
+
// load Cormorant Garamond, Geist and Geist Mono from Google Fonts
|
|
1796
|
+
};
|
|
1797
|
+
var FONTS = "https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;1,300&family=Geist:wght@400;500&family=Geist+Mono:wght@400;500&display=swap";
|
|
1798
|
+
var HOLD = 3.3;
|
|
1799
|
+
var MORPH_V = 1.7;
|
|
1800
|
+
var MORPH = 1.45;
|
|
1801
|
+
var AR = 1.6;
|
|
1802
|
+
var D2R = Math.PI / 180;
|
|
1803
|
+
var R2D = 180 / Math.PI;
|
|
1804
|
+
var CSS5 = `
|
|
1805
|
+
.cup { --cup-ink: #0b0a09; --cup-paper: #ece7de; --cup-soft: cubic-bezier(.22, .61, .21, 1);
|
|
1806
|
+
--cup-display: "Cormorant Garamond", "Cormorant", Garamond, "Times New Roman", serif;
|
|
1807
|
+
--cup-ui: "Geist", -apple-system, "Helvetica Neue", Arial, sans-serif; --cup-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
1808
|
+
position: relative; overflow: hidden; background: var(--cup-ink); color: var(--cup-paper); container-type: size; touch-action: pan-y;
|
|
1809
|
+
-webkit-font-smoothing: antialiased; -webkit-user-select: none; user-select: none; }
|
|
1810
|
+
.cup.cup-cur, .cup.cup-cur * { cursor: none; }
|
|
1811
|
+
.cup-gl { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
|
1812
|
+
.cup-src { position: absolute; left: 0; top: 0; width: 2px; height: 2px; opacity: 0; pointer-events: none; }
|
|
1813
|
+
.cup-scrim { position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity 1.2s var(--cup-soft);
|
|
1814
|
+
background: radial-gradient(34% 28% at 50% 50%, rgba(11, 10, 9, .9), rgba(11, 10, 9, .45) 60%, rgba(11, 10, 9, 0) 100%); }
|
|
1815
|
+
.cup.is-settled .cup-scrim { opacity: 1; }
|
|
1816
|
+
|
|
1817
|
+
/* the title tracks in like an opening credit: the letters drift together from a wide spacing and out of a blur, centre
|
|
1818
|
+
first; then a thin anamorphic streak crosses it and each letter catches the light as it passes */
|
|
1819
|
+
.cup-title { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); text-align: center; white-space: nowrap; pointer-events: none; will-change: transform; }
|
|
1820
|
+
/* padding-bottom keeps the descenders (the p of "ipsum") clear of the line below */
|
|
1821
|
+
.cup-name { position: relative; margin: 0; padding-bottom: .15em; font-family: var(--cup-display); font-weight: 300; font-size: clamp(44px, 10.5cqw, 168px); line-height: .9; letter-spacing: -.01em; }
|
|
1822
|
+
.cup-name span { display: inline-block; opacity: 0; transform: translateX(calc(var(--o) * .42em)); filter: blur(14px);
|
|
1823
|
+
transition: opacity 1.6s var(--cup-soft) var(--d), transform 2.8s cubic-bezier(.16, 1, .3, 1) var(--d), filter 2.2s var(--cup-soft) var(--d); }
|
|
1824
|
+
.cup-title.is-in .cup-name span { opacity: 1; transform: none; filter: blur(0); animation: cup-glint 1.5s ease-in-out var(--g) both; }
|
|
1825
|
+
@keyframes cup-glint { 0%, 100% { text-shadow: 0 0 0 rgba(255, 238, 210, 0); } 45% { text-shadow: 0 0 22px rgba(255, 238, 210, .6), 0 0 2px rgba(255, 250, 240, .85); } }
|
|
1826
|
+
.cup-flare { position: absolute; left: -20%; right: -20%; top: 54%; height: 1px; pointer-events: none; opacity: 0;
|
|
1827
|
+
background: linear-gradient(90deg, transparent, rgba(255, 240, 215, .9) 50%, transparent); box-shadow: 0 0 14px 2px rgba(255, 226, 180, .3); }
|
|
1828
|
+
.cup-title.is-in .cup-flare { animation: cup-flare 2.4s cubic-bezier(.45, 0, .2, 1) 1.5s both; }
|
|
1829
|
+
@keyframes cup-flare { 0% { opacity: 0; transform: translateX(-55%) scaleX(.35); } 25% { opacity: 1; } 75% { opacity: .85; } 100% { opacity: 0; transform: translateX(55%) scaleX(.35); } }
|
|
1830
|
+
.cup-text { margin: 18px 0 0; font: 400 clamp(13px, 1.15cqw, 16px)/1.4 var(--cup-ui); letter-spacing: .01em; opacity: .82; }
|
|
1831
|
+
.cup-text em { font-family: var(--cup-display); font-size: 1.32em; }
|
|
1832
|
+
/* each word rises from its own mask: a clip-path, not overflow, so every word keeps its own baseline (both faces sit on
|
|
1833
|
+
one line), and wider than the word so the italic's leaning strokes stay whole */
|
|
1834
|
+
.cup-text .w { display: inline-block; clip-path: inset(0 -.25em); }
|
|
1835
|
+
.cup-text .w > span { display: inline-block; transform: translateY(110%); transition: transform 1.4s cubic-bezier(.16, 1, .3, 1) var(--d); }
|
|
1836
|
+
.cup-title.is-in .cup-text .w > span { transform: none; }
|
|
1837
|
+
.cup-title.is-reset, .cup-title.is-reset * { transition: none !important; animation: none !important; }
|
|
1838
|
+
|
|
1839
|
+
.cup-bars i { position: absolute; left: 0; right: 0; height: var(--cup-bar, 0px); background: #000; transition: transform 1.3s var(--cup-soft); }
|
|
1840
|
+
.cup-bars i:first-child { top: 0; } .cup-bars i:last-child { bottom: 0; }
|
|
1841
|
+
.cup.is-morph .cup-bars i:first-child { transform: translateY(-101%); } .cup.is-morph .cup-bars i:last-child { transform: translateY(101%); }
|
|
1842
|
+
|
|
1843
|
+
/* focus brackets on the screen under the pointer, and its name */
|
|
1844
|
+
.cup-aim { position: absolute; left: 0; top: 0; pointer-events: none; opacity: 0; will-change: transform, width, height; }
|
|
1845
|
+
.cup-aim > i { position: absolute; width: 16px; height: 16px; border: 0 solid var(--cup-paper); }
|
|
1846
|
+
.cup-aim > i:nth-child(1) { left: -7px; top: -7px; border-width: 1px 0 0 1px; }
|
|
1847
|
+
.cup-aim > i:nth-child(2) { right: -7px; top: -7px; border-width: 1px 1px 0 0; }
|
|
1848
|
+
.cup-aim > i:nth-child(3) { left: -7px; bottom: -7px; border-width: 0 0 1px 1px; }
|
|
1849
|
+
.cup-aim > i:nth-child(4) { right: -7px; bottom: -7px; border-width: 0 1px 1px 0; }
|
|
1850
|
+
.cup-tag { position: absolute; left: -7px; top: calc(100% + 18px); white-space: nowrap; }
|
|
1851
|
+
.cup-tag em { display: block; font: italic 300 28px/1 var(--cup-display); text-shadow: 0 1px 12px rgba(0, 0, 0, .6); }
|
|
1852
|
+
.cup-tag small { display: block; margin-top: 8px; font: 10.5px/1 var(--cup-mono); letter-spacing: .1em; opacity: .6; }
|
|
1853
|
+
.cup-tag.is-new em, .cup-tag.is-new small { animation: cup-tag .8s var(--cup-soft) both; }
|
|
1854
|
+
.cup-tag.is-new small { animation-delay: .1s; }
|
|
1855
|
+
@keyframes cup-tag { from { opacity: 0; transform: translateY(8px); filter: blur(5px); } }
|
|
1856
|
+
|
|
1857
|
+
/* the pointer: a dot, and a ring that trails it and stretches with speed */
|
|
1858
|
+
.cup-cursor { position: absolute; left: 0; top: 0; pointer-events: none; opacity: 0; transition: opacity .35s var(--cup-soft); }
|
|
1859
|
+
.cup-cursor.is-on { opacity: 1; }
|
|
1860
|
+
.cup-cursor i { position: absolute; left: 0; top: 0; border-radius: 50%; will-change: transform; }
|
|
1861
|
+
.cup-dot { width: 6px; height: 6px; margin: -3px 0 0 -3px; background: var(--cup-paper); }
|
|
1862
|
+
.cup-ring { width: 38px; height: 38px; margin: -19px 0 0 -19px; border: 1px solid rgba(236, 231, 222, .5); }
|
|
1863
|
+
|
|
1864
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1865
|
+
.cup-name span, .cup-text .w > span, .cup-bars i, .cup-scrim { transition: none; }
|
|
1866
|
+
.cup-title.is-in .cup-name span, .cup-title.is-in .cup-flare { animation: none; }
|
|
1867
|
+
}`;
|
|
1868
|
+
var VS = `
|
|
1869
|
+
attribute vec3 aPos; attribute vec2 aUv; attribute vec4 aRect; attribute vec4 aMisc; attribute vec2 aPan; attribute vec2 aLab; attribute float aGlow;
|
|
1870
|
+
uniform vec2 uHalf; uniform float uP;
|
|
1871
|
+
varying vec2 vUv; varying vec4 vRect; varying vec4 vMisc; varying vec2 vPan; varying vec2 vLab; varying float vGlow;
|
|
1872
|
+
void main() {
|
|
1873
|
+
vUv = aUv; vRect = aRect; vMisc = aMisc; vPan = aPan; vLab = aLab; vGlow = aGlow;
|
|
1874
|
+
gl_Position = vec4(aPos.x / uHalf.x, -aPos.y / uHalf.y, 0.0, (uP - aPos.z) / uP); // CSS-style perspective, divided on the GPU
|
|
1875
|
+
}`;
|
|
1876
|
+
var FS = `
|
|
1877
|
+
#extension GL_OES_standard_derivatives : enable
|
|
1878
|
+
precision mediump float;
|
|
1879
|
+
varying vec2 vUv; varying vec4 vRect; varying vec4 vMisc; varying vec2 vPan; varying vec2 vLab; varying float vGlow;
|
|
1880
|
+
uniform sampler2D uAtlas; uniform sampler2D uVideo; uniform sampler2D uLabels; uniform float uLab;
|
|
1881
|
+
uniform highp vec2 uView; uniform highp float uDpr; uniform highp vec4 uColor;
|
|
1882
|
+
vec3 grade(vec3 c) { // black and white, colour in a soft circle at the pointer
|
|
1883
|
+
if (uColor.w <= 0.0) return c;
|
|
1884
|
+
highp vec2 sp = vec2(gl_FragCoord.x / uDpr - uView.x, uView.y - gl_FragCoord.y / uDpr);
|
|
1885
|
+
float k = 1.0 - smoothstep(uColor.z * 0.4, uColor.z, length(sp - uColor.xy));
|
|
1886
|
+
vec3 g = vec3(dot(c, vec3(0.299, 0.587, 0.114))) * vec3(1.02, 1.0, 0.96);
|
|
1887
|
+
return mix(c, mix(g * 0.92, c * (1.0 + 0.08 * k), k), uColor.w);
|
|
1888
|
+
}
|
|
1889
|
+
void main() {
|
|
1890
|
+
vec2 uv = clamp((vUv - 0.5) / 1.16 + 0.5 + vPan, 0.002, 0.998); // the picture a little larger, panned
|
|
1891
|
+
vec2 t = vRect.xy + uv * vRect.zw;
|
|
1892
|
+
vec3 col = vMisc.z > 0.5 ? texture2D(uVideo, t).rgb : texture2D(uAtlas, t).rgb;
|
|
1893
|
+
vec2 p = (vUv - 0.5) * vec2(1.6, 1.0);
|
|
1894
|
+
float kb = vMisc.w, r = 0.035 * kb;
|
|
1895
|
+
vec2 q = abs(p) - vec2(0.8, 0.5) + r;
|
|
1896
|
+
float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; // a rounded screen
|
|
1897
|
+
col *= mix(0.5, 1.0, smoothstep(0.0, 0.14 * kb, -d)); // its dark inner edge
|
|
1898
|
+
vec2 lp = (vUv - vec2(0.07, 0.836)) / vec2(0.42, 0.084);
|
|
1899
|
+
if (uLab > 0.0 && lp.x > 0.0 && lp.x < 1.0 && lp.y > 0.0 && lp.y < 1.0) {
|
|
1900
|
+
float a = texture2D(uLabels, vLab + lp * vec2(0.25, 0.03125)).a;
|
|
1901
|
+
col = mix(col, vec3(0.925, 0.906, 0.871), a * 0.8 * uLab);
|
|
1902
|
+
}
|
|
1903
|
+
col *= 1.0 - vMisc.y;
|
|
1904
|
+
col = grade(col);
|
|
1905
|
+
float aa = fwidth(d);
|
|
1906
|
+
float alpha = (1.0 - smoothstep(-aa, aa, d)) * vMisc.x;
|
|
1907
|
+
vec4 outc = vec4(col * alpha, alpha);
|
|
1908
|
+
if (vGlow > 0.001) { // the screen's own light on the dark around it
|
|
1909
|
+
vec2 cu = clamp(vUv, 0.0, 1.0) * 0.86 + 0.07;
|
|
1910
|
+
vec3 gc = vMisc.z > 0.5 ? texture2D(uVideo, vRect.xy + cu * vRect.zw).rgb : texture2D(uAtlas, vRect.xy + cu * vRect.zw, 5.0).rgb;
|
|
1911
|
+
float ga = vGlow * 0.85 * exp(-max(d, 0.0) / (0.28 * kb)) * vMisc.x * (1.0 - alpha);
|
|
1912
|
+
outc += vec4(grade(gc) * 1.6 * ga, ga);
|
|
1913
|
+
}
|
|
1914
|
+
gl_FragColor = outc;
|
|
1915
|
+
}`;
|
|
1916
|
+
var clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
|
|
1917
|
+
var lerp = (a, b, u) => a + (b - a) * u;
|
|
1918
|
+
var ease = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
|
|
1919
|
+
var sstep = (a, b, v) => {
|
|
1920
|
+
const u = clamp((v - a) / (b - a));
|
|
1921
|
+
return u * u * (3 - 2 * u);
|
|
1922
|
+
};
|
|
1923
|
+
var damp = (cur, to, rate, dt) => cur + (to - cur) * (1 - Math.exp(-rate * dt));
|
|
1924
|
+
var esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
1925
|
+
function rot(x, y, z, a, b) {
|
|
1926
|
+
const y1 = y * Math.cos(b) - z * Math.sin(b), z1 = y * Math.sin(b) + z * Math.cos(b);
|
|
1927
|
+
return [x * Math.cos(a) + z1 * Math.sin(a), y1, -x * Math.sin(a) + z1 * Math.cos(a)];
|
|
1928
|
+
}
|
|
1929
|
+
var styled = false;
|
|
1930
|
+
function injectStyle(fonts) {
|
|
1931
|
+
if (styled) return;
|
|
1932
|
+
styled = true;
|
|
1933
|
+
if (fonts && !document.querySelector(`link[href="${FONTS}"]`)) {
|
|
1934
|
+
const l = document.createElement("link");
|
|
1935
|
+
l.rel = "stylesheet";
|
|
1936
|
+
l.href = FONTS;
|
|
1937
|
+
document.head.append(l);
|
|
1938
|
+
}
|
|
1939
|
+
const s = document.createElement("style");
|
|
1940
|
+
s.textContent = CSS5;
|
|
1941
|
+
document.head.append(s);
|
|
1942
|
+
}
|
|
1943
|
+
function markup(o) {
|
|
1944
|
+
const ch = [...String(o.title)], mid = (ch.length - 1) / 2;
|
|
1945
|
+
const letters = ch.map((c, i) => `<span aria-hidden="true" style="--o:${(i - mid).toFixed(1)};--d:${(Math.abs(i - mid) * 0.07).toFixed(2)}s;--g:${(1.5 + i / Math.max(1, ch.length - 1) * 1.2).toFixed(2)}s">${c === " " ? " " : esc(c)}</span>`).join("");
|
|
1946
|
+
let k = 0;
|
|
1947
|
+
const words = (s) => String(s || "").split(/\s+/).filter(Boolean).map((w) => `<span class="w"><span style="--d:${(1.7 + k++ * 0.07).toFixed(2)}s">${esc(w)}</span></span>`).join(" ");
|
|
1948
|
+
const text = words(o.text) + (o.textItalic ? ` <em>${words(o.textItalic)}</em>` : "");
|
|
1949
|
+
return `<canvas class="cup-gl" aria-hidden="true"></canvas>
|
|
1950
|
+
<div class="cup-scrim"></div>
|
|
1951
|
+
<div class="cup-title"><div class="cup-name" role="heading" aria-level="1" aria-label="${esc(o.title)}">${letters}<i class="cup-flare" aria-hidden="true"></i></div><p class="cup-text">${text}</p></div>
|
|
1952
|
+
<div class="cup-bars" aria-hidden="true"><i></i><i></i></div>
|
|
1953
|
+
<div class="cup-aim" aria-hidden="true"><i></i><i></i><i></i><i></i><div class="cup-tag"></div></div>
|
|
1954
|
+
<div class="cup-cursor" aria-hidden="true"><i class="cup-ring"></i><i class="cup-dot"></i></div>`;
|
|
1955
|
+
}
|
|
1956
|
+
function mountPanorama(root, options = {}) {
|
|
1957
|
+
const o = { ...DEFAULTS, ...options };
|
|
1958
|
+
injectStyle(o.fonts);
|
|
1959
|
+
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1960
|
+
const fine = matchMedia("(pointer: fine)").matches;
|
|
1961
|
+
root.classList.add("cup");
|
|
1962
|
+
if (o.cursor && fine) root.classList.add("cup-cur");
|
|
1963
|
+
root.innerHTML = markup(o);
|
|
1964
|
+
const $ = (s) => root.querySelector(s);
|
|
1965
|
+
const canvas = $(".cup-gl"), titleEl = $(".cup-title"), aim = $(".cup-aim"), tag = $(".cup-tag");
|
|
1966
|
+
const cursor = $(".cup-cursor"), dot = $(".cup-dot"), ring = $(".cup-ring");
|
|
1967
|
+
const stills = (o.stills || []).slice(0, 16);
|
|
1968
|
+
let video = null;
|
|
1969
|
+
if (o.video && o.video.src) {
|
|
1970
|
+
video = document.createElement("video");
|
|
1971
|
+
video.className = "cup-src";
|
|
1972
|
+
video.crossOrigin = "anonymous";
|
|
1973
|
+
Object.assign(video, { src: o.video.src, muted: true, loop: true, playsInline: true, preload: "auto" });
|
|
1974
|
+
if (o.video.poster) video.poster = o.video.poster;
|
|
1975
|
+
video.setAttribute("muted", "");
|
|
1976
|
+
video.setAttribute("playsinline", "");
|
|
1977
|
+
video.setAttribute("aria-hidden", "true");
|
|
1978
|
+
root.append(video);
|
|
1979
|
+
}
|
|
1980
|
+
const S = Math.max(1, stills.length);
|
|
1981
|
+
const filmFor = (i) => i === 0 && video ? o.video : stills[(i * 7 + Math.floor(i / S) * 4) % S] || {};
|
|
1982
|
+
const stillIndex = (i) => i === 0 && video ? -1 : (i * 7 + Math.floor(i / S) * 4) % S;
|
|
1983
|
+
const gl = canvas.getContext("webgl", { alpha: true, premultipliedAlpha: true, antialias: true });
|
|
1984
|
+
if (!gl || !gl.getExtension("OES_standard_derivatives")) {
|
|
1985
|
+
const pic = o.video && o.video.poster || stills[0] && stills[0].src;
|
|
1986
|
+
if (pic) Object.assign(root.style, { backgroundImage: `url("${pic}")`, backgroundSize: "cover", backgroundPosition: "center" });
|
|
1987
|
+
titleEl.classList.add("is-in");
|
|
1988
|
+
return { replay() {
|
|
1989
|
+
}, destroy() {
|
|
1990
|
+
root.innerHTML = "";
|
|
1991
|
+
root.classList.remove("cup", "cup-cur");
|
|
1992
|
+
} };
|
|
1993
|
+
}
|
|
1994
|
+
const compile = (type, src) => {
|
|
1995
|
+
const s = gl.createShader(type);
|
|
1996
|
+
gl.shaderSource(s, src);
|
|
1997
|
+
gl.compileShader(s);
|
|
1998
|
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
|
|
1999
|
+
return s;
|
|
2000
|
+
};
|
|
2001
|
+
const prog = gl.createProgram();
|
|
2002
|
+
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VS));
|
|
2003
|
+
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FS));
|
|
2004
|
+
gl.linkProgram(prog);
|
|
2005
|
+
gl.useProgram(prog);
|
|
2006
|
+
const MAX = 100, F = 18;
|
|
2007
|
+
const data = new Float32Array(MAX * 4 * F);
|
|
2008
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
2009
|
+
const idx = new Uint16Array(MAX * 6);
|
|
2010
|
+
for (let i = 0; i < MAX; i++) idx.set([i * 4, i * 4 + 1, i * 4 + 2, i * 4, i * 4 + 2, i * 4 + 3], i * 6);
|
|
2011
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
|
|
2012
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, idx, gl.STATIC_DRAW);
|
|
2013
|
+
[["aPos", 3, 0], ["aUv", 2, 3], ["aRect", 4, 5], ["aMisc", 4, 9], ["aPan", 2, 13], ["aLab", 2, 15], ["aGlow", 1, 17]].forEach(([name, size, off]) => {
|
|
2014
|
+
const loc = gl.getAttribLocation(prog, name);
|
|
2015
|
+
gl.enableVertexAttribArray(loc);
|
|
2016
|
+
gl.vertexAttribPointer(loc, size, gl.FLOAT, false, F * 4, off * 4);
|
|
2017
|
+
});
|
|
2018
|
+
const U = (n2) => gl.getUniformLocation(prog, n2);
|
|
2019
|
+
const uHalf = U("uHalf"), uP = U("uP"), uLab = U("uLab"), uView = U("uView"), uDpr = U("uDpr"), uColor = U("uColor");
|
|
2020
|
+
gl.uniform1i(U("uAtlas"), 0);
|
|
2021
|
+
gl.uniform1i(U("uVideo"), 1);
|
|
2022
|
+
gl.uniform1i(U("uLabels"), 2);
|
|
2023
|
+
gl.enable(gl.BLEND);
|
|
2024
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
2025
|
+
const texture = (unit, mip) => {
|
|
2026
|
+
const t = gl.createTexture();
|
|
2027
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
2028
|
+
gl.bindTexture(gl.TEXTURE_2D, t);
|
|
2029
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2030
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2031
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mip ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);
|
|
2032
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
2033
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([21, 19, 17, 255]));
|
|
2034
|
+
return { t, unit, mip };
|
|
2035
|
+
};
|
|
2036
|
+
const upload = (tx, src) => {
|
|
2037
|
+
try {
|
|
2038
|
+
gl.activeTexture(gl.TEXTURE0 + tx.unit);
|
|
2039
|
+
gl.bindTexture(gl.TEXTURE_2D, tx.t);
|
|
2040
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
|
|
2041
|
+
if (tx.mip) gl.generateMipmap(gl.TEXTURE_2D);
|
|
2042
|
+
} catch (e) {
|
|
2043
|
+
console.warn("Panorama: a picture could not be read (another site without CORS?)", e);
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
const tAtlas = texture(0, true), tVideo = texture(1, false), tLab = texture(2, true);
|
|
2047
|
+
const sheet = document.createElement("canvas");
|
|
2048
|
+
sheet.width = 2048;
|
|
2049
|
+
sheet.height = 1024;
|
|
2050
|
+
const sctx = sheet.getContext("2d");
|
|
2051
|
+
sctx.fillStyle = "#151311";
|
|
2052
|
+
sctx.fillRect(0, 0, 2048, 1024);
|
|
2053
|
+
let pending = stills.length;
|
|
2054
|
+
stills.forEach((f, j) => {
|
|
2055
|
+
const im = new Image();
|
|
2056
|
+
im.crossOrigin = "anonymous";
|
|
2057
|
+
im.decoding = "async";
|
|
2058
|
+
im.onload = im.onerror = () => {
|
|
2059
|
+
if (im.naturalWidth) {
|
|
2060
|
+
const iw = im.naturalWidth, ih = im.naturalHeight, sw = Math.min(iw, ih * AR), sh = sw / AR;
|
|
2061
|
+
sctx.drawImage(im, (iw - sw) / 2, (ih - sh) / 2, sw, sh, j % 4 * 512, Math.floor(j / 4) * 256, 400, 250);
|
|
2062
|
+
}
|
|
2063
|
+
if (--pending === 0) upload(tAtlas, sheet);
|
|
2064
|
+
};
|
|
2065
|
+
im.src = f.src;
|
|
2066
|
+
});
|
|
2067
|
+
const cellRect = (j) => [j % 4 * 0.25, Math.floor(j / 4) * 0.25, 400 / 2048, 250 / 1024];
|
|
2068
|
+
let videoRect = [0.05, 0, 0.9, 1], lastVT = -1;
|
|
2069
|
+
if (video) {
|
|
2070
|
+
if (o.video.poster) {
|
|
2071
|
+
const p = new Image();
|
|
2072
|
+
p.crossOrigin = "anonymous";
|
|
2073
|
+
p.onload = () => upload(tVideo, p);
|
|
2074
|
+
p.src = o.video.poster;
|
|
2075
|
+
}
|
|
2076
|
+
video.addEventListener("loadedmetadata", () => {
|
|
2077
|
+
const a = video.videoWidth / video.videoHeight;
|
|
2078
|
+
videoRect = a > AR ? [(1 - AR / a) / 2, 0, AR / a, 1] : [0, (1 - a / AR) / 2, 1, a / AR];
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
let labelsReady = false;
|
|
2082
|
+
function makeLabels() {
|
|
2083
|
+
const c = document.createElement("canvas");
|
|
2084
|
+
c.width = c.height = 1024;
|
|
2085
|
+
const x = c.getContext("2d");
|
|
2086
|
+
x.fillStyle = "#fff";
|
|
2087
|
+
x.font = '500 19px "Geist Mono", ui-monospace, Menlo, monospace';
|
|
2088
|
+
x.textBaseline = "middle";
|
|
2089
|
+
try {
|
|
2090
|
+
x.letterSpacing = "1.5px";
|
|
2091
|
+
} catch (e) {
|
|
2092
|
+
}
|
|
2093
|
+
for (let i = 0; i < 128; i++) {
|
|
2094
|
+
const ox = i % 4 * 256, oy = Math.floor(i / 4) * 32;
|
|
2095
|
+
x.globalAlpha = 0.7;
|
|
2096
|
+
x.beginPath();
|
|
2097
|
+
x.arc(ox + 9, oy + 16, 4, 0, 7);
|
|
2098
|
+
x.fill();
|
|
2099
|
+
x.globalAlpha = 1;
|
|
2100
|
+
x.fillText("CAM " + String(i + 1).padStart(2, "0"), ox + 22, oy + 17);
|
|
2101
|
+
}
|
|
2102
|
+
upload(tLab, c);
|
|
2103
|
+
labelsReady = true;
|
|
2104
|
+
}
|
|
2105
|
+
let W = 0, H = 0, P = 600, R = 0, tw = 0, cells = [], n = 0;
|
|
2106
|
+
let g = new Float32Array(MAX), hv = new Float32Array(MAX);
|
|
2107
|
+
function layout() {
|
|
2108
|
+
W = root.clientWidth;
|
|
2109
|
+
H = root.clientHeight;
|
|
2110
|
+
if (!W || !H) return;
|
|
2111
|
+
P = Math.max(W * 0.42, 340);
|
|
2112
|
+
R = Math.max(W, H * 1.3) * 0.95;
|
|
2113
|
+
const C = W < 720 ? 7 : 11, Rw = 7, da = (W < 720 ? 18 : 15) * D2R, db = 13 * D2R;
|
|
2114
|
+
cells = [];
|
|
2115
|
+
for (let r = 0; r < Rw; r++) for (let c = 0; c < C; c++) cells.push([(c - (C - 1) / 2) * da, (r - (Rw - 1) / 2) * db]);
|
|
2116
|
+
cells.sort((p, q) => Math.hypot(p[0], p[1]) - Math.hypot(q[0], q[1]));
|
|
2117
|
+
n = Math.min(MAX, cells.length);
|
|
2118
|
+
tw = R * da * 0.84;
|
|
2119
|
+
const dpr = Math.min(devicePixelRatio || 1, 2);
|
|
2120
|
+
canvas.width = Math.round(W * dpr);
|
|
2121
|
+
canvas.height = Math.round(H * dpr);
|
|
2122
|
+
root.style.setProperty("--cup-bar", (W > H ? Math.max(0, (H - W / 2.39) / 2) : 0) + "px");
|
|
2123
|
+
}
|
|
2124
|
+
let px = 0, py = 0, inside = false, mx = 0, my = 0;
|
|
2125
|
+
const onMove = (e) => {
|
|
2126
|
+
const b = root.getBoundingClientRect();
|
|
2127
|
+
px = e.clientX - b.left;
|
|
2128
|
+
py = e.clientY - b.top;
|
|
2129
|
+
inside = true;
|
|
2130
|
+
mx = clamp(px / W * 2 - 1, -1, 1);
|
|
2131
|
+
my = clamp(py / H * 2 - 1, -1, 1);
|
|
2132
|
+
};
|
|
2133
|
+
const onLeave = () => {
|
|
2134
|
+
inside = false;
|
|
2135
|
+
mx = my = 0;
|
|
2136
|
+
};
|
|
2137
|
+
root.addEventListener("pointermove", onMove);
|
|
2138
|
+
root.addEventListener("pointerdown", onMove);
|
|
2139
|
+
root.addEventListener("pointerleave", onLeave);
|
|
2140
|
+
root.addEventListener("pointercancel", onLeave);
|
|
2141
|
+
let t0 = 0, last = 0, raf = 0, visible = false, started = false, ready = false, settledAt = 0;
|
|
2142
|
+
let headH = 0, headP = 0, clock = 0, fx = 0, fy = 0, on = 0, mono = 0, labOn = 0, hover = -1;
|
|
2143
|
+
const AIM = { x: 0, y: 0, w: 0, h: 0, o: 0, idx: -1 }, CUR = { x: 0, y: 0, s: 1, d: 1, seen: false };
|
|
2144
|
+
function begin() {
|
|
2145
|
+
started = true;
|
|
2146
|
+
titleEl.classList.add("is-reset");
|
|
2147
|
+
titleEl.classList.remove("is-in");
|
|
2148
|
+
void titleEl.offsetWidth;
|
|
2149
|
+
titleEl.classList.remove("is-reset");
|
|
2150
|
+
root.classList.remove("is-morph", "is-settled");
|
|
2151
|
+
settledAt = 0;
|
|
2152
|
+
mono = 0;
|
|
2153
|
+
labOn = 0;
|
|
2154
|
+
hover = -1;
|
|
2155
|
+
g.fill(0);
|
|
2156
|
+
hv.fill(0);
|
|
2157
|
+
if (video) {
|
|
2158
|
+
try {
|
|
2159
|
+
video.currentTime = 0;
|
|
2160
|
+
video.play().catch(() => {
|
|
2161
|
+
});
|
|
2162
|
+
} catch (e) {
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
const skip = reduce || !o.intro;
|
|
2166
|
+
t0 = performance.now() / 1e3 - (skip ? HOLD + 4 : 0);
|
|
2167
|
+
if (skip) {
|
|
2168
|
+
titleEl.classList.add("is-in");
|
|
2169
|
+
root.classList.add("is-morph", "is-settled");
|
|
2170
|
+
settledAt = 1;
|
|
2171
|
+
} else setTimeout(() => titleEl.classList.add("is-in"), 450);
|
|
2172
|
+
}
|
|
2173
|
+
function pose(i, dt) {
|
|
2174
|
+
const [a, b] = cells[i];
|
|
2175
|
+
let x = Math.sin(a) * Math.cos(b), y = Math.sin(b), z = -Math.cos(a) * Math.cos(b);
|
|
2176
|
+
[x, y, z] = rot(x, y, z, headH, 0);
|
|
2177
|
+
[x, y, z] = rot(x, y, z, 0, -headP);
|
|
2178
|
+
const fwd = -z, Xs = x / Math.max(0.05, fwd) * P, Ys = y / Math.max(0.05, fwd) * P;
|
|
2179
|
+
const h = hv[i] = damp(hv[i], hover === i ? 1 : 0, 6, dt);
|
|
2180
|
+
const near = g[i] = damp(g[i], Math.exp(-((Math.hypot(Xs - fx, Ys - fy) / 240) ** 2)) * on, 4, dt);
|
|
2181
|
+
const r = R * (1 - 0.07 * near - 0.12 * h);
|
|
2182
|
+
return {
|
|
2183
|
+
x: x * r,
|
|
2184
|
+
y: y * r,
|
|
2185
|
+
z: P + z * r,
|
|
2186
|
+
rx: Math.asin(clamp(y, -1, 1)) * R2D,
|
|
2187
|
+
ry: Math.atan2(-x, -z) * R2D,
|
|
2188
|
+
w: tw * (1 + 0.08 * h),
|
|
2189
|
+
o: sstep(0.3, 0.52, fwd),
|
|
2190
|
+
sh: clamp((1 - fwd) * 1.6) * 0.55 * (1 - 0.6 * near) * (1 - h),
|
|
2191
|
+
ix: -Xs / W * 6,
|
|
2192
|
+
iy: -Ys / H * 6,
|
|
2193
|
+
glow: h
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
const mix = (a, b, u) => {
|
|
2197
|
+
const r = {};
|
|
2198
|
+
for (const k in b) r[k] = lerp(a[k] ?? b[k], b[k], u);
|
|
2199
|
+
return r;
|
|
2200
|
+
};
|
|
2201
|
+
const CORNERS = [[-0.5, -0.5, 0, 0], [0.5, -0.5, 1, 0], [0.5, 0.5, 1, 1], [-0.5, 0.5, 0, 1]];
|
|
2202
|
+
function item(i, q, kb) {
|
|
2203
|
+
const w = q.w, h = w / AR, cx = Math.cos(q.rx * D2R), sx = Math.sin(q.rx * D2R), cy = Math.cos(q.ry * D2R), sy = Math.sin(q.ry * D2R);
|
|
2204
|
+
const pts = [];
|
|
2205
|
+
for (const [lx, ly] of CORNERS) {
|
|
2206
|
+
const X = lx * w, Y = ly * h * cx, Z = ly * h * sx;
|
|
2207
|
+
pts.push(X * cy + Z * sy + q.x, Y + q.y, -X * sy + Z * cy + q.z);
|
|
2208
|
+
}
|
|
2209
|
+
const m = q.glow > 1e-3 ? 0.75 : 0, hp = pts.slice();
|
|
2210
|
+
if (m) for (let k = 0; k < 3; k++) {
|
|
2211
|
+
const c = (pts[k] + pts[3 + k] + pts[6 + k] + pts[9 + k]) / 4;
|
|
2212
|
+
for (let v = 0; v < 4; v++) pts[v * 3 + k] = c + (pts[v * 3 + k] - c) * (1 + 2 * m);
|
|
2213
|
+
}
|
|
2214
|
+
const si = stillIndex(i);
|
|
2215
|
+
return { i, z: q.z, pts, hp, m, rect: si < 0 ? videoRect : cellRect(si), misc: [q.o, q.sh, si < 0 ? 1 : 0, kb], pan: [-q.ix / 116, -q.iy / 116], lab: [i % 4 * 0.25, Math.floor(i / 4) / 32], glow: q.glow };
|
|
2216
|
+
}
|
|
2217
|
+
function draw(list2) {
|
|
2218
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
2219
|
+
gl.clearColor(0, 0, 0, 0);
|
|
2220
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
2221
|
+
if (!labelsReady && o.labels) makeLabels();
|
|
2222
|
+
if (video && video.readyState >= 2 && video.currentTime !== lastVT) {
|
|
2223
|
+
lastVT = video.currentTime;
|
|
2224
|
+
upload(tVideo, video);
|
|
2225
|
+
}
|
|
2226
|
+
list2.sort((a, b) => a.z - b.z);
|
|
2227
|
+
let k = 0;
|
|
2228
|
+
for (const it of list2) for (let v = 0; v < 4; v++) {
|
|
2229
|
+
const e = 1 + 2 * it.m;
|
|
2230
|
+
data.set([
|
|
2231
|
+
it.pts[v * 3],
|
|
2232
|
+
it.pts[v * 3 + 1],
|
|
2233
|
+
it.pts[v * 3 + 2],
|
|
2234
|
+
0.5 + (CORNERS[v][2] - 0.5) * e,
|
|
2235
|
+
0.5 + (CORNERS[v][3] - 0.5) * e,
|
|
2236
|
+
...it.rect,
|
|
2237
|
+
...it.misc,
|
|
2238
|
+
...it.pan,
|
|
2239
|
+
...it.lab,
|
|
2240
|
+
it.glow
|
|
2241
|
+
], k);
|
|
2242
|
+
k += F;
|
|
2243
|
+
}
|
|
2244
|
+
gl.bufferData(gl.ARRAY_BUFFER, data.subarray(0, k), gl.DYNAMIC_DRAW);
|
|
2245
|
+
gl.uniform2f(uHalf, W / 2, H / 2);
|
|
2246
|
+
gl.uniform1f(uP, P);
|
|
2247
|
+
gl.uniform1f(uLab, o.labels ? labOn : 0);
|
|
2248
|
+
gl.uniform2f(uView, W / 2, H / 2);
|
|
2249
|
+
gl.uniform1f(uDpr, canvas.width / W);
|
|
2250
|
+
gl.uniform4f(uColor, fx, fy, Math.min(W, H) * 0.3 * (0.35 + 0.65 * on), mono);
|
|
2251
|
+
[tAtlas, tVideo, tLab].forEach((tx) => {
|
|
2252
|
+
gl.activeTexture(gl.TEXTURE0 + tx.unit);
|
|
2253
|
+
gl.bindTexture(gl.TEXTURE_2D, tx.t);
|
|
2254
|
+
});
|
|
2255
|
+
gl.drawElements(gl.TRIANGLES, list2.length * 6, gl.UNSIGNED_SHORT, 0);
|
|
2256
|
+
}
|
|
2257
|
+
const project = (it) => {
|
|
2258
|
+
const s = [];
|
|
2259
|
+
for (let v = 0; v < 4; v++) {
|
|
2260
|
+
const d = P - it.hp[v * 3 + 2];
|
|
2261
|
+
if (d <= 1) return null;
|
|
2262
|
+
s.push(it.hp[v * 3] * P / d, it.hp[v * 3 + 1] * P / d);
|
|
2263
|
+
}
|
|
2264
|
+
return s;
|
|
2265
|
+
};
|
|
2266
|
+
function hit(list2, x, y) {
|
|
2267
|
+
for (let j = list2.length - 1; j >= 0; j--) {
|
|
2268
|
+
const it = list2[j];
|
|
2269
|
+
if (it.misc[0] < 0.3) continue;
|
|
2270
|
+
const s = project(it);
|
|
2271
|
+
if (!s) continue;
|
|
2272
|
+
let pos = 0, neg = 0;
|
|
2273
|
+
for (let v = 0; v < 4; v++) {
|
|
2274
|
+
const ax = s[v * 2], ay = s[v * 2 + 1], bx = s[(v + 1) % 4 * 2], by = s[(v + 1) % 4 * 2 + 1];
|
|
2275
|
+
if ((bx - ax) * (y - ay) - (by - ay) * (x - ax) > 0) pos++;
|
|
2276
|
+
else neg++;
|
|
2277
|
+
}
|
|
2278
|
+
if (!pos || !neg) return it;
|
|
2279
|
+
}
|
|
2280
|
+
return null;
|
|
2281
|
+
}
|
|
2282
|
+
function aimAt(it, dt) {
|
|
2283
|
+
let r = null;
|
|
2284
|
+
if (it) {
|
|
2285
|
+
const s = project(it);
|
|
2286
|
+
if (s) {
|
|
2287
|
+
const xs = [s[0], s[2], s[4], s[6]].map((v) => v + W / 2), ys = [s[1], s[3], s[5], s[7]].map((v) => v + H / 2);
|
|
2288
|
+
r = { x: Math.min(...xs), y: Math.min(...ys), w: Math.max(...xs) - Math.min(...xs), h: Math.max(...ys) - Math.min(...ys) };
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const i = it && r ? it.i : -1;
|
|
2292
|
+
if (i !== AIM.idx) {
|
|
2293
|
+
if (i >= 0) {
|
|
2294
|
+
const f = filmFor(i);
|
|
2295
|
+
tag.innerHTML = `<em>${esc(f.title || "")}</em><small>${o.labels ? `CAM ${String(i + 1).padStart(2, "0")}` : ""}${o.labels && f.year ? " \xB7 " : ""}${esc(f.year || "")}</small>`;
|
|
2296
|
+
tag.classList.remove("is-new");
|
|
2297
|
+
void tag.offsetWidth;
|
|
2298
|
+
tag.classList.add("is-new");
|
|
2299
|
+
if (AIM.o < 0.05) Object.assign(AIM, { x: r.x - r.w * 0.1, y: r.y - r.h * 0.1, w: r.w * 1.2, h: r.h * 1.2 });
|
|
2300
|
+
}
|
|
2301
|
+
AIM.idx = i;
|
|
2302
|
+
}
|
|
2303
|
+
if (r) {
|
|
2304
|
+
AIM.x = damp(AIM.x, r.x, 11, dt);
|
|
2305
|
+
AIM.y = damp(AIM.y, r.y, 11, dt);
|
|
2306
|
+
AIM.w = damp(AIM.w, r.w, 11, dt);
|
|
2307
|
+
AIM.h = damp(AIM.h, r.h, 11, dt);
|
|
2308
|
+
}
|
|
2309
|
+
AIM.o = damp(AIM.o, r ? 1 : 0, r ? 9 : 5, dt);
|
|
2310
|
+
aim.style.opacity = AIM.o.toFixed(3);
|
|
2311
|
+
aim.style.transform = `translate(${AIM.x.toFixed(1)}px,${AIM.y.toFixed(1)}px)`;
|
|
2312
|
+
aim.style.width = AIM.w.toFixed(1) + "px";
|
|
2313
|
+
aim.style.height = AIM.h.toFixed(1) + "px";
|
|
2314
|
+
}
|
|
2315
|
+
function cursorStep(dt) {
|
|
2316
|
+
const show = o.cursor && fine && inside;
|
|
2317
|
+
cursor.classList.toggle("is-on", show);
|
|
2318
|
+
if (!show) {
|
|
2319
|
+
CUR.seen = false;
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
if (!CUR.seen) {
|
|
2323
|
+
CUR.x = px;
|
|
2324
|
+
CUR.y = py;
|
|
2325
|
+
CUR.seen = true;
|
|
2326
|
+
}
|
|
2327
|
+
CUR.x = damp(CUR.x, px, 15, dt);
|
|
2328
|
+
CUR.y = damp(CUR.y, py, 15, dt);
|
|
2329
|
+
const vx = px - CUR.x, vy = py - CUR.y, sp = Math.min(0.4, Math.hypot(vx, vy) / 110);
|
|
2330
|
+
CUR.s = damp(CUR.s, hover >= 0 ? 0.42 : 1, 10, dt);
|
|
2331
|
+
CUR.d = damp(CUR.d, hover >= 0 ? 1.35 : 1, 10, dt);
|
|
2332
|
+
dot.style.transform = `translate(${px.toFixed(1)}px,${py.toFixed(1)}px) scale(${CUR.d.toFixed(3)})`;
|
|
2333
|
+
ring.style.transform = `translate(${CUR.x.toFixed(1)}px,${CUR.y.toFixed(1)}px) rotate(${Math.atan2(vy, vx).toFixed(3)}rad) scale(${(CUR.s * (1 + sp)).toFixed(3)},${(CUR.s * (1 - sp * 0.45)).toFixed(3)})`;
|
|
2334
|
+
}
|
|
2335
|
+
const list = [];
|
|
2336
|
+
function frame(nowMs) {
|
|
2337
|
+
raf = 0;
|
|
2338
|
+
if (!visible) return;
|
|
2339
|
+
const now = nowMs / 1e3, dt = Math.min(0.05, now - (last || now));
|
|
2340
|
+
last = now;
|
|
2341
|
+
if (!started && ready) begin();
|
|
2342
|
+
if (!started || !n) {
|
|
2343
|
+
raf = requestAnimationFrame(frame);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const t = now - t0;
|
|
2347
|
+
clock += dt;
|
|
2348
|
+
headH = damp(headH, mx * 0.42 + (reduce ? 0 : Math.sin(clock * 0.13) * 0.05), 3.2, dt);
|
|
2349
|
+
headP = damp(headP, my * 0.26, 3.2, dt);
|
|
2350
|
+
const live = inside && settledAt;
|
|
2351
|
+
if (live) {
|
|
2352
|
+
fx = damp(fx, px - W / 2, 9, dt);
|
|
2353
|
+
fy = damp(fy, py - H / 2, 9, dt);
|
|
2354
|
+
}
|
|
2355
|
+
on = damp(on, live ? 1 : 0, 2.4, dt);
|
|
2356
|
+
mono = damp(mono, settledAt ? 1 : 0, 1.4, dt);
|
|
2357
|
+
labOn = damp(labOn, settledAt ? 1 : 0, 2.5, dt);
|
|
2358
|
+
if (t > HOLD - 0.05) root.classList.add("is-morph");
|
|
2359
|
+
if (t > HOLD + MORPH_V + 0.4 && !settledAt) {
|
|
2360
|
+
settledAt = now;
|
|
2361
|
+
root.classList.add("is-settled");
|
|
2362
|
+
}
|
|
2363
|
+
list.length = 0;
|
|
2364
|
+
const diag = Math.hypot(W, H);
|
|
2365
|
+
let p0 = null, u0 = 1;
|
|
2366
|
+
for (let i = 0; i < n; i++) {
|
|
2367
|
+
const p = pose(i, dt);
|
|
2368
|
+
let q;
|
|
2369
|
+
if (i === 0) {
|
|
2370
|
+
p0 = p;
|
|
2371
|
+
u0 = ease(clamp((t - HOLD) / MORPH_V));
|
|
2372
|
+
q = mix({ x: 0, y: 0, z: 0, rx: 0, ry: 0, w: Math.max(W, H * AR), o: 1, sh: 0, ix: 0, iy: 0, glow: 0 }, p, u0);
|
|
2373
|
+
} else {
|
|
2374
|
+
const u = ease(clamp((t - HOLD - 0.3 - Math.hypot(p.x - p0.x, p.y - p0.y) / diag * 1.1) / MORPH));
|
|
2375
|
+
q = u >= 1 ? p : mix({ ...p, z: p.z - 700, o: 0 }, p, u);
|
|
2376
|
+
}
|
|
2377
|
+
if (q.o > 2e-3) list.push(item(i, q, i === 0 ? lerp(200 / (Math.max(W, H * AR) / AR), 1, u0) : 1));
|
|
2378
|
+
}
|
|
2379
|
+
draw(list);
|
|
2380
|
+
const it = inside && settledAt ? hit(list, px - W / 2, py - H / 2) : null;
|
|
2381
|
+
hover = it ? it.i : -1;
|
|
2382
|
+
aimAt(it, dt);
|
|
2383
|
+
cursorStep(dt);
|
|
2384
|
+
titleEl.style.transform = `translate(-50%, -50%) scale(${lerp(1, 0.9, ease(clamp((t - HOLD - 0.15) / 1.7))).toFixed(4)})`;
|
|
2385
|
+
raf = requestAnimationFrame(frame);
|
|
2386
|
+
}
|
|
2387
|
+
const wake = () => {
|
|
2388
|
+
if (!raf && visible) {
|
|
2389
|
+
last = 0;
|
|
2390
|
+
raf = requestAnimationFrame(frame);
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
const io = new IntersectionObserver((es) => {
|
|
2394
|
+
visible = es[0].isIntersecting;
|
|
2395
|
+
wake();
|
|
2396
|
+
});
|
|
2397
|
+
io.observe(root);
|
|
2398
|
+
const ro = new ResizeObserver(() => {
|
|
2399
|
+
layout();
|
|
2400
|
+
wake();
|
|
2401
|
+
});
|
|
2402
|
+
ro.observe(root);
|
|
2403
|
+
layout();
|
|
2404
|
+
Promise.all([document.fonts ? document.fonts.ready : null, video ? new Promise((r) => {
|
|
2405
|
+
video.addEventListener("canplay", r, { once: true });
|
|
2406
|
+
setTimeout(r, 2200);
|
|
2407
|
+
}) : null]).then(() => {
|
|
2408
|
+
ready = true;
|
|
2409
|
+
wake();
|
|
2410
|
+
});
|
|
2411
|
+
return {
|
|
2412
|
+
replay() {
|
|
2413
|
+
if (ready) {
|
|
2414
|
+
begin();
|
|
2415
|
+
wake();
|
|
2416
|
+
}
|
|
2417
|
+
},
|
|
2418
|
+
destroy() {
|
|
2419
|
+
cancelAnimationFrame(raf);
|
|
2420
|
+
raf = 0;
|
|
2421
|
+
visible = false;
|
|
2422
|
+
io.disconnect();
|
|
2423
|
+
ro.disconnect();
|
|
2424
|
+
root.removeEventListener("pointermove", onMove);
|
|
2425
|
+
root.removeEventListener("pointerdown", onMove);
|
|
2426
|
+
root.removeEventListener("pointerleave", onLeave);
|
|
2427
|
+
root.removeEventListener("pointercancel", onLeave);
|
|
2428
|
+
if (video) {
|
|
2429
|
+
video.pause();
|
|
2430
|
+
video.removeAttribute("src");
|
|
2431
|
+
video.load();
|
|
2432
|
+
}
|
|
2433
|
+
const lose = gl.getExtension("WEBGL_lose_context");
|
|
2434
|
+
if (lose) lose.loseContext();
|
|
2435
|
+
root.innerHTML = "";
|
|
2436
|
+
root.classList.remove("cup", "cup-cur", "is-morph", "is-settled");
|
|
2437
|
+
root.style.removeProperty("--cup-bar");
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// src/panorama-assets-cdn.js
|
|
2443
|
+
var me3 = typeof document !== "undefined" && document.currentScript ? document.currentScript.src : "";
|
|
2444
|
+
var ASSETS = (me3 ? me3.slice(0, me3.lastIndexOf("/") + 1) : "https://cdn.jsdelivr.net/npm/craft-library@0/dist/") + "panorama/";
|
|
2445
|
+
|
|
2446
|
+
// dist/panorama-mount.js
|
|
2447
|
+
var FILMS = [
|
|
2448
|
+
["s01", "The Coast Road", 2025],
|
|
2449
|
+
["s02", "Small Hours", 2024],
|
|
2450
|
+
["s03", "Vacancy", 2025],
|
|
2451
|
+
["s13", "Blue Room", 2024],
|
|
2452
|
+
["s05", "Two Bodies", 2024],
|
|
2453
|
+
["s06", "Salt", 2025],
|
|
2454
|
+
["s07", "After Rain", 2023],
|
|
2455
|
+
["s14", "Keeper", 2025],
|
|
2456
|
+
["s08", "High Country", 2025],
|
|
2457
|
+
["s09", "Last Picture House", 2024],
|
|
2458
|
+
["s10", "Mr. Albescu", 2023],
|
|
2459
|
+
["s15", "Summer, Ohio", 2023],
|
|
2460
|
+
["s11", "Pinewood", 2025],
|
|
2461
|
+
["s12", "Riviera", 2026],
|
|
2462
|
+
["s16", "Room 414", 2026]
|
|
2463
|
+
];
|
|
2464
|
+
function mountPanoramaReel(host, o = {}) {
|
|
2465
|
+
const opts = {};
|
|
2466
|
+
for (const k in o) if (o[k] !== void 0) opts[k] = o[k];
|
|
2467
|
+
if (opts.video === void 0) opts.video = { src: ASSETS + "reel.mp4", poster: ASSETS + "poster.webp", title: "Wild Grass", year: 2026 };
|
|
2468
|
+
if (opts.stills === void 0) opts.stills = FILMS.map(([n, title, year]) => ({ src: ASSETS + "t/" + n + ".webp", title, year }));
|
|
2469
|
+
return mountPanorama(host, opts);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
1778
2472
|
// dist/elements.js
|
|
1779
2473
|
function define(name, mount, options) {
|
|
1780
2474
|
if (typeof customElements === "undefined" || customElements.get(name)) return;
|
|
@@ -1841,7 +2535,32 @@
|
|
|
1841
2535
|
});
|
|
1842
2536
|
define("magic-sphere", thinkingTag("sphere"), thinkingOptions);
|
|
1843
2537
|
define("sweep-sphere", thinkingTag("sweep"), thinkingOptions);
|
|
2538
|
+
var json = (s) => {
|
|
2539
|
+
if (!s) return void 0;
|
|
2540
|
+
try {
|
|
2541
|
+
return JSON.parse(s);
|
|
2542
|
+
} catch (e) {
|
|
2543
|
+
console.warn("panorama-reel: not JSON", s);
|
|
2544
|
+
return void 0;
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
define("panorama-reel", (el, o) => {
|
|
2548
|
+
css("craft-panorama-host", ":where(panorama-reel){display:block;position:relative;width:100%;height:640px}");
|
|
2549
|
+
const m = mountPanoramaReel(el, o);
|
|
2550
|
+
el.replay = m.replay;
|
|
2551
|
+
return m;
|
|
2552
|
+
}, (el) => ({
|
|
2553
|
+
title: el.getAttribute("heading") ?? void 0,
|
|
2554
|
+
text: el.getAttribute("text") ?? void 0,
|
|
2555
|
+
textItalic: el.getAttribute("text-italic") ?? void 0,
|
|
2556
|
+
video: json(el.getAttribute("video")),
|
|
2557
|
+
stills: json(el.getAttribute("stills")),
|
|
2558
|
+
labels: el.getAttribute("labels") === "false" ? false : void 0,
|
|
2559
|
+
intro: el.getAttribute("intro") === "false" ? false : void 0,
|
|
2560
|
+
cursor: el.getAttribute("cursor") === "false" ? false : void 0,
|
|
2561
|
+
fonts: el.getAttribute("fonts") === "false" ? false : void 0
|
|
2562
|
+
}));
|
|
1844
2563
|
|
|
1845
2564
|
// dist/cdn.js
|
|
1846
|
-
window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking };
|
|
2565
|
+
window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel };
|
|
1847
2566
|
})();
|