<section class="demo">
<canvas id="dither"></canvas>
</section>.demo {
min-height: 180vh;
background: #08080a;
}
#dither {
position: sticky;
top: 0;
display: block;
width: 100%;
height: 100vh;
}const canvas = document.querySelector("#dither");
const ctx = canvas.getContext("2d");
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const bayer = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
const state = { reveal: 0.22, mx: 0.5, my: 0.5 };
let source = null;
function makeSource(w, h) {
const off = document.createElement("canvas");
off.width = w;
off.height = h;
const c = off.getContext("2d");
const g = c.createLinearGradient(0, 0, w, h);
g.addColorStop(0, "#6f7bd8");
g.addColorStop(0.5, "#d8b26f");
g.addColorStop(1, "#e0705f");
c.fillStyle = g;
c.fillRect(0, 0, w, h);
return c.getImageData(0, 0, w, h);
}
function fit() {
const w = Math.max(1, Math.floor((canvas.clientWidth || window.innerWidth) / 3));
const h = Math.max(1, Math.floor((canvas.clientHeight || window.innerHeight) / 3));
if (canvas.width === w && canvas.height === h && source) {
paint();
return;
}
canvas.width = w;
canvas.height = h;
source = makeSource(w, h);
paint();
}
function paint() {
if (!source) return;
const w = canvas.width;
const h = canvas.height;
const out = ctx.createImageData(w, h);
const src = source.data;
const dst = out.data;
for (let y = 0; y < h; y += 1) {
for (let x = 0; x < w; x += 1) {
const i = (y * w + x) * 4;
const dx = x / w - state.mx;
const dy = y / h - state.my;
const local = Math.max(0, 1 - Math.hypot(dx, dy) / 0.42);
const threshold = bayer[(y % 4) * 4 + (x % 4)] / 16;
const pass = threshold < state.reveal + local * 0.35;
dst[i] = pass ? src[i] : 8;
dst[i + 1] = pass ? src[i + 1] : 8;
dst[i + 2] = pass ? src[i + 2] : 10;
dst[i + 3] = 255;
}
}
ctx.putImageData(out, 0, 0);
}
canvas.addEventListener("pointermove", function (event) {
const rect = canvas.getBoundingClientRect();
state.mx = (event.clientX - rect.left) / rect.width;
state.my = (event.clientY - rect.top) / rect.height;
paint();
});
fit();
window.addEventListener("resize", fit);
if (reduce) {
state.reveal = 1;
paint();
} else if (window.ScrollTrigger) {
gsap.to(state, {
reveal: 1,
ease: "none",
onUpdate: paint,
scrollTrigger: {
trigger: ".demo",
start: "top top",
end: "bottom bottom",
scrub: 0.35,
},
});
}