Water surface
A sunlit pool floor of sand and drifting caustics with a refracted SUN DIP headline painted into it; a wandering virtual swimmer, random drops and the pointer send damped ripples across the water.
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>Sun Dip</title>
<style>
html,
body {
margin: 0;
height: 100%;
overflow: hidden;
background: #e6d2a6;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
sans-serif;
}
.hero {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background: #e6d2a6;
}
@supports (height: 100dvh) {
.hero {
height: 100dvh;
}
}
#water {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
/* The headline is painted into the pool floor by the canvas; this copy is for screen readers. */
.headline {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.panel {
position: absolute;
left: 4vw;
bottom: 5vh;
max-width: 400px;
box-sizing: border-box;
padding: 22px 24px 24px;
background: #fffaf0;
color: #0f4c5c;
border-radius: 18px;
box-shadow: 0 14px 40px rgba(14, 70, 84, 0.22);
}
.eyebrow {
margin: 0 0 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #c4562f;
}
.sub {
margin: 0 0 18px;
font-size: 17px;
line-height: 1.45;
}
.btn {
display: inline-block;
border: 0;
cursor: pointer;
padding: 13px 24px;
border-radius: 999px;
background: #0f4c5c;
color: #fffaf0;
font: inherit;
font-weight: 700;
font-size: 16px;
transition:
background 0.25s ease,
transform 0.25s ease;
}
.btn:hover {
background: #c4562f;
transform: translateY(-1px);
}
.btn:focus-visible {
outline: 3px solid #ff9b7a;
outline-offset: 3px;
}
@media (max-width: 640px) {
.panel {
left: 16px;
right: 16px;
bottom: 20px;
max-width: none;
padding: 18px 20px 20px;
}
.sub {
font-size: 16px;
margin-bottom: 14px;
}
}
/* CSS-only fallback when canvas 2D is unavailable */
.hero.fallback #water {
display: none;
}
.hero.fallback::before {
content: "";
position: absolute;
inset: -20%;
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.55), transparent 28%),
radial-gradient(circle at 70% 60%, rgba(255, 255, 240, 0.45), transparent 30%),
radial-gradient(circle at 45% 80%, rgba(120, 210, 215, 0.45), transparent 35%);
animation: drift 14s ease-in-out infinite alternate;
}
.hero.fallback .headline {
position: absolute;
inset: 0 0 25% 0;
width: auto;
height: auto;
margin: 0;
clip: auto;
overflow: visible;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
white-space: normal;
font-family: "Arial Black", "Helvetica Neue", Arial, sans-serif;
font-weight: 900;
font-size: clamp(90px, 26vw, 360px);
line-height: 0.86;
letter-spacing: -0.03em;
text-transform: uppercase;
color: #166880;
}
@keyframes drift {
from {
transform: translate(-3%, -2%) scale(1);
}
to {
transform: translate(3%, 2%) scale(1.08);
}
}
</style>
</head>
<body>
<main class="hero" id="hero">
<canvas id="water" aria-hidden="true"></canvas>
<h1 class="headline">Sun dip</h1>
<section class="panel" id="panel">
<p class="eyebrow">Lido Studio</p>
<p class="sub">Slow, sunlit tools for unhurried teams. Come on in, the water is fine.</p>
<button class="btn" type="button">Dip in</button>
</section>
</main>
<script>
(function () {
var P = window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
var hero = document.getElementById("hero");
var canvas = document.getElementById("water");
var panel = document.getElementById("panel");
var FONT = "'Arial Black', 'Helvetica Neue', Arial, system-ui, sans-serif";
var LINES = ["SUN", "DIP"];
var TILE = 256;
var REF = 0.6;
var STILL_T = 4;
var SUBSTEPS = 2;
var DAMP = 0.994;
var INK = [20, 98, 120];
var ctx = null;
try {
ctx = canvas.getContext("2d");
} catch (e) {
ctx = null;
}
var cssW = 0,
cssH = 0,
cell = 2,
gw = 0,
gh = 0;
var gridCanvas, gridCtx, img, px32;
var cur, prev, mask, shade;
var causticA, causticB, sand;
var stillDirty = true;
var simT = 0;
var lastFrame = 0;
var acc = 0;
var nextDrop = 1.2;
var userActive = false;
var lastMoveAt = -99999;
var lastX = null,
lastY = null,
lastT = 0;
var travel = 0;
var virtX = null,
virtY = null;
function useFallback() {
hero.classList.add("fallback");
}
/* ---------- procedural textures (tileable) ---------- */
function makeCausticTile(phase) {
var out = new Float32Array(TILE * TILE);
var TAU = Math.PI * 2;
for (var y = 0; y < TILE; y++) {
for (var x = 0; x < TILE; x++) {
var px = (x / TILE) * TAU;
var py = (y / TILE) * TAU;
// integer wave numbers keep the whole field periodic
var wx =
px + 0.9 * Math.sin(py * 2 + phase) + 0.5 * Math.sin(py * 3 - phase * 1.3 + 1);
var wy =
py +
0.9 * Math.sin(px * 2 - phase * 0.8 + 2) +
0.5 * Math.sin(px * 3 + phase * 1.1);
var f1 =
(Math.sin(wx * 2 + wy) +
Math.sin(wy * 3 - wx) +
Math.sin(wx * 3 + wy * 2 + phase * 0.5)) /
3;
var f2 =
(Math.sin(wx - wy * 2 + 1.7) +
Math.sin(wy * 2 + wx * 3 - phase) +
Math.sin(wx * 2 - wy * 3 + 0.4)) /
3;
var r1 = 1 - Math.abs(f1);
var r2 = 1 - Math.abs(f2);
var c = (Math.pow(r1, 6) + Math.pow(r2, 6)) * 0.7;
out[y * TILE + x] = c > 1 ? 1 : c;
}
}
return out;
}
function makeSandTile() {
var n = new Float32Array(TILE * TILE);
var i;
for (i = 0; i < n.length; i++) n[i] = Math.random();
var out = new Float32Array(TILE * TILE);
var TAU = Math.PI * 2;
for (var y = 0; y < TILE; y++) {
for (var x = 0; x < TILE; x++) {
var s = 0;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
s += n[((y + dy) & 255) * TILE + ((x + dx) & 255)];
}
}
var px = (x / TILE) * TAU;
var py = (y / TILE) * TAU;
var mottle = 0.25 * Math.sin(px * 2 + py) * Math.sin(py * 3 - px);
out[y * TILE + x] = (s / 9 - 0.5) * 2.2 + (n[y * TILE + x] - 0.5) * 0.5 + mottle;
}
}
return out;
}
/* ---------- layout / buffers ---------- */
function buildText() {
var tc = document.createElement("canvas");
tc.width = gw;
tc.height = gh;
var t2 = tc.getContext("2d", { willReadFrequently: true });
var pr = panel.getBoundingClientRect();
var top = cssH * 0.04;
var bottom = pr.top > cssH * 0.3 ? pr.top - cssH * 0.03 : cssH * 0.9;
var availH = Math.max(120, bottom - top);
t2.font = "900 100px " + FONT;
try {
t2.letterSpacing = "-0.03em";
} catch (e) {
/* older browsers */
}
var widest = 1;
for (var i = 0; i < LINES.length; i++) {
widest = Math.max(widest, t2.measureText(LINES[i]).width);
}
var sizeW = (100 * cssW * 0.94) / widest;
var sizeH = availH / (LINES.length * 0.84);
var size = Math.min(sizeW, sizeH);
t2.clearRect(0, 0, gw, gh);
t2.font = "900 " + size / cell + "px " + FONT;
try {
t2.letterSpacing = "-0.03em";
} catch (e) {
/* older browsers */
}
t2.textAlign = "center";
t2.textBaseline = "middle";
t2.fillStyle = "#fff";
var lineH = size * 0.86;
var total = lineH * LINES.length;
var y0 = top + (availH - total) / 2 + lineH / 2;
for (var k = 0; k < LINES.length; k++) {
t2.fillText(LINES[k], cssW / 2 / cell, (y0 + k * lineH) / cell);
}
var data = t2.getImageData(0, 0, gw, gh).data;
mask = new Uint8Array(gw * gh);
for (var p = 0; p < mask.length; p++) mask[p] = data[p * 4 + 3];
}
function buildShade() {
shade = new Float32Array(gw * gh);
for (var y = 0; y < gh; y++) {
var vy = (y / gh - 0.5) * 2;
for (var x = 0; x < gw; x++) {
var vx = (x / gw - 0.5) * 2;
var v2 = (vx * vx + vy * vy) / 2;
shade[y * gw + x] = 1.06 - 0.24 * v2 + 0.04 * (1 - y / gh);
}
}
}
function resize() {
cssW = hero.clientWidth || window.innerWidth;
cssH = hero.clientHeight || window.innerHeight;
var dpr = Math.min(window.devicePixelRatio || 1, P.maxDpr || 1);
canvas.width = Math.max(1, Math.round(cssW * dpr));
canvas.height = Math.max(1, Math.round(cssH * dpr));
cell = Math.max(2, cssW / 640);
gw = Math.ceil(cssW / cell);
gh = Math.ceil(cssH / cell);
gridCanvas = document.createElement("canvas");
gridCanvas.width = gw;
gridCanvas.height = gh;
gridCtx = gridCanvas.getContext("2d");
img = gridCtx.createImageData(gw, gh);
px32 = new Uint32Array(img.data.buffer);
cur = new Float32Array(gw * gh);
prev = new Float32Array(gw * gh);
virtX = null;
buildText();
buildShade();
if (P.reducedMotion) primeStill();
stillDirty = true;
render(P.reducedMotion ? STILL_T : simT);
}
/* ---------- wave simulation ---------- */
function step() {
var W = gw,
H = gh;
var a = cur,
b = prev;
for (var y = 1; y < H - 1; y++) {
var row = y * W;
for (var x = 1; x < W - 1; x++) {
var i = row + x;
var v = (a[i - 1] + a[i + 1] + a[i - W] + a[i + W]) * 0.5 - b[i];
v *= DAMP;
if (v < 0.0005 && v > -0.0005) v = 0;
b[i] = v;
}
}
prev = a;
cur = b;
}
function splash(gx, gy, amp, rad) {
var cx = Math.round(gx);
var cy = Math.round(gy);
var r = Math.ceil(rad);
for (var dy = -r; dy <= r; dy++) {
var y = cy + dy;
if (y < 1 || y >= gh - 1) continue;
for (var dx = -r; dx <= r; dx++) {
var x = cx + dx;
if (x < 1 || x >= gw - 1) continue;
var d = Math.sqrt(dx * dx + dy * dy);
if (d > rad) continue;
var f = 0.5 + 0.5 * Math.cos((Math.PI * d) / rad);
var i = y * gw + x;
var v = cur[i] + amp * f;
cur[i] = v > 120 ? 120 : v < -120 ? -120 : v;
}
}
}
// Drags a body through the water along a segment (css px). v = speed in px/ms.
function dragSegment(x0, y0, x1, y1, v) {
var dist = Math.hypot(x1 - x0, y1 - y0);
var n = Math.min(60, Math.max(1, Math.ceil(dist / (cell * 1.4))));
var amp = -(3 + v * 5);
var rad = 2.2 + v * 1.2;
if (n === 1) amp *= Math.min(1, dist / (cell * 1.4));
for (var k = 1; k <= n; k++) {
var t = k / n;
splash((x0 + (x1 - x0) * t) / cell, (y0 + (y1 - y0) * t) / cell, amp, rad);
}
travel += dist;
if (travel > 150) {
travel = 0;
splash(x1 / cell, y1 / cell, -(16 + v * 6), 5.5);
}
}
function primeStill() {
splash(gw * 0.3, gh * 0.42, -40, 4);
splash(gw * 0.7, gh * 0.55, -34, 4);
splash(gw * 0.52, gh * 0.25, -28, 3.5);
for (var i = 0; i < 70; i++) step();
}
/* ---------- pointer ---------- */
function onMove(x, y) {
var now = performance.now();
userActive = true;
if (lastX === null) {
lastX = x;
lastY = y;
lastT = now;
lastMoveAt = now;
return;
}
var dist = Math.hypot(x - lastX, y - lastY);
if (dist < 0.5) return;
var dt = Math.max(now - lastT, 8);
var v = Math.min(dist / dt, 2.5);
if (!P.paused && !P.reducedMotion) dragSegment(lastX, lastY, x, y, v);
lastX = x;
lastY = y;
lastT = now;
lastMoveAt = now;
}
window.addEventListener("pointermove", function (e) {
onMove(e.clientX, e.clientY);
});
window.addEventListener("mousemove", function (e) {
onMove(e.clientX, e.clientY);
});
window.addEventListener("pointerdown", function (e) {
userActive = true;
lastX = e.clientX;
lastY = e.clientY;
lastT = performance.now();
lastMoveAt = lastT;
if (!P.paused && !P.reducedMotion) splash(e.clientX / cell, e.clientY / cell, -50, 5);
});
// gentle autonomous motion: a wandering virtual pointer and occasional drops
function autonomous() {
var now = performance.now();
var wander = !userActive || now - lastMoveAt > 3500;
if (wander) {
var x =
cssW * (0.5 + 0.32 * Math.sin(simT * 0.21 + 1)) + cssW * 0.1 * Math.sin(simT * 0.57);
var y =
cssH * (0.45 + 0.26 * Math.sin(simT * 0.17)) +
cssH * 0.06 * Math.sin(simT * 0.43 + 2);
if (virtX === null) {
virtX = x;
virtY = y;
}
if (Math.hypot(x - virtX, y - virtY) > cell * 2.5) {
dragSegment(virtX, virtY, x, y, 0.12);
virtX = x;
virtY = y;
}
}
if (simT > nextDrop) {
var gx = (0.1 + Math.random() * 0.8) * gw;
var gy = (0.1 + Math.random() * 0.8) * gh;
splash(gx, gy, -(28 + Math.random() * 14), 3.5 + Math.random() * 1.5);
nextDrop = simT + 1.8 + Math.random() * 2.4;
}
}
/* ---------- rendering ---------- */
function render(t) {
var W = gw,
H = gh;
var A0 = causticA,
A1 = causticB;
var k1 = (cell * TILE) / 420;
var k2 = (cell * TILE) / 300;
var k3 = cell * 0.6;
var o1x = t * 9,
o1y = t * 5;
var o2x = -t * 6,
o2y = t * 8;
var w = 0.5 + 0.5 * Math.sin(t * 0.35);
var iw = 1 - w;
var c0 = cur;
for (var y = 0; y < H; y++) {
var row = y * W;
var up = y > 0 ? -W : 0;
var dn = y < H - 1 ? W : 0;
for (var x = 0; x < W; x++) {
var i = row + x;
var l = x > 0 ? -1 : 0;
var rr = x < W - 1 ? 1 : 0;
var hx = c0[i + rr] - c0[i + l];
var hy = c0[i + dn] - c0[i + up];
var lap = c0[i + l] + c0[i + rr] + c0[i + up] + c0[i + dn] - 4 * c0[i];
// refracted sample position for the floor and the caustics
var sx = x + hx * REF;
var sy = y + hy * REF;
var j1 = ((((sy * k1 + o1y) | 0) & 255) << 8) | (((sx * k1 + o1x) | 0) & 255);
var j2 = ((((sy * k2 + o2y) | 0) & 255) << 8) | (((sx * k2 + o2x) | 0) & 255);
var c = (A0[j1] * iw + A1[j1] * w + (A0[j2] * iw + A1[j2] * w)) * 0.6 - lap * 0.05;
if (c < 0) c = 0;
var g = sand[((((sy * k3) | 0) & 255) << 8) | (((sx * k3) | 0) & 255)];
var sh = shade[i];
var r = (226 + g * 26) * sh * 0.88;
var gr = (205 + g * 24) * sh * 0.97 + 6;
var b = (158 + g * 24) * sh * 0.96 + 22;
// the headline lies on the floor, refracted a little more
var tx = (x + hx * REF * 1.7 + 0.5) | 0;
var ty = (y + hy * REF * 1.7 + 0.5) | 0;
tx = tx < 0 ? 0 : tx >= W ? W - 1 : tx;
ty = ty < 0 ? 0 : ty >= H ? H - 1 : ty;
var m = mask[ty * W + tx] * (0.92 / 255);
if (m > 0) {
r += (INK[0] - r) * m;
gr += (INK[1] - gr) * m;
b += (INK[2] - b) * m;
}
// caustic light over everything, then ripple glints
var L = 0.78 + c * 0.6;
r = r * L + c * 34;
gr = gr * L + c * 32;
b = b * L + c * 24;
var add = -(hx + hy) * 7;
add = add < -30 ? -30 : add > 80 ? 80 : add;
r += add;
gr += add;
b += add * 1.05;
r = r > 255 ? 255 : r < 0 ? 0 : r;
gr = gr > 255 ? 255 : gr < 0 ? 0 : gr;
b = b > 255 ? 255 : b < 0 ? 0 : b;
px32[i] = (255 << 24) | ((b | 0) << 16) | ((gr | 0) << 8) | (r | 0);
}
}
gridCtx.putImageData(img, 0, 0);
ctx.imageSmoothingEnabled = true;
try {
ctx.imageSmoothingQuality = "high";
} catch (e) {
/* optional */
}
ctx.drawImage(gridCanvas, 0, 0, canvas.width, canvas.height);
}
/* ---------- loop ---------- */
function frame(now) {
requestAnimationFrame(frame);
try {
if (P.paused) {
lastFrame = now;
return;
}
if (P.reducedMotion) {
if (stillDirty) {
render(STILL_T);
stillDirty = false;
}
lastFrame = now;
return;
}
var dt = Math.min((now - lastFrame) / 1000, 0.05);
lastFrame = now;
simT += dt;
autonomous();
acc += dt;
var n = 0;
while (acc >= 1 / 60 && n < 3) {
for (var s = 0; s < SUBSTEPS; s++) step();
acc -= 1 / 60;
n++;
}
if (acc > 1 / 60) acc = 0;
render(simT);
} catch (e) {
useFallback();
}
}
function start() {
causticA = makeCausticTile(0);
causticB = makeCausticTile(2.2);
sand = makeSandTile();
resize();
window.addEventListener("resize", function () {
try {
resize();
} catch (e) {
useFallback();
}
});
lastFrame = performance.now();
requestAnimationFrame(frame);
}
if (!ctx) {
useFallback();
} else {
try {
start();
} catch (e) {
useFallback();
}
}
})();
</script>
</body>
</html>
Made by 6to9. Back to all motion patterns.