270 lines
9.0 KiB
TypeScript
270 lines
9.0 KiB
TypeScript
|
|
;(() => {
|
||
|
|
let defer: (() => void)[] = []
|
||
|
|
let running = false
|
||
|
|
let paramQuality: 'standard' | 'transparent'
|
||
|
|
let paramID: bigint
|
||
|
|
|
||
|
|
const elemHost = document.querySelector<HTMLAnchorElement>('a.layout-wrapper')
|
||
|
|
// @ts-expect-error
|
||
|
|
const BASE_CDN = window.__ENV__.CDN
|
||
|
|
// @ts-expect-error
|
||
|
|
const BASE_WEB = window.__ENV__.WEB
|
||
|
|
|
||
|
|
if (BASE_CDN === undefined || BASE_WEB === undefined || !elemHost) {
|
||
|
|
console.error('[gifuu] Invalid Document')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const search = new URLSearchParams(window.location.search)
|
||
|
|
const givenQuality = search.get('quality')
|
||
|
|
const givenID = BigInt(search.get('id') ?? '0')
|
||
|
|
|
||
|
|
if (givenID < 1) {
|
||
|
|
return exit('Invalid ID')
|
||
|
|
}
|
||
|
|
if (givenQuality !== 'standard' && givenQuality !== 'transparent') {
|
||
|
|
return exit('Invalid Quality')
|
||
|
|
}
|
||
|
|
|
||
|
|
paramQuality = givenQuality
|
||
|
|
paramID = givenID
|
||
|
|
} catch (error) {
|
||
|
|
exit(error)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
function setupGL() {
|
||
|
|
if (!elemHost) throw 'Missing Anchor Node'
|
||
|
|
|
||
|
|
// Setup Elements
|
||
|
|
const elemCanvas = document.createElement('canvas')
|
||
|
|
elemCanvas.classList.add('render')
|
||
|
|
elemCanvas.addEventListener('webglcontextlost', (ev) => {
|
||
|
|
ev.preventDefault()
|
||
|
|
console.warn('[gifuu] Failed to allocate a WebGL context for us! Using image fallback...')
|
||
|
|
teardown()
|
||
|
|
setupImage()
|
||
|
|
})
|
||
|
|
|
||
|
|
elemHost.appendChild(elemCanvas)
|
||
|
|
defer.push(() => elemCanvas.remove())
|
||
|
|
|
||
|
|
const elemVideo = document.createElement('video')
|
||
|
|
elemVideo.classList.add('decode')
|
||
|
|
elemVideo.crossOrigin = 'anonymous'
|
||
|
|
elemVideo.playsInline = true
|
||
|
|
elemVideo.autoplay = true
|
||
|
|
elemVideo.muted = true
|
||
|
|
elemVideo.loop = true
|
||
|
|
|
||
|
|
elemHost.appendChild(elemVideo)
|
||
|
|
defer.push(() => elemVideo.remove())
|
||
|
|
|
||
|
|
// Setup Context
|
||
|
|
const gl = elemCanvas.getContext('webgl', {
|
||
|
|
powerPreference: 'low-power',
|
||
|
|
premultipliedAlpha: false,
|
||
|
|
antialias: false,
|
||
|
|
alpha: true,
|
||
|
|
depth: false,
|
||
|
|
})
|
||
|
|
if (!gl) {
|
||
|
|
console.warn('[gifuu] WebGL is unsupported, using image fallback...')
|
||
|
|
teardown()
|
||
|
|
setupImage()
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const VERT = `
|
||
|
|
precision mediump float;
|
||
|
|
attribute vec2 aPos;
|
||
|
|
uniform mat3 uMatrix;
|
||
|
|
varying vec2 vUV;
|
||
|
|
void main() {
|
||
|
|
gl_Position = vec4(uMatrix * vec3(aPos, 1.0), 1.0);
|
||
|
|
vUV = aPos;
|
||
|
|
}`
|
||
|
|
|
||
|
|
const FRAG = `
|
||
|
|
precision mediump float;
|
||
|
|
uniform sampler2D uFrame;
|
||
|
|
varying vec2 vUV;
|
||
|
|
void main() {
|
||
|
|
vec2 colorUV = vec2(vUV.x, vUV.y * 0.5);
|
||
|
|
vec2 alphaUV = vec2(vUV.x, 0.5 + vUV.y * 0.5);
|
||
|
|
vec4 color = texture2D(uFrame, colorUV);
|
||
|
|
float alpha = texture2D(uFrame, alphaUV).r;
|
||
|
|
gl_FragColor = vec4(color.rgb, alpha);
|
||
|
|
}`
|
||
|
|
|
||
|
|
function compileShader(type: number, src: string) {
|
||
|
|
if (!gl) throw 'Missing GL Context'
|
||
|
|
|
||
|
|
const s = gl.createShader(type)
|
||
|
|
if (!s) throw 'Shader compilation failed'
|
||
|
|
|
||
|
|
gl.shaderSource(s, src)
|
||
|
|
gl.compileShader(s)
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
const prog = gl.createProgram()
|
||
|
|
gl.attachShader(prog, compileShader(gl.VERTEX_SHADER, VERT))
|
||
|
|
gl.attachShader(prog, compileShader(gl.FRAGMENT_SHADER, FRAG))
|
||
|
|
gl.linkProgram(prog)
|
||
|
|
gl.useProgram(prog)
|
||
|
|
defer.push(() => gl.deleteProgram(prog))
|
||
|
|
|
||
|
|
// --- Quad ---
|
||
|
|
const buf = gl.createBuffer()
|
||
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
|
||
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1]), gl.STATIC_DRAW)
|
||
|
|
defer.push(() => gl.deleteBuffer(buf))
|
||
|
|
|
||
|
|
const aPos = gl.getAttribLocation(prog, 'aPos')
|
||
|
|
gl.enableVertexAttribArray(aPos)
|
||
|
|
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0)
|
||
|
|
gl.uniformMatrix3fv(gl.getUniformLocation(prog, 'uMatrix'), false, [2, 0, 0, 0, -2, 0, -1, 1, 1])
|
||
|
|
|
||
|
|
// --- Texture ---
|
||
|
|
const tex = gl.createTexture()
|
||
|
|
gl.bindTexture(gl.TEXTURE_2D, tex)
|
||
|
|
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)
|
||
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||
|
|
gl.uniform1i(gl.getUniformLocation(prog, 'uFrame'), 0)
|
||
|
|
defer.push(() => gl.deleteTexture(tex))
|
||
|
|
} catch (error) {
|
||
|
|
console.error('[gifuu] Failed to Initialize WebGL dependencies:', error)
|
||
|
|
teardown()
|
||
|
|
setupImage()
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Tick Function
|
||
|
|
let cancel = 0
|
||
|
|
let sized = false
|
||
|
|
function tick() {
|
||
|
|
cancel = requestAnimationFrame(tick)
|
||
|
|
try {
|
||
|
|
if (!gl) throw 'Missing GL Context'
|
||
|
|
if (!sized && elemVideo.videoWidth > 0) {
|
||
|
|
sized = true
|
||
|
|
elemCanvas.width = elemVideo.videoWidth
|
||
|
|
elemCanvas.height = Math.floor(elemVideo.videoHeight / 2)
|
||
|
|
gl.viewport(0, 0, elemCanvas.width, elemCanvas.height)
|
||
|
|
}
|
||
|
|
if (!sized) return
|
||
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, elemVideo)
|
||
|
|
gl.drawArrays(gl.TRIANGLES, 0, 6)
|
||
|
|
} catch (error) {
|
||
|
|
console.warn('[gifuu] Draw failed, using image fallback...', error)
|
||
|
|
teardown()
|
||
|
|
setupImage()
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
defer.push(() => cancelAnimationFrame(cancel))
|
||
|
|
|
||
|
|
// Download Video
|
||
|
|
fetch(`${BASE_CDN}/${paramID}/alpha.webm`, { mode: 'cors', cache: 'force-cache' })
|
||
|
|
.then((r) => r.blob())
|
||
|
|
.then((blob) => {
|
||
|
|
const content = URL.createObjectURL(blob)
|
||
|
|
defer.push(() => URL.revokeObjectURL(content))
|
||
|
|
|
||
|
|
elemVideo.src = content
|
||
|
|
elemVideo.play().catch(() => {})
|
||
|
|
tick()
|
||
|
|
})
|
||
|
|
.catch((error) => {
|
||
|
|
console.warn('[gifuu] Video download failed, using image fallback...', error)
|
||
|
|
teardown()
|
||
|
|
setupImage()
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function setupImage() {
|
||
|
|
if (!elemHost) throw 'Missing Image Node'
|
||
|
|
|
||
|
|
// Create Element
|
||
|
|
const elemImage = document.createElement('img')
|
||
|
|
elemImage.classList.add('render')
|
||
|
|
|
||
|
|
elemHost.appendChild(elemImage)
|
||
|
|
defer.push(() => elemImage.remove())
|
||
|
|
|
||
|
|
// Download Image
|
||
|
|
fetch(`${BASE_CDN}/${paramID}/${paramQuality}.avif`, { mode: 'cors', cache: 'force-cache' })
|
||
|
|
.then((r) => r.blob())
|
||
|
|
.then((blob) => {
|
||
|
|
const content = URL.createObjectURL(blob)
|
||
|
|
defer.push(() => URL.revokeObjectURL(content))
|
||
|
|
|
||
|
|
elemImage.src = content
|
||
|
|
})
|
||
|
|
.catch((error) => {
|
||
|
|
console.warn('[gifuu] Image download failed, quitting...', error)
|
||
|
|
teardown()
|
||
|
|
exit('Media Unavailable')
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function teardown() {
|
||
|
|
let func
|
||
|
|
while ((func = defer.shift())) {
|
||
|
|
try {
|
||
|
|
func()
|
||
|
|
} catch (error) {
|
||
|
|
console.error('[gifuu] Teardown failed:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function exit(error: any) {
|
||
|
|
console.error('[gifuu] Exiting Embed:', error)
|
||
|
|
teardown()
|
||
|
|
|
||
|
|
if (!elemHost) return
|
||
|
|
elemHost.href = BASE_WEB
|
||
|
|
|
||
|
|
const container = document.createElement('div')
|
||
|
|
container.classList.add('error')
|
||
|
|
|
||
|
|
const message = document.createElement('p')
|
||
|
|
message.classList.add('message', 'effect-centered')
|
||
|
|
message.textContent = String(error)
|
||
|
|
container.append(message)
|
||
|
|
|
||
|
|
elemHost.append(container)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Lifecycle
|
||
|
|
elemHost.href = `${BASE_WEB}/art/${paramID}`
|
||
|
|
const observer = new IntersectionObserver(
|
||
|
|
(entries) => {
|
||
|
|
if (entries[0].isIntersecting && !running) {
|
||
|
|
running = true
|
||
|
|
console.log('[gifuu] Showtime! Setting up...')
|
||
|
|
paramQuality === 'transparent' ? setupGL() : setupImage()
|
||
|
|
} else if (!entries[0].isIntersecting && running) {
|
||
|
|
running = false
|
||
|
|
console.log('[gifuu] Out of View! Tearing down...')
|
||
|
|
teardown()
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{ threshold: 0.1 },
|
||
|
|
)
|
||
|
|
observer.observe(elemHost)
|
||
|
|
|
||
|
|
window.addEventListener('pagehide', () => {
|
||
|
|
console.log('[gifuu] Goodbye!')
|
||
|
|
observer.disconnect()
|
||
|
|
elemHost.remove()
|
||
|
|
teardown()
|
||
|
|
})
|
||
|
|
})()
|