diff --git a/assets/app.js b/assets/app.js index bdbab02..994c0ee 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,4 +1,4 @@ -/* Progress line, reveals, diagram draw. No dependencies. */ +/* Progress line, reveals, diagram draw, space backdrop. No dependencies. */ (function () { var root = document.documentElement; root.classList.add("anims"); @@ -43,6 +43,121 @@ window.requestAnimationFrame(paint); } + function makeSpace() { + var space = document.createElement("div"); + space.className = "space"; + var w = window.innerWidth; + var h = window.innerHeight; + + var starCount = w < 700 ? 80 : 150; + var stars = []; + for (var i = 0; i < starCount; i++) { + var s = document.createElement("div"); + s.className = "space__star"; + var size = 0.8 + Math.random() * 1.6; + var base = 0.2 + Math.random() * 0.55; + s.style.width = size + "px"; + s.style.height = size + "px"; + s.style.boxShadow = "0 0 " + (size * 2) + "px rgba(255,255,255," + (base * 0.5) + ")"; + space.appendChild(s); + stars.push({ + el: s, + x: Math.random() * w, + y: Math.random() * h, + vx: (Math.random() - 0.5) * 1.4, + vy: (Math.random() - 0.5) * 1.4, + base: base, + phase: Math.random() * Math.PI * 2, + twinkle: 0.3 + Math.random() * 0.5 + }); + } + + var nebulae = [ + { x: 0.18, y: 0.30, rx: 38, ry: 22, angle: -20, c1: "120,150,255", c2: "80,200,200", strength: 0.5 }, + { x: 0.78, y: 0.22, rx: 30, ry: 44, angle: 160, c1: "180,120,255", c2: "240,150,120", strength: 0.4 }, + { x: 0.55, y: 0.78, rx: 46, ry: 26, angle: 70, c1: "150,210,255", c2: "120,240,180", strength: 0.45 } + ]; + + nebulae.forEach(function (n, idx) { + var el = document.createElement("div"); + el.className = "space__nebula"; + el.style.width = (n.rx * 2.6) + "%"; + el.style.height = (n.ry * 2.6) + "%"; + el.style.left = (n.x * 100 - n.rx * 1.3) + "%"; + el.style.top = (n.y * 100 - n.ry * 1.3) + "%"; + el.style.background = + "radial-gradient(ellipse at center, rgba(" + n.c1 + "," + (0.22 * n.strength) + + "), rgba(" + n.c2 + "," + (0.16 * n.strength) + "), transparent 70%)"; + n.el = el; + n.seed = idx * 19.1; + n.amp = 1.6; + space.appendChild(el); + }); + + document.body.appendChild(space); + + function place(star) { + star.el.style.transform = + "translate3d(" + star.x.toFixed(1) + "px," + star.y.toFixed(1) + "px,0)"; + } + + function placeNebula(n, t) { + var dx = Math.sin(t * 0.25 + n.seed) * n.amp; + var dy = Math.cos(t * 0.20 + n.seed * 0.7) * n.amp; + n.el.style.transform = + "translate3d(" + dx.toFixed(2) + "vw," + dy.toFixed(2) + "vh,0) rotate(" + n.angle + "deg)"; + n.el.style.opacity = 0.09 + 0.03 * Math.sin(t * 0.4 + n.seed); + } + + if (reduce) { + for (var k = 0; k < stars.length; k++) { + place(stars[k]); + stars[k].el.style.opacity = stars[k].base; + } + nebulae.forEach(function (n) { placeNebula(n, 0); }); + window.addEventListener("resize", function () { + w = window.innerWidth; + h = window.innerHeight; + for (var k = 0; k < stars.length; k++) { + if (stars[k].x > w) stars[k].x = Math.random() * w; + if (stars[k].y > h) stars[k].y = Math.random() * h; + place(stars[k]); + } + }, { passive: true }); + return; + } + + var last = performance.now(); + function frame(now) { + var dt = Math.min((now - last) / 1000, 0.1); + last = now; + var t = now * 0.00005; + + for (var k = 0; k < stars.length; k++) { + var st = stars[k]; + st.x += st.vx * dt; + st.y += st.vy * dt; + if (st.x < -4) st.x += w + 8; + else if (st.x > w + 4) st.x -= w + 8; + if (st.y < -4) st.y += h + 8; + else if (st.y > h + 4) st.y -= h + 8; + place(st); + st.el.style.opacity = + st.base * (0.65 + 0.35 * Math.sin(t * st.twinkle * 12 + st.phase)); + } + + for (var j = 0; j < nebulae.length; j++) placeNebula(nebulae[j], t); + + requestAnimationFrame(frame); + } + requestAnimationFrame(frame); + + window.addEventListener("resize", function () { + w = window.innerWidth; + h = window.innerHeight; + }, { passive: true }); + } + try { [].forEach.call(groups, function (group) { [].forEach.call(group.children, function (item, i) { @@ -57,6 +172,7 @@ window.addEventListener("resize", queue, { passive: true }); paint(); } + makeSpace(); } catch (err) { root.classList.remove("anims"); } diff --git a/assets/style.css b/assets/style.css index 054bf42..96f29b3 100644 --- a/assets/style.css +++ b/assets/style.css @@ -68,6 +68,38 @@ body { overflow-x: hidden; } +/* ---------- space backdrop ---------- */ + +.space { + position: fixed; + inset: -5% -5% -5% -5%; + z-index: -1; + overflow: hidden; + pointer-events: none; + will-change: transform; +} + +.space__star { + position: absolute; + left: 0; + top: 0; + border-radius: 50%; + background: #ffffff; +} + +.space__nebula { + position: absolute; + border-radius: 50%; + filter: blur(80px); + will-change: transform, opacity; +} + +@media (prefers-reduced-motion: reduce) { + .space { + display: none; + } +} + img, svg { max-width: 100%;