Ink mist
A warm paper-textured hero where indigo and coral ink drifts through a live fluid simulation, wandering on its own and swirling into new eddies as the pointer moves, with a serif headline over it.
Code: MIT license.
<!doctype html>
<html lang="en">
<head
><script>
/* 6to9 pattern harness */ (function () {
var parentPaused = false,
hidden = document.hidden,
p = (window.__pattern = { paused: false, reducedMotion: false, maxDpr: 2 });
function sync() {
p.paused = parentPaused || hidden;
}
sync();
try {
p.reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
} catch (e) {}
document.addEventListener("visibilitychange", function () {
hidden = document.hidden;
sync();
});
addEventListener("message", function (e) {
var d = e.data || {};
if (d.pattern === "pause") {
parentPaused = true;
sync();
}
if (d.pattern === "play") {
parentPaused = false;
sync();
}
});
})();
</script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ombra - ink mist</title>
<style>
:root {
--paper: #f2eee4;
--ink: #16143a;
--coral: #ff6a52;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
overflow: hidden;
background: var(--paper);
color: var(--ink);
font-family: Georgia, "Iowan Old Style", "Palatino Linotype", "Times New Roman", serif;
touch-action: none;
}
.hero {
position: relative;
width: 100vw;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.fallback {
display: none;
position: absolute;
inset: 0;
background: radial-gradient(40% 50% at 25% 30%, rgba(40, 38, 120, 0.55), transparent 70%),
radial-gradient(45% 45% at 75% 70%, rgba(255, 106, 82, 0.5), transparent 70%),
radial-gradient(30% 35% at 60% 25%, rgba(70, 60, 160, 0.3), transparent 70%), var(--paper);
background-size: 140% 140%;
animation: drift 18s ease-in-out infinite alternate;
}
.no-canvas .fallback {
display: block;
}
@keyframes drift {
from {
background-position: 0% 0%;
}
to {
background-position: 100% 100%;
}
}
.vignette {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(
ellipse at center,
transparent 55%,
rgba(60, 50, 30, 0.14) 100%
);
}
.brand {
position: absolute;
z-index: 2;
top: clamp(20px, 3.5vw, 44px);
left: clamp(24px, 7vw, 120px);
font-size: 1.05rem;
letter-spacing: 0.22em;
text-transform: uppercase;
text-shadow: 0 0 14px var(--paper);
}
.content {
position: relative;
z-index: 2;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
padding: 0 clamp(24px, 7vw, 120px);
pointer-events: none;
}
h1 {
font-weight: 500;
font-size: clamp(2.7rem, 8.4vw, 8rem);
line-height: 0.98;
letter-spacing: -0.03em;
max-width: 10.5em;
text-wrap: balance;
text-shadow:
0 0 22px var(--paper),
0 0 8px var(--paper);
}
h1 em {
font-style: italic;
color: #2a2790;
}
.sub {
margin-top: clamp(20px, 3vw, 34px);
max-width: 29em;
font-size: clamp(1rem, 1.35vw, 1.22rem);
line-height: 1.55;
text-shadow:
0 0 16px var(--paper),
0 0 6px var(--paper);
}
.cta {
pointer-events: auto;
margin-top: clamp(22px, 3vw, 36px);
display: inline-flex;
align-items: center;
gap: 12px;
padding: 16px 30px;
border: 0;
border-radius: 999px;
background: var(--ink);
color: var(--paper);
font-family: inherit;
font-size: 1.02rem;
letter-spacing: 0.02em;
cursor: pointer;
transition:
transform 0.3s ease,
background 0.3s ease;
}
.cta:hover,
.cta:focus-visible {
transform: translateY(-3px);
background: #2a2790;
outline: none;
}
</style>
</head>
<body>
<main class="hero">
<div class="fallback"></div>
<canvas id="mist"></canvas>
<div class="vignette"></div>
<div class="brand">Ombra</div>
<section class="content">
<h1>Let the quiet thing <em>move.</em></h1>
<p class="sub"
>A calm workspace for notes, drafts and slow ideas. Stir the page and watch the thinking
bloom.</p
>
<button class="cta" type="button"
>Start writing <span aria-hidden="true">→</span></button
>
</section>
</main>
<script>
(function () {
var canvas = document.getElementById("mist");
var ctx = null;
try {
ctx = canvas.getContext("2d");
} catch (err) {
ctx = null;
}
if (!ctx) {
document.body.classList.add("no-canvas");
return;
}
function getP() {
return window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
}
// ---- paper grain tile (generated in JS) ----
var grain = null;
function buildGrain() {
var size = 384;
var tile = document.createElement("canvas");
tile.width = size;
tile.height = size;
var tc = tile.getContext("2d");
var data = tc.createImageData(size, size);
for (var i = 0; i < size * size; i++) {
var v = 232 + Math.random() * 23;
data.data[i * 4] = v;
data.data[i * 4 + 1] = v - 2;
data.data[i * 4 + 2] = v - 6;
data.data[i * 4 + 3] = 255;
}
tc.putImageData(data, 0, 0);
tc.lineWidth = 1;
for (var f = 0; f < 90; f++) {
var x = Math.random() * size;
var y = Math.random() * size;
var a = Math.random() * Math.PI * 2;
var l = 6 + Math.random() * 18;
tc.strokeStyle = "rgba(120,108,86," + (0.04 + Math.random() * 0.05) + ")";
tc.beginPath();
tc.moveTo(x, y);
tc.quadraticCurveTo(
x + Math.cos(a + 0.6) * l * 0.5,
y + Math.sin(a + 0.6) * l * 0.5,
x + Math.cos(a) * l,
y + Math.sin(a) * l,
);
tc.stroke();
}
grain = ctx.createPattern(tile, "repeat");
}
buildGrain();
// ---- fluid state ----
var W = 0,
H = 0,
dpr = 1,
gw = 0,
gh = 0;
var u, v, u2, v2, d1, d1b, d2, d2b, p, div, curl;
var off = document.createElement("canvas");
var offCtx = off.getContext("2d");
var img = null;
var simT = 0;
var idle = 0.8;
var hasPointer = false;
var lastX = 0,
lastY = 0;
var needStill = true;
var PAPER = [242, 238, 228];
var INDIGO = [0.12, 0.13, 0.45];
var CORAL = [1.0, 0.38, 0.3];
function clamp(x, a, b) {
return x < a ? a : x > b ? b : x;
}
function splat(cx, cy, r, fx, fy, a1, a2) {
var reach = Math.ceil(r * 2.6);
var x0 = Math.max(1, Math.floor(cx - reach));
var x1 = Math.min(gw - 2, Math.ceil(cx + reach));
var y0 = Math.max(1, Math.floor(cy - reach));
var y1 = Math.min(gh - 2, Math.ceil(cy + reach));
var rr = r * r;
for (var y = y0; y <= y1; y++) {
for (var x = x0; x <= x1; x++) {
var dx = x - cx;
var dy = y - cy;
var g = Math.exp(-(dx * dx + dy * dy) / rr);
var i = y * gw + x;
u[i] += fx * g;
v[i] += fy * g;
d1[i] += a1 * g;
d2[i] += a2 * g;
}
}
}
function wander(t) {
return [
0.5 + 0.38 * Math.sin(0.11 * t + 0.5) + 0.08 * Math.sin(0.37 * t),
0.5 + 0.34 * Math.sin(0.083 * t + 2.0) + 0.07 * Math.cos(0.29 * t),
];
}
function emit() {
var a = wander(simT);
var b = wander(simT + 0.2);
var tx = b[0] * gw - a[0] * gw;
var ty = b[1] * gh - a[1] * gh;
var len = Math.sqrt(tx * tx + ty * ty) + 1e-6;
tx /= len;
ty /= len;
var target = hasPointer ? 0.25 : 0.8;
idle += (target - idle) * 0.01;
var cx = a[0] * gw;
var cy = a[1] * gh;
var side = Math.sin(simT * 0.2) > 0 ? 1 : -1;
var px = -ty * 2.0 * side;
var py = tx * 2.0 * side;
splat(cx + px, cy + py, 4.5, tx * 0.9 * idle, ty * 0.9 * idle, 0.06 * idle, 0);
splat(cx - px, cy - py, 4.5, tx * 0.9 * idle, ty * 0.9 * idle, 0, 0.06 * idle);
}
function vorticity() {
var x, y, i;
for (y = 1; y < gh - 1; y++) {
for (x = 1; x < gw - 1; x++) {
i = y * gw + x;
curl[i] = (v[i + 1] - v[i - 1] - (u[i + gw] - u[i - gw])) * 0.5;
}
}
for (y = 2; y < gh - 2; y++) {
for (x = 2; x < gw - 2; x++) {
i = y * gw + x;
var dx = (Math.abs(curl[i + 1]) - Math.abs(curl[i - 1])) * 0.5;
var dy = (Math.abs(curl[i + gw]) - Math.abs(curl[i - gw])) * 0.5;
var l = Math.sqrt(dx * dx + dy * dy) + 1e-5;
u[i] += 0.2 * (dy / l) * curl[i];
v[i] -= 0.2 * (dx / l) * curl[i];
}
}
}
function current() {
for (var y = 1; y < gh - 1; y++) {
for (var x = 1; x < gw - 1; x++) {
var i = y * gw + x;
u[i] += 0.0035 * Math.sin(y * 0.09 + simT * 0.45 + x * 0.02);
v[i] += 0.0035 * Math.sin(x * 0.08 - simT * 0.35 + y * 0.03);
}
}
}
function project() {
var x, y, i, k;
for (y = 1; y < gh - 1; y++) {
for (x = 1; x < gw - 1; x++) {
i = y * gw + x;
div[i] = -0.5 * (u[i + 1] - u[i - 1] + v[i + gw] - v[i - gw]);
}
}
for (k = 0; k < 18; k++) {
for (y = 1; y < gh - 1; y++) {
for (x = 1; x < gw - 1; x++) {
i = y * gw + x;
p[i] = (div[i] + p[i - 1] + p[i + 1] + p[i - gw] + p[i + gw]) * 0.25;
}
}
for (x = 0; x < gw; x++) {
p[x] = p[x + gw];
p[(gh - 1) * gw + x] = p[(gh - 2) * gw + x];
}
for (y = 0; y < gh; y++) {
p[y * gw] = p[y * gw + 1];
p[y * gw + gw - 1] = p[y * gw + gw - 2];
}
}
for (y = 1; y < gh - 1; y++) {
for (x = 1; x < gw - 1; x++) {
i = y * gw + x;
u[i] -= 0.5 * (p[i + 1] - p[i - 1]);
v[i] -= 0.5 * (p[i + gw] - p[i - gw]);
}
}
for (y = 0; y < gh; y++) {
u[y * gw] = 0;
u[y * gw + gw - 1] = 0;
}
for (x = 0; x < gw; x++) {
v[x] = 0;
v[(gh - 1) * gw + x] = 0;
}
}
function advect(dst, src, diss) {
var maxX = gw - 1.001;
var maxY = gh - 1.001;
for (var y = 0; y < gh; y++) {
for (var x = 0; x < gw; x++) {
var i = y * gw + x;
var px = clamp(x - u[i], 0, maxX);
var py = clamp(y - v[i], 0, maxY);
var x0 = px | 0;
var y0 = py | 0;
var fx = px - x0;
var fy = py - y0;
var j = y0 * gw + x0;
var top = src[j] * (1 - fx) + src[j + 1] * fx;
var bot = src[j + gw] * (1 - fx) + src[j + gw + 1] * fx;
dst[i] = (top * (1 - fy) + bot * fy) * diss;
}
}
}
function step() {
simT += 1 / 60;
emit();
current();
vorticity();
project();
advect(u2, u, 0.997);
advect(v2, v, 0.997);
var t = u;
u = u2;
u2 = t;
t = v;
v = v2;
v2 = t;
for (var i = 0; i < u.length; i++) {
u[i] = clamp(u[i], -3, 3);
v[i] = clamp(v[i], -3, 3);
}
advect(d1b, d1, 0.992);
advect(d2b, d2, 0.992);
t = d1;
d1 = d1b;
d1b = t;
t = d2;
d2 = d2b;
d2b = t;
}
function render() {
var data = img.data;
var n = gw * gh;
for (var i = 0; i < n; i++) {
var a1 = Math.min(0.96, 1 - Math.exp(-2.0 * d1[i]));
var a2 = Math.min(0.92, 1 - Math.exp(-2.0 * d2[i]));
a1 = a1 * a1 * (3 - 2 * a1) * 0.6 + a1 * 0.4;
a2 = a2 * a2 * (3 - 2 * a2) * 0.6 + a2 * 0.4;
var o = i * 4;
data[o] = PAPER[0] * (1 - a1 * (1 - INDIGO[0])) * (1 - a2 * (1 - CORAL[0]));
data[o + 1] = PAPER[1] * (1 - a1 * (1 - INDIGO[1])) * (1 - a2 * (1 - CORAL[1]));
data[o + 2] = PAPER[2] * (1 - a1 * (1 - INDIGO[2])) * (1 - a2 * (1 - CORAL[2]));
data[o + 3] = 255;
}
offCtx.putImageData(img, 0, 0);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.globalCompositeOperation = "source-over";
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(off, 0, 0, canvas.width, canvas.height);
if (grain) {
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = grain;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = "source-over";
}
}
function init() {
W = window.innerWidth;
H = window.innerHeight;
var maxDpr = getP().maxDpr || 1.5;
dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
canvas.width = Math.max(1, Math.round(W * dpr));
canvas.height = Math.max(1, Math.round(H * dpr));
var s = 170 / Math.max(W, H);
gw = Math.max(48, Math.round(W * s));
gh = Math.max(48, Math.round(H * s));
var n = gw * gh;
u = new Float32Array(n);
v = new Float32Array(n);
u2 = new Float32Array(n);
v2 = new Float32Array(n);
d1 = new Float32Array(n);
d1b = new Float32Array(n);
d2 = new Float32Array(n);
d2b = new Float32Array(n);
p = new Float32Array(n);
div = new Float32Array(n);
curl = new Float32Array(n);
off.width = gw;
off.height = gh;
img = offCtx.createImageData(gw, gh);
simT = 0;
var warm = getP().reducedMotion ? 300 : 160;
for (var k = 0; k < warm; k++) {
step();
}
render();
needStill = true;
}
function onMove(e) {
var P = getP();
if (P.paused || P.reducedMotion) {
return;
}
var x = e.clientX;
var y = e.clientY;
if (typeof x !== "number" || typeof y !== "number") {
return;
}
if (!hasPointer) {
hasPointer = true;
lastX = x;
lastY = y;
return;
}
var sx = gw / W;
var sy = gh / H;
var cdx = (x - lastX) * sx;
var cdy = (y - lastY) * sy;
var dist = Math.sqrt(cdx * cdx + cdy * cdy);
if (dist < 0.01) {
return;
}
var n = Math.max(1, Math.ceil(dist / 1.5));
var speed = Math.min(dist, 8);
var dirx = cdx / dist;
var diry = cdy / dist;
var fx = dirx * Math.min(dist, 10) * 0.09;
var fy = diry * Math.min(dist, 10) * 0.09;
var amt = Math.min(0.1 + speed * 0.02, 0.35);
var r = 3 + speed * 0.35;
var side = Math.sin(simT * 0.4) > 0 ? 1 : -1;
var px = -diry * 1.6 * side;
var py = dirx * 1.6 * side;
for (var k = 1; k <= n; k++) {
var f = k / n;
var gx = (lastX + (x - lastX) * f) * sx;
var gy = (lastY + (y - lastY) * f) * sy;
splat(gx + px, gy + py, r, fx, fy, amt, 0);
splat(gx - px, gy - py, r, fx, fy, 0, amt * 1.1);
}
lastX = x;
lastY = y;
}
window.addEventListener("pointermove", onMove, { passive: true });
window.addEventListener("mousemove", onMove, { passive: true });
window.addEventListener("resize", init);
var lastTs = 0;
var acc = 0;
function frame(ts) {
requestAnimationFrame(frame);
var P = getP();
if (!lastTs) {
lastTs = ts;
}
if (P.paused) {
lastTs = ts;
return;
}
if (P.reducedMotion) {
if (needStill) {
render();
needStill = false;
}
lastTs = ts;
return;
}
acc += Math.min(ts - lastTs, 50);
lastTs = ts;
var n = 0;
while (acc >= 16.67 && n < 3) {
step();
acc -= 16.67;
n++;
}
if (n > 0) {
render();
}
}
try {
init();
} catch (err) {
document.body.classList.add("no-canvas");
return;
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Made by 6to9. Back to all motion patterns.