Skip to content

Instantly share code, notes, and snippets.

@jerlendds
Last active January 4, 2026 17:05
Show Gist options
  • Select an option

  • Save jerlendds/f31d0ae9973b7f5684bdf66d5993eedb to your computer and use it in GitHub Desktop.

Select an option

Save jerlendds/f31d0ae9973b7f5684bdf66d5993eedb to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pure CSS Dotted Aurora</title>
<style>
:root {
--bg: #05060a;
--dot: rgba(255, 255, 255, 0.11);
--dot-size: 1.5px;
--dot-gap: 16px;
/* blob colors */
--c1: rgba(0, 140, 255, 0.35);
--c2: rgba(170, 0, 255, 0.28);
--c3: rgba(255, 60, 160, 0.22);
--c4: rgba(0, 255, 200, 0.18);
/* motion tuning */
--dur: 38s;
}
html,
body {
height: 100%;
}
body {
margin: 0;
background: var(--bg);
color: white;
overflow: hidden;
}
.stage {
position: relative;
height: 100%;
width: 100%;
display: grid;
place-items: center;
isolation: isolate;
}
.bg {
position: absolute;
inset: 0;
z-index: 0;
background: var(--bg);
}
.dotlayer {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
/* neutral dot color ink */
background: var(--dot);
/* fixed dot mask (grid never moves) */
-webkit-mask-image: radial-gradient(
circle at center,
#fff var(--dot-size),
transparent calc(var(--dot-size) + 0.1px)
);
mask-image: radial-gradient(
circle at center,
#fff var(--dot-size),
transparent calc(var(--dot-size) + 0.1px)
);
-webkit-mask-size: var(--dot-gap) var(--dot-gap);
mask-size: var(--dot-gap) var(--dot-gap);
-webkit-mask-position: 0 0;
mask-position: 0 0;
-webkit-mask-repeat: repeat;
mask-repeat: repeat;
/* shapes tint/brighten the dot ink */
mix-blend-mode: screen;
isolation: isolate;
}
/* ===== Moving solid-color shapes (they live INSIDE .dotlayer) ===== */
.shapes {
position: absolute;
inset: 0;
pointer-events: none;
}
.shape {
position: absolute;
left: 0;
top: 0;
opacity: 1;
will-change: transform;
transform-origin: 50% 50%;
/* SOLID decal color (no gradients)
NOTE: some shapes (e.g. .hollow) override background/border. */
background: var(--fill);
box-shadow: none;
/* JS drives motion via transform */
transform: translate3d(0, 0, 0) rotate(0deg);
}
/* Individual decals (SOLID fills). Motion is driven by JS (bouncing). */
.hex {
width: 300px;
height: 300px;
clip-path: polygon(25% 6%, 75% 6%, 97% 50%, 75% 94%, 25% 94%, 3% 50%);
--fill: rgba(0, 140, 255, 0.38);
}
.tri {
width: 320px;
height: 320px;
clip-path: polygon(50% 4%, 96% 92%, 4% 92%);
--fill: rgba(255, 60, 160, 0.25);
}
.sq {
width: 240px;
height: 240px;
border-radius: 12px;
--fill: rgba(0, 255, 200, 0.3);
}
.circ {
width: 360px;
height: 360px;
border-radius: 9999px;
--fill: rgba(255, 255, 255, 0.2);
opacity: 0.55;
}
/* 6-protrusion asterisk decal (LAYERED approach)*/
.asterisk {
width: 300px;
height: 300px;
--fill: rgba(255, 160, 60, 0.315);
background: transparent; /* bars carry fill */
opacity: 0.85;
}
.asterisk .bar {
position: absolute;
left: 50%;
top: 50%;
width: 86%;
height: 20%;
background: var(--fill);
border-radius: 6px;
transform: translate(-50%, -50%) rotate(var(--a));
}
/* rectangular protrusions (caps) */
.asterisk .bar::before,
.asterisk .bar::after {
content: "";
position: absolute;
top: 50%;
width: 16%;
height: 140%;
background: inherit;
transform: translateY(-50%);
border-radius: 4px;
}
.asterisk .bar::before {
left: -6%;
}
.asterisk .bar::after {
right: -6%;
}
/* angles (3 bars => 6 ends) */
.asterisk .b0 {
--a: 0deg;
}
.asterisk .b1 {
--a: 60deg;
}
.asterisk .b2 {
--a: 120deg;
}
/* Hollow ring: border only (extra thick) */
.hollow {
width: 360px;
height: 360px;
border-radius: 9999px;
background: transparent;
/* extra-thick ring */
border: 54px solid rgba(80, 180, 255, 0.315);
opacity: 0.75;
}
/* Small inner circle (matches hollow cutout scale) */
.dotcirc {
width: 260px;
height: 260px;
border-radius: 9999px;
--fill: rgba(0, 183, 255, 0.315);
opacity: 0.65;
}
/* Simple rectangle */
.rect {
width: 420px;
height: 200px;
border-radius: 14px;
--fill: rgba(120, 90, 255, 0.315);
opacity: 0.7;
}
@media (prefers-reduced-motion: reduce) {
.shape {
animation: none;
}
}
</style>
</head>
<body>
<main class="stage">
<div class="bg" aria-hidden="true"></div>
<div class="dotlayer" aria-hidden="true">
<div class="shapes" aria-hidden="true">
<div class="shape hex"></div>
<div class="shape tri"></div>
<div class="shape sq"></div>
<div class="shape circ"></div>
<div class="shape asterisk" aria-hidden="true">
<span class="bar b0"></span>
<span class="bar b1"></span>
<span class="bar b2"></span>
</div>
<div class="shape hollow"></div>
<div class="shape dotcirc"></div>
<div class="shape rect"></div>
</div>
</div>
</main>
<script>
// Bouncing decals with overshoot.
const OVERSHOOT = 50; // px beyond edge before bounce
const MIN_SPEED = 150; // px/s
const MAX_SPEED = 170; // px/s
const ANGLE_JITTER = 0.15; // radians; how much to change angle on bounce
const stage = document.querySelector(".stage");
const shapes = Array.from(document.querySelectorAll(".shape"));
function rand(min, max) {
return min + Math.random() * (max - min);
}
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
function makeState(el, i) {
const r = el.getBoundingClientRect();
const speed = rand(MIN_SPEED, MAX_SPEED);
const angle = rand(0, Math.PI * 2);
// start roughly distributed
const W = stage.clientWidth;
const H = stage.clientHeight;
const x = rand(0, Math.max(1, W - r.width));
const y = rand(0, Math.max(1, H - r.height));
return {
el,
w: r.width,
h: r.height,
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
speed,
rot: rand(0, Math.PI * 2),
// default slow rotation; some shapes override below
rotSpeed: rand(-0.22, 0.22),
};
}
let states = shapes.map(makeState);
// Per-shape rotation tuning
// Asterisk: much slower than everything else
for (const st of states) {
if (st.el.classList.contains("asterisk")) {
st.rotSpeed = rand(-0.035, 0.035);
}
}
let last = performance.now();
function renorm(state) {
const s = Math.hypot(state.vx, state.vy) || 1;
const target = state.speed;
state.vx = (state.vx / s) * target;
state.vy = (state.vy / s) * target;
}
function jitter(state) {
// rotate velocity vector by a small random angle
const a = rand(-ANGLE_JITTER, ANGLE_JITTER);
const ca = Math.cos(a),
sa = Math.sin(a);
const vx = state.vx * ca - state.vy * sa;
const vy = state.vx * sa + state.vy * ca;
state.vx = vx;
state.vy = vy;
renorm(state);
// also nudge rotation speed on bounce (subtle)
// keep asterisk very slow even after bounce
if (state.el.classList.contains("asterisk")) {
state.rotSpeed = clamp(
state.rotSpeed + rand(-0.008, 0.008),
-0.05,
0.05
);
} else {
state.rotSpeed = clamp(
state.rotSpeed + rand(-0.06, 0.06),
-0.35,
0.35
);
}
}
function step(now) {
const dt = clamp((now - last) / 1000, 0, 0.05);
last = now;
const W = stage.clientWidth;
const H = stage.clientHeight;
for (const s of states) {
s.x += s.vx * dt;
s.y += s.vy * dt;
s.rot += s.rotSpeed * dt;
// extended bounds so it travels ~50px past the edge
const minX = -OVERSHOOT;
const maxX = W - s.w + OVERSHOOT;
const minY = -OVERSHOOT;
const maxY = H - s.h + OVERSHOOT;
let bounced = false;
if (s.x < minX) {
s.x = minX;
s.vx = Math.abs(s.vx);
bounced = true;
} else if (s.x > maxX) {
s.x = maxX;
s.vx = -Math.abs(s.vx);
bounced = true;
}
if (s.y < minY) {
s.y = minY;
s.vy = Math.abs(s.vy);
bounced = true;
} else if (s.y > maxY) {
s.y = maxY;
s.vy = -Math.abs(s.vy);
bounced = true;
}
if (bounced) jitter(s);
const deg = (s.rot * 180) / Math.PI;
s.el.style.transform = `translate3d(${s.x}px, ${s.y}px, 0) rotate(${deg}deg)`;
}
requestAnimationFrame(step);
}
// Keep sizes in sync on resize (important for correct bounds)
const ro = new ResizeObserver(() => {
states = states.map((st) => {
const r = st.el.getBoundingClientRect();
st.w = r.width;
st.h = r.height;
return st;
});
});
ro.observe(document.body);
requestAnimationFrame(step);
</script>
</body>
</html>
@jerlendds
Copy link
Author

animated-dot-shape-mask.html.webm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment