motion-library 0.0.0-stage → 0.1.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/LICENSE +16 -0
- package/README.md +37 -2
- package/anims/basics/anticipation.js +56 -0
- package/anims/basics/card-flip.js +39 -0
- package/anims/basics/easing-race.js +57 -0
- package/anims/basics/fade-scale.js +44 -0
- package/anims/basics/stagger-grid.js +43 -0
- package/anims/chrome/black-hole.js +81 -0
- package/anims/chrome/cloth-reveal.js +156 -0
- package/anims/chrome/dna-helix.js +52 -0
- package/anims/chrome/dust-bust.js +109 -0
- package/anims/chrome/extruded-type.js +64 -0
- package/anims/chrome/fractal-tree.js +75 -0
- package/anims/chrome/gem.js +111 -0
- package/anims/chrome/geode.js +102 -0
- package/anims/chrome/glass-lens.js +73 -0
- package/anims/chrome/glass-prism.js +95 -0
- package/anims/chrome/gyroid.js +69 -0
- package/anims/chrome/holo-card.js +52 -0
- package/anims/chrome/infinite-tunnel.js +81 -0
- package/anims/chrome/inflated-type.js +141 -0
- package/anims/chrome/isometric-city.js +97 -0
- package/anims/chrome/jelly-cube.js +329 -0
- package/anims/chrome/klein-bottle.js +80 -0
- package/anims/chrome/liquid-metal.js +94 -0
- package/anims/chrome/low-poly-planet.js +163 -0
- package/anims/chrome/mandelbulb.js +79 -0
- package/anims/chrome/melting-logo.js +126 -0
- package/anims/chrome/menger-sponge.js +79 -0
- package/anims/chrome/neon-geodesic.js +149 -0
- package/anims/chrome/paper-fold.js +135 -0
- package/anims/chrome/path-spheres.js +112 -0
- package/anims/chrome/point-globe.js +81 -0
- package/anims/chrome/portal.js +105 -0
- package/anims/chrome/rubiks-cube.js +74 -0
- package/anims/chrome/sdf-morph.js +129 -0
- package/anims/chrome/shatter.js +120 -0
- package/anims/chrome/soap-bubble.js +86 -0
- package/anims/chrome/terrain-flyover.js +78 -0
- package/anims/chrome/tesseract.js +148 -0
- package/anims/chrome/torus-knot.js +89 -0
- package/anims/chrome/volumetric-clouds.js +70 -0
- package/anims/flow/bio-shore.js +140 -0
- package/anims/flow/calligraphy.js +116 -0
- package/anims/flow/chladni.js +152 -0
- package/anims/flow/cloud-chamber.js +101 -0
- package/anims/flow/confetti.js +46 -0
- package/anims/flow/curl-flow.js +52 -0
- package/anims/flow/dandelion.js +80 -0
- package/anims/flow/dla-crystal.js +57 -0
- package/anims/flow/drone-show.js +357 -0
- package/anims/flow/embers.js +63 -0
- package/anims/flow/firefly-sync.js +107 -0
- package/anims/flow/galaxy.js +69 -0
- package/anims/flow/galton-board.js +155 -0
- package/anims/flow/gpu-particles.js +73 -0
- package/anims/flow/harmonograph.js +61 -0
- package/anims/flow/holi.js +153 -0
- package/anims/flow/iron-filings.js +112 -0
- package/anims/flow/lightning.js +72 -0
- package/anims/flow/murmuration.js +105 -0
- package/anims/flow/orbitals.js +195 -0
- package/anims/flow/particle-life.js +87 -0
- package/anims/flow/physarum.js +171 -0
- package/anims/flow/pin-art.js +181 -0
- package/anims/flow/rainbow.js +127 -0
- package/anims/flow/roche-limit.js +161 -0
- package/anims/flow/sand-mandala.js +121 -0
- package/anims/flow/scramble-crossing.js +152 -0
- package/anims/flow/snowfall.js +84 -0
- package/anims/flow/sparks.js +71 -0
- package/anims/flow/spider-web.js +123 -0
- package/anims/flow/starry-night.js +121 -0
- package/anims/flow/states-of-matter.js +113 -0
- package/anims/flow/strange-attractor.js +54 -0
- package/anims/flow/wheat-field.js +186 -0
- package/anims/flow/wind-tunnel.js +227 -0
- package/anims/fluid/bubbles.js +152 -0
- package/anims/fluid/caustics.js +45 -0
- package/anims/fluid/dripping-text.js +53 -0
- package/anims/fluid/ferrofluid.js +56 -0
- package/anims/fluid/fire-smoke.js +83 -0
- package/anims/fluid/honey-drip.js +167 -0
- package/anims/fluid/ink-drop.js +50 -0
- package/anims/fluid/ink-fluid.js +125 -0
- package/anims/fluid/lava-lamp.js +56 -0
- package/anims/fluid/liquid-distort.js +64 -0
- package/anims/fluid/liquid-glass-fill.js +53 -0
- package/anims/fluid/marbling.js +53 -0
- package/anims/fluid/mercury.js +177 -0
- package/anims/fluid/ocean-waves.js +50 -0
- package/anims/fluid/paint-swirl.js +153 -0
- package/anims/fluid/raindrops-glass.js +60 -0
- package/anims/fluid/ripples.js +57 -0
- package/anims/fluid/sph-water.js +75 -0
- package/anims/fluid/underwater.js +55 -0
- package/anims/fluid/water-surface-3d.js +98 -0
- package/anims/hover/cursor-trail.js +37 -0
- package/anims/hover/magnetic.js +41 -0
- package/anims/hover/ripple.js +31 -0
- package/anims/hover/spotlight.js +31 -0
- package/anims/hover/tilt.js +36 -0
- package/anims/micro/arc-spinner.js +22 -0
- package/anims/micro/like-burst.js +42 -0
- package/anims/micro/skeleton.js +36 -0
- package/anims/micro/submit-success.js +45 -0
- package/anims/micro/toggle.js +35 -0
- package/anims/morph/gooey.js +36 -0
- package/anims/morph/icon-morph.js +45 -0
- package/anims/morph/line-draw.js +34 -0
- package/anims/morph/liquid-button.js +53 -0
- package/anims/morph/shape-morph.js +61 -0
- package/anims/registry-more.js +327 -0
- package/anims/registry.js +294 -0
- package/anims/scroll/parallax.js +50 -0
- package/anims/scroll/pinned-track.js +40 -0
- package/anims/scroll/scrub-reveal.js +34 -0
- package/anims/scroll/velocity-skew.js +38 -0
- package/anims/scroll/zoom-through.js +41 -0
- package/anims/shaders/aurora.js +47 -0
- package/anims/shaders/dither.js +69 -0
- package/anims/shaders/domain-warp.js +52 -0
- package/anims/shaders/guilloche.js +42 -0
- package/anims/shaders/kaleidoscope.js +64 -0
- package/anims/shaders/reaction-diffusion.js +85 -0
- package/anims/shaders/rgb-glitch.js +56 -0
- package/anims/shaders/truchet.js +57 -0
- package/anims/shaders/voronoi.js +50 -0
- package/anims/springs/ball-collisions.js +47 -0
- package/anims/springs/pendulum-wave.js +35 -0
- package/anims/springs/rubber-band.js +50 -0
- package/anims/springs/squash-bounce.js +50 -0
- package/anims/springs/verlet-rope.js +46 -0
- package/anims/text/letter-rise.js +40 -0
- package/anims/text/line-mask.js +26 -0
- package/anims/text/scramble.js +32 -0
- package/anims/text/typewriter.js +35 -0
- package/anims/text/vf-wave.js +26 -0
- package/anims/transitions/circle-reveal.js +42 -0
- package/anims/transitions/push-slide.js +40 -0
- package/anims/transitions/shared-expand.js +43 -0
- package/anims/transitions/staircase-wipe.js +41 -0
- package/anims/transitions/view-transition.js +43 -0
- package/bin.mjs +155 -0
- package/js/code.js +122 -0
- package/package.json +10 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Paper fold: a printed letter folded the way you would post it, in real CSS 3D. The sheet is six panels
|
|
2
|
+
// on nested hinges (two halves, each with three rows): the bottom third folds up, the top third folds down
|
|
3
|
+
// over it, the strip folds in half, then it all opens again. Every panel is shaded from its true 3D normal
|
|
4
|
+
// against a desk lamp, creases darken on the inside and catch light on the outside, and stay faintly marked.
|
|
5
|
+
const PARAMS = { cycle: 9, tilt: 46, crease: 1 };
|
|
6
|
+
|
|
7
|
+
export default function (stage) {
|
|
8
|
+
const root = document.createElement('div');
|
|
9
|
+
root.style.cssText = 'position:absolute;inset:0;overflow:hidden;background:#13201f';
|
|
10
|
+
const paper = `url("data:image/svg+xml,${encodeURIComponent("<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 .35 0 0 0 0 .3 0 0 0 0 .22 0 0 0 .09 0'/></filter><rect width='100%' height='100%' filter='url(#n)'/></svg>")}")`;
|
|
11
|
+
root.innerHTML = `
|
|
12
|
+
<style>
|
|
13
|
+
.pf-scene{position:absolute;left:50%;top:51%;width:0;height:0;transform-style:preserve-3d}
|
|
14
|
+
.pf-mat{position:absolute;transform:translateZ(-3px);
|
|
15
|
+
background:linear-gradient(rgba(220,255,240,.11) 1px,transparent 1px) 0 0/calc(var(--g)*5) calc(var(--g)*5),
|
|
16
|
+
linear-gradient(90deg,rgba(220,255,240,.11) 1px,transparent 1px) 0 0/calc(var(--g)*5) calc(var(--g)*5),
|
|
17
|
+
linear-gradient(rgba(220,255,240,.045) 1px,transparent 1px) 0 0/var(--g) var(--g),
|
|
18
|
+
linear-gradient(90deg,rgba(220,255,240,.045) 1px,transparent 1px) 0 0/var(--g) var(--g),#1c3633}
|
|
19
|
+
.pf-shadow{position:absolute;background:rgba(4,10,9,.9);filter:blur(calc(var(--h)*.025));border-radius:2px;transform-origin:0 0}
|
|
20
|
+
.pf-sheet,.pf-half,.pf-p{position:absolute;transform-style:preserve-3d}
|
|
21
|
+
.pf-face{position:absolute;inset:-.5px;overflow:hidden;backface-visibility:hidden;-webkit-backface-visibility:hidden;background:${paper},#f3efe6}
|
|
22
|
+
.pf-back{transform:rotateY(180deg);background:repeating-linear-gradient(45deg,rgba(60,90,140,.07) 0 1px,transparent 1px 4px),repeating-linear-gradient(-45deg,rgba(60,90,140,.05) 0 1px,transparent 1px 4px),${paper},#ebe7de}
|
|
23
|
+
.pf-art{position:absolute;width:var(--w);height:var(--h);font-family:"Mona Sans",system-ui,sans-serif;color:#1b2233}
|
|
24
|
+
.pf-o{position:absolute;inset:0;opacity:0;pointer-events:none}
|
|
25
|
+
.pf-dk{background:#1a120c}.pf-lt{background:#fff8ec}
|
|
26
|
+
.pf-c{position:absolute;opacity:0;pointer-events:none}
|
|
27
|
+
.pf-band{position:absolute;left:0;right:0;top:0;height:1.3%;background:#d8452b}
|
|
28
|
+
.pf-kick{position:absolute;left:9%;top:6.5%;font:650 calc(var(--h)*.017)/1 "Mona Sans";letter-spacing:.16em;text-transform:uppercase;color:#d8452b}
|
|
29
|
+
.pf-title{position:absolute;left:9%;top:10%;width:74%;font:760 calc(var(--h)*.054)/1 "Mona Sans";font-stretch:112%;letter-spacing:-.025em}
|
|
30
|
+
.pf-date{position:absolute;left:9%;top:25.5%;font:500 calc(var(--h)*.016)/1 "Mona Sans";color:#717480}
|
|
31
|
+
.pf-txt{position:absolute;left:9%;right:9%;font:430 calc(var(--h)*.0172)/1.6 "Mona Sans";color:#2e3342;margin:0}
|
|
32
|
+
.pf-tick{position:absolute;background:#d8452b;opacity:.55}
|
|
33
|
+
.pf-sig{position:absolute;left:8%;top:85.5%;width:24%}
|
|
34
|
+
.pf-diag{position:absolute;right:9%;top:82%;width:14%}
|
|
35
|
+
.pf-lamp{position:absolute;inset:0;pointer-events:none;background:radial-gradient(60% 55% at 42% 36%,rgba(255,236,205,.13),transparent 70%),radial-gradient(130% 110% at 50% 45%,transparent 38%,rgba(0,0,0,.62))}
|
|
36
|
+
</style>
|
|
37
|
+
<div class="pf-scene"><div class="pf-mat"></div><div class="pf-shadow"></div><div class="pf-sheet"></div></div><div class="pf-lamp"></div>`;
|
|
38
|
+
stage.append(root);
|
|
39
|
+
const scene = root.querySelector('.pf-scene'), mat = root.querySelector('.pf-mat'), shadow = root.querySelector('.pf-shadow'), sheet = root.querySelector('.pf-sheet');
|
|
40
|
+
|
|
41
|
+
// the printed side: one full-size layout, cropped into each panel
|
|
42
|
+
const ART = `<div class="pf-band"></div><div class="pf-kick">Notes on folding</div><div class="pf-title">In thirds, then in half</div><div class="pf-date">9 November</div>
|
|
43
|
+
<p class="pf-txt" style="top:38.5%">A sheet becomes an envelope of itself. The bottom third lifts and lies flat on the middle, the top third closes over it, and the whole strip folds once more down the centre. Each panel is a flat plane hinged at a crease; nothing bends, so the light has to do the work.</p>
|
|
44
|
+
<p class="pf-txt" style="top:68.5%">Where a panel turns away from the lamp it darkens, and where two panels meet the crease holds a little shadow. Open it again and the creases stay, faintly, like a memory of the shape.</p>
|
|
45
|
+
<svg class="pf-sig" viewBox="0 0 120 40" fill="none" stroke="#24304f" stroke-width="1.6" stroke-linecap="round"><path d="M4 28c8-14 14-22 18-20s-6 22-2 22 10-16 14-15-3 13 1 13 7-9 11-9-2 8 2 8 9-12 14-12c4 0-3 11 2 11 6 0 14-9 22-9s14 4 30 2"/></svg>
|
|
46
|
+
<svg class="pf-diag" viewBox="0 0 60 76" fill="none" stroke="#2e3342" stroke-width="1.2"><rect x="6" y="4" width="48" height="66" rx="1"/><path d="M6 26h48M6 48h48M30 4v66" stroke-dasharray="3 3" stroke="#d8452b"/></svg>
|
|
47
|
+
<i class="pf-tick" style="left:2.5%;top:33.33%;width:3%;height:1px"></i><i class="pf-tick" style="right:2.5%;top:33.33%;width:3%;height:1px"></i>
|
|
48
|
+
<i class="pf-tick" style="left:2.5%;top:66.67%;width:3%;height:1px"></i><i class="pf-tick" style="right:2.5%;top:66.67%;width:3%;height:1px"></i>
|
|
49
|
+
<i class="pf-tick" style="left:50%;top:1.8%;width:1px;height:2.2%"></i><i class="pf-tick" style="left:50%;bottom:1.5%;width:1px;height:2.2%"></i>`;
|
|
50
|
+
|
|
51
|
+
// build: two halves, three rows each; every panel has a printed front and a tinted back
|
|
52
|
+
const halves = [0, 1].map(h => { const d = document.createElement('div'); d.className = 'pf-half'; sheet.append(d); return d; });
|
|
53
|
+
const panels = [], faces = [];
|
|
54
|
+
const edge = { top: 'linear-gradient(to bottom,C,transparent)', bottom: 'linear-gradient(to top,C,transparent)', left: 'linear-gradient(to right,C,transparent)', right: 'linear-gradient(to left,C,transparent)' };
|
|
55
|
+
for (let h = 0; h < 2; h++) for (let r = 0; r < 3; r++) {
|
|
56
|
+
const p = document.createElement('div'); p.className = 'pf-p'; halves[h].append(p);
|
|
57
|
+
const sides = []; // which creases touch this panel: [side as seen on the front, crease id]
|
|
58
|
+
if (r === 0) sides.push(['bottom', 1]); if (r === 1) sides.push(['top', 1], ['bottom', 0]); if (r === 2) sides.push(['top', 0]);
|
|
59
|
+
sides.push([h === 0 ? 'right' : 'left', 2]);
|
|
60
|
+
for (const back of [false, true]) {
|
|
61
|
+
const f = document.createElement('div'); f.className = 'pf-face' + (back ? ' pf-back' : '');
|
|
62
|
+
if (!back) { const a = document.createElement('div'); a.className = 'pf-art'; a.innerHTML = ART; a.dataset.h = h; a.dataset.r = r; f.append(a); }
|
|
63
|
+
const strips = sides.map(([side, cr]) => {
|
|
64
|
+
const s = document.createElement('div'); s.className = 'pf-c';
|
|
65
|
+
const sd = back && (side === 'left' || side === 'right') ? (side === 'left' ? 'right' : 'left') : side; // the back is mirrored left to right
|
|
66
|
+
const horiz = sd === 'top' || sd === 'bottom';
|
|
67
|
+
s.style.cssText += `${sd}:0;${horiz ? 'left:0;right:0;height:26%' : 'top:0;bottom:0;width:30%'};background:${edge[sd].replace('C', back ? 'rgba(255,250,240,.75)' : 'rgba(45,30,18,.6)')}`;
|
|
68
|
+
f.append(s); return { s, cr };
|
|
69
|
+
});
|
|
70
|
+
const dk = document.createElement('div'); dk.className = 'pf-o pf-dk'; const lt = document.createElement('div'); lt.className = 'pf-o pf-lt'; f.append(dk, lt);
|
|
71
|
+
p.append(f); faces.push({ h, r, back, dk, lt, strips });
|
|
72
|
+
}
|
|
73
|
+
panels.push(p);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let W = 0, H = 0, Wp = 0, Hp = 0;
|
|
77
|
+
const resize = () => {
|
|
78
|
+
W = stage.clientWidth || 1; H = stage.clientHeight || 1;
|
|
79
|
+
Hp = Math.min(H * 0.8, W * 0.66 / 0.773); Wp = Hp * 0.773;
|
|
80
|
+
const w2 = Wp / 2, h3 = Hp / 3, M = Math.max(W, H) * 2.6;
|
|
81
|
+
root.style.setProperty('--w', Wp + 'px'); root.style.setProperty('--h', Hp + 'px'); root.style.setProperty('--g', (Hp / 26).toFixed(2) + 'px');
|
|
82
|
+
root.style.perspective = Math.max(W, H) * 1.5 + 'px'; root.style.perspectiveOrigin = '50% 30%';
|
|
83
|
+
Object.assign(mat.style, { width: M + 'px', height: M + 'px', left: -M / 2 + 'px', top: -M / 2 + 'px' });
|
|
84
|
+
Object.assign(sheet.style, { left: -w2 + 'px', top: -Hp / 2 + 'px', width: Wp + 'px', height: Hp + 'px' });
|
|
85
|
+
halves.forEach((d, h) => Object.assign(d.style, { left: h * w2 + 'px', top: 0, width: w2 + 'px', height: Hp + 'px', transformOrigin: '0 50%' }));
|
|
86
|
+
panels.forEach((p, i) => { const r = i % 3; Object.assign(p.style, { left: 0, top: r * h3 + 'px', width: w2 + 'px', height: h3 + 'px', transformOrigin: r === 0 ? '50% 100%' : r === 2 ? '50% 0' : '50% 50%' }); });
|
|
87
|
+
root.querySelectorAll('.pf-art').forEach(a => { a.style.left = -a.dataset.h * w2 + 'px'; a.style.top = -a.dataset.r * h3 + 'px'; });
|
|
88
|
+
Object.assign(shadow.style, { width: Wp + 'px', height: Hp + 'px' });
|
|
89
|
+
};
|
|
90
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
91
|
+
|
|
92
|
+
// strong ease-in-out, cubic-bezier(0.77, 0, 0.175, 1), solved by Newton steps
|
|
93
|
+
const bez = (x1, y1, x2, y2) => x => { let t = x; for (let i = 0; i < 8; i++) { const fx = 3 * x1 * t * (1 - t) ** 2 + 3 * x2 * t * t * (1 - t) + t ** 3 - x, dx = 3 * x1 * (1 - t) ** 2 + 6 * (x2 - x1) * t * (1 - t) + 3 * (1 - x2) * t * t; if (Math.abs(fx) < 1e-5 || dx < 1e-6) break; t = Math.min(1, Math.max(0, t - fx / dx)); } return 3 * y1 * t * (1 - t) ** 2 + 3 * y2 * t * t * (1 - t) + t ** 3; };
|
|
94
|
+
const ease = bez(0.77, 0, 0.175, 1), seg = (c, a, b) => ease(Math.min(1, Math.max(0, (c - a) / (b - a))));
|
|
95
|
+
// rotations in CSS axes (y down, z toward the viewer)
|
|
96
|
+
const rx = (a, v) => [v[0], v[1] * Math.cos(a) - v[2] * Math.sin(a), v[1] * Math.sin(a) + v[2] * Math.cos(a)];
|
|
97
|
+
const ry = (a, v) => [v[0] * Math.cos(a) + v[2] * Math.sin(a), v[1], -v[0] * Math.sin(a) + v[2] * Math.cos(a)];
|
|
98
|
+
const rz = (a, v) => [v[0] * Math.cos(a) - v[1] * Math.sin(a), v[0] * Math.sin(a) + v[1] * Math.cos(a), v[2]];
|
|
99
|
+
const Ll = Math.hypot(-0.35, -0.75, 0.56), L = [-0.35 / Ll, -0.75 / Ll, 0.56 / Ll];
|
|
100
|
+
const memory = [0, 0, 0];
|
|
101
|
+
let raf; const t0 = performance.now();
|
|
102
|
+
function frame(now) {
|
|
103
|
+
const t = (now - t0) / 1000, c = (t % Math.max(3, PARAMS.cycle)) / Math.max(3, PARAMS.cycle);
|
|
104
|
+
// fold in: bottom up, top down, then in half; open in reverse
|
|
105
|
+
const e = [c < 0.5 ? seg(c, 0.07, 0.21) : 1 - seg(c, 0.8, 0.93), c < 0.5 ? seg(c, 0.19, 0.33) : 1 - seg(c, 0.76, 0.89), c < 0.56 ? seg(c, 0.37, 0.51) : 1 - seg(c, 0.62, 0.75)];
|
|
106
|
+
e.forEach((v, i) => { memory[i] = Math.max(memory[i], v); });
|
|
107
|
+
const tilt = (PARAMS.tilt + Math.sin(t * 0.19) * 4) * Math.PI / 180, spin = (-16 + Math.sin(t * 0.23) * 9) * Math.PI / 180;
|
|
108
|
+
scene.style.transform = `rotateX(${tilt}rad) rotateZ(${spin}rad)`;
|
|
109
|
+
const a0 = Math.PI * e[0], a1 = -Math.PI * e[1], a2 = -Math.PI * e[2];
|
|
110
|
+
for (let h = 0; h < 2; h++) { panels[h * 3].style.transform = `translateZ(${e[1].toFixed(3)}px) rotateX(${a1}rad)`; panels[h * 3 + 2].style.transform = `translateZ(${(e[0] * 0.5).toFixed(3)}px) rotateX(${a0}rad)`; }
|
|
111
|
+
halves[1].style.transform = `translateZ(${(e[2] * 3).toFixed(3)}px) rotateY(${a2}rad)`;
|
|
112
|
+
// shading: each face's normal through its hinges and the view tilt, against the lamp
|
|
113
|
+
const flat = rx(tilt, [0, 0, 1]), base = flat[0] * L[0] + flat[1] * L[1] + flat[2] * L[2], K = PARAMS.crease;
|
|
114
|
+
for (const f of faces) {
|
|
115
|
+
let n = [0, 0, f.back ? -1 : 1];
|
|
116
|
+
if (f.r === 0) n = rx(a1, n); else if (f.r === 2) n = rx(a0, n);
|
|
117
|
+
if (f.h === 1) n = ry(a2, n);
|
|
118
|
+
n = rx(tilt, rz(spin, n));
|
|
119
|
+
const d = n[0] * L[0] + n[1] * L[1] + n[2] * L[2] - base;
|
|
120
|
+
f.dk.style.opacity = Math.min(0.62, Math.max(0, -d * 0.75)).toFixed(3); f.lt.style.opacity = Math.min(0.35, Math.max(0, d * 0.9)).toFixed(3);
|
|
121
|
+
for (const { s, cr } of f.strips) {
|
|
122
|
+
const v = f.back ? Math.sin(Math.PI * e[cr] / 2) * 0.55 : Math.sin(Math.PI * e[cr]) * 0.85;
|
|
123
|
+
s.style.opacity = Math.min(1, K * (v + memory[cr] * (f.back ? 0.1 : 0.16))).toFixed(3);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// the shadow on the mat follows the footprint: what has folded no longer reaches the table
|
|
127
|
+
const w2 = Wp / 2, h3 = Hp / 3, top = h3 * Math.max(0, Math.cos(Math.PI * e[1])), bot = h3 * Math.max(0, Math.cos(Math.PI * e[0])), right = w2 * Math.max(0, Math.cos(Math.PI * e[2]));
|
|
128
|
+
const sx = (w2 + right) / Wp, sy = (h3 + top + bot) / Hp, thick = (e[0] + e[1]) * 0.5 + e[2];
|
|
129
|
+
shadow.style.transform = `translate3d(${-w2 + Hp * 0.012 * (1 + thick)}px,${-h3 / 2 - top + Hp * 0.018 * (1 + thick)}px,-1px) scale(${sx.toFixed(4)},${sy.toFixed(4)})`;
|
|
130
|
+
shadow.style.opacity = (0.42 + thick * 0.14).toFixed(3);
|
|
131
|
+
raf = requestAnimationFrame(frame);
|
|
132
|
+
}
|
|
133
|
+
raf = requestAnimationFrame(frame);
|
|
134
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); root.remove(); };
|
|
135
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Path spheres: glossy balls riding a trefoil knot above a studio floor, each one breathing in size. Spheres
|
|
2
|
+
// and a plane can be hit exactly, so every pixel is ray traced analytically: up to three bounces (balls mirror
|
|
3
|
+
// each other and the floor), soft shadows from each ball's penumbra, and ambient occlusion where they crowd.
|
|
4
|
+
const PARAMS = { spheres: 20, speed: 1, breathe: 1 };
|
|
5
|
+
|
|
6
|
+
export default function (stage) {
|
|
7
|
+
const canvas = document.createElement('canvas');
|
|
8
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
9
|
+
stage.append(canvas);
|
|
10
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
11
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
12
|
+
const vs = `#version 300 es
|
|
13
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
14
|
+
const fs = `#version 300 es
|
|
15
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
16
|
+
uniform vec2 R; uniform int N; uniform vec4 S[24]; uniform vec3 CE, CF, CR, CU; uniform float AA;
|
|
17
|
+
const vec3 LD = vec3(-.36, .9, -.3);
|
|
18
|
+
vec3 sky(vec3 d, float strips) {
|
|
19
|
+
vec3 l = normalize(LD);
|
|
20
|
+
vec3 c = mix(vec3(.16, .15, .15), vec3(.5, .48, .46), smoothstep(-.3, .15, d.y));
|
|
21
|
+
c = mix(c, vec3(.1, .1, .11), smoothstep(.2, .8, d.y)); // dark ceiling, so chrome has contrast
|
|
22
|
+
c += vec3(1., .97, .92) * 4. * smoothstep(.965, .985, dot(d, l)); // round key softbox
|
|
23
|
+
float az = atan(d.x, d.z), band = smoothstep(-.05, .2, d.y) * smoothstep(.7, .35, d.y) * strips; // the floor's faint reflection skips the strips
|
|
24
|
+
c += vec3(2.3, 1., .35) * smoothstep(.08, .02, abs(az - 1.2)) * band; // tangerine strip
|
|
25
|
+
c += vec3(.4, .75, 2.2) * smoothstep(.08, .02, abs(az + 2.1)) * band; // cobalt strip
|
|
26
|
+
return c;
|
|
27
|
+
}
|
|
28
|
+
float iSph(vec3 ro, vec3 rd, vec4 s) { vec3 oc = ro - s.xyz; float b = dot(oc, rd), c = dot(oc, oc) - s.w * s.w, h = b * b - c; return h < 0. ? -1. : -b - sqrt(h); }
|
|
29
|
+
float softSh(vec3 ro, vec3 rd, vec4 s, float k) { // penumbra from how near the ray passes the ball
|
|
30
|
+
vec3 oc = ro - s.xyz; float b = dot(oc, rd), c = dot(oc, oc) - s.w * s.w, h = b * b - c;
|
|
31
|
+
float d = sqrt(max(0., s.w * s.w - h)) - s.w, t = -b - sqrt(max(h, 0.));
|
|
32
|
+
return t < 0. ? 1. : smoothstep(0., 1., 2.5 * k * d / t);
|
|
33
|
+
}
|
|
34
|
+
float occl(vec3 p, vec3 n, vec4 s) { vec3 di = s.xyz - p; float l = length(di), h = l / s.w; return max(0., dot(n, di / l)) / (h * h); }
|
|
35
|
+
int trace(vec3 ro, vec3 rd, out float t) {
|
|
36
|
+
t = 1e9; int id = -1;
|
|
37
|
+
for (int i = 0; i < 24; i++) { if (i >= N) break; float d = iSph(ro, rd, S[i]); if (d > 1e-3 && d < t) { t = d; id = i; } }
|
|
38
|
+
if (rd.y < 0.) { float tp = -ro.y / rd.y; if (tp > 1e-3 && tp < t) { t = tp; id = 99; } }
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
vec3 backdrop(vec3 d) { return mix(vec3(.12, .115, .11), vec3(.06, .06, .065), smoothstep(-.2, .3, d.y)); }
|
|
42
|
+
vec3 render(vec2 q) {
|
|
43
|
+
vec3 ro = CE, rd = normalize(CF * 1.65 + CR * q.x + CU * q.y), col = vec3(0), thr = vec3(1), l = normalize(LD); bool onFloor = false;
|
|
44
|
+
for (int b = 0; b < 3; b++) {
|
|
45
|
+
float t; int id = trace(ro, rd, t);
|
|
46
|
+
if (id < 0) { col += thr * (b == 0 ? backdrop(rd) : sky(rd, onFloor ? 0. : 1.)); break; }
|
|
47
|
+
vec3 p = ro + rd * t, n = id == 99 ? vec3(0, 1, 0) : normalize(p - S[id].xyz);
|
|
48
|
+
onFloor = id == 99;
|
|
49
|
+
float sh = 1., ao = 1.;
|
|
50
|
+
for (int i = 0; i < 24; i++) { if (i >= N) break; if (i == id) continue; sh *= softSh(p + n * 1e-3, l, S[i], 7.); ao -= occl(p, n, S[i]); }
|
|
51
|
+
ao = clamp(ao, 0., 1.);
|
|
52
|
+
int m = id == 99 ? 4 : id - (id / 2) * 2 == 0 ? 0 : 1 + (id / 2) - ((id / 2) / 3) * 3; // every other ball chrome, the rest tangerine, bone, cobalt
|
|
53
|
+
vec3 alb = m == 1 ? vec3(.95, .36, .1) : m == 2 ? vec3(.86, .82, .74) : m == 3 ? vec3(.06, .16, .62) : vec3(.25, .24, .23);
|
|
54
|
+
float cv = max(dot(n, -rd), 0.), f0 = m == 0 ? .92 : id == 99 ? .02 : .045, fr = f0 + (1. - f0) * pow(1. - cv, 5.);
|
|
55
|
+
vec3 h = normalize(l - rd);
|
|
56
|
+
float dif = max(dot(n, l), 0.) * sh * 1.25, spec = pow(max(dot(n, h), 0.), m == 0 ? 400. : 140.) * sh;
|
|
57
|
+
vec3 amb = mix(vec3(.2, .19, .19), vec3(.42, .41, .4), n.y * .5 + .5) * ao;
|
|
58
|
+
float pool = id == 99 ? exp(-dot(p.xz, p.xz) * .05) : 1.; // a pool of light under the knot
|
|
59
|
+
vec3 direct = m == 0 ? vec3(0) : alb * (dif * pool * vec3(1.15, 1.1, 1.02) + amb);
|
|
60
|
+
col += thr * ((1. - fr) * direct + spec * 2.2 * (m == 0 ? 1. : fr * 6.));
|
|
61
|
+
float fog = b == 0 && id == 99 ? smoothstep(6., 16., t) : 0.;
|
|
62
|
+
col = mix(col, backdrop(rd), fog); // the floor fades into the studio
|
|
63
|
+
thr *= m == 0 ? vec3(.93, .93, .95) * (fr * .9 + .1) * mix(1., ao, .6) : vec3(id == 99 ? min(fr, .1) * .7 : fr);
|
|
64
|
+
thr *= 1. - fog; // and so does its reflection
|
|
65
|
+
if (max(thr.x, max(thr.y, thr.z)) < .02) break;
|
|
66
|
+
ro = p + n * 1e-3; rd = reflect(rd, n);
|
|
67
|
+
}
|
|
68
|
+
return col;
|
|
69
|
+
}
|
|
70
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
71
|
+
void main() {
|
|
72
|
+
vec3 c = vec3(0);
|
|
73
|
+
for (int j = 0; j < 2; j++) for (int i = 0; i < 2; i++) {
|
|
74
|
+
if (float(i) >= AA || float(j) >= AA) continue;
|
|
75
|
+
vec2 f = gl_FragCoord.xy + (vec2(i, j) + .5) / AA - .5;
|
|
76
|
+
c += render((f / R * 2. - 1.) * vec2(R.x / R.y, 1.));
|
|
77
|
+
}
|
|
78
|
+
c = aces(c / (AA * AA) * .85);
|
|
79
|
+
c *= 1. - .22 * pow(length(uv - .5) * 1.3, 2.);
|
|
80
|
+
o = vec4(pow(c, vec3(1. / 2.2)), 1);
|
|
81
|
+
}`;
|
|
82
|
+
const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x); if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(x)); return x; };
|
|
83
|
+
const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, fs)); gl.bindAttribLocation(prog, 0, 'p'); gl.linkProgram(prog); gl.useProgram(prog);
|
|
84
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
85
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
86
|
+
const U = {}; ['R', 'N', 'S', 'CE', 'CF', 'CR', 'CU', 'AA'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
87
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 1.5); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
88
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
89
|
+
// the trefoil, laid flat with its over/under crossings becoming height
|
|
90
|
+
const knot = s => [(Math.sin(s) + 2 * Math.sin(2 * s)) * 0.6, 1.05 - Math.sin(3 * s) * 0.45, (Math.cos(s) - 2 * Math.cos(2 * s) + 0.5) * 0.6];
|
|
91
|
+
const Sf = new Float32Array(96), nrm = a => { const l = Math.hypot(...a); return a.map(v => v / l); };
|
|
92
|
+
let raf, last = performance.now(), phase = 0; const t0 = last;
|
|
93
|
+
function frame(now) {
|
|
94
|
+
const t = (now - t0) / 1000, dt = Math.min(0.05, (now - last) / 1000); last = now;
|
|
95
|
+
phase += dt * 0.16 * PARAMS.speed;
|
|
96
|
+
const n = Math.max(1, Math.min(24, Math.round(PARAMS.spheres))), base = Math.min(0.42, 6.2 / n);
|
|
97
|
+
for (let i = 0; i < n; i++) {
|
|
98
|
+
const p = knot(phase + i / n * Math.PI * 2), r = base * (1 + 0.2 * PARAMS.breathe * Math.sin(t * 1.7 + i * 2.1));
|
|
99
|
+
Sf.set([p[0], p[1], p[2], r], i * 4);
|
|
100
|
+
}
|
|
101
|
+
const asp = canvas.width / canvas.height, fit = Math.max(1, 1.3 / asp), a = t * 0.09 + 0.6;
|
|
102
|
+
const eye = [Math.sin(a) * 3.4 * fit, 4.9 * fit, Math.cos(a) * 3.4 * fit], ta = [0, 0.35, 0];
|
|
103
|
+
const f = nrm(ta.map((v, i) => v - eye[i])), r = nrm([f[2], 0, -f[0]].map(v => -v)), u = [r[1] * f[2] - r[2] * f[1], r[2] * f[0] - r[0] * f[2], r[0] * f[1] - r[1] * f[0]];
|
|
104
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1i(U.N, n); gl.uniform4fv(U.S, Sf);
|
|
105
|
+
gl.uniform3fv(U.CE, eye); gl.uniform3fv(U.CF, f); gl.uniform3fv(U.CR, r); gl.uniform3fv(U.CU, u);
|
|
106
|
+
gl.uniform1f(U.AA, devicePixelRatio < 1.5 && canvas.width * canvas.height < 420000 ? 2 : 1); // supersample only small canvases
|
|
107
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
108
|
+
raf = requestAnimationFrame(frame);
|
|
109
|
+
}
|
|
110
|
+
raf = requestAnimationFrame(frame);
|
|
111
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
112
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Point globe: thousands of dots spread evenly over a sphere (a Fibonacci spiral), kept where a noise
|
|
2
|
+
// "continent" map says land. Flight arcs leave the surface along great circles, rise with distance and land
|
|
3
|
+
// with a ping. Everything is projected by hand on a 2D canvas; drag to spin.
|
|
4
|
+
const PARAMS = { dots: 12000, arcs: 6, spin: 1 };
|
|
5
|
+
|
|
6
|
+
export default function (stage) {
|
|
7
|
+
const canvas = document.createElement('canvas');
|
|
8
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:grab';
|
|
9
|
+
stage.append(canvas);
|
|
10
|
+
const ctx = canvas.getContext('2d');
|
|
11
|
+
let W = 0, H = 0;
|
|
12
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth; H = stage.clientHeight; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); };
|
|
13
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
14
|
+
|
|
15
|
+
// value noise for the continents
|
|
16
|
+
const perm = Uint8Array.from({ length: 256 }, () => Math.random() * 256);
|
|
17
|
+
const hsh = (x, y, z) => perm[(perm[(perm[x & 255] + y) & 255] + z) & 255] / 255, sm = t => t * t * (3 - 2 * t), lp = (a, b, t) => a + (b - a) * t;
|
|
18
|
+
const noise = (x, y, z) => { const X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z), u = sm(x - X), v = sm(y - Y), w = sm(z - Z);
|
|
19
|
+
return lp(lp(lp(hsh(X, Y, Z), hsh(X + 1, Y, Z), u), lp(hsh(X, Y + 1, Z), hsh(X + 1, Y + 1, Z), u), v), lp(lp(hsh(X, Y, Z + 1), hsh(X + 1, Y, Z + 1), u), lp(hsh(X, Y + 1, Z + 1), hsh(X + 1, Y + 1, Z + 1), u), v), w); };
|
|
20
|
+
const land = (x, y, z) => noise(x * 1.6 + 5, y * 1.6 + 5, z * 1.6 + 5) * 0.65 + noise(x * 4 + 9, y * 4 + 9, z * 4 + 9) * 0.35 > 0.53 - Math.abs(y) * 0.06;
|
|
21
|
+
const N = Math.round(PARAMS.dots), dots = [], landIdx = [];
|
|
22
|
+
for (let i = 0; i < N; i++) {
|
|
23
|
+
const y = 1 - (i + 0.5) / N * 2, r = Math.sqrt(1 - y * y), a = i * 2.39996323; // golden-angle spiral: even spacing
|
|
24
|
+
const x = Math.cos(a) * r, z = Math.sin(a) * r, isLand = land(x, y, z);
|
|
25
|
+
if (isLand || i % 5 === 0) { dots.push([x, y, z, isLand]); if (isLand) landIdx.push(dots.length - 1); }
|
|
26
|
+
}
|
|
27
|
+
const arcs = [], pings = [];
|
|
28
|
+
const slerp = (a, b, t) => { const d = Math.acos(Math.min(1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2])), s = Math.sin(d) || 1; const k0 = Math.sin((1 - t) * d) / s, k1 = Math.sin(t * d) / s; return [a[0] * k0 + b[0] * k1, a[1] * k0 + b[1] * k1, a[2] * k0 + b[2] * k1, d]; };
|
|
29
|
+
const newArc = now => { const a = dots[landIdx[Math.random() * landIdx.length | 0]], b = dots[landIdx[Math.random() * landIdx.length | 0]]; return { a, b, t0: now + Math.random() * 1200, dur: 1600 + Math.random() * 1400, hue: Math.random() }; };
|
|
30
|
+
|
|
31
|
+
let ang = 0.5, vel = 0, dragX = null;
|
|
32
|
+
const onDown = e => { dragX = e.clientX; canvas.style.cursor = 'grabbing'; canvas.setPointerCapture?.(e.pointerId); };
|
|
33
|
+
const onMove = e => { if (dragX == null) return; vel = (e.clientX - dragX) * 0.01; ang += vel; dragX = e.clientX; };
|
|
34
|
+
const onUp = () => { dragX = null; canvas.style.cursor = 'grab'; };
|
|
35
|
+
canvas.addEventListener('pointerdown', onDown); canvas.addEventListener('pointermove', onMove); canvas.addEventListener('pointerup', onUp); canvas.addEventListener('pointercancel', onUp);
|
|
36
|
+
|
|
37
|
+
let raf, last = performance.now();
|
|
38
|
+
function frame(now) {
|
|
39
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now;
|
|
40
|
+
if (dragX == null) { ang += vel + dt * 0.18 * PARAMS.spin; vel *= 0.94; }
|
|
41
|
+
const R = Math.min(W, H) * 0.36, cx = W / 2, cy = H / 2, tilt = 0.38, ca = Math.cos(ang), sa = Math.sin(ang), ct = Math.cos(tilt), st = Math.sin(tilt);
|
|
42
|
+
const proj = (x, y, z) => { const x1 = x * ca + z * sa, z1 = -x * sa + z * ca, y1 = y * ct - z1 * st, z2 = y * st + z1 * ct, k = 3.2 / (3.2 - z2); return [cx + x1 * R * k, cy - y1 * R * k, z2, k]; };
|
|
43
|
+
const bg = ctx.createRadialGradient(cx, cy, R * 0.2, cx, cy, Math.max(W, H) * 0.75); bg.addColorStop(0, '#0d1b33'); bg.addColorStop(1, '#050913');
|
|
44
|
+
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
|
|
45
|
+
const glow = ctx.createRadialGradient(cx, cy, R * 0.95, cx, cy, R * 1.35); glow.addColorStop(0, 'rgba(70,150,255,.35)'); glow.addColorStop(1, 'rgba(70,150,255,0)');
|
|
46
|
+
ctx.fillStyle = glow; ctx.fillRect(0, 0, W, H);
|
|
47
|
+
ctx.fillStyle = 'rgba(10,22,45,.9)'; ctx.beginPath(); ctx.arc(cx, cy, R * 1.0, 0, 7); ctx.fill();
|
|
48
|
+
// dots: only the near side, brighter toward the light and the limb fading out
|
|
49
|
+
for (const [x, y, z, isLand] of dots) {
|
|
50
|
+
const [sx, sy, sz, k] = proj(x, y, z); if (sz < -0.05) continue;
|
|
51
|
+
const lit = 0.35 + 0.65 * Math.max(0, x * ca * 0.6 + y * 0.5 + 0.4), a = Math.min(1, sz * 3 + 0.15);
|
|
52
|
+
if (isLand) { ctx.fillStyle = `rgba(${120 + 135 * lit | 0},${200 + 55 * lit | 0},255,${a})`; const s = 1.5 * k; ctx.fillRect(sx - s / 2, sy - s / 2, s, s); }
|
|
53
|
+
else { ctx.fillStyle = `rgba(80,130,200,${a * 0.25})`; ctx.fillRect(sx - 0.5, sy - 0.5, 1, 1); }
|
|
54
|
+
}
|
|
55
|
+
// arcs along great circles, lifted by distance, with a bright head and a fading tail
|
|
56
|
+
while (arcs.length < PARAMS.arcs) arcs.push(newArc(now));
|
|
57
|
+
for (let i = arcs.length - 1; i >= 0; i--) {
|
|
58
|
+
const A = arcs[i], p = (now - A.t0) / A.dur; if (p < 0) continue;
|
|
59
|
+
if (p > 1.6) { arcs.splice(i, 1); continue; }
|
|
60
|
+
const head = Math.min(1, p), tail = Math.max(0, p - 0.6), dist = slerp(A.a, A.b, 0)[3];
|
|
61
|
+
const col = A.hue < 0.5 ? [255, 120, 200] : [255, 190, 90];
|
|
62
|
+
let prev = null;
|
|
63
|
+
for (let s = 0; s <= 40; s++) {
|
|
64
|
+
const t = tail + (head - tail) * s / 40, q = slerp(A.a, A.b, t), lift = 1 + Math.sin(Math.PI * t) * dist * 0.22;
|
|
65
|
+
const P = proj(q[0] * lift, q[1] * lift, q[2] * lift);
|
|
66
|
+
if (prev && P[2] > -0.15 && prev[2] > -0.15) { ctx.strokeStyle = `rgba(${col},${(s / 40) * 0.9})`; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(prev[0], prev[1]); ctx.lineTo(P[0], P[1]); ctx.stroke(); }
|
|
67
|
+
prev = P;
|
|
68
|
+
}
|
|
69
|
+
if (p < 1 && prev && prev[2] > -0.15) { ctx.fillStyle = `rgb(${col})`; ctx.beginPath(); ctx.arc(prev[0], prev[1], 2.4, 0, 7); ctx.fill(); }
|
|
70
|
+
if (p >= 1 && !A.pinged) { A.pinged = true; pings.push({ p: A.b, t0: now, col }); }
|
|
71
|
+
}
|
|
72
|
+
for (let i = pings.length - 1; i >= 0; i--) {
|
|
73
|
+
const g = pings[i], k = (now - g.t0) / 900; if (k > 1) { pings.splice(i, 1); continue; }
|
|
74
|
+
const [sx, sy, sz] = proj(g.p[0], g.p[1], g.p[2]); if (sz < 0) continue;
|
|
75
|
+
ctx.strokeStyle = `rgba(${g.col},${1 - k})`; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.ellipse(sx, sy, 3 + k * 16, (3 + k * 16) * Math.max(0.3, sz), 0, 0, 7); ctx.stroke();
|
|
76
|
+
}
|
|
77
|
+
raf = requestAnimationFrame(frame);
|
|
78
|
+
}
|
|
79
|
+
raf = requestAnimationFrame(frame);
|
|
80
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointerdown', onDown); canvas.removeEventListener('pointermove', onMove); canvas.removeEventListener('pointerup', onUp); canvas.removeEventListener('pointercancel', onUp); canvas.remove(); };
|
|
81
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Portal: a stone ring stands on a dune, and through it you see a different world: a night sea under a ringed planet.
|
|
2
|
+
// It is not a picture on a disc. Every pixel's ray is traced through the desert, and if it passes through the ring
|
|
3
|
+
// before it hits anything, the rest of its journey is traced in the other world instead, from that point on, in the
|
|
4
|
+
// same direction. So the view through the portal shifts with perspective as the camera moves, and when the camera
|
|
5
|
+
// itself flies through, the worlds swap: now the ring stands on the sea and shows the desert. Then back again.
|
|
6
|
+
const PARAMS = { speed: 1, glow: 1, waves: 1 };
|
|
7
|
+
|
|
8
|
+
export default function (stage) {
|
|
9
|
+
const canvas = document.createElement('canvas');
|
|
10
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
11
|
+
stage.append(canvas);
|
|
12
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
13
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
14
|
+
const FS = `#version 300 es
|
|
15
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
16
|
+
uniform vec2 R; uniform float T, WORLD, GLOW, WAV; uniform vec3 CP, CF, CR, CU;
|
|
17
|
+
const vec3 RC = vec3(0., 1.25, 0.); const float RR = 1.12, RT = .14;
|
|
18
|
+
float h(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
19
|
+
float n2(vec2 p) { vec2 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f); return mix(mix(h(i), h(i + vec2(1, 0)), f.x), mix(h(i + vec2(0, 1)), h(i + 1.), f.x), f.y); }
|
|
20
|
+
float dunes(vec2 p) { return .55 * sin(p.x * .35 + sin(p.y * .22) * 1.5) * cos(p.y * .18) + .25 * n2(p * .4) + .06 * n2(p * 2.1) - .1 * smoothstep(3., 0., length(p)); }
|
|
21
|
+
float sea(vec2 p) { return WAV * (.05 * sin(p.x * 1.3 + T * 1.4) + .035 * sin(p.y * 1.9 - T * 1.1 + p.x * .5) + .02 * n2(p * 3. + T * .5)); }
|
|
22
|
+
float ring(vec3 p) { vec3 q = p - RC; return length(vec2(length(q.xy) - RR, q.z)) - RT; }
|
|
23
|
+
float map(vec3 p, float w) { return min(w < .5 ? (p.y - dunes(p.xz)) * .42 : (p.y - sea(p.xz)) * .6, ring(p)); }
|
|
24
|
+
vec3 nrm(vec3 p, float w) { vec2 e = vec2(.004, 0); return normalize(vec3(map(p + e.xyy, w) - map(p - e.xyy, w), map(p + e.yxy, w) - map(p - e.yxy, w), map(p + e.yyx, w) - map(p - e.yyx, w))); }
|
|
25
|
+
vec3 skyA(vec3 d) { // the desert's sky: deep blue overhead, pale at the horizon, a high sun
|
|
26
|
+
vec3 c = mix(vec3(.85, .82, .74), vec3(.22, .45, .82), smoothstep(-.02, .5, d.y));
|
|
27
|
+
float s = max(dot(d, normalize(vec3(.4, .55, -.7))), 0.); return c + vec3(1., .9, .7) * (pow(s, 300.) * 4. + pow(s, 8.) * .25);
|
|
28
|
+
}
|
|
29
|
+
vec3 skyB(vec3 d) { // the other world: night, stars, a ringed planet, an aurora
|
|
30
|
+
vec3 c = mix(vec3(.02, .03, .07), vec3(.0, .005, .02), smoothstep(0., .6, d.y));
|
|
31
|
+
vec2 sp = vec2(atan(d.x, d.z), asin(d.y)) * 260., ci = floor(sp); vec3 p3 = fract(vec3(ci.xyx) * .1031); p3 += dot(p3, p3.yzx + 33.33); float st = fract((p3.x + p3.y) * p3.z);
|
|
32
|
+
c += vec3(.8, .85, 1.) * step(.993, st) * (st - .993) * 110. * smoothstep(.45, .1, length(fract(sp) - .5)) * step(0., d.y);
|
|
33
|
+
c += vec3(.1, .9, .5) * .35 * exp(-pow((d.y - .28 - .06 * sin(d.x * 6. + T * .2)) / .07, 2.)) * n2(vec2(d.x * 14. + T * .3, 0.)) * step(0., d.y);
|
|
34
|
+
vec3 pc = normalize(vec3(-.5, .32, -1.)); float pr = .2, cosA = dot(d, pc);
|
|
35
|
+
if (cosA > cos(pr)) { vec3 n = normalize(d - pc * cosA); c = mix(vec3(.85, .55, .35), vec3(.6, .3, .25), .5 + .5 * sin(dot(d, vec3(0, 1, 0)) * 80.)) * (.25 + .75 * max(dot(n, normalize(vec3(.6, .5, .2))), 0.)); }
|
|
36
|
+
float ringR = length(cross(d, vec3(.3, .9, -.2))) ; c += vec3(.8, .7, .6) * .25 * smoothstep(.02, 0., abs(acos(clamp(dot(d, pc), -1., 1.)) - .33)) * step(.3, ringR) * step(cosA, cos(pr));
|
|
37
|
+
return c;
|
|
38
|
+
}
|
|
39
|
+
vec3 shade(vec3 ro, vec3 rd, float w, float tmax, out float tHit) {
|
|
40
|
+
float t = .02; tHit = 1e9;
|
|
41
|
+
for (int i = 0; i < 190; i++) { float d = map(ro + rd * t, w); if (d < .002 * (1. + t * .05)) { tHit = t; break; } t += d; if (t > tmax) break; }
|
|
42
|
+
if (tHit > 1e8) return w < .5 ? skyA(rd) : skyB(rd);
|
|
43
|
+
vec3 p = ro + rd * tHit, n = nrm(p, w);
|
|
44
|
+
if (ring(p) < .01) { // the ring: dark stone with a glowing inner edge
|
|
45
|
+
vec3 q = p - RC; float inner = smoothstep(.06, -.02, length(q.xy) - RR + RT * .55);
|
|
46
|
+
vec3 sun = w < .5 ? normalize(vec3(.4, .55, -.7)) : normalize(vec3(-.5, .32, -1.));
|
|
47
|
+
vec3 c = vec3(.18, .17, .16) * (.25 + .75 * max(dot(n, sun), 0.)) * (w < .5 ? 1.2 : .45);
|
|
48
|
+
return c + vec3(.3, .85, 1.) * inner * 1.6 * GLOW * (.7 + .3 * sin(T * 3. + atan(q.y, q.x) * 6.));
|
|
49
|
+
}
|
|
50
|
+
if (w < .5) { // sand
|
|
51
|
+
float dif = max(dot(n, normalize(vec3(.4, .55, -.7))), 0.);
|
|
52
|
+
vec3 c = vec3(.86, .66, .42) * (.35 + .85 * dif) * (.92 + .08 * n2(p.xz * 30.));
|
|
53
|
+
return mix(c, skyA(rd) * .95, 1. - exp(-tHit * .018));
|
|
54
|
+
}
|
|
55
|
+
vec3 r = reflect(rd, n); float fr = .04 + .96 * pow(1. - max(dot(n, -rd), 0.), 5.); // night sea: mostly reflection
|
|
56
|
+
return mix(vec3(.004, .01, .02) + skyB(r) * fr, skyB(rd) * .6, 1. - exp(-tHit * .02));
|
|
57
|
+
}
|
|
58
|
+
void main() {
|
|
59
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
60
|
+
vec3 rd = normalize(CF * 1.8 + CR * q.x + CU * q.y), ro = CP;
|
|
61
|
+
// where (if anywhere) the ray goes through the ring's opening
|
|
62
|
+
float tp = abs(rd.z) > 1e-4 ? -ro.z / rd.z : -1.; vec3 pp = ro + rd * tp;
|
|
63
|
+
bool through = tp > 0. && length(pp.xy - RC.xy) < RR - RT * .4;
|
|
64
|
+
float tHit; vec3 col = shade(ro, rd, WORLD, through ? tp : 60., tHit);
|
|
65
|
+
if (through && tHit > tp) { float t2; col = shade(pp, rd, 1. - WORLD, 60., t2); } // the rest of the journey is in the other world
|
|
66
|
+
col = col / (1. + col * .3);
|
|
67
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
68
|
+
}`;
|
|
69
|
+
const VS = `#version 300 es
|
|
70
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
71
|
+
const sh = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); return s; };
|
|
72
|
+
const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, FS)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p);
|
|
73
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
74
|
+
gl.useProgram(p); const U = n => gl.getUniformLocation(p, n);
|
|
75
|
+
gl.bindVertexArray(gl.createVertexArray()); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
76
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
77
|
+
let W = 1, H = 1;
|
|
78
|
+
const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, 2, Math.max(0.5, Math.sqrt(340000 / (w * h)))); W = canvas.width = Math.max(1, (w * d) | 0); H = canvas.height = Math.max(1, (h * d) | 0); gl.viewport(0, 0, W, H); };
|
|
79
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
80
|
+
// the camera's loop: approach, fly through, turn round, look back, fly back through, turn round
|
|
81
|
+
const path = c => {
|
|
82
|
+
const L = 18, k = c % L, ease = x => x * x * (3 - 2 * x);
|
|
83
|
+
if (k < 7) { const u = ease(k / 7); return { z: 6.5 - 10.5 * u, yaw: Math.PI, sway: Math.sin(k * 0.6) * 0.6 }; }
|
|
84
|
+
if (k < 9) { const u = ease((k - 7) / 2); return { z: -4, yaw: Math.PI + Math.PI * u, sway: 0 }; }
|
|
85
|
+
if (k < 16) { const u = ease((k - 9) / 7); return { z: -4 + 10.5 * u, yaw: 0, sway: Math.sin((k - 9) * 0.6) * 0.6 }; }
|
|
86
|
+
const u = ease((k - 16) / 2); return { z: 6.5, yaw: Math.PI * u, sway: 0 };
|
|
87
|
+
};
|
|
88
|
+
let raf, last = performance.now(), t = 0, world = 0, prevZ = 6.5;
|
|
89
|
+
function frame(now) {
|
|
90
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt * PARAMS.speed;
|
|
91
|
+
const c = path(t), x = c.sway, y = 1.25 + 0.15 * Math.sin(t * 0.7);
|
|
92
|
+
// crossing the plane of the ring inside its opening swaps which world we are standing in
|
|
93
|
+
if (Math.sign(c.z) !== Math.sign(prevZ) && Math.hypot(x, y - 1.25) < 1) world = 1 - world;
|
|
94
|
+
prevZ = c.z;
|
|
95
|
+
const yaw = c.yaw + 0.12 * Math.sin(t * 0.4), CF = [Math.sin(yaw), -0.04, Math.cos(yaw)], fl = Math.hypot(...CF); CF.forEach((v, i) => CF[i] = v / fl);
|
|
96
|
+
const CR = [CF[2], 0, -CF[0]], rl = Math.hypot(CR[0], CR[2]); CR[0] /= rl; CR[2] /= rl;
|
|
97
|
+
const CU = [CF[1] * CR[2] - CF[2] * CR[1], CF[2] * CR[0] - CF[0] * CR[2], CF[0] * CR[1] - CF[1] * CR[0]];
|
|
98
|
+
gl.uniform2f(U('R'), W, H); gl.uniform1f(U('T'), t); gl.uniform1f(U('WORLD'), world); gl.uniform1f(U('GLOW'), PARAMS.glow); gl.uniform1f(U('WAV'), PARAMS.waves);
|
|
99
|
+
gl.uniform3f(U('CP'), x, y, c.z); gl.uniform3f(U('CF'), ...CF); gl.uniform3f(U('CR'), ...CR.map(v => -v)); gl.uniform3f(U('CU'), ...CU);
|
|
100
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
101
|
+
raf = requestAnimationFrame(frame);
|
|
102
|
+
}
|
|
103
|
+
raf = requestAnimationFrame(frame);
|
|
104
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
105
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Rubik's cube in CSS 3D: 27 little cubes (cubies), each a div with six faces. A cubie stores where it is and how it is
|
|
2
|
+
// turned; a move turns one layer of nine a quarter turn with a strong ease-in-out, and when it lands the turn is
|
|
3
|
+
// baked into each cubie's position and orientation (rounded back to whole numbers, so errors never pile up). It
|
|
4
|
+
// scrambles with random moves, pauses, then plays them backwards to solve itself. Only outward faces carry stickers.
|
|
5
|
+
const PARAMS = { moves: 14, turn: 0.42 };
|
|
6
|
+
|
|
7
|
+
export default function (stage) {
|
|
8
|
+
const wrap = document.createElement('div');
|
|
9
|
+
wrap.style.cssText = 'position:absolute;inset:0;perspective:900px;background:radial-gradient(circle at 50% 40%,#2b3140,#0e1016 75%);overflow:hidden';
|
|
10
|
+
const scene = document.createElement('div');
|
|
11
|
+
scene.style.cssText = 'position:absolute;left:50%;top:50%;width:0;height:0;transform-style:preserve-3d';
|
|
12
|
+
wrap.append(scene); stage.append(wrap);
|
|
13
|
+
let S = 60;
|
|
14
|
+
const resize = () => { S = Math.min(stage.clientWidth, stage.clientHeight) * 0.17; build(); };
|
|
15
|
+
const COL = { px: '#c4151c', nx: '#ff7a00', py: '#ffffff', ny: '#ffd500', pz: '#009b48', nz: '#0046ad' };
|
|
16
|
+
const DIRS = [['px', [1, 0, 0], 'rotateY(90deg)'], ['nx', [-1, 0, 0], 'rotateY(-90deg)'], ['py', [0, -1, 0], 'rotateX(90deg)'], ['ny', [0, 1, 0], 'rotateX(-90deg)'], ['pz', [0, 0, 1], ''], ['nz', [0, 0, -1], 'rotateY(180deg)']];
|
|
17
|
+
let cubies = [];
|
|
18
|
+
function build() {
|
|
19
|
+
scene.replaceChildren(); cubies = [];
|
|
20
|
+
for (let x = -1; x <= 1; x++) for (let y = -1; y <= 1; y++) for (let z = -1; z <= 1; z++) {
|
|
21
|
+
const el = document.createElement('div');
|
|
22
|
+
el.style.cssText = `position:absolute;left:${-S / 2}px;top:${-S / 2}px;width:${S}px;height:${S}px;transform-style:preserve-3d`;
|
|
23
|
+
for (const [k, d, rot] of DIRS) {
|
|
24
|
+
const f = document.createElement('div'), outward = d[0] === x && x || d[1] === y && y || d[2] === z && z;
|
|
25
|
+
f.style.cssText = `position:absolute;inset:0;background:#111;transform:${rot} translateZ(${S / 2}px);backface-visibility:hidden;border-radius:${S * 0.08}px`;
|
|
26
|
+
if (outward) { const st = document.createElement('div'); st.style.cssText = `position:absolute;inset:${S * 0.06}px;border-radius:${S * 0.1}px;background:${COL[k]};box-shadow:inset 0 0 ${S * 0.2}px rgba(0,0,0,.18)`; f.append(st); }
|
|
27
|
+
el.append(f);
|
|
28
|
+
}
|
|
29
|
+
scene.append(el);
|
|
30
|
+
cubies.push({ el, p: [x, y, z], m: [1, 0, 0, 0, 1, 0, 0, 0, 1] }); // position and a 3×3 orientation (row-major)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
34
|
+
// rotation matrix for a quarter turn (or part of one) about axis a (0 x, 1 y, 2 z)
|
|
35
|
+
const R = (a, ang) => { const c = Math.cos(ang), s = Math.sin(ang); return a === 0 ? [1, 0, 0, 0, c, -s, 0, s, c] : a === 1 ? [c, 0, s, 0, 1, 0, -s, 0, c] : [c, -s, 0, s, c, 0, 0, 0, 1]; };
|
|
36
|
+
const mm = (A, B) => { const o = []; for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) o.push(A[i * 3] * B[j] + A[i * 3 + 1] * B[3 + j] + A[i * 3 + 2] * B[6 + j]); return o; };
|
|
37
|
+
const mv = (A, v) => [A[0] * v[0] + A[1] * v[1] + A[2] * v[2], A[3] * v[0] + A[4] * v[1] + A[5] * v[2], A[6] * v[0] + A[7] * v[1] + A[8] * v[2]];
|
|
38
|
+
const css = (m, p) => `matrix3d(${m[0]},${m[3]},${m[6]},0,${m[1]},${m[4]},${m[7]},0,${m[2]},${m[5]},${m[8]},0,${p[0] * S * 1.04},${p[1] * S * 1.04},${p[2] * S * 1.04},1)`;
|
|
39
|
+
const ease = x => x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2; // strong ease-in-out: a decisive flick
|
|
40
|
+
let queue = [], hist = [], cur = null, phase = 'scramble', wait = 0.6;
|
|
41
|
+
const plan = () => { queue = []; let lastA = -1; for (let i = 0; i < Math.round(PARAMS.moves); i++) { let a; do a = Math.random() * 3 | 0; while (a === lastA); lastA = a; queue.push({ a, layer: (Math.random() * 3 | 0) - 1, dir: Math.random() < 0.5 ? 1 : -1 }); } };
|
|
42
|
+
plan();
|
|
43
|
+
let raf, last = performance.now();
|
|
44
|
+
function frame(now) {
|
|
45
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now;
|
|
46
|
+
const t = now / 1000;
|
|
47
|
+
if (!cur) {
|
|
48
|
+
if (wait > 0) wait -= dt;
|
|
49
|
+
else if (queue.length) { cur = { ...queue.shift(), k: 0 }; }
|
|
50
|
+
else if (phase === 'scramble') { phase = 'solve'; queue = hist.reverse().map(m => ({ ...m, dir: -m.dir })); hist = []; wait = 0.9; }
|
|
51
|
+
else { phase = 'scramble'; plan(); wait = 1.6; }
|
|
52
|
+
}
|
|
53
|
+
let turnM = null;
|
|
54
|
+
if (cur) {
|
|
55
|
+
cur.k = Math.min(1, cur.k + dt / Math.max(0.12, PARAMS.turn));
|
|
56
|
+
turnM = R(cur.a, cur.dir * Math.PI / 2 * ease(cur.k));
|
|
57
|
+
if (cur.k >= 1) { // bake the turn in, snapped to whole numbers
|
|
58
|
+
const Q = R(cur.a, cur.dir * Math.PI / 2).map(Math.round);
|
|
59
|
+
for (const c of cubies) if (c.p[cur.a] === cur.layer) { c.p = mv(Q, c.p).map(Math.round); c.m = mm(Q, c.m).map(Math.round); }
|
|
60
|
+
if (phase === 'scramble') hist.push({ a: cur.a, layer: cur.layer, dir: cur.dir });
|
|
61
|
+
cur = null; turnM = null; wait = phase === 'solve' ? 0.04 : 0.08;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const view = mm(R(0, -0.5 + Math.sin(t * 0.3) * 0.12), R(1, 0.65 + t * 0.25)); // a slow orbit
|
|
65
|
+
for (const c of cubies) {
|
|
66
|
+
let m = c.m, p = c.p;
|
|
67
|
+
if (turnM && c.p[cur.a] === cur.layer) { m = mm(turnM, m); p = mv(turnM, p); }
|
|
68
|
+
c.el.style.transform = css(mm(view, m), mv(view, p));
|
|
69
|
+
}
|
|
70
|
+
raf = requestAnimationFrame(frame);
|
|
71
|
+
}
|
|
72
|
+
raf = requestAnimationFrame(frame);
|
|
73
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); wrap.remove(); };
|
|
74
|
+
}
|