topfans/frontend/utils/laser-card/laserPreviewWebgl.js
2026-05-16 02:42:32 +08:00

397 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 镭射卡预览专用 WebGL1 渲染器(全息光栅风格单 Pass
* 预览与导出分离:导出仍走 2D Canvas本模块仅用于页面内实时预览。
*
* 依赖WebGL1 + OES_texture_float_linear 非必须;仅用 RGBA8 纹理。
*/
const VERT_SRC = `
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_uv * 2.0 - 1.0, 0.0, 1.0);
}
`
const FRAG_SRC = `
precision highp float;
varying vec2 v_uv;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec4 u_photoRect;
uniform vec4 u_backRect;
uniform float u_hasBackdrop;
uniform float u_corner;
uniform vec3 u_view;
uniform float u_vivid;
uniform float u_strength;
uniform float u_styleHue;
uniform sampler2D u_photo;
uniform sampler2D u_back;
const vec3 LUMA = vec3(0.299, 0.587, 0.114);
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
float n2(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.52;
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
for (int i = 0; i < 6; i++) {
v += a * n2(p);
p = m * p;
a *= 0.5;
}
return v;
}
float roundedBox(vec2 p, vec2 b, float r) {
vec2 q = abs(p) - b + r;
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
}
vec3 hsl2rgb(float h, float s, float l) {
float c = (1.0 - abs(2.0 * l - 1.0)) * s;
float x = c * (1.0 - abs(mod(h / 60.0, 2.0) - 1.0));
float m = l - 0.5 * c;
vec3 rgb;
if (h < 60.0) rgb = vec3(c, x, 0.0);
else if (h < 120.0) rgb = vec3(x, c, 0.0);
else if (h < 180.0) rgb = vec3(0.0, c, x);
else if (h < 240.0) rgb = vec3(0.0, x, c);
else if (h < 300.0) rgb = vec3(x, 0.0, c);
else rgb = vec3(c, 0.0, x);
return rgb + m;
}
vec4 samplePhoto(vec2 uv) {
vec2 pRaw = (uv - u_photoRect.xy) / u_photoRect.zw;
vec2 p = clamp(pRaw, 0.0, 1.0);
return texture2D(u_photo, p);
}
float photoInside(vec2 uv) {
vec2 pRaw = (uv - u_photoRect.xy) / u_photoRect.zw;
return step(0.0, pRaw.x) * step(pRaw.x, 1.0) * step(0.0, pRaw.y) * step(pRaw.y, 1.0);
}
vec3 sampleBackdrop(vec2 uv) {
vec2 p = (uv - u_backRect.xy) / u_backRect.zw;
if (p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0) return vec3(0.04, 0.03, 0.06);
return texture2D(u_back, p).rgb;
}
void main() {
vec2 uv = v_uv;
vec2 pN = (uv - 0.5) * vec2(u_resolution.x / u_resolution.y, 1.0);
float rb = roundedBox(pN, vec2(0.5 * u_resolution.x / u_resolution.y, 0.5), u_corner);
if (rb > 0.002) discard;
vec3 baseBg = vec3(0.06, 0.045, 0.09);
if (u_hasBackdrop > 0.5) {
baseBg = sampleBackdrop(uv);
}
vec4 pc = samplePhoto(uv);
float photoMask = photoInside(uv);
vec3 photoRgb = pc.rgb;
float eps = 1.2 / u_resolution.x;
vec4 px1 = samplePhoto(uv + vec2(eps, 0.0));
vec4 px2 = samplePhoto(uv + vec2(0.0, eps));
float l0 = dot(photoRgb, LUMA);
float l1 = dot(px1.rgb, LUMA);
float l2 = dot(px2.rgb, LUMA);
float edge = clamp(abs(l1 - l0) + abs(l2 - l0), 0.0, 1.0) * 3.5;
vec2 pF = uv * vec2(u_resolution.x / max(u_resolution.y, 1.0), 1.0) * 3.4;
float f = fbm(pF + u_view.xy * 0.15);
float f2 = fbm(pF * 1.7 + vec2(13.1, 9.7) + u_view.z * 0.08);
vec2 g = vec2(fbm(pF + vec2(1.7, 0.0)) - fbm(pF - vec2(1.7, 0.0)), fbm(pF + vec2(0.0, 1.7)) - fbm(pF - vec2(0.0, 1.7)));
float ang = atan(g.y, g.x) + u_view.x * 1.15 + u_view.y * 0.9 + f * 6.28318 * 0.35;
float film = sin(dot(uv * u_resolution.xy * 0.02, vec2(1.0, 1.73)) * 3.14159 + u_time * 0.0012 + u_view.z);
float hue = fract(ang / 6.28318 + f2 * 0.22 + u_styleHue / 360.0 + u_time * 0.00008 + film * 0.08);
float sat = 0.72 + u_vivid * 0.28;
float lit = 0.48 + f * 0.22;
vec3 holo = hsl2rgb(hue * 360.0, sat, lit);
float carrier = smoothstep(0.15, 0.95, pow(f, 0.85)) * (0.35 + edge * 0.85);
carrier = clamp(carrier * u_strength * (0.55 + u_vivid * 0.45), 0.0, 1.35);
vec3 holoRgb = holo * (0.55 + 0.45 * sin(u_time * 0.001 + f * 10.0));
float sp = step(0.988, hash(floor(uv * u_resolution * 1.2) + floor(u_time * 0.02)));
vec3 sparkle = vec3(1.0) * sp * 0.55;
vec3 base = mix(baseBg, photoRgb, photoMask);
vec3 ir = holoRgb * carrier;
vec3 screenBlend = 1.0 - (1.0 - base) * (1.0 - clamp(ir * 1.1, 0.0, 1.0));
vec3 col = mix(base, screenBlend, clamp(carrier * 0.92, 0.0, 1.0));
col += sparkle * carrier;
float ca = 0.0018 * (0.5 + u_vivid);
vec2 o = vec2(ca * sin(u_view.z), ca * cos(u_view.z * 0.7));
vec3 cr = hsl2rgb(fract(hue + 0.02) * 360.0, sat * 0.9, lit * 1.05);
vec3 cb = hsl2rgb(fract(hue - 0.03) * 360.0, sat * 0.95, lit * 0.95);
col.r = mix(col.r, cr.r, 0.12 * carrier);
col.b = mix(col.b, cb.b, 0.12 * carrier);
float vign = 1.0 - pow(length(pN) / 0.72, 2.2) * 0.12;
col *= vign;
gl_FragColor = vec4(col, 1.0);
}
`
function compileShader(gl, type, src) {
const sh = gl.createShader(type)
gl.shaderSource(sh, src)
gl.compileShader(sh)
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
const err = gl.getShaderInfoLog(sh)
gl.deleteShader(sh)
throw new Error(err || 'shader compile')
}
return sh
}
function createProgram(gl, vs, fs) {
const prog = gl.createProgram()
gl.attachShader(prog, compileShader(gl, gl.VERTEX_SHADER, vs))
gl.attachShader(prog, compileShader(gl, gl.FRAGMENT_SHADER, fs))
gl.linkProgram(prog)
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
const err = gl.getProgramInfoLog(prog)
gl.deleteProgram(prog)
throw new Error(err || 'program link')
}
return prog
}
function orthoTexImage(gl, tex, source) {
gl.bindTexture(gl.TEXTURE_2D, tex)
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source)
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.bindTexture(gl.TEXTURE_2D, null)
}
/**
* @param {WebGLRenderingContext} gl
* @param {{ width: number, height: number, canvas?: any }} opts
*/
export function createLaserPreviewWebgl(gl, opts) {
const w = Math.max(2, Math.floor(opts.width))
const h = Math.max(2, Math.floor(opts.height))
const program = createProgram(gl, VERT_SRC, FRAG_SRC)
const loc = {
a_uv: gl.getAttribLocation(program, 'a_uv'),
u_resolution: gl.getUniformLocation(program, 'u_resolution'),
u_time: gl.getUniformLocation(program, 'u_time'),
u_photoRect: gl.getUniformLocation(program, 'u_photoRect'),
u_backRect: gl.getUniformLocation(program, 'u_backRect'),
u_hasBackdrop: gl.getUniformLocation(program, 'u_hasBackdrop'),
u_corner: gl.getUniformLocation(program, 'u_corner'),
u_view: gl.getUniformLocation(program, 'u_view'),
u_vivid: gl.getUniformLocation(program, 'u_vivid'),
u_strength: gl.getUniformLocation(program, 'u_strength'),
u_styleHue: gl.getUniformLocation(program, 'u_styleHue'),
u_photo: gl.getUniformLocation(program, 'u_photo'),
u_back: gl.getUniformLocation(program, 'u_back')
}
const buf = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
const quad = new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1])
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW)
const texPhoto = gl.createTexture()
const texBack = gl.createTexture()
let photoReady = false
let backReady = false
function bindUnit(unit, tex, loc) {
gl.activeTexture(gl.TEXTURE0 + unit)
gl.bindTexture(gl.TEXTURE_2D, tex)
gl.uniform1i(loc, unit)
}
return {
gl,
w,
h,
program,
/** 释放 GPU 资源 */
destroy() {
gl.deleteProgram(program)
gl.deleteBuffer(buf)
gl.deleteTexture(texPhoto)
gl.deleteTexture(texBack)
},
/** @param {HTMLImageElement|ImageBitmap|HTMLCanvasElement} img */
uploadPhoto(img) {
if (!img || !img.width) return
orthoTexImage(gl, texPhoto, img)
photoReady = true
},
/** @param {HTMLImageElement|ImageBitmap|HTMLCanvasElement|null} img */
uploadBackdrop(img) {
if (!img || !img.width) {
backReady = false
return
}
orthoTexImage(gl, texBack, img)
backReady = true
},
hasPhoto() {
return photoReady
},
hasBackdrop() {
return backReady
},
/**
* @param {number} timeMs
* @param {object} u
* @param {{ dx: number, dy: number, dw: number, dh: number }} u.photoRect 像素 cover 矩形
* @param {object} [u.backRect]
* @param {boolean} [u.hasBackdrop]
* @param {number} [u.cornerRadiusPx]
* @param {number} [u.vivid] 0.75~1.35
* @param {number} [u.strength] 0.35~1.2
* @param {number} [u.styleHue] beam 风格色相偏置 0~360
*/
render(timeMs, u) {
if (!photoReady) return
const pr = u.photoRect
const cw = u.canvasW || w
const ch = u.canvasH || h
const nx = pr.dx / cw
const ny = pr.dy / ch
const nw = pr.dw / cw
const nh = pr.dh / ch
let bx = 0
let nyb = 0
let nbw = 1
let nbh = 1
let hasB = !!(u.hasBackdrop && backReady && u.backRect)
if (hasB) {
const br = u.backRect
bx = br.dx / cw
nyb = br.dy / ch
nbw = br.dw / cw
nbh = br.dh / ch
}
const rPx = typeof u.cornerRadiusPx === 'number' ? u.cornerRadiusPx : Math.min(32, cw * 0.065, ch * 0.048)
const cornerUv = rPx / Math.min(cw, ch)
const t = (timeMs || 0) * 0.001
const speed = typeof u.animSpeed === 'number' ? u.animSpeed : 1
const vx = Math.sin(t * 0.42 * speed) * 0.7 + Math.sin(t * 0.17 * speed) * 0.35
const vy = Math.cos(t * 0.31 * speed) * 0.55 + Math.cos(t * 0.21 * speed) * 0.28
const vz = t * 0.55 * speed
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
gl.disable(gl.DEPTH_TEST)
gl.disable(gl.STENCIL_TEST)
gl.clearColor(0.04, 0.03, 0.06, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.useProgram(program)
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
gl.enableVertexAttribArray(loc.a_uv)
gl.vertexAttribPointer(loc.a_uv, 2, gl.FLOAT, false, 0, 0)
gl.uniform2f(loc.u_resolution, cw, ch)
gl.uniform1f(loc.u_time, timeMs || 0)
gl.uniform4f(loc.u_photoRect, nx, ny, nw, nh)
gl.uniform4f(loc.u_backRect, bx, nyb, nbw, nbh)
gl.uniform1f(loc.u_hasBackdrop, hasB ? 1 : 0)
gl.uniform1f(loc.u_corner, cornerUv)
gl.uniform3f(loc.u_view, vx, vy, vz)
gl.uniform1f(loc.u_vivid, typeof u.vivid === 'number' ? u.vivid : 1)
gl.uniform1f(loc.u_strength, typeof u.strength === 'number' ? u.strength : 0.82)
gl.uniform1f(loc.u_styleHue, typeof u.styleHue === 'number' ? u.styleHue : 0)
bindUnit(0, texPhoto, loc.u_photo)
bindUnit(1, texBack, loc.u_back)
gl.drawArrays(gl.TRIANGLES, 0, 6)
gl.bindTexture(gl.TEXTURE_2D, null)
}
}
}
/**
* 从本地路径或 URL 加载图片App/H5 用 Image小程序等用 canvas.createImage。
* @param {string} src
* @param {any} [canvasForCreateImage] type=webgl 的 canvas node
*/
export function loadTextureImage(src, canvasForCreateImage) {
return new Promise((resolve, reject) => {
if (!src) {
reject(new Error('empty src'))
return
}
const c = canvasForCreateImage
if (c && typeof c.createImage === 'function') {
try {
const img = c.createImage()
img.onload = () => resolve(img)
img.onerror = () => reject(new Error('createImage load'))
img.src = src
return
} catch (e2) {
/* fall through */
}
}
if (typeof Image !== 'undefined') {
try {
const img = new Image()
try {
img.crossOrigin = 'anonymous'
} catch (e0) {
/* noop */
}
img.onload = () => resolve(img)
img.onerror = () => reject(new Error('image load'))
img.src = src
return
} catch (e) {
reject(e)
return
}
}
reject(new Error('no image loader'))
})
}
/** 各 beamEffect 对应色相偏置(度),与 2D 视觉大致同系 */
export const BEAM_STYLE_HUE = {
white: 0,
prism: 18,
aurora: 42,
sunset: 6,
neon: 72
}