Dot Grid Background
Hover
Canvas
Three.js
JS
Preview Links
Custom JS
Enable custom code in preview or view on published site.
Enable custom code?
Last Updated: Mar 3, 2026
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".hover-8_wrap").forEach((component) => {
if (component.hasAttribute("data-hover-8")) return;
component.setAttribute("data-hover-8", "");
const canvas = component.querySelector("canvas");
const DOT_SPACING = 10;
const DOT_RADIUS = 1.4;
const HOVER_RADIUS = 200;
const HOVER_SCALE = 6;
const MIN_GAP = 1.0;
const LERP_SPEED_UP = 0.12;
const LERP_SPEED_DOWN = 0.025;
const MOUSE_LERP = 0.35;
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let W = component.clientWidth;
let H = component.clientHeight;
const computedStyle = getComputedStyle(canvas);
function parseCSSColor(str) {
const ctx = document.createElement('canvas').getContext('2d');
ctx.fillStyle = str;
ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
return new THREE.Color(r / 255, g / 255, b / 255);
}
const BASE_COLOR = parseCSSColor(computedStyle.color || '#8090b8');
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-W / 2, W / 2, H / 2, -H / 2, 1, 10);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true });
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(W, H);
const geo = new THREE.CircleGeometry(DOT_RADIUS, 32);
const mat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const dummy = new THREE.Object3D();
let cols, rows, count, mesh, basePositions, currentScales, currentOffsets;
function buildGrid() {
W = component.clientWidth;
H = component.clientHeight;
camera.left = -W / 2;
camera.right = W / 2;
camera.top = H / 2;
camera.bottom = -H / 2;
camera.updateProjectionMatrix();
renderer.setSize(W, H);
if (mesh) scene.remove(mesh);
cols = Math.ceil(W / DOT_SPACING) + 2;
rows = Math.ceil(H / DOT_SPACING) + 2;
count = cols * rows;
mesh = new THREE.InstancedMesh(geo, mat, count);
scene.add(mesh);
const colors = new Float32Array(count * 3);
basePositions = new Float32Array(count * 2);
currentScales = new Float32Array(count).fill(1);
currentOffsets = new Float32Array(count * 2).fill(0);
let idx = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = c * DOT_SPACING - W / 2;
const y = H / 2 - r * DOT_SPACING;
basePositions[idx * 2] = x;
basePositions[idx * 2 + 1] = y;
dummy.position.set(x, y, 0);
dummy.scale.set(1, 1, 1);
dummy.updateMatrix();
mesh.setMatrixAt(idx, dummy.matrix);
colors[idx * 3] = BASE_COLOR.r;
colors[idx * 3 + 1] = BASE_COLOR.g;
colors[idx * 3 + 2] = BASE_COLOR.b;
idx++;
}
}
geo.setAttribute('color', new THREE.InstancedBufferAttribute(colors, 3));
mat.vertexColors = false;
mesh.instanceMatrix.needsUpdate = true;
mesh.instanceColor = new THREE.InstancedBufferAttribute(colors, 3);
}
buildGrid();
const mouse = { x: -9999, y: -9999 };
const mouseSmooth = { x: -9999, y: -9999 };
const lastClient = { x: -9999, y: -9999, active: false };
function updatePointer(clientX, clientY) {
const rect = component.getBoundingClientRect();
if (
clientX >= rect.left &&
clientX <= rect.right &&
clientY >= rect.top &&
clientY <= rect.bottom
) {
lastClient.x = clientX;
lastClient.y = clientY;
lastClient.active = true;
mouse.x = clientX - rect.left - W / 2;
mouse.y = H / 2 - (clientY - rect.top);
} else {
mouse.x = -9999;
mouse.y = -9999;
lastClient.active = false;
}
}
function recalcFromLastClient() {
if (!lastClient.active) return;
const rect = component.getBoundingClientRect();
if (
lastClient.x >= rect.left &&
lastClient.x <= rect.right &&
lastClient.y >= rect.top &&
lastClient.y <= rect.bottom
) {
mouse.x = lastClient.x - rect.left - W / 2;
mouse.y = H / 2 - (lastClient.y - rect.top);
} else {
mouse.x = -9999;
mouse.y = -9999;
lastClient.active = false;
}
}
document.addEventListener('mousemove', (e) => updatePointer(e.clientX, e.clientY));
if (window.lenis) {
window.lenis.on('scroll', recalcFromLastClient);
} else {
document.addEventListener('scroll', recalcFromLastClient, { passive: true });
}
document.addEventListener('touchmove', (e) => {
const touch = e.touches[0];
if (touch) updatePointer(touch.clientX, touch.clientY);
}, { passive: true });
document.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
if (touch) updatePointer(touch.clientX, touch.clientY);
}, { passive: true });
document.addEventListener('touchend', () => {
mouse.x = -9999;
mouse.y = -9999;
lastClient.active = false;
}, { passive: true });
function animate() {
requestAnimationFrame(animate);
if (prefersReducedMotion) {
renderer.render(scene, camera);
return;
}
if (mouse.x < -9000) {
mouseSmooth.x = mouse.x;
mouseSmooth.y = mouse.y;
} else {
if (mouseSmooth.x < -9000) {
mouseSmooth.x = mouse.x;
mouseSmooth.y = mouse.y;
} else {
mouseSmooth.x += (mouse.x - mouseSmooth.x) * MOUSE_LERP;
mouseSmooth.y += (mouse.y - mouseSmooth.y) * MOUSE_LERP;
}
}
for (let i = 0; i < count; i++) {
const bx = basePositions[i * 2];
const by = basePositions[i * 2 + 1];
let targetScale = 1;
const dx = bx - mouseSmooth.x;
const dy = by - mouseSmooth.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < HOVER_RADIUS) {
const t = 1 - dist / HOVER_RADIUS;
targetScale = 1 + (HOVER_SCALE - 1) * t * t * t;
}
const lerpSpeed = targetScale > currentScales[i] ? LERP_SPEED_UP : LERP_SPEED_DOWN;
currentScales[i] += (targetScale - currentScales[i]) * lerpSpeed;
}
for (let i = 0; i < count; i++) {
let targetOffsetX = 0;
let targetOffsetY = 0;
const bx = basePositions[i * 2];
const by = basePositions[i * 2 + 1];
const ri = DOT_RADIUS * currentScales[i];
const col = i % cols;
const row = (i - col) / cols;
for (let dr = -2; dr <= 2; dr++) {
for (let dc = -2; dc <= 2; dc++) {
if (dr === 0 && dc === 0) continue;
const nr = row + dr;
const nc = col + dc;
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
const j = nr * cols + nc;
const rj = DOT_RADIUS * currentScales[j];
const nbx = basePositions[j * 2];
const nby = basePositions[j * 2 + 1];
const ddx = bx - nbx;
const ddy = by - nby;
const dist = Math.sqrt(ddx * ddx + ddy * ddy);
const minDist = ri + rj + MIN_GAP;
if (dist < minDist && dist > 0.01) {
const overlap = minDist - dist;
targetOffsetX += (ddx / dist) * overlap * 0.5;
targetOffsetY += (ddy / dist) * overlap * 0.5;
}
}
}
const offsetLerp = (targetOffsetX !== 0 || targetOffsetY !== 0) ? 0.15 : LERP_SPEED_DOWN;
currentOffsets[i * 2] += (targetOffsetX - currentOffsets[i * 2]) * offsetLerp;
currentOffsets[i * 2 + 1] += (targetOffsetY - currentOffsets[i * 2 + 1]) * offsetLerp;
const s = currentScales[i];
dummy.position.set(bx + currentOffsets[i * 2], by + currentOffsets[i * 2 + 1], 0);
dummy.scale.set(s, s, 1);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
mesh.instanceColor.array[i * 3] = BASE_COLOR.r;
mesh.instanceColor.array[i * 3 + 1] = BASE_COLOR.g;
mesh.instanceColor.array[i * 3 + 2] = BASE_COLOR.b;
}
mesh.instanceMatrix.needsUpdate = true;
mesh.instanceColor.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
let resizeTimer = null;
let lastW = W;
let lastH = H;
const ro = new ResizeObserver(() => {
const newW = component.clientWidth;
const newH = component.clientHeight;
const widthChanged = newW !== lastW;
const heightChanged = Math.abs(newH - lastH) > 100;
if (!widthChanged && !heightChanged) return;
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
lastW = component.clientWidth;
lastH = component.clientHeight;
buildGrid();
}, 150);
});
ro.observe(component);
});
});
</script>









