Files
bakonpancakz 96011d6454 bugfix: minor legibility changes
move elements around and less jitter on the crt effect because it was kinda annoying
2026-06-19 15:05:09 -07:00

392 lines
14 KiB
TypeScript

const elemHeader = document.querySelector<HTMLParagraphElement>('p.header')!
const elemDescription = document.querySelector<HTMLParagraphElement>('p.description')!
const elemRedirect = document.querySelector<HTMLAnchorElement>('a.redirect')!
function createImage(src: string): HTMLImageElement {
const i = document.createElement('img')
i.src = src
return i
}
const slides = new Array<{
image: HTMLImageElement
url?: string | null
tooltip?: string
header: string
description: string
}>({
image: createImage('/index/slide-1.gif'),
header: 'Tech, Chunibyo & Other Delusions',
url: '/secret-picross.html',
tooltip: 'Picross',
description:
'Welcome to my personal homepage, use the buttons at the top of the page to visit other sections of the site.',
})
const CODE_VERTEX = `
attribute vec2 p;
varying vec2 uv;
void main(){
uv = p * 0.5 + 0.5;
gl_Position = vec4(p, 0, 1);
}
`
const CODE_SHADER = `
precision highp float;
varying vec2 uv;
uniform vec2 res;
uniform sampler2D texA;
uniform sampler2D texB;
uniform float transition, degauss, time, barrel, scan, mask, chroma, bloom, vig, noise, flicker, jitter, bright, sat, corner;
float rand(vec2 co){
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
}
vec2 barrelD(vec2 u, float k) {
vec2 c = u - 0.5;
return u + c * dot(c, c) * k;
}
vec3 sampleChroma(sampler2D t, vec2 u) {
float ca = chroma * (1. + dot(u - .5, u - .5) * 2.);
float r = texture2D(t, barrelD(u + vec2(ca, 0), barrel)).r;
float g = texture2D(t, barrelD(u, barrel)).g;
float b = texture2D(t, barrelD(u - vec2(ca, 0), barrel)).b;
return vec3(r, g, b);
}
vec3 phosphorBloom(sampler2D t, vec2 u) {
vec3 glow = vec3(0.0);
float w = 0.0;
// two rings — tight inner glow + wide outer halo
for (int x = -4; x <= 4; x++) {
for (int y = -4; y <= 4; y++) {
vec2 off = vec2(float(x), float(y)) / res * 3.5;
vec3 s = texture2D(t, barrelD(u + off, barrel)).rgb;
// only bright pixels contribute — threshold at 0.6 luma
float lum = dot(s, vec3(0.299, 0.587, 0.114));
float thresh = max(lum - 0.55, 0.0) * 2.5;
s *= thresh;
float wt = exp(-dot(vec2(x, y), vec2(x, y)) * 0.18);
glow += s * wt;
w += wt;
}
}
glow /= w;
// wide halo pass at 3x the offset
vec3 halo = vec3(0.0);
float hw = 0.0;
for (int x = -3; x <= 3; x++) {
for (int y = -3; y <= 3; y++) {
vec2 off = vec2(float(x), float(y)) / res * 10.0;
vec3 s = texture2D(t, barrelD(u + off, barrel)).rgb;
float lum = dot(s, vec3(0.299, 0.587, 0.114));
float thresh = max(lum - 0.7, 0.0) * 4.0;
s *= thresh;
float wt = exp(-dot(vec2(x, y), vec2(x, y)) * 0.25);
halo += s * wt;
hw += wt;
}
}
halo /= hw;
// phosphor tint — warm yellow-green inner, cooler blue outer halo
glow *= vec3(1.1, 1.05, 0.85);
halo *= vec3(0.85, 0.95, 1.15);
return glow * bloom + halo * bloom * 0.4;
}
vec3 shadowMask(vec2 u) {
float px = mod(u.x * res.x, 3.);
vec3 m = vec3(step(px, 1.), step(1., px) * step(px, 2.), step(2., px));
return mix(vec3(1.), m * 1.6 + .15, mask * .5);
}
float scanlineW(vec2 u) {
return mix(1., max(0., sin(u.y * res.y * 3.14159)) * .5 + .5, scan);
}
void main() {
vec2 u = uv;
u.x += sin(u.y * 200. + time * 3.) * (jitter * .5) + sin(u.y * 50. + time * 7.) * jitter;
// degauss
vec2 centered = u - 0.5;
float dist = length(centered);
float wavefront = degauss * 0.9;
float ring = exp(-pow((dist - wavefront) * 12.0, 2.0));
float push = ring * 0.08 * sin(degauss * 3.14159);
u += normalize(centered + 0.0001) * push;
vec2 bd = barrelD(u, barrel);
if (bd.x < 0. || bd.x > 1. || bd.y < 0. || bd.y > 1.) {
gl_FragColor = vec4(0, 0, 0, 1);
return;
}
vec2 cr = vec2(corner * res.y / res.x, corner);
vec2 e = smoothstep(vec2(0.), cr, bd) * smoothstep(vec2(1.), vec2(1.) - cr, bd);
if (e.x * e.y < .01) {
gl_FragColor = vec4(0, 0, 0, 1);
return;
}
// chroma boost at ring
float ca_boost = chroma + ring * 0.03 * sin(degauss * 3.14159);
// old texture
float rA = texture2D(texA, barrelD(u + vec2(ca_boost, 0.), barrel)).r;
float gA = texture2D(texA, barrelD(u, barrel)).g;
float bA = texture2D(texA, barrelD(u - vec2(ca_boost, 0.), barrel)).b;
// new texture
float rB = texture2D(texB, barrelD(u + vec2(ca_boost, 0.), barrel)).r;
float gB = texture2D(texB, barrelD(u, barrel)).g;
float bB = texture2D(texB, barrelD(u - vec2(ca_boost, 0.), barrel)).b;
// wipe: pixels closer to center than wavefront show new texture
float wipe = smoothstep(wavefront - 0.05, wavefront + 0.05, dist);
vec3 col = mix(vec3(rB, gB, bB), vec3(rA, gA, bA), wipe);
// rainbow tint at ring
col += ring * 0.4 * vec3(
sin(degauss * 6.28 + 0.0),
sin(degauss * 6.28 + 2.09),
sin(degauss * 6.28 + 4.18)
) * sin(degauss * 3.14159);
vec3 bl = mix(phosphorBloom(texB, u), phosphorBloom(texA, u), wipe);
col += bl;
float lum = dot(col, vec3(.299, .587, .114));
col = mix(col, bl, bloom * (lum * lum * .5 + .1));
col *= shadowMask(bd); col *= scanlineW(bd);
float v = 1. - dot(bd - .5, bd - .5) * vig * 2.; col *= clamp(v, 0., 1.);
float fl = 1. - flicker * .5 + flicker * sin(time * 73. + bd.y * 100.) * .5; col *= fl;
col += (rand(bd + fract(time * .1)) - .5) * noise;
col *= bright;
float l2 = dot(col, vec3(.299, .587, .114)); col = mix(vec3(l2), col, sat);
col = pow(max(col, vec3(0.)), vec3(1. / 2.2));
col *= e.x * e.y;
gl_FragColor = vec4(clamp(col, 0., 1.), 0);
}
`
const canvas = document.querySelector<HTMLCanvasElement>('canvas')!
const W = canvas.clientWidth
const H = canvas.clientHeight
let degauss = 0,
noise = 0,
slideIndex = -1,
slideCounter = 0
canvas.width = W
canvas.height = H
const gl = canvas.getContext('webgl')!
const program = gl.createProgram()
function createShader(shaderType: number, shaderCode: string) {
const shader = gl.createShader(shaderType)!
gl.shaderSource(shader, shaderCode)
gl.compileShader(shader)
return shader
}
gl.attachShader(program, createShader(gl.VERTEX_SHADER, CODE_VERTEX))
gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, CODE_SHADER))
gl.linkProgram(program)
gl.useProgram(program)
const bf = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, bf)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW)
const ap = gl.getAttribLocation(program, 'p')
gl.enableVertexAttribArray(ap)
gl.vertexAttribPointer(ap, 2, gl.FLOAT, false, 0, 0)
const U = {
transition: gl.getUniformLocation(program, 'transition'),
degauss: /**/ gl.getUniformLocation(program, 'degauss'),
time: /*---*/ gl.getUniformLocation(program, 'time'),
res: /*----*/ gl.getUniformLocation(program, 'res'),
texA: /*---*/ gl.getUniformLocation(program, 'texA'),
texB: /*---*/ gl.getUniformLocation(program, 'texB'),
barrel: /*-*/ gl.getUniformLocation(program, 'barrel'),
scan: /*---*/ gl.getUniformLocation(program, 'scan'),
mask: /*---*/ gl.getUniformLocation(program, 'mask'),
chroma: /*-*/ gl.getUniformLocation(program, 'chroma'),
bloom: /*--*/ gl.getUniformLocation(program, 'bloom'),
vig: /*----*/ gl.getUniformLocation(program, 'vig'),
noise: /*--*/ gl.getUniformLocation(program, 'noise'),
flicker: /**/ gl.getUniformLocation(program, 'flicker'),
jitter: /*-*/ gl.getUniformLocation(program, 'jitter'),
bright: /*-*/ gl.getUniformLocation(program, 'bright'),
sat: /*----*/ gl.getUniformLocation(program, 'sat'),
corner: /*-*/ gl.getUniformLocation(program, 'corner'),
}
const textureA = gl.createTexture()
const textureCanvasA = document.createElement('canvas')
const textureContextA = textureCanvasA.getContext('2d')!
{
textureCanvasA.width = W
textureCanvasA.height = H
gl.bindTexture(gl.TEXTURE_2D, textureA)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
}
const textureB = gl.createTexture()
const textureCanvasB = document.createElement('canvas')
const textureContextB = textureCanvasB.getContext('2d')!
{
textureCanvasB.width = W
textureCanvasB.height = H
gl.bindTexture(gl.TEXTURE_2D, textureB)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
}
// function renderSMPTE() {
// const canvas = textureCanvasA
// const ctx = textureContextA
// canvas.width = W
// canvas.height = H
// const bars = [
// ['#c0c0c0', '#c0c000', '#00c0c0', '#00c000', '#c000c0', '#c00000', '#0000c0'],
// ['#0000c0', '#101010', '#c000c0', '#101010', '#00c0c0', '#101010', '#c0c0c0'],
// ['#001b4b', '#ffffff', '#2e006a', '#0b0b0b', '#0b0b0b', '#0b0b0b'],
// ]
// let bw = W / 7
// bars[0].forEach((c, i) => {
// ctx.fillStyle = c
// ctx.fillRect(i * bw, 0, bw, H * 0.675)
// })
// bars[1].forEach((c, i) => {
// ctx.fillStyle = c
// ctx.fillRect(i * bw, H * 0.675, bw, H * 0.125)
// })
// bw = W / 6
// bars[2].forEach((c, i) => {
// ctx.fillStyle = c
// ctx.fillRect(i * bw, H * 0.75, bw, H * 0.25)
// })
// ctx.fillStyle = '#fff'
// ctx.fillRect(W * 0.625, H * 0.8, W * 0.25, H * 0.12)
// ctx.font = 'bold 24px monospace'
// ctx.fillStyle = '#000'
// ctx.fillText('PLEASE STAND BY', W * 0.65, H * 0.875)
// gl.activeTexture(gl.TEXTURE0)
// gl.bindTexture(gl.TEXTURE_2D, textureA)
// gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
// gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas)
// }
function renderFrame(t: number) {
gl.uniform1f(U.time, t)
gl.uniform2f(U.res, W, H)
gl.uniform1f(U.degauss, degauss)
gl.uniform1i(U.texA, 0)
gl.uniform1i(U.texB, 1)
gl.uniform1f(U.barrel, /*-*/ 0.25)
gl.uniform1f(U.scan, /*---*/ 0.25)
gl.uniform1f(U.mask, /*---*/ 1)
gl.uniform1f(U.chroma, /*-*/ 0.0025)
gl.uniform1f(U.bloom, /*--*/ 0.5)
gl.uniform1f(U.vig, /*----*/ 1)
gl.uniform1f(U.noise, /*--*/ noise)
gl.uniform1f(U.flicker, /**/ 0.0375)
gl.uniform1f(U.jitter, /*-*/ 0.0005)
gl.uniform1f(U.bright, /*-*/ 0.8)
gl.uniform1f(U.sat, /*----*/ 1.0)
gl.uniform1f(U.corner, /*-*/ 0.1)
gl.viewport(0, 0, W, H)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
requestAnimationFrame(renderFrame)
}
async function nextSlide(init = false) {
const transitionTime = 1000
const nextIndex = slideCounter++ % slides.length
if (nextIndex === slideIndex) return
slideIndex = nextIndex
const next = slides[nextIndex]
elemHeader.textContent = next.header
elemDescription.textContent = next.description
if (next.url) {
elemRedirect.href = next.url
elemRedirect.style.opacity = '1'
elemRedirect.style.pointerEvents = 'all'
} else {
elemRedirect.style.opacity = '0'
elemRedirect.style.pointerEvents = 'none'
}
elemRedirect.textContent = `Discover ${next.tooltip || 'More'} >>>`
function tween(from: number, to: number, ms: number, fn: (v: number) => void) {
return new Promise<void>((resolve) => {
const start = performance.now()
function tick() {
const t = Math.min((performance.now() - start) / ms, 1)
const e = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t // easing
fn(from + (to - from) * e)
if (t < 1) requestAnimationFrame(tick)
else resolve()
}
requestAnimationFrame(tick)
})
}
textureContextB.drawImage(next.image!, 0, 0, W, H)
gl.activeTexture(gl.TEXTURE1)
gl.bindTexture(gl.TEXTURE_2D, textureB)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureCanvasB)
if (init) {
tween(0, 0.025, transitionTime, (t) => (noise = t))
setInterval(nextSlide, transitionTime + 8000)
}
await tween(0, 1, transitionTime, (t) => (degauss = t))
textureContextA.drawImage(next.image!, 0, 0, W, H)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, textureA)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureCanvasA)
degauss = 0
}
// Startup Sequence (Blank Screen)
slides[0].image.onload = () => {
nextSlide(true)
requestAnimationFrame(renderFrame)
}
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, textureA)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas)