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/dist/craft.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Craft Library, by Sara Lobo · liquid glass nav bar, bursting heart, magnetic icon, iridescent card, glass piggy bank, magic sphere, sweep · <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere>, <sweep-sphere> */
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 Craft Library, by Sara Lobo
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 Craft Library, by Sara Lobo\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";
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 clamp(x, a, b) {
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 * clamp(m.d / PULL, 0, 1);
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 clamp((Math.abs(m.d) + 3 * m.heat) / PULL, 0, 1);
481
+ return clamp2((Math.abs(m.d) + 3 * m.heat) / PULL, 0, 1);
482
482
  }
483
483
  function damage(m, t) {
484
- return clamp((Math.abs(m.d) + 3 * m.heat) / 9, 0, 1) * bell(wrap(t - m.a), 0.7);
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 : clamp((damage(m, t) - 0.15) / 0.85, 0, 1);
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 * clamp(dist / R, 0, 1));
518
- return { d, a: Math.atan2(y, x), x, y, dist, near, heat: edge >= 0 ? near * near : clamp(dist / R, 0, 1) };
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 * clamp(t.dist / 8, 0, 1)));
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 ? clamp(m.d / PULL, 0, 1) : 0.5 * clamp(-m.d / DENT, 0, 1);
554
- m.acc += dt * RATE * Math.pow(pull, 1.4) * (0.35 + 0.65 * clamp(m.pace / 160, 0, 1));
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 * clamp((r - R) / (m.d * (1 + BODY)), 0, 1);
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 * clamp(dist / 20, 0, 1);
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 * clamp(dist / 12, 0, 1);
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 = clamp((ro - f) / top, 0, 1);
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 = clamp((damage(m, c.a) + 0.08 * tremble(300 + k, now) - c.at) / 0.12, 0, 1), z, r, x, y, dx, dy, l;
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 Craft Library, by Sara Lobo
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 clamp(v, a, b) {
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 = clamp((43 - l) / 40, 0, 1), fr = clamp((r - 248) / 40, 0, 1);
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 = clamp(to, 0, 4);
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 Craft Library, by Sara Lobo
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 ease(t) {
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, HOLD = 1.1, R0 = 0.8, D = [], v = [0, 0, 0], o = [0, 0, 0, 0, 0, 0];
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 >= HOLD) {
1586
+ } else if (mt >= HOLD2) {
1587
1587
  plan();
1588
1588
  mi = 0;
1589
- mt -= HOLD;
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 * ease(clamp01(mt / DUR)) : 0, cs = Math.cos(th), sn = Math.sin(th);
1596
- var glow = m ? 0 : Math.sin(Math.PI * clamp01(mt / HOLD)), rb = R0 * (1 + 0.03 * glow);
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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 === " " ? "&nbsp;" : 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
  })();