96011d6454
move elements around and less jitter on the crt effect because it was kinda annoying
392 lines
14 KiB
TypeScript
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)
|