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
407 changes: 407 additions & 0 deletions website/deck.js

Large diffs are not rendered by default.

72 changes: 36 additions & 36 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" />
<script type="module" src="/main.js"></script>
<link rel="stylesheet" href="/styles.css?v=3" />
<script type="module" src="/main.js?v=3"></script>
</head>
<body>
<header class="nav">
Expand Down Expand Up @@ -73,41 +73,21 @@ <h1 class="headline">
</div>
</section>

<!-- Fanned cards, every one rendered by the app itself -->
<section class="made" id="made">
<!-- A fan of collectible cards, one per new animated poster, with real settings -->
<section class="deck" id="made">
<div class="section-head">
<p class="eyebrow px">Made with Morphotype</p>
<h2>Every frame on this page<br />came out of the app.</h2>
</div>
<div class="fan" data-fan>
<figure class="card" style="--r:-9deg; --x:-330px; --y:30px; --z:1" tabindex="0">
<video src="/media/neon-sign.mp4" poster="/media/neon-sign.webp" muted loop playsinline preload="none"></video>
<figcaption><b>Neon sign</b><span>Neon material, with flicker and glow keyframed.</span></figcaption>
</figure>
<figure class="card" style="--r:4deg; --x:-165px; --y:-10px; --z:2" tabindex="0">
<video src="/media/holo-card.mp4" poster="/media/holo-card.webp" muted loop playsinline preload="none"></video>
<figcaption><b>Holo card</b><span>A holographic shift that ramps over the clock.</span></figcaption>
</figure>
<figure class="card" style="--r:-2deg; --x:0px; --y:-40px; --z:5" tabindex="0">
<video src="/media/liquid-chrome.mp4" poster="/media/liquid-chrome.webp" muted loop playsinline preload="none"></video>
<figcaption><b>Liquid chrome</b><span>Chrome that flows by itself, with orbiting sparkles.</span></figcaption>
</figure>
<figure class="card" style="--r:3deg; --x:165px; --y:0px; --z:3" tabindex="0">
<video src="/media/sticker-pack.mp4" poster="/media/sticker-pack.webp" muted loop playsinline preload="none"></video>
<figcaption><b>Sticker pack</b><span>Group motion, staggered one by one.</span></figcaption>
</figure>
<figure class="card" style="--r:9deg; --x:330px; --y:40px; --z:2" tabindex="0">
<video src="/media/pop-3d.mp4" poster="/media/pop-3d.webp" muted loop playsinline preload="none"></video>
<figcaption><b>Pop 3D</b><span>Extrusion whose angle turns as it plays.</span></figcaption>
</figure>
<p class="eyebrow px">The poster deck</p>
<h2 class="poster-head"><span class="w-puff">Puffy</span>, <span class="w-chrome">chrome</span>, <span class="w-glitch" data-text="pixel">pixel</span>. <br />Six posters from the app.</h2>
<p class="sub">Each card is a real template with its real settings. Point at one to tilt it, then click to turn it over.</p>
</div>
<div class="hand" data-hand></div>
</section>

<!-- Features with pixel icons -->
<section class="features" id="features">
<div class="section-head">
<p class="eyebrow px">What it does</p>
<h2>A whole type studio,<br />in one small app.</h2>
<h2>A whole type studio, <br />in one small app.</h2>
</div>
<ul class="feature-grid">
<li><i class="picon" data-icon="morph"></i><h3>Morph fonts</h3><p>Blend any two typefaces curve to curve. Letters stay on the baseline and x-height.</p></li>
Expand All @@ -121,29 +101,37 @@ <h2>A whole type studio,<br />in one small app.</h2>
</ul>
</section>

<!-- A Pasito-style stepper over the animated showcases -->
<section class="reel" id="reel">
<!-- The reel: one scene at a time, over its real timeline -->
<section class="reel" id="motion">
<div class="section-head">
<p class="eyebrow px">Ten materials, all moving</p>
<h2>Open any of these in Learn<br />and take it apart.</h2>
<p class="eyebrow px">Motion, frame for frame</p>
<h2>The timeline you edit <br />is the one that plays.</h2>
<p class="sub">Lines that draw themselves on, pixel badges that build back, vinyl that breathes. Under each scene are its real tracks: drag across them to scrub.</p>
</div>
<div class="reel-frame" data-reel>
<div class="reel-media"></div>
<button class="reel-nav prev" aria-label="Previous"><svg viewBox="0 0 16 16"><path d="M10 3 5 8l5 5" /></svg></button>
<button class="reel-nav next" aria-label="Next"><svg viewBox="0 0 16 16"><path d="m6 3 5 5-5 5" /></svg></button>
<div class="reel-bar">
<div class="pasito pasito-glass" role="tablist" aria-label="Showcase"></div>
<div class="pasito pasito-glass" role="tablist" aria-label="Scene"></div>
<button class="reel-play" aria-label="Pause"><svg viewBox="0 0 16 16"><path class="i-pause" d="M5 3v10M11 3v10" /><path class="i-play" d="M5 3l8 5-8 5z" /></svg></button>
</div>
</div>
<p class="reel-caption"><b data-reel-title></b> <span data-reel-note></span></p>
<div class="reel-tl">
<div class="reel-meta"><p class="reel-caption"><b data-reel-title></b> <span data-reel-note></span></p><span class="tl-time px" data-tl-time>0.00 s · f0</span></div>
<div class="tl-dock" data-tl-dock>
<div class="tl-ruler" data-tl-ruler></div>
<div class="tl-rows" data-tl-rows></div>
<div class="tl-head" data-tl-head><i></i></div>
</div>
</div>
</section>

<!-- The editor itself -->
<section class="editor" id="editor">
<div class="section-head">
<p class="eyebrow px">Two modes</p>
<h2>Design for logos and posts.<br />Type for whole fonts.</h2>
<h2>Design for logos and posts. <br />Type for whole fonts.</h2>
</div>
<div class="seg" role="tablist" aria-label="Mode">
<span class="seg-pill" aria-hidden="true"></span>
Expand Down Expand Up @@ -222,5 +210,17 @@ <h2>Get Morphotype.</h2>
</span>
</div>
</footer>

<!-- Card inspector: the picked card, large, and its back -->
<div class="inspector" data-inspector hidden>
<div class="inspector-backdrop" data-close></div>
<div class="inspector-stage" data-stage></div>
<div class="inspector-bar">
<button class="ibtn" data-prev aria-label="Previous card"><svg viewBox="0 0 16 16"><path d="M10 3 5 8l5 5" /></svg></button>
<button class="ibtn wide" data-flip><span class="px">Turn over</span></button>
<button class="ibtn" data-next aria-label="Next card"><svg viewBox="0 0 16 16"><path d="m6 3 5 5-5 5" /></svg></button>
<button class="ibtn" data-close aria-label="Close"><svg viewBox="0 0 16 16"><path d="m4 4 8 8M12 4l-8 8" /></svg></button>
</div>
</div>
</body>
</html>
162 changes: 6 additions & 156 deletions website/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// 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';

const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
Expand Down Expand Up @@ -302,59 +306,6 @@ const MODES = [
{ id: 'neon', label: 'Neon' },
];

/** A Pasito-style stepper (after joshpuckett/pasito): dots, an active pill, and a fill while autoplaying. */
function stepper(el, count, { duration, labels, onChange }) {
if (labels) el.classList.add('labelled');
const buttons = Array.from({ length: count }, (_, i) => {
const b = document.createElement('button');
b.type = 'button';
b.setAttribute('role', 'tab');
b.setAttribute('aria-label', labels ? labels[i] : `Show ${i + 1} of ${count}`);
if (labels) b.innerHTML = `<span>${labels[i]}</span>`;
b.addEventListener('click', () => api.set(i, true));
el.append(b);
return b;
});
let active = 0, timer = 0, playing = false, held = 0;
const fill = () => {
const b = buttons[active];
b.classList.remove('fill');
if (!playing) return;
b.style.setProperty('--fill', `${duration(active)}ms`);
void b.offsetWidth;
b.classList.add('fill');
};
const schedule = () => {
clearTimeout(timer);
if (playing) timer = setTimeout(() => api.set((active + 1) % count), duration(active));
};
const api = {
get index() { return active; },
set(i, byUser = false) {
buttons[active].classList.remove('on', 'fill');
buttons[active].setAttribute('aria-selected', 'false');
active = (i + count) % count;
buttons[active].classList.add('on');
buttons[active].setAttribute('aria-selected', 'true');
onChange(active, byUser);
if (byUser && playing) {
// A pick holds for a while before autoplay resumes.
api.pause();
clearTimeout(held);
held = setTimeout(() => api.play(), 9000);
}
fill();
schedule();
},
play() { playing = true; fill(); schedule(); },
pause() { playing = false; clearTimeout(timer); buttons[active].classList.remove('fill'); },
get playing() { return playing; },
};
buttons[0].classList.add('on');
buttons[0].setAttribute('aria-selected', 'true');
return api;
}

async function hero() {
const stage = $('.stage');
const canvas = $('.morph', stage);
Expand Down Expand Up @@ -507,107 +458,6 @@ function icons() {
}
}

/* ---------- Fanned cards ---------- */

function fan() {
const el = $('[data-fan]');
$$('.card', el).forEach((c, i) => c.style.setProperty('--i', i));
const videos = $$('video', el);
new IntersectionObserver(
([en]) => {
if (en.isIntersecting) {
el.classList.add('spread');
if (!reduced) videos.forEach((v) => v.play().catch(() => {}));
} else videos.forEach((v) => v.pause());
},
{ threshold: 0.25 },
).observe(el);
}

/* ---------- Reel of animated showcases ---------- */

const REEL = [
['neon-sign', 'Neon sign', 'Neon material with flicker; the glow is keyframed.'],
['liquid-chrome', 'Liquid chrome', 'Chrome that flows on its own, with a spinning sparkle.'],
['sticker-pack', 'Sticker pack', 'Sticker material on five shapes, grown one by one.'],
['keyboard', 'Keyboard', 'Keycap letters that drop in turn, then pulse.'],
['melting', 'Melting', 'A melt modifier on Anton, with rays turning behind.'],
['arcade', 'Arcade', 'A pixelate modifier, a floating ghost and a one-way lane.'],
['plush-weather', 'Plush weather', 'Plush material on a cloud, a sun and a bolt.'],
['holo-card', 'Holo card', 'A holographic shift that ramps while the card sways.'],
['pop-3d', 'Pop 3D', 'Extruded type whose angle turns as it plays.'],
['print-club', 'Print club', 'Riso offset flicker, with words entering from both sides.'],
];

function reel() {
const frame = $('[data-reel]');
const media = $('.reel-media', frame);
const title = $('[data-reel-title]'), note = $('[data-reel-note]');
const play = $('.reel-play', frame);
const videos = REEL.map(([id]) => {
const v = document.createElement('video');
v.muted = true;
v.loop = true;
v.playsInline = true;
v.preload = 'none';
v.poster = `/media/${id}.webp`;
v.dataset.src = `/media/${id}.mp4`;
media.append(v);
return v;
});
let inView = false;
const show = (i) => {
videos.forEach((v, k) => {
if (k === i) {
if (!v.src) v.src = v.dataset.src;
v.currentTime = 0;
v.classList.add('on');
if (inView && !reduced) v.play().catch(() => {});
} else {
v.classList.remove('on');
v.pause();
}
});
// Warm the next one.
const n = videos[(i + 1) % videos.length];
if (!n.src) {
n.preload = 'auto';
n.src = n.dataset.src;
}
title.textContent = REEL[i][1];
note.textContent = REEL[i][2];
};
const steps = stepper($('.pasito', frame), REEL.length, { duration: () => 4800, onChange: (i) => show(i) });
show(0);
$('.prev', frame).addEventListener('click', () => steps.set(steps.index - 1, true));
$('.next', frame).addEventListener('click', () => steps.set(steps.index + 1, true));
let userPaused = false;
play.addEventListener('click', () => {
userPaused = !userPaused;
play.classList.toggle('paused', userPaused);
play.setAttribute('aria-label', userPaused ? 'Play' : 'Pause');
const v = videos[steps.index];
if (userPaused) {
steps.pause();
v.pause();
} else {
steps.play();
v.play().catch(() => {});
}
});
new IntersectionObserver(([en]) => {
inView = en.isIntersecting;
const v = videos[steps.index];
if (inView && !userPaused && !reduced) {
v.play().catch(() => {});
steps.play();
} else {
v.pause();
steps.pause();
}
}, { threshold: 0.35 }).observe(frame);
}

/* ---------- Design / Type toggle ---------- */

function modeShots() {
Expand Down Expand Up @@ -720,8 +570,8 @@ function chrome() {

icons();
chrome();
fan();
reel();
deck();
timeline();
modeShots();
marquees();
downloads();
Expand Down
Binary file removed website/media/arcade.mp4
Binary file not shown.
Binary file removed website/media/arcade.webp
Binary file not shown.
Binary file added website/media/aura.mp4
Binary file not shown.
Binary file added website/media/aura.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/media/crystal.mp4
Binary file not shown.
Binary file added website/media/crystal.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/media/frosted-404.mp4
Binary file not shown.
Binary file modified website/media/frosted-404.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/media/glass-lens.mp4
Binary file not shown.
Binary file modified website/media/glass-lens.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/media/glow-glass.mp4
Binary file not shown.
Binary file modified website/media/glow-glass.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/media/inflatable.mp4
Binary file not shown.
Binary file added website/media/inflatable.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed website/media/keyboard.mp4
Binary file not shown.
Binary file removed website/media/keyboard.webp
Binary file not shown.
Binary file removed website/media/liquid-chrome.mp4
Binary file not shown.
Binary file removed website/media/liquid-chrome.webp
Binary file not shown.
Binary file removed website/media/melting.mp4
Binary file not shown.
Binary file removed website/media/melting.webp
Binary file not shown.
Binary file removed website/media/neon-sign.mp4
Binary file not shown.
Binary file removed website/media/neon-sign.webp
Binary file not shown.
Binary file added website/media/pixel-badge.mp4
Binary file not shown.
Binary file added website/media/pixel-badge.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified website/media/pixel-office.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed website/media/plush-weather.mp4
Binary file not shown.
Binary file removed website/media/plush-weather.webp
Binary file not shown.
Binary file removed website/media/print-club.mp4
Binary file not shown.
Binary file removed website/media/print-club.webp
Binary file not shown.
Binary file added website/media/puffy.mp4
Binary file not shown.
Binary file added website/media/puffy.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/media/shape-shifter.mp4
Binary file not shown.
Binary file added website/media/shape-shifter.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed website/media/sticker-pack.mp4
Binary file not shown.
Binary file removed website/media/sticker-pack.webp
Binary file not shown.
Binary file added website/media/thermal-wings.mp4
Binary file not shown.
Binary file modified website/media/thermal-wings.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/media/tube-burst.mp4
Binary file not shown.
Binary file added website/media/tube-burst.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/media/y2k-set.mp4
Binary file not shown.
Binary file added website/media/y2k-set.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading