forked from 77media/video-flow
135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
// Adapted from https://github.com/shuding/liquid-glass
|
|
|
|
export interface Vec2 {
|
|
x: number
|
|
y: number
|
|
}
|
|
|
|
export interface ShaderOptions {
|
|
width: number
|
|
height: number
|
|
fragment: (uv: Vec2, mouse?: Vec2) => Vec2
|
|
mousePosition?: Vec2
|
|
}
|
|
|
|
function smoothStep(a: number, b: number, t: number): number {
|
|
t = Math.max(0, Math.min(1, (t - a) / (b - a)))
|
|
return t * t * (3 - 2 * t)
|
|
}
|
|
|
|
function length(x: number, y: number): number {
|
|
return Math.sqrt(x * x + y * y)
|
|
}
|
|
|
|
function roundedRectSDF(x: number, y: number, width: number, height: number, radius: number): number {
|
|
const qx = Math.abs(x) - width + radius
|
|
const qy = Math.abs(y) - height + radius
|
|
return Math.min(Math.max(qx, qy), 0) + length(Math.max(qx, 0), Math.max(qy, 0)) - radius
|
|
}
|
|
|
|
function texture(x: number, y: number): Vec2 {
|
|
return { x, y }
|
|
}
|
|
|
|
// Shader fragment functions for different effects
|
|
export const fragmentShaders = {
|
|
liquidGlass: (uv: Vec2): Vec2 => {
|
|
const ix = uv.x - 0.5
|
|
const iy = uv.y - 0.5
|
|
const distanceToEdge = roundedRectSDF(ix, iy, 0.3, 0.2, 0.6)
|
|
const displacement = smoothStep(0.8, 0, distanceToEdge - 0.15)
|
|
const scaled = smoothStep(0, 1, displacement)
|
|
return texture(ix * scaled + 0.5, iy * scaled + 0.5)
|
|
},
|
|
}
|
|
|
|
export type FragmentShaderType = keyof typeof fragmentShaders
|
|
|
|
export class ShaderDisplacementGenerator {
|
|
private canvas: HTMLCanvasElement
|
|
private context: CanvasRenderingContext2D
|
|
private canvasDPI = 1
|
|
|
|
constructor(private options: ShaderOptions) {
|
|
this.canvas = document.createElement("canvas")
|
|
this.canvas.width = options.width * this.canvasDPI
|
|
this.canvas.height = options.height * this.canvasDPI
|
|
this.canvas.style.display = "none"
|
|
|
|
const context = this.canvas.getContext("2d")
|
|
if (!context) {
|
|
throw new Error("Could not get 2D context")
|
|
}
|
|
this.context = context
|
|
}
|
|
|
|
updateShader(mousePosition?: Vec2): string {
|
|
const w = this.options.width * this.canvasDPI
|
|
const h = this.options.height * this.canvasDPI
|
|
|
|
let maxScale = 0
|
|
const rawValues: number[] = []
|
|
|
|
// Calculate displacement values
|
|
for (let y = 0; y < h; y++) {
|
|
for (let x = 0; x < w; x++) {
|
|
const uv: Vec2 = { x: x / w, y: y / h }
|
|
|
|
const pos = this.options.fragment(uv, mousePosition)
|
|
const dx = pos.x * w - x
|
|
const dy = pos.y * h - y
|
|
|
|
maxScale = Math.max(maxScale, Math.abs(dx), Math.abs(dy))
|
|
rawValues.push(dx, dy)
|
|
}
|
|
}
|
|
|
|
// Improved normalization to prevent artifacts while maintaining intensity
|
|
if (maxScale > 0) {
|
|
maxScale = Math.max(maxScale, 1) // Ensure minimum scale to prevent over-normalization
|
|
} else {
|
|
maxScale = 1
|
|
}
|
|
|
|
// Create ImageData and fill it
|
|
const imageData = this.context.createImageData(w, h)
|
|
const data = imageData.data
|
|
|
|
// Convert to image data with smoother normalization
|
|
let rawIndex = 0
|
|
for (let y = 0; y < h; y++) {
|
|
for (let x = 0; x < w; x++) {
|
|
const dx = rawValues[rawIndex++]
|
|
const dy = rawValues[rawIndex++]
|
|
|
|
// Smooth the displacement values at edges to prevent hard transitions
|
|
const edgeDistance = Math.min(x, y, w - x - 1, h - y - 1)
|
|
const edgeFactor = Math.min(1, edgeDistance / 2) // Smooth within 2 pixels of edge
|
|
|
|
const smoothedDx = dx * edgeFactor
|
|
const smoothedDy = dy * edgeFactor
|
|
|
|
const r = smoothedDx / maxScale + 0.5
|
|
const g = smoothedDy / maxScale + 0.5
|
|
|
|
const pixelIndex = (y * w + x) * 4
|
|
data[pixelIndex] = Math.max(0, Math.min(255, r * 255)) // Red channel (X displacement)
|
|
data[pixelIndex + 1] = Math.max(0, Math.min(255, g * 255)) // Green channel (Y displacement)
|
|
data[pixelIndex + 2] = Math.max(0, Math.min(255, g * 255)) // Blue channel (Y displacement for SVG filter compatibility)
|
|
data[pixelIndex + 3] = 255 // Alpha channel
|
|
}
|
|
}
|
|
|
|
this.context.putImageData(imageData, 0, 0)
|
|
return this.canvas.toDataURL()
|
|
}
|
|
|
|
destroy(): void {
|
|
this.canvas.remove()
|
|
}
|
|
|
|
getScale(): number {
|
|
return this.canvasDPI
|
|
}
|
|
}
|