Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 60 additions & 10 deletions website/deck.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// The poster deck: one collectible card per new animated poster, fanned out.
// Cards tilt toward the pointer under a foil, play their loop on hover (one at
// a time), and open in an inspector where they turn over to show their timeline.
import { DECK, SHOWCASES } from './showcases.js?v=3';
// Cards tilt toward the pointer under a foil, play their loops while on screen,
// and open in an inspector where they turn over to show their timeline.
import { DECK, SHOWCASES } from './showcases.js?v=4';

const CARDS = DECK.map((id) => SHOWCASES.find((s) => s.id === id));

Expand Down Expand Up @@ -62,7 +62,7 @@ function front(s, i) {
return `
<div class="face front"><div class="panel"><div class="foil"></div>
<div class="tc-head"><b class="tc-name">${esc(s.title)}</b><span class="tc-no">No.${String(i + 1).padStart(2, '0')}/${String(CARDS.length).padStart(2, '0')}</span></div>
<div class="tc-art"><img src="/media/${s.id}.webp" alt="" loading="lazy" decoding="async" /><video muted loop playsinline preload="none" data-src="/media/${s.id}.mp4"></video></div>
<div class="tc-art"><img src="/media/${s.id}.webp" alt="" loading="lazy" decoding="async" /><canvas data-feed="${s.id}" aria-hidden="true"></canvas></div>
<div class="tc-type"><span class="tc-chip">${esc(s.material)}</span><span class="tc-meta">${s.duration.toFixed(1)} s · ${s.layers} layers</span></div>
<ul class="tc-stats">${stats}</ul>
<p class="tc-flavor">${esc(s.flavor)}</p>
Expand Down Expand Up @@ -165,15 +165,65 @@ function tilter(el, range) {
};
}

/* ---------- Card screens ---------- */

// Chrome drops the whole page to 30 fps while two or more videos play on
// screen, and the fan plays six. So each scene plays in a video that never goes
// on the page, and its cards paint the frames onto canvases in one frame loop.
const feeds = new Map();
let painting = 0;

function feed(id) {
let f = feeds.get(id);
if (!f) {
const video = document.createElement('video');
video.muted = true;
video.loop = true;
video.playsInline = true;
video.src = `/media/${id}.mp4`;
f = { video, screens: new Set(), at: -1 };
feeds.set(id, f);
}
return f;
}

function paint() {
painting = 0;
let live = false;
for (const f of feeds.values()) {
if (!f.screens.size) continue;
live = true;
// Only when the video has moved on to a new frame.
if (f.video.readyState < 2 || f.video.currentTime === f.at) continue;
f.at = f.video.currentTime;
for (const c of f.screens) {
c.getContext('2d').drawImage(f.video, 0, 0, c.width, c.height);
c.classList.add('playing');
}
}
if (live) painting = requestAnimationFrame(paint);
}

function playIn(card, on) {
const v = $('video', card);
if (!v) return;
const c = $('canvas[data-feed]', card);
if (!c) return;
if (on && !reduced) {
if (!v.src) v.src = v.dataset.src;
v.onplaying = () => v.classList.add('playing');
v.play().catch(() => {});
if (!c.dataset.sized) {
c.dataset.sized = '1';
const w = Math.min(1080, Math.round((c.clientWidth || 360) * Math.min(devicePixelRatio || 1, 2)));
c.width = w;
c.height = Math.round(w / 1.5);
}
const f = feed(c.dataset.feed);
f.screens.add(c);
f.at = -1;
f.video.play().catch(() => {});
painting ||= requestAnimationFrame(paint);
} else {
v.pause();
const f = feeds.get(c.dataset.feed);
if (!f) return;
f.screens.delete(c);
if (!f.screens.size) f.video.pause();
}
}

Expand Down
4 changes: 2 additions & 2 deletions website/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
<link rel="icon" href="/icon.png" />
<link rel="preload" href="/fonts/BricolageGrotesque-Variable.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Silkscreen-Regular.woff" as="font" type="font/woff" crossorigin />
<link rel="stylesheet" href="/styles.css?v=3" />
<script type="module" src="/main.js?v=3"></script>
<link rel="stylesheet" href="/styles.css?v=4" />
<script type="module" src="/main.js?v=4"></script>
</head>
<body>
<header class="nav">
Expand Down
6 changes: 3 additions & 3 deletions website/main.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Morphotype site. No dependencies: the hero morphs signed distance fields of
// our bundled fonts on the CPU and draws them as a grid of pixels.

import { deck } from './deck.js?v=3';
import { stepper } from './stepper.js?v=3';
import { timeline } from './timeline.js?v=3';
import { deck } from './deck.js?v=4';
import { stepper } from './stepper.js?v=4';
import { timeline } from './timeline.js?v=4';

const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
const $ = (s, el = document) => el.querySelector(s);
Expand Down
6 changes: 3 additions & 3 deletions website/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,9 @@ li:hover .picon rect, .platform:hover .picon rect { transform: translate(var(--d
.tc-name { font: 400 9.4cqw/1 var(--serif); letter-spacing: -0.02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tc-no { font: 3.6cqw var(--pixel); opacity: 0.7; white-space: nowrap; }
.tc-art { position: relative; aspect-ratio: 3 / 2; border-radius: 3cqw; overflow: hidden; background: #000; box-shadow: 0 0 0 0.5cqw rgba(255, 255, 255, 0.35), 0 1.4cqw 3cqw -1cqw rgba(0, 0, 0, 0.5); }
.tc-art img, .tc-art video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.tc-art video { opacity: 0; transition: opacity 0.3s; }
.tc-art video.playing { opacity: 1; }
.tc-art img, .tc-art canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.tc-art canvas { opacity: 0; transition: opacity 0.3s; }
.tc-art canvas.playing { opacity: 1; }
.tc-type { display: flex; align-items: center; justify-content: space-between; gap: 2cqw; }
.tc-chip { font: 3.5cqw var(--pixel); text-transform: uppercase; letter-spacing: 0.03em; padding: 1.1cqw 2.2cqw; border-radius: 1.6cqw; background: var(--chip); white-space: nowrap; }
.tc-meta { font-size: 3.8cqw; opacity: 0.72; white-space: nowrap; }
Expand Down
4 changes: 2 additions & 2 deletions website/timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// Under the stage are that scene's real timeline rows, laid out the way the
// app's timeline dock lays them out, with a playhead that follows the video.
// Dragging across the rows scrubs.
import { REEL, SHOWCASES } from './showcases.js?v=3';
import { stepper } from './stepper.js?v=3';
import { REEL, SHOWCASES } from './showcases.js?v=4';
import { stepper } from './stepper.js?v=4';

const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
const $ = (s, el = document) => el.querySelector(s);
Expand Down
Loading