SILKY - RETROSCOPIC CRT SHADER SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Being Silky</title>
<style>
*, *::before, *::after {
margin: 0; padding: 0;
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
html, body {
width: 100%; height: 100%;
overflow: hidden;
background: #000;
touch-action: none;
}
#stage {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 2;
}
#grid {
position: relative;
transform: scale(1.5);
}
.p {
position: absolute;
border-radius: 50%;
will-change: transform, opacity;
mix-blend-mode: plus-lighter;
}
@keyframes pulse-glow {
from { filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) contrast(1.2) brightness(1) drop-shadow(0 0 3px rgba(120,180,255,.3)); }
to { filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) contrast(1.2) brightness(1.15) drop-shadow(0 0 10px rgba(160,210,255,.7)); }
}
#stage { animation: pulse-glow 4s infinite alternate ease-in-out; }
/* Retroscopic Overlay */
#scope {
position: fixed;
inset: 0;
z-index: 4;
pointer-events: none;
display: block;
}
#scope>div {
position: absolute;
inset: 0;
}
#scope .glow {
background: radial-gradient(circle at 50% 46%, rgba(30, 255, 130, .08), rgba(0, 0, 0, 0) 62%);
animation: nvflick 3.6s steps(24) infinite;
}
#scope .scan {
background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 25, 8, .45) 2px 3px);
animation: nvscan 7s linear infinite;
}
#scope .vig {
box-shadow: inset 0 0 200px 50px rgba(0, 0, 0, .8);
}
#scope .mask {
background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 54%, rgba(0, 0, 0, .5) 72%, rgba(0, 0, 0, .94) 100%);
}
@keyframes nvscan {
from { background-position: 0 0; }
to { background-position: 0 240px; }
}
@keyframes nvflick {
0%, 100% { opacity: .55; }
40% { opacity: .9; }
62% { opacity: .4; }
80% { opacity: .75; }
}
</style>
</head>
<body>
<div id="scope">
<div class="glow"></div>
<div class="scan"></div>
<div class="vig"></div>
<div class="mask"></div>
</div>
<div id="stage">
<div id="grid"></div>
</div>
<script>
(function () {
'use strict';
var ROWS = 13, COLS = 13;
var CELL = 14, GAP = 8, STEP = CELL + GAP;
var GW = COLS * STEP, GH = ROWS * STEP;
var COUNT = ROWS * COLS;
var vw = window.innerWidth * 0.5;
var vh = window.innerHeight * 0.5;
var grid = document.getElementById('grid');
grid.style.width = GW + 'px';
grid.style.height = GH + 'px';
var particles = [];
var baseScale = [], baseOpacity = [];
var cR = (ROWS - 1) / 2, cC = (COLS - 1) / 2;
var maxD = Math.sqrt(cR * cR + cC * cC);
for (var i = 0; i < COUNT; i++) {
var row = Math.floor(i / COLS);
var col = i % COLS;
var dr = row - cR, dc = col - cC;
var d = Math.sqrt(dr * dr + dc * dc) / maxD;
var sc = 2 + (1 - d) * 3;
var op = 0.12 + (1 - d) * 0.88;
var lum = Math.round(18 + (1 - d) * 77);
var gr = (0.8 + (1 - d) * 7.2).toFixed(1);
baseScale[i] = sc;
baseOpacity[i] = op;
var el = document.createElement('div');
el.className = 'p';
el.style.width = CELL + 'px';
el.style.height = CELL + 'px';
el.style.left = (col * STEP) + 'px';
el.style.top = (row * STEP) + 'px';
el.style.background = 'hsl(210,85%,' + lum + '%)';
el.style.boxShadow = '0 0 ' + gr + 'px rgba(100,170,255,.85)';
el.style.zIndex = Math.round(COUNT * (1 - d));
el.style.opacity = op;
el.style.transform = 'translate(-50%,-50%) translate(0px,0px) scale(' + sc + ')';
/* offset left/top by half cell so transform-origin works from true centre */
el.style.left = (col * STEP + CELL * 0.5) + 'px';
el.style.top = (row * STEP + CELL * 0.5) + 'px';
grid.appendChild(el);
particles.push({ el: el, d: d, tx: 0, ty: 0, cx: 0, cy: 0 });
}
var curX = 0, curY = 0;
function easeInOutExpo(t) {
if (t === 0) return 0;
if (t === 1) return 1;
t *= 2;
return t < 1
? 0.5 * Math.pow(2, 10 * (t - 1))
: 0.5 * (2 - Math.pow(2, -10 * (t - 1)));
}
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
function lerp(a, b, t) { return a + (b - a) * t; }
var FOLLOW_MIN = 0.04; /* centre particles — fastest */
var FOLLOW_MAX = 0.012; /* edge particles — slowest */
function followSpeed(d) {
return FOLLOW_MIN + (FOLLOW_MAX - FOLLOW_MIN) * d;
}
function pulse() {
particles.forEach(function (p, i) {
var delay = Math.round(p.d * 400);
var sc = baseScale[i];
var op = baseOpacity[i];
var el = p.el;
setTimeout(function () {
/* burst outward */
el.style.transition = 'opacity 0.15s ease-out, filter 0.15s ease-out';
el.style.opacity = 1;
p.pulseScale = sc * 2.5;
setTimeout(function () {
/* settle back */
el.style.transition = 'opacity 0.6s ease-in-out, filter 0.6s ease-in-out';
el.style.opacity = op;
p.pulseScale = null;
}, 160);
}, delay);
});
}
var lastPulseCycle = -1;
var autoStart = performance.now();
var autoActive = true;
function autoX(e) {
var dur = 6000;
var t = (e % dur) / dur;
var dir = (Math.floor(e / dur) % 2 === 0) ? 1 : -1;
var pos = (dir === 1) ? easeInOutExpo(t) : 1 - easeInOutExpo(t);
return ((pos * 2 - 1) * vw * 0.4 + Math.sin(e * 0.0007) * vw * 0.2) / 1.5;
}
function autoY(e) {
var dur = 2000;
var t = (e % dur) / dur;
var dir = (Math.floor(e / dur) % 2 === 0) ? 1 : -1;
var pos = (dir === 1) ? easeInOutQuad(t) : 1 - easeInOutQuad(t);
return ((pos * 2 - 1) * vh * 0.4 + Math.cos(e * 0.00012) * vh * 0.2) / 1.5;
}
function tick(now) {
if (autoActive) {
var elapsed = now - autoStart;
curX = autoX(elapsed);
curY = autoY(elapsed);
var cycle = Math.floor(elapsed / 6000);
if (cycle > lastPulseCycle) { lastPulseCycle = cycle; pulse(); }
}
for (var i = 0; i < COUNT; i++) {
var p = particles[i];
var sp = followSpeed(p.d);
p.cx = lerp(p.cx, curX, sp);
p.cy = lerp(p.cy, curY, sp);
var sc = (p.pulseScale !== undefined && p.pulseScale !== null)
? p.pulseScale : baseScale[i];
p.el.style.transform =
'translate(-50%,-50%) translate(' + p.cx.toFixed(2) + 'px,' +
p.cy.toFixed(2) + 'px) scale(' + sc + ')';
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
setTimeout(pulse, 200);
var idleTimer = null;
function onMove(e) {
var ev = (e.touches && e.touches.length) ? e.touches[0] : e;
if (!ev) return;
autoActive = false;
clearTimeout(idleTimer);
curX = (ev.pageX - vw) / 1.5;
curY = (ev.pageY - vh) / 1.5;
idleTimer = setTimeout(function () {
autoActive = true;
autoStart = performance.now() - 200; /* slight offset to avoid jump */
lastPulseCycle = -1;
}, 1500);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('touchmove', onMove, { passive: true });
document.addEventListener('touchstart', onMove, { passive: true });
window.addEventListener('resize', function () {
vw = window.innerWidth * 0.5;
vh = window.innerHeight * 0.5;
});
}());
</script>
</body>
</html>