LUMA - RETROSCOPIC CRT SHADER SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<title>Luma</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body { margin: 0; overflow: hidden; background-color: black; }
#container {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
z-index: 2;
}
/* 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>
<script type="importmap">
{
"imports": {
"three": "https://esm.sh/three@0.128.0",
"three/addons/": "https://esm.sh/three@0.128.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="scope">
<div class="glow"></div>
<div class="scan"></div>
<div class="vig"></div>
<div class="mask"></div>
</div>
<div id="container"></div>
<script type="module">
import * as THREE from 'three';
let container;
let camera, scene, renderer;
let wireframe, points;
let pointsPositions;
let edgesPositions;
let lineColorAttribute, pointColorAttribute;
init();
function init() {
container = document.getElementById( 'container' );
camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 1, 3500 );
camera.position.z = 917;
scene = new THREE.Scene();
scene.background = new THREE.Color( 0x050505 );
scene.fog = new THREE.Fog( 0x050505, 2000, 3500 );
const triangles = 5000;
const geometry = new THREE.BufferGeometry();
const positions = [];
const normals = [];
const colors = [];
const color = new THREE.Color();
const n = 800, n2 = n / 2;
const d = 12, d2 = d / 2;
const pA = new THREE.Vector3();
const pB = new THREE.Vector3();
const pC = new THREE.Vector3();
const cb = new THREE.Vector3();
const ab = new THREE.Vector3();
for ( let i = 0; i < triangles; i ++ ) {
const x = Math.random() * n - n2;
const y = Math.random() * n - n2;
const z = Math.random() * n - n2;
const ax = x + Math.random() * d - d2;
const ay = y + Math.random() * d - d2;
const az = z + Math.random() * d - d2;
const bx = x + Math.random() * d - d2;
const by = y + Math.random() * d - d2;
const bz = z + Math.random() * d - d2;
const cx = x + Math.random() * d - d2;
const cy = y + Math.random() * d - d2;
const cz = z + Math.random() * d - d2;
positions.push( ax, ay, az );
positions.push( bx, by, bz );
positions.push( cx, cy, cz );
pA.set( ax, ay, az );
pB.set( bx, by, bz );
pC.set( cx, cy, cz );
cb.subVectors( pC, pB );
ab.subVectors( pA, pB );
cb.cross( ab );
cb.normalize();
const nx = cb.x;
const ny = cb.y;
const nz = cb.z;
normals.push( nx * 32767, ny * 32767, nz * 32767 );
normals.push( nx * 32767, ny * 32767, nz * 32767 );
normals.push( nx * 32767, ny * 32767, nz * 32767 );
const vx = ( x / n ) + 0.5;
const vy = ( y / n ) + 0.5;
const vz = ( z / n ) + 0.5;
color.setRGB( vx, vy, vz );
colors.push( color.r * 255, color.g * 255, color.b * 255 );
colors.push( color.r * 255, color.g * 255, color.b * 255 );
colors.push( color.r * 255, color.g * 255, color.b * 255 );
}
const positionAttribute = new THREE.Float32BufferAttribute( positions, 3 );
const normalAttribute = new THREE.Int16BufferAttribute( normals, 3 );
const colorAttribute = new THREE.Uint8BufferAttribute( colors, 3 );
normalAttribute.normalized = true;
colorAttribute.normalized = true;
geometry.setAttribute( 'position', positionAttribute );
geometry.setAttribute( 'normal', normalAttribute );
geometry.setAttribute( 'color', colorAttribute );
geometry.computeBoundingSphere();
pointsPositions = positions;
const edgesGeo = new THREE.EdgesGeometry( geometry );
edgesPositions = edgesGeo.attributes.position.array.slice();
const lineGeo = new THREE.BufferGeometry();
lineGeo.setAttribute( 'position', new THREE.Float32BufferAttribute( edgesPositions, 3 ) );
const lineCount = edgesPositions.length / 3;
const lineColors = [];
for ( let i = 0; i < lineCount; i ++ ) {
const ix = i * 3;
const x = edgesPositions[ ix ];
const y = edgesPositions[ ix + 1 ];
const z = edgesPositions[ ix + 2 ];
let hue = ( ( x / 800 ) * 0.5 + ( y / 800 ) * 0.3 + ( z / 800 ) * 0.2 + 1.0 ) % 1.0;
const rgb = hslToRgb( hue, 0.8, 0.3 );
lineColors.push( rgb[0], rgb[1], rgb[2] );
}
lineColorAttribute = new THREE.Float32BufferAttribute( lineColors, 3 );
lineGeo.setAttribute( 'color', lineColorAttribute );
const lineMat = new THREE.LineBasicMaterial( {
vertexColors: true,
linewidth: 1,
depthTest: false
} );
wireframe = new THREE.LineSegments( lineGeo, lineMat );
scene.add( wireframe );
const pointsGeo = new THREE.BufferGeometry();
pointsGeo.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
const pointCount = positions.length / 3;
const pointColors = [];
for ( let i = 0; i < pointCount; i ++ ) {
pointColors.push( 0.8, 0.5, 0.1 );
}
pointColorAttribute = new THREE.Float32BufferAttribute( pointColors, 3 );
pointsGeo.setAttribute( 'color', pointColorAttribute );
const pointsMat = new THREE.PointsMaterial( {
size: 6.0,
vertexColors: true,
sizeAttenuation: true,
transparent: true,
opacity: 0.95
} );
points = new THREE.Points( pointsGeo, pointsMat );
scene.add( points );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
container.appendChild( renderer.domElement );
window.addEventListener( 'resize', onWindowResize );
}
function hslToRgb( h, s, l ) {
let r, g, b;
if ( s === 0 ) {
r = g = b = l;
} else {
const hue2rgb = ( p, q, t ) => {
if ( t < 0 ) t += 1;
if ( t > 1 ) t -= 1;
if ( t < 1/6 ) return p + ( q - p ) * 6 * t;
if ( t < 1/2 ) return q;
if ( t < 2/3 ) return p + ( q - p ) * ( 2/3 - t ) * 6;
return p;
};
const q = l < 0.5 ? l * ( 1 + s ) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb( p, q, h + 1/3 );
g = hue2rgb( p, q, h );
b = hue2rgb( p, q, h - 1/3 );
}
return [ r, g, b ];
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
const time = Date.now() * 0.001;
wireframe.rotation.x = time * 0.25;
wireframe.rotation.y = time * 0.5;
points.rotation.x = time * 0.25;
points.rotation.y = time * 0.5;
const lineColors = wireframe.geometry.attributes.color.array;
const lPos = edgesPositions;
const lCount = lPos.length / 3;
const lineSpeed = 3.5;
const lineFreq = 0.008;
for ( let i = 0; i < lCount; i ++ ) {
const ix = i * 3;
const x = lPos[ ix ];
const y = lPos[ ix + 1 ];
const z = lPos[ ix + 2 ];
const wave1 = Math.sin( time * lineSpeed + x * lineFreq + y * lineFreq * 0.7 + z * lineFreq * 1.1 );
const wave2 = Math.cos( time * 2.2 + y * lineFreq * 1.3 + z * lineFreq * 0.9 );
const wave3 = Math.sin( time * 1.4 + x * lineFreq * 1.2 + z * lineFreq * 0.8 );
let t = ( wave1 * 0.6 + wave2 * 0.3 + wave3 * 0.2 + 1 ) / 2;
t = Math.pow( t, 1.2 ) * 1.5;
const r0 = lineColors[ ix ];
const g0 = lineColors[ ix + 1 ];
const b0 = lineColors[ ix + 2 ];
const mix = Math.min( t, 1.0 );
const r = r0 * (1 - mix) + 1.0 * mix;
const g = g0 * (1 - mix) + 1.0 * mix;
const b = b0 * (1 - mix) + 1.0 * mix;
lineColors[ ix ] = r;
lineColors[ ix + 1 ] = g;
lineColors[ ix + 2 ] = b;
}
wireframe.geometry.attributes.color.needsUpdate = true;
const pointColors = points.geometry.attributes.color.array;
const pPos = pointsPositions;
const pCount = pPos.length / 3;
const pointSpeed = 2.8;
const pointFreq = 0.006;
for ( let i = 0; i < pCount; i ++ ) {
const ix = i * 3;
const x = pPos[ ix ];
const y = pPos[ ix + 1 ];
const z = pPos[ ix + 2 ];
const wave1 = Math.sin( time * pointSpeed + x * pointFreq + y * pointFreq * 0.8 + z * pointFreq * 1.2 );
const wave2 = Math.cos( time * 1.8 + y * pointFreq * 1.5 + z * pointFreq * 0.7 );
const wave3 = Math.sin( time * 1.0 + x * pointFreq * 0.9 + z * pointFreq * 1.1 );
let t = ( wave1 * 0.5 + wave2 * 0.3 + wave3 * 0.2 + 1 ) / 2;
t = Math.pow( t, 1.8 ) * 1.5;
const r0 = 0.8, g0 = 0.5, b0 = 0.1;
const mix = Math.min( t, 1.0 );
const r = r0 * (1 - mix) + 1.0 * mix;
const g = g0 * (1 - mix) + 1.0 * mix;
const b = b0 * (1 - mix) + 0.8 * mix;
pointColors[ ix ] = r;
pointColors[ ix + 1 ] = g;
pointColors[ ix + 2 ] = b;
}
points.geometry.attributes.color.needsUpdate = true;
renderer.render( scene, camera );
}
</script>
</body>
</html>