forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { Mesh, Program, Renderer, Triangle } from 'ogl'
|
||||
import { hexToRgbTuple, shadeHex } from '@/lib/color'
|
||||
|
||||
export interface CardShaderMouse {
|
||||
/** 0..1, links → rechts */
|
||||
x: number
|
||||
/** 0..1, unten → oben (GL-Konvention) */
|
||||
y: number
|
||||
/** 0..1 weich eingedämpft — 1 solange der Zeiger über der Karte ist */
|
||||
active: number
|
||||
}
|
||||
|
||||
interface CardShaderOptions {
|
||||
vertex: string
|
||||
fragment: string
|
||||
uniforms: Record<string, { value: unknown }>
|
||||
/** Wird pro Frame vor dem Rendern aufgerufen — Uniforms hier aktualisieren */
|
||||
frame?: (ctx: { t: number, program: Program, mouse: CardShaderMouse }) => void
|
||||
/** Zeiger-Tracking aktivieren (mousemove am window, relativ zur Karte) */
|
||||
mouse?: boolean
|
||||
/** Renderbuffer relativ zur CSS-Größe (0.1–1) — Perf-Hebel für teure Shader */
|
||||
renderScale?: number
|
||||
/** Getter für die Kartenfarbe — bei Änderung werden die colorUniforms aktualisiert */
|
||||
color?: () => string
|
||||
/** uniform-Name → Shade-Offset (0 = Basisfarbe, <0 dunkler, >0 heller) */
|
||||
colorUniforms?: Record<string, number>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemeinsames Setup für Vue-Bits-Shader-Karteneffekte:
|
||||
* WebGL-Renderer → 2D-Canvas-Blit (Waterfox zeichnet WebGL-Layer nach Scrollen
|
||||
* teils versetzt und ignoriert CSS-Clips), ResizeObserver, Pause wenn die Karte
|
||||
* nicht sichtbar ist, plus uCardRes/uRadius-Uniforms für die Rounded-Rect-Maske.
|
||||
* Erwartet premultiplied Output: `fragColor = vec4(col * a, a)` — die Maske
|
||||
* wird bereits in den Shadern auf a angewendet.
|
||||
*/
|
||||
export function useCardShader(container: Ref<HTMLElement | null>, options: CardShaderOptions) {
|
||||
let teardown: (() => void) | null = null
|
||||
let programRef: Program | null = null
|
||||
|
||||
// Farbwechsel live in die Uniforms übernehmen — sonst zeigt die
|
||||
// Settings-Vorschau die neue Farbe erst nach Remount
|
||||
if (options.color && options.colorUniforms) {
|
||||
watch(options.color, (c) => {
|
||||
if (!programRef) return
|
||||
for (const [name, shade] of Object.entries(options.colorUniforms!)) {
|
||||
const u = programRef.uniforms[name]
|
||||
if (u) u.value = new Float32Array(hexToRgbTuple(shadeHex(c, shade)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const ctn = container.value
|
||||
if (!ctn) return
|
||||
|
||||
const renderer = new Renderer({
|
||||
webgl: 2,
|
||||
alpha: true,
|
||||
premultipliedAlpha: true,
|
||||
antialias: false,
|
||||
dpr: Math.min(window.devicePixelRatio || 1, 2),
|
||||
})
|
||||
const gl = renderer.gl
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
gl.enable(gl.BLEND)
|
||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
|
||||
|
||||
const outCanvas = document.createElement('canvas')
|
||||
outCanvas.className = 'absolute inset-0 size-full rounded-[inherit]'
|
||||
const outCtx = outCanvas.getContext('2d')
|
||||
ctn.appendChild(outCanvas)
|
||||
|
||||
const geometry = new Triangle(gl)
|
||||
// OGL legt ein uv-Attribut an — entfernen, wenn der Vertex-Shader es nicht nutzt
|
||||
if (!options.vertex.includes('uv') && geometry.attributes.uv) {
|
||||
delete geometry.attributes.uv
|
||||
}
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex: options.vertex,
|
||||
fragment: options.fragment,
|
||||
uniforms: {
|
||||
uCardRes: { value: new Float32Array([1, 1]) },
|
||||
uRadius: { value: 0 },
|
||||
...options.uniforms,
|
||||
},
|
||||
})
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program })
|
||||
programRef = program
|
||||
|
||||
const mouse: CardShaderMouse = { x: 0.5, y: 0.5, active: 0 }
|
||||
const mouseTarget = { x: 0.5, y: 0.5, active: 0 }
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const rect = ctn.getBoundingClientRect()
|
||||
if (!rect.width || !rect.height) return
|
||||
const x = (e.clientX - rect.left) / rect.width
|
||||
const y = 1 - (e.clientY - rect.top) / rect.height
|
||||
const inside = x >= 0 && x <= 1 && y >= 0 && y <= 1
|
||||
mouseTarget.x = Math.min(1, Math.max(0, x))
|
||||
mouseTarget.y = Math.min(1, Math.max(0, y))
|
||||
mouseTarget.active = inside ? 1 : 0
|
||||
}
|
||||
if (options.mouse) {
|
||||
window.addEventListener('pointermove', onPointerMove, { passive: true })
|
||||
}
|
||||
|
||||
const scale = Math.min(1, Math.max(0.1, options.renderScale ?? 1))
|
||||
|
||||
const setSize = () => {
|
||||
const rect = ctn.getBoundingClientRect()
|
||||
renderer.setSize(
|
||||
Math.max(1, Math.floor(rect.width * scale)),
|
||||
Math.max(1, Math.floor(rect.height * scale)),
|
||||
)
|
||||
const res = program.uniforms.uCardRes!.value as Float32Array
|
||||
res[0] = gl.drawingBufferWidth
|
||||
res[1] = gl.drawingBufferHeight
|
||||
const cssRadius = Number.parseFloat(getComputedStyle(ctn).borderTopLeftRadius) || 0
|
||||
program.uniforms.uRadius!.value = rect.width > 0
|
||||
? cssRadius * (gl.drawingBufferWidth / rect.width)
|
||||
: 0
|
||||
outCanvas.width = gl.drawingBufferWidth
|
||||
outCanvas.height = gl.drawingBufferHeight
|
||||
}
|
||||
|
||||
const ro = new ResizeObserver(setSize)
|
||||
ro.observe(ctn)
|
||||
setSize()
|
||||
|
||||
let raf = 0
|
||||
let visible = true
|
||||
let pageVisible = document.visibilityState !== 'hidden'
|
||||
|
||||
const loop = (t: number) => {
|
||||
if (!visible || !pageVisible) {
|
||||
raf = 0
|
||||
return
|
||||
}
|
||||
raf = requestAnimationFrame(loop)
|
||||
mouse.x += 0.05 * (mouseTarget.x - mouse.x)
|
||||
mouse.y += 0.05 * (mouseTarget.y - mouse.y)
|
||||
mouse.active += 0.05 * (mouseTarget.active - mouse.active)
|
||||
options.frame?.({ t, program, mouse })
|
||||
renderer.render({ scene: mesh })
|
||||
outCtx?.clearRect(0, 0, outCanvas.width, outCanvas.height)
|
||||
outCtx?.drawImage(gl.canvas, 0, 0, outCanvas.width, outCanvas.height)
|
||||
}
|
||||
|
||||
const start = () => {
|
||||
if (!raf && visible && pageVisible) raf = requestAnimationFrame(loop)
|
||||
}
|
||||
const stop = () => {
|
||||
if (raf) {
|
||||
cancelAnimationFrame(raf)
|
||||
raf = 0
|
||||
}
|
||||
}
|
||||
|
||||
const io = new IntersectionObserver(([entry]) => {
|
||||
visible = entry?.isIntersecting ?? true
|
||||
if (visible) start()
|
||||
else stop()
|
||||
})
|
||||
io.observe(ctn)
|
||||
|
||||
const onVisibility = () => {
|
||||
pageVisible = document.visibilityState !== 'hidden'
|
||||
if (pageVisible) start()
|
||||
else stop()
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVisibility)
|
||||
|
||||
start()
|
||||
|
||||
teardown = () => {
|
||||
stop()
|
||||
ro.disconnect()
|
||||
io.disconnect()
|
||||
document.removeEventListener('visibilitychange', onVisibility)
|
||||
if (options.mouse) window.removeEventListener('pointermove', onPointerMove)
|
||||
if (outCanvas.parentNode === ctn) ctn.removeChild(outCanvas)
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
teardown?.()
|
||||
teardown = null
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user