layershift 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/components/layershift.js +1782 -653
- package/dist/npm/layershift.es.js +5212 -2583
- package/dist/types/components/layershift/layershift-element.d.ts +10 -8
- package/dist/types/components/layershift/layershift-element.d.ts.map +1 -1
- package/dist/types/components/layershift/lifecycle.d.ts +77 -0
- package/dist/types/components/layershift/lifecycle.d.ts.map +1 -0
- package/dist/types/components/layershift/portal-element.d.ts +10 -8
- package/dist/types/components/layershift/portal-element.d.ts.map +1 -1
- package/dist/types/components/layershift/types.d.ts +8 -0
- package/dist/types/components/layershift/types.d.ts.map +1 -1
- package/dist/types/gpu-backend.d.ts +37 -0
- package/dist/types/gpu-backend.d.ts.map +1 -0
- package/dist/types/jfa-distance-field.d.ts +78 -0
- package/dist/types/jfa-distance-field.d.ts.map +1 -0
- package/dist/types/parallax-renderer-webgpu.d.ts +103 -0
- package/dist/types/parallax-renderer-webgpu.d.ts.map +1 -0
- package/dist/types/parallax-renderer.d.ts +54 -91
- package/dist/types/parallax-renderer.d.ts.map +1 -1
- package/dist/types/portal-renderer-webgpu.d.ts +199 -0
- package/dist/types/portal-renderer-webgpu.d.ts.map +1 -0
- package/dist/types/portal-renderer.d.ts +64 -65
- package/dist/types/portal-renderer.d.ts.map +1 -1
- package/dist/types/precomputed-depth.d.ts +9 -51
- package/dist/types/precomputed-depth.d.ts.map +1 -1
- package/dist/types/quality.d.ts +95 -0
- package/dist/types/quality.d.ts.map +1 -0
- package/dist/types/render-pass-webgpu.d.ts +76 -0
- package/dist/types/render-pass-webgpu.d.ts.map +1 -0
- package/dist/types/render-pass.d.ts +171 -0
- package/dist/types/render-pass.d.ts.map +1 -0
- package/dist/types/renderer-base.d.ts +124 -0
- package/dist/types/renderer-base.d.ts.map +1 -0
- package/dist/types/webgl-utils.d.ts +29 -0
- package/dist/types/webgl-utils.d.ts.map +1 -0
- package/dist/types/webgpu-utils.d.ts +42 -0
- package/dist/types/webgpu-utils.d.ts.map +1 -0
- package/package.json +38 -4
|
@@ -1,196 +1,456 @@
|
|
|
1
|
-
var Layershift=(function(
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
var Layershift=(function(ne){"use strict";class be{constructor(e){this.depthData=e;const t=e.meta.width*e.meta.height;this.uint8Output=new Uint8Array(t)}uint8Output;lastFrameIndex=-1;lastNextFrameIndex=-1;lastLerpFactor=-1;sample(e){const t=nt(e*this.depthData.meta.fps,0,this.depthData.meta.frameCount-1),i=Math.floor(t),n=Math.min(i+1,this.depthData.meta.frameCount-1),r=t-i,s=i!==this.lastFrameIndex||n!==this.lastNextFrameIndex,a=Math.abs(r-this.lastLerpFactor)>.001;if(!s&&!a)return this.uint8Output;this.lastFrameIndex=i,this.lastNextFrameIndex=n,this.lastLerpFactor=r;const l=1-r,u=this.depthData.frames[i],h=this.depthData.frames[n];for(let f=0;f<this.uint8Output.length;f+=1)this.uint8Output[f]=u[f]*l+h[f]*r+.5|0;return this.uint8Output}}async function Se(o,e,t){const[i,n]=await Promise.all([Qe(e),et(o)]);return tt(n,i)}async function Qe(o){const e=await fetch(o);if(!e.ok)throw new Error(`Failed to fetch depth metadata (${e.status} ${e.statusText}).`);const t=await e.json();return it(t),{frameCount:t.frameCount,fps:t.fps,width:t.width,height:t.height,sourceFps:t.sourceFps}}async function et(o,e){const t=await fetch(o);if(!t.ok)throw new Error(`Failed to fetch depth data (${t.status} ${t.statusText}).`);t.headers.get("content-length");const i=t.body;if(!i)return new Uint8Array(await t.arrayBuffer());const n=[];let r=0;const s=i.getReader();for(;;){const{done:u,value:h}=await s.read();if(u)break;h&&(n.push(h),r+=h.byteLength)}const a=new Uint8Array(r);let l=0;for(const u of n)a.set(u,l),l+=u.byteLength;return a}function tt(o,e){if(o.byteLength<4)throw new Error("Depth data binary is missing the frame-count header.");const i=new DataView(o.buffer,o.byteOffset,o.byteLength).getUint32(0,!0),n=e.width*e.height,r=4+i*n;if(o.byteLength!==r)throw new Error(`Depth data byte length mismatch. Expected ${r} bytes, received ${o.byteLength}.`);if(i!==e.frameCount)throw new Error(`Depth frame count mismatch between metadata (${e.frameCount}) and binary header (${i}).`);const s=o.subarray(4),a=new Array(i);for(let l=0;l<i;l+=1){const u=l*n;a[l]=s.subarray(u,u+n)}return{meta:e,frames:a}}function it(o){if(!o||typeof o.frameCount!="number"||typeof o.fps!="number"||typeof o.width!="number"||typeof o.height!="number"||typeof o.sourceFps!="number")throw new Error("Depth metadata is malformed.");if(!Number.isFinite(o.frameCount)||!Number.isFinite(o.fps)||!Number.isFinite(o.width)||!Number.isFinite(o.height)||!Number.isFinite(o.sourceFps)||o.frameCount<=0||o.fps<=0||o.width<=0||o.height<=0||o.sourceFps<=0)throw new Error("Depth metadata contains invalid numeric values.")}function nt(o,e,t){return Math.min(t,Math.max(e,o))}const rt={parallaxStrength:.05,contrastLow:.05,contrastHigh:.95,verticalReduction:.5,dofStart:.6,dofStrength:.4,pomSteps:16,overscanPadding:.08};function ot(o,e,t){const i=new Float32Array(256);if(o.length===0||e<=0||t<=0)return Te(i);const n=at(o.length),r=e*t;let s=0;const a=new Uint32Array(256);for(const S of n){const x=o[S],E=Math.min(x.length,r);for(let P=0;P<E;P+=1)a[x[P]]+=1;s+=E}if(s===0)return Te(i);const l=1/s;for(let S=0;S<256;S+=1)i[S]=a[S]*l;const u=new Float32Array(256);u[0]=i[0];for(let S=1;S<256;S+=1)u[S]=u[S-1]+i[S];const h=Z(u,.05),f=Z(u,.25),c=Z(u,.5),g=Z(u,.75),v=Z(u,.95);let d=0;for(let S=0;S<256;S+=1)d+=S/255*i[S];let A=0;for(let S=0;S<256;S+=1){const x=S/255-d;A+=i[S]*x*x}const b=Math.sqrt(A),y=v-h,p=g-f,m=lt(i);return{mean:d,stdDev:b,p5:h,p25:f,median:c,p75:g,p95:v,effectiveRange:y,iqr:p,bimodality:m,histogram:i}}function st(o){if(o.effectiveRange<.05||o.stdDev<.02)return{...rt};const e=o.effectiveRange-.5,t=o.bimodality-.4,i=V(.05-e*.03+t*.01,.035,.065),n=V(o.p5-.03,0,.25),r=V(o.p95+.03,.75,1),s=V((i-.03)/.05,0,1),a=V(.6-s*.25,.35,.6),l=V(.6-e*.2,.5,.7),u=V(.4+e*.2,.25,.5),h=16,f=V(i+.03,.06,.1);return{parallaxStrength:i,contrastLow:n,contrastHigh:r,verticalReduction:a,dofStart:l,dofStrength:u,pomSteps:h,overscanPadding:f}}function at(o){if(o<=0)return[];if(o===1)return[0];const e=o-1,t=[0,Math.floor(o/4),Math.floor(o/2),Math.floor(3*o/4),e],i=new Set,n=[];for(const r of t)i.has(r)||(i.add(r),n.push(r));return n}function Z(o,e){for(let t=0;t<256;t+=1)if(o[t]>=e)return t/255;return 1}function lt(o){const e=new Float32Array(256);for(let c=0;c<256;c+=1){let g=0,v=0;for(let d=c-2;d<=c+2;d+=1)d>=0&&d<256&&(g+=o[d],v+=1);e[c]=g/v}let t=0;for(let c=0;c<256;c+=1)t+=e[c];t/=256;const i=t*2,n=25,r=[];for(let c=1;c<255;c+=1)e[c]>e[c-1]&&e[c]>e[c+1]&&e[c]>=i&&r.push({bin:c,height:e[c]});if(e[0]>e[1]&&e[0]>=i&&r.push({bin:0,height:e[0]}),e[255]>e[254]&&e[255]>=i&&r.push({bin:255,height:e[255]}),r.sort((c,g)=>g.height-c.height),r.length<2)return 0;const s=r[0];let a=null;for(let c=1;c<r.length;c+=1)if(Math.abs(r[c].bin-s.bin)>=n){a=r[c];break}if(!a)return 0;const l=Math.min(s.bin,a.bin),u=Math.max(s.bin,a.bin);let h=1/0;for(let c=l;c<=u;c+=1)e[c]<h&&(h=e[c]);const f=Math.min(s.height,a.height);return f<=0?0:V(1-h/f,0,1)}function Te(o){return{mean:0,stdDev:0,p5:0,p25:0,median:0,p75:0,p95:0,effectiveRange:0,iqr:0,bimodality:0,histogram:o}}function V(o,e,t){return Math.min(t,Math.max(e,o))}function W(o,e,t){const i=o.createShader(e);if(!i)throw new Error("Failed to create shader.");if(o.shaderSource(i,t),o.compileShader(i),!o.getShaderParameter(i,o.COMPILE_STATUS)){const n=o.getShaderInfoLog(i)??"";throw o.deleteShader(i),new Error(`Shader compilation failed:
|
|
2
|
+
${n}`)}return i}function de(o,e,t){const i=o.createProgram();if(!i)throw new Error("Failed to create program.");if(o.attachShader(i,e),o.attachShader(i,t),o.linkProgram(i),!o.getProgramParameter(i,o.LINK_STATUS)){const n=o.getProgramInfoLog(i)??"";throw o.deleteProgram(i),new Error(`Program linking failed:
|
|
3
|
+
${n}`)}return o.detachShader(i,e),o.detachShader(i,t),o.deleteShader(e),o.deleteShader(t),i}function pe(o,e,t){const i={};for(const n of t)i[n]=o.getUniformLocation(e,n);return i}const ut=new Float32Array([-1,-1,1,-1,-1,1,1,1]);function ye(o,e){const t=o.createVertexArray();if(!t)throw new Error("Failed to create VAO.");o.bindVertexArray(t);const i=o.createBuffer();o.bindBuffer(o.ARRAY_BUFFER,i),o.bufferData(o.ARRAY_BUFFER,ut,o.STATIC_DRAW);const n=o.getAttribLocation(e,"aPosition");return o.enableVertexAttribArray(n),o.vertexAttribPointer(n,2,o.FLOAT,!1,0,0),o.bindVertexArray(null),t}class Ee{slots=new Map;nextUnit=0;register(e){if(this.slots.has(e))throw new Error(`TextureRegistry: slot '${e}' already registered.`);const t={name:e,unit:this.nextUnit++,texture:null};return this.slots.set(e,t),t}get(e){const t=this.slots.get(e);if(!t)throw new Error(`TextureRegistry: slot '${e}' not found.`);return t}disposeAll(e){for(const t of this.slots.values())t.texture&&(e.deleteTexture(t.texture),t.texture=null)}get size(){return this.slots.size}}function G(o,e,t,i,n){const r=W(o,o.VERTEX_SHADER,t),s=W(o,o.FRAGMENT_SHADER,i),a=de(o,r,s),l=pe(o,a,n);return{name:e,program:a,uniforms:l,dispose(u){u.deleteProgram(a)}}}const Pe={high:{dprCap:2,depthMaxDim:512,pomSteps:16,bilateralRadius:2,jfaDivisor:2},medium:{dprCap:1.5,depthMaxDim:512,pomSteps:16,bilateralRadius:2,jfaDivisor:2},low:{dprCap:1,depthMaxDim:256,pomSteps:8,bilateralRadius:1,jfaDivisor:4}};function ht(o){let e="unknown";const t=o.getExtension("WEBGL_debug_renderer_info");t&&(e=o.getParameter(t.UNMASKED_RENDERER_WEBGL)||"unknown");const i=o.getParameter(o.MAX_TEXTURE_SIZE),n=typeof navigator<"u"&&navigator.hardwareConcurrency||0,r=typeof navigator<"u"&&navigator.deviceMemory||0,s=typeof window<"u"&&window.devicePixelRatio||1,a=typeof screen<"u"?(screen.width||0)*(screen.height||0):0,l=typeof navigator<"u"&&("ontouchstart"in window||navigator.maxTouchPoints>0),u=a>0&&a<1920*1080;return{gpuRenderer:e,maxTextureSize:i,hardwareConcurrency:n,deviceMemory:r,devicePixelRatio:s,screenPixels:a,isMobile:l&&u}}const ct=["mali-4","mali-t","adreno 3","adreno 4","adreno 5","powervr sgx","intel hd graphics","intel uhd graphics","intel iris","llvmpipe","swiftshader","software"],ft=["nvidia","geforce","radeon rx","radeon pro","apple m","apple gpu","adreno 7","adreno 6","mali-g7","mali-g6"];function Ae(o){let e=0;const t=o.gpuRenderer.toLowerCase(),i=ct.some(r=>t.includes(r)),n=ft.some(r=>t.includes(r));return i&&(e-=30),n&&(e+=20),o.maxTextureSize>=16384?e+=10:o.maxTextureSize>=8192?e+=5:o.maxTextureSize<=4096&&(e-=15),o.hardwareConcurrency>=8?e+=5:o.hardwareConcurrency>=4?e+=0:o.hardwareConcurrency>0&&o.hardwareConcurrency<4&&(e-=10),o.deviceMemory>=8?e+=5:o.deviceMemory>=4?e+=0:o.deviceMemory>0&&o.deviceMemory<4&&(e-=15),o.isMobile&&(e-=10),e>=0?"high":e>=-25?"medium":"low"}function we(o,e){const t=e&&e!=="auto"?e:Ae(ht(o));return{tier:t,...Pe[t]}}function dt(o){const e=o.description||o.device||"unknown",t=8192,i=typeof navigator<"u"&&navigator.hardwareConcurrency||0,n=typeof navigator<"u"&&navigator.deviceMemory||0,r=typeof window<"u"&&window.devicePixelRatio||1,s=typeof screen<"u"?(screen.width||0)*(screen.height||0):0,a=typeof navigator<"u"&&("ontouchstart"in window||navigator.maxTouchPoints>0),l=s>0&&s<1920*1080;return{gpuRenderer:e,maxTextureSize:t,hardwareConcurrency:i,deviceMemory:n,devicePixelRatio:r,screenPixels:s,isMobile:a&&l}}function De(o,e){const t=e&&e!=="auto"?e:Ae(dt(o));return{tier:t,...Pe[t]}}class k{static RESIZE_DEBOUNCE_MS=100;canvas;container;depthWidth=0;depthHeight=0;sourceDepthWidth=0;sourceDepthHeight=0;depthSubsampleBuffer=null;videoAspect=1.7777777777777777;uvOffset=[0,0];uvScale=[1,1];readDepth=null;readInput=null;playbackVideo=null;onVideoFrame=null;animationFrameHandle=0;rvfcHandle=0;rvfcSupported=!1;resizeObserver=null;resizeTimer=null;qualityParams;constructor(e){this.container=e,this.canvas=document.createElement("canvas"),this.container.appendChild(this.canvas),this.canvas.addEventListener("webglcontextlost",this._handleContextLost),this.canvas.addEventListener("webglcontextrestored",this._handleContextRestored)}start(e,t,i,n){this.stop(),this.playbackVideo=e,this.readDepth=t,this.readInput=i,this.onVideoFrame=n??null,this.rvfcSupported=k.isRVFCSupported(),this.rvfcSupported&&(this.rvfcHandle=e.requestVideoFrameCallback(this._videoFrameLoop)),this.animationFrameHandle=window.requestAnimationFrame(this._rafLoop)}stop(){this.animationFrameHandle&&(window.cancelAnimationFrame(this.animationFrameHandle),this.animationFrameHandle=0),this.rvfcHandle&&this.playbackVideo&&(this.playbackVideo.cancelVideoFrameCallback(this.rvfcHandle),this.rvfcHandle=0),this.playbackVideo=null,this.readDepth=null,this.readInput=null,this.onVideoFrame=null,this.rvfcSupported=!1}dispose(){this.stop(),this.disposeRenderer(),this.canvas.removeEventListener("webglcontextlost",this._handleContextLost),this.canvas.removeEventListener("webglcontextrestored",this._handleContextRestored),this.canvas.remove(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),window.removeEventListener("resize",this.scheduleResizeRecalculate),this.resizeTimer!==null&&(window.clearTimeout(this.resizeTimer),this.resizeTimer=null)}static isRVFCSupported(){return"requestVideoFrameCallback"in HTMLVideoElement.prototype}_rafLoop=()=>{this.animationFrameHandle=window.requestAnimationFrame(this._rafLoop),this.onRenderFrame()};_videoFrameLoop=(e,t)=>{const i=this.playbackVideo;if(!i)return;this.rvfcHandle=i.requestVideoFrameCallback(this._videoFrameLoop);const n=t.mediaTime??i.currentTime;this.onDepthUpdate(n),this.onVideoFrame&&this.onVideoFrame(n,t.presentedFrames??0)};_handleContextLost=e=>{e.preventDefault(),this.animationFrameHandle&&(window.cancelAnimationFrame(this.animationFrameHandle),this.animationFrameHandle=0)};_handleContextRestored=()=>{this.onContextRestored()};setupResizeHandling(){typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(()=>{this.scheduleResizeRecalculate()}),this.resizeObserver.observe(this.container)),window.addEventListener("resize",this.scheduleResizeRecalculate),this.recalculateViewportLayout()}scheduleResizeRecalculate=()=>{this.resizeTimer!==null&&window.clearTimeout(this.resizeTimer),this.resizeTimer=window.setTimeout(()=>{this.resizeTimer=null,this.recalculateViewportLayout()},k.RESIZE_DEBOUNCE_MS)};getViewportSize(){const e=Math.max(1,Math.round(this.container.clientWidth||window.innerWidth)),t=Math.max(1,Math.round(this.container.clientHeight||window.innerHeight));return{width:e,height:t}}clampDepthDimensions(e,t,i){this.sourceDepthWidth=e,this.sourceDepthHeight=t;let n=e,r=t;if(n>i||r>i){const s=i/Math.max(n,r);n=Math.max(1,Math.round(n*s)),r=Math.max(1,Math.round(r*s))}this.depthWidth=n,this.depthHeight=r,n!==e||r!==t?this.depthSubsampleBuffer=new Uint8Array(n*r):this.depthSubsampleBuffer=null}subsampleDepth(e){if(!this.depthSubsampleBuffer)return e;const t=this.depthSubsampleBuffer,i=this.sourceDepthWidth,n=this.depthWidth,r=this.depthHeight;for(let s=0;s<r;s++){const l=Math.min(Math.round(s*i/n),this.sourceDepthHeight-1)*i,u=s*n;for(let h=0;h<n;h++){const f=Math.min(Math.round(h*i/n),i-1);t[u+h]=e[l+f]}}return t}computeCoverFitUV(e,t){const{width:i,height:n}=this.getViewportSize(),r=i/n,s=e+t;let a=1,l=1;r>this.videoAspect?l=this.videoAspect/r:a=r/this.videoAspect;const u=1+s*2;a/=u,l/=u,this.uvOffset=[(1-a)/2,(1-l)/2],this.uvScale=[a,l]}}const pt=`#version 300 es
|
|
4
|
+
in vec2 aPosition;
|
|
5
|
+
|
|
6
|
+
// UV coordinates for cover-fit + overscan.
|
|
7
|
+
// Computed on the CPU and passed as a uniform to avoid
|
|
8
|
+
// recreating geometry on every resize.
|
|
9
|
+
uniform vec2 uUvOffset;
|
|
10
|
+
uniform vec2 uUvScale;
|
|
11
|
+
|
|
12
|
+
out vec2 vUv;
|
|
13
|
+
out vec2 vScreenUv;
|
|
14
|
+
|
|
15
|
+
void main() {
|
|
16
|
+
// Map from clip space [-1,1] to [0,1], then apply cover-fit transform
|
|
17
|
+
vec2 baseUv = aPosition * 0.5 + 0.5;
|
|
18
|
+
vUv = baseUv * uUvScale + uUvOffset;
|
|
19
|
+
// Screen-space UV always [0,1] -- used for vignette and edge fade
|
|
20
|
+
// which should operate on screen position, not texture coordinates.
|
|
21
|
+
vScreenUv = baseUv;
|
|
22
|
+
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
23
|
+
}
|
|
24
|
+
`,mt=`#version 300 es
|
|
25
|
+
in vec2 aPosition;
|
|
26
|
+
out vec2 vUv;
|
|
27
|
+
|
|
28
|
+
void main() {
|
|
29
|
+
vUv = aPosition * 0.5 + 0.5;
|
|
30
|
+
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
31
|
+
}
|
|
32
|
+
`,gt=`#version 300 es
|
|
33
|
+
precision highp float;
|
|
34
|
+
|
|
35
|
+
// BILATERAL_RADIUS is injected as a #define at compile time.
|
|
36
|
+
// Radius 2 -> 5x5 kernel (high/medium), radius 1 -> 3x3 kernel (low).
|
|
37
|
+
|
|
38
|
+
uniform sampler2D uRawDepth;
|
|
39
|
+
uniform vec2 uTexelSize;
|
|
40
|
+
uniform float uSpatialSigma2;
|
|
41
|
+
|
|
42
|
+
in vec2 vUv;
|
|
43
|
+
out vec4 fragColor;
|
|
44
|
+
|
|
45
|
+
void main() {
|
|
46
|
+
const float depthSigma2 = 0.01; // 0.1^2
|
|
47
|
+
|
|
48
|
+
float center = texture(uRawDepth, vUv).r;
|
|
49
|
+
float totalWeight = 1.0;
|
|
50
|
+
float totalDepth = center;
|
|
51
|
+
|
|
52
|
+
for (int dy = -BILATERAL_RADIUS; dy <= BILATERAL_RADIUS; dy++) {
|
|
53
|
+
for (int dx = -BILATERAL_RADIUS; dx <= BILATERAL_RADIUS; dx++) {
|
|
54
|
+
if (dx == 0 && dy == 0) continue;
|
|
55
|
+
|
|
56
|
+
vec2 offset = vec2(float(dx), float(dy)) * uTexelSize;
|
|
57
|
+
float neighbor = texture(uRawDepth, vUv + offset).r;
|
|
58
|
+
|
|
59
|
+
float spatialDist2 = float(dx * dx + dy * dy);
|
|
60
|
+
float depthDiff = neighbor - center;
|
|
61
|
+
float w = exp(-spatialDist2 / uSpatialSigma2 - (depthDiff * depthDiff) / depthSigma2);
|
|
62
|
+
|
|
63
|
+
totalWeight += w;
|
|
64
|
+
totalDepth += neighbor * w;
|
|
65
|
+
}
|
|
21
66
|
}
|
|
22
|
-
`,Wt=`#version 300 es
|
|
23
|
-
precision highp float;
|
|
24
|
-
|
|
25
|
-
// ---- Uniforms ----
|
|
26
|
-
|
|
27
|
-
/** Color video frame, uploaded from HTMLVideoElement. */
|
|
28
|
-
uniform sampler2D uImage;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Single-channel depth map (R channel, 0=near, 1=far).
|
|
32
|
-
* Pre-filtered with a bilateral filter on the CPU before upload,
|
|
33
|
-
* so a single texture() read gives smooth, edge-preserving depth.
|
|
34
|
-
* Uploaded as R8 format (auto-normalized to [0,1]).
|
|
35
|
-
*/
|
|
36
|
-
uniform sampler2D uDepth;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Current parallax input from mouse or gyroscope.
|
|
40
|
-
* Range [-1, 1] for both x (horizontal) and y (vertical).
|
|
41
|
-
*/
|
|
42
|
-
uniform vec2 uOffset;
|
|
43
|
-
|
|
44
|
-
/** Parallax displacement magnitude in UV space (e.g. 0.05 = 5%). */
|
|
45
|
-
uniform float uStrength;
|
|
46
|
-
|
|
47
|
-
/** Whether to use POM ray-marching instead of basic displacement. */
|
|
48
|
-
uniform bool uPomEnabled;
|
|
49
|
-
|
|
50
|
-
/** Number of ray-march steps for POM (runtime-adjustable). */
|
|
51
|
-
uniform int uPomSteps;
|
|
52
|
-
|
|
53
|
-
/** Smoothstep lower bound for depth contrast curve (depth-adaptive). */
|
|
54
|
-
uniform float uContrastLow;
|
|
55
|
-
|
|
56
|
-
/** Smoothstep upper bound for depth contrast curve (depth-adaptive). */
|
|
57
|
-
uniform float uContrastHigh;
|
|
58
|
-
|
|
59
|
-
/** Y-axis displacement multiplier (depth-adaptive). */
|
|
60
|
-
uniform float uVerticalReduction;
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
fragColor = vec4(totalDepth / totalWeight, 0.0, 0.0, 1.0);
|
|
69
|
+
}
|
|
70
|
+
`,vt=`#version 300 es
|
|
71
|
+
precision highp float;
|
|
72
|
+
|
|
73
|
+
// ---- Uniforms ----
|
|
74
|
+
|
|
75
|
+
/** Color video frame, uploaded from HTMLVideoElement. */
|
|
76
|
+
uniform sampler2D uImage;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Single-channel depth map (R channel, 0=near, 1=far).
|
|
80
|
+
* Bilateral-filtered on the GPU via a dedicated render pass,
|
|
81
|
+
* so a single texture() read gives smooth, edge-preserving depth.
|
|
82
|
+
*/
|
|
83
|
+
uniform sampler2D uDepth;
|
|
64
84
|
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Current parallax input from mouse or gyroscope.
|
|
87
|
+
* Range [-1, 1] for both x (horizontal) and y (vertical).
|
|
88
|
+
*/
|
|
89
|
+
uniform vec2 uOffset;
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* Used by the depth-of-field effect to sample neighboring pixels.
|
|
71
|
-
*/
|
|
72
|
-
uniform vec2 uImageTexelSize;
|
|
91
|
+
/** Parallax displacement magnitude in UV space (e.g. 0.05 = 5%). */
|
|
92
|
+
uniform float uStrength;
|
|
73
93
|
|
|
74
|
-
|
|
94
|
+
/** Whether to use POM ray-marching instead of basic displacement. */
|
|
95
|
+
uniform bool uPomEnabled;
|
|
96
|
+
|
|
97
|
+
/** Number of ray-march steps for POM (runtime-adjustable). */
|
|
98
|
+
uniform int uPomSteps;
|
|
99
|
+
|
|
100
|
+
/** Smoothstep lower bound for depth contrast curve (depth-adaptive). */
|
|
101
|
+
uniform float uContrastLow;
|
|
102
|
+
|
|
103
|
+
/** Smoothstep upper bound for depth contrast curve (depth-adaptive). */
|
|
104
|
+
uniform float uContrastHigh;
|
|
105
|
+
|
|
106
|
+
/** Y-axis displacement multiplier (depth-adaptive). */
|
|
107
|
+
uniform float uVerticalReduction;
|
|
108
|
+
|
|
109
|
+
/** Depth threshold where DOF blur ramp begins (depth-adaptive). */
|
|
110
|
+
uniform float uDofStart;
|
|
111
|
+
|
|
112
|
+
/** Maximum DOF blur blend factor (depth-adaptive). */
|
|
113
|
+
uniform float uDofStrength;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Texel size for video/image texture (1.0 / videoResolution).
|
|
117
|
+
* Used by the depth-of-field effect to sample neighboring pixels.
|
|
118
|
+
*/
|
|
119
|
+
uniform vec2 uImageTexelSize;
|
|
120
|
+
|
|
121
|
+
// ---- Varyings ----
|
|
122
|
+
|
|
123
|
+
/** Interpolated texture coordinates from vertex shader (cover-fit transformed). */
|
|
124
|
+
in vec2 vUv;
|
|
125
|
+
|
|
126
|
+
/** Screen-space UV [0,1] -- always covers the full viewport. */
|
|
127
|
+
in vec2 vScreenUv;
|
|
128
|
+
|
|
129
|
+
/** Fragment output color. */
|
|
130
|
+
out vec4 fragColor;
|
|
131
|
+
|
|
132
|
+
// ---- Helper functions ----
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compute an edge fade factor that reduces displacement near UV
|
|
136
|
+
* boundaries.
|
|
137
|
+
*/
|
|
138
|
+
float edgeFade(vec2 uv) {
|
|
139
|
+
float margin = uStrength * 1.5;
|
|
140
|
+
float fadeX = smoothstep(0.0, margin, uv.x) * smoothstep(0.0, margin, 1.0 - uv.x);
|
|
141
|
+
float fadeY = smoothstep(0.0, margin, uv.y) * smoothstep(0.0, margin, 1.0 - uv.y);
|
|
142
|
+
return fadeX * fadeY;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Compute a subtle vignette darkening factor.
|
|
147
|
+
*/
|
|
148
|
+
float vignette(vec2 uv) {
|
|
149
|
+
float dist = length(uv - 0.5) * 1.4;
|
|
150
|
+
return 1.0 - pow(dist, 2.5);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- Displacement functions ----
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Basic UV displacement with edge fade.
|
|
157
|
+
*/
|
|
158
|
+
vec2 basicDisplace(vec2 uv) {
|
|
159
|
+
float depth = texture(uDepth, uv).r;
|
|
160
|
+
depth = smoothstep(uContrastLow, uContrastHigh, depth);
|
|
161
|
+
float displacement = (1.0 - depth) * uStrength;
|
|
162
|
+
displacement *= edgeFade(uv);
|
|
163
|
+
vec2 offset = uOffset * displacement;
|
|
164
|
+
offset.y *= uVerticalReduction;
|
|
165
|
+
return uv + offset;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Parallax Occlusion Mapping (POM) ray-marching displacement.
|
|
170
|
+
*/
|
|
171
|
+
vec2 pomDisplace(vec2 uv) {
|
|
172
|
+
float layerDepth = 1.0 / float(uPomSteps);
|
|
173
|
+
|
|
174
|
+
vec2 scaledOffset = uOffset;
|
|
175
|
+
scaledOffset.y *= uVerticalReduction;
|
|
176
|
+
|
|
177
|
+
vec2 deltaUV = scaledOffset * uStrength / float(uPomSteps);
|
|
178
|
+
|
|
179
|
+
float currentLayerDepth = 0.0;
|
|
180
|
+
vec2 currentUV = uv;
|
|
181
|
+
|
|
182
|
+
float fade = edgeFade(uv);
|
|
183
|
+
|
|
184
|
+
for (int i = 0; i < MAX_POM_STEPS; i++) {
|
|
185
|
+
if (i >= uPomSteps) break;
|
|
186
|
+
|
|
187
|
+
float rawDepth = texture(uDepth, currentUV).r;
|
|
188
|
+
rawDepth = smoothstep(uContrastLow, uContrastHigh, rawDepth);
|
|
189
|
+
float depthAtUV = 1.0 - rawDepth;
|
|
75
190
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
191
|
+
if (currentLayerDepth > depthAtUV) {
|
|
192
|
+
vec2 prevUV = currentUV - deltaUV;
|
|
193
|
+
float prevLayerDepth = currentLayerDepth - layerDepth;
|
|
194
|
+
float prevRaw = texture(uDepth, prevUV).r;
|
|
195
|
+
prevRaw = smoothstep(uContrastLow, uContrastHigh, prevRaw);
|
|
196
|
+
float prevDepthAtUV = 1.0 - prevRaw;
|
|
197
|
+
|
|
198
|
+
float afterDepth = depthAtUV - currentLayerDepth;
|
|
199
|
+
float beforeDepth = prevDepthAtUV - prevLayerDepth;
|
|
200
|
+
float t = afterDepth / (afterDepth - beforeDepth);
|
|
201
|
+
|
|
202
|
+
vec2 hitUV = mix(currentUV, prevUV, t);
|
|
203
|
+
return mix(uv, hitUV, fade);
|
|
204
|
+
}
|
|
86
205
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
* boundaries.
|
|
90
|
-
*/
|
|
91
|
-
float edgeFade(vec2 uv) {
|
|
92
|
-
float margin = uStrength * 1.5;
|
|
93
|
-
float fadeX = smoothstep(0.0, margin, uv.x) * smoothstep(0.0, margin, 1.0 - uv.x);
|
|
94
|
-
float fadeY = smoothstep(0.0, margin, uv.y) * smoothstep(0.0, margin, 1.0 - uv.y);
|
|
95
|
-
return fadeX * fadeY;
|
|
206
|
+
currentUV += deltaUV;
|
|
207
|
+
currentLayerDepth += layerDepth;
|
|
96
208
|
}
|
|
97
209
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
210
|
+
return mix(uv, currentUV, fade);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- Main ----
|
|
214
|
+
|
|
215
|
+
void main() {
|
|
216
|
+
vec2 displaced = uPomEnabled ? pomDisplace(vUv) : basicDisplace(vUv);
|
|
217
|
+
displaced = clamp(displaced, vec2(0.0), vec2(1.0));
|
|
218
|
+
|
|
219
|
+
vec4 color = texture(uImage, displaced);
|
|
220
|
+
|
|
221
|
+
// Depth-of-field hint
|
|
222
|
+
float dofDepth = texture(uDepth, displaced).r;
|
|
223
|
+
float dof = smoothstep(uDofStart, 1.0, dofDepth) * uDofStrength;
|
|
224
|
+
vec4 blurred = (
|
|
225
|
+
texture(uImage, displaced + vec2( uImageTexelSize.x, 0.0)) +
|
|
226
|
+
texture(uImage, displaced + vec2(-uImageTexelSize.x, 0.0)) +
|
|
227
|
+
texture(uImage, displaced + vec2( 0.0, uImageTexelSize.y)) +
|
|
228
|
+
texture(uImage, displaced + vec2( 0.0, -uImageTexelSize.y))
|
|
229
|
+
) * 0.25;
|
|
230
|
+
color = mix(color, blurred, dof);
|
|
231
|
+
|
|
232
|
+
// Vignette (screen-space, not texture-space)
|
|
233
|
+
color.rgb *= vignette(vScreenUv);
|
|
234
|
+
|
|
235
|
+
fragColor = color;
|
|
236
|
+
}
|
|
237
|
+
`,J={contrastLow:.05,contrastHigh:.95,verticalReduction:.5,dofStart:.6,dofStrength:.4},xt=["uRawDepth","uTexelSize","uSpatialSigma2"],bt={2:2.25,1:.5625};function St(o,e){const t=gt.replace("#version 300 es",`#version 300 es
|
|
238
|
+
#define BILATERAL_RADIUS ${e}`),i=W(o,o.VERTEX_SHADER,mt),n=W(o,o.FRAGMENT_SHADER,t),r=de(o,i,n),s=pe(o,r,xt),a=bt[e]??2.25;let l=null;const u={name:"bilateral-filter",program:r,uniforms:s,fbo:null,outputs:[],width:0,height:0,resize(h,f,c){},initFBO(h,f,c,g){l&&h.deleteFramebuffer(l),u.width=c,u.height=g,l=h.createFramebuffer(),u.fbo=l,h.bindFramebuffer(h.FRAMEBUFFER,l),h.framebufferTexture2D(h.FRAMEBUFFER,h.COLOR_ATTACHMENT0,h.TEXTURE_2D,f,0),h.bindFramebuffer(h.FRAMEBUFFER,null),h.useProgram(r),h.uniform1i(s.uRawDepth,2),h.uniform2f(s.uTexelSize,1/c,1/g),h.uniform1f(s.uSpatialSigma2,a)},execute(h,f,c,g,v,d,A,b){l&&(h.activeTexture(h.TEXTURE2),h.bindTexture(h.TEXTURE_2D,c),h.texSubImage2D(h.TEXTURE_2D,0,0,0,v,d,h.RED,h.UNSIGNED_BYTE,g),h.bindFramebuffer(h.FRAMEBUFFER,l),h.viewport(0,0,v,d),h.useProgram(r),h.bindVertexArray(f),h.drawArrays(h.TRIANGLE_STRIP,0,4),h.bindFramebuffer(h.FRAMEBUFFER,null),h.viewport(0,0,A,b))},dispose(h){l&&(h.deleteFramebuffer(l),l=null,u.fbo=null),h.deleteProgram(r)}};return u}const Tt=["uImage","uDepth","uOffset","uStrength","uPomEnabled","uPomSteps","uContrastLow","uContrastHigh","uVerticalReduction","uDofStart","uDofStrength","uImageTexelSize","uUvOffset","uUvScale"],yt=64;function Et(o){const e=vt.replace("#version 300 es",`#version 300 es
|
|
239
|
+
#define MAX_POM_STEPS ${yt}`),t=W(o,o.VERTEX_SHADER,pt),i=W(o,o.FRAGMENT_SHADER,e),n=de(o,t,i),r=pe(o,n,Tt);return{name:"parallax",program:n,uniforms:r,setStaticUniforms(s,a,l,u){s.useProgram(n),s.uniform1i(r.uImage,0),s.uniform1i(r.uDepth,1),s.uniform1f(r.uStrength,a.parallaxStrength),s.uniform1i(r.uPomEnabled,a.pomEnabled?1:0),s.uniform1i(r.uPomSteps,a.pomSteps),s.uniform1f(r.uContrastLow,a.contrastLow),s.uniform1f(r.uContrastHigh,a.contrastHigh),s.uniform1f(r.uVerticalReduction,a.verticalReduction),s.uniform1f(r.uDofStart,a.dofStart),s.uniform1f(r.uDofStrength,a.dofStrength),s.uniform2f(r.uImageTexelSize,1/l,1/u)},updateUvTransform(s,a,l){s.useProgram(n),s.uniform2f(r.uUvOffset,a[0],a[1]),s.uniform2f(r.uUvScale,l[0],l[1])},dispose(s){s.deleteProgram(n)}}}class Pt extends k{gl=null;quadVao=null;bilateralPass=null;parallaxPass=null;textures=new Ee;videoSlot;filteredDepthSlot;rawDepthSlot;config;constructor(e,t){super(e),this.videoSlot=this.textures.register("video"),this.filteredDepthSlot=this.textures.register("filteredDepth"),this.rawDepthSlot=this.textures.register("rawDepth"),this.config={parallaxStrength:t.parallaxStrength,pomEnabled:t.pomEnabled,pomSteps:t.pomSteps,overscanPadding:t.overscanPadding,contrastLow:t.contrastLow??J.contrastLow,contrastHigh:t.contrastHigh??J.contrastHigh,verticalReduction:t.verticalReduction??J.verticalReduction,dofStart:t.dofStart??J.dofStart,dofStrength:t.dofStrength??J.dofStrength};const i=this.canvas.getContext("webgl2",{antialias:!1,alpha:!1,desynchronized:!0,powerPreference:"high-performance"});if(!i)throw new Error("WebGL 2 is not supported.");this.gl=i,this.qualityParams=we(i,t.quality),"drawingBufferColorSpace"in i&&(i.drawingBufferColorSpace="srgb"),i.clearColor(0,0,0,1),i.pixelStorei(i.UNPACK_FLIP_Y_WEBGL,!0),this.initGPUResources(),this.setupResizeHandling()}initialize(e,t,i){const n=this.gl;n&&(this.disposeTextures(),this.videoAspect=e.videoWidth/e.videoHeight,this.clampDepthDimensions(t,i,this.qualityParams.depthMaxDim),this.videoSlot.texture=n.createTexture(),n.activeTexture(n.TEXTURE0+this.videoSlot.unit),n.bindTexture(n.TEXTURE_2D,this.videoSlot.texture),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MIN_FILTER,n.LINEAR),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MAG_FILTER,n.LINEAR),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_S,n.CLAMP_TO_EDGE),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_T,n.CLAMP_TO_EDGE),this.rawDepthSlot.texture=n.createTexture(),n.activeTexture(n.TEXTURE0+this.rawDepthSlot.unit),n.bindTexture(n.TEXTURE_2D,this.rawDepthSlot.texture),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MIN_FILTER,n.LINEAR),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MAG_FILTER,n.LINEAR),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_S,n.CLAMP_TO_EDGE),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_T,n.CLAMP_TO_EDGE),n.texStorage2D(n.TEXTURE_2D,1,n.R8,t,i),this.filteredDepthSlot.texture=n.createTexture(),n.activeTexture(n.TEXTURE0+this.filteredDepthSlot.unit),n.bindTexture(n.TEXTURE_2D,this.filteredDepthSlot.texture),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MIN_FILTER,n.LINEAR),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MAG_FILTER,n.LINEAR),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_S,n.CLAMP_TO_EDGE),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_T,n.CLAMP_TO_EDGE),n.texStorage2D(n.TEXTURE_2D,1,n.R8,t,i),this.bilateralPass&&this.filteredDepthSlot.texture&&this.bilateralPass.initFBO(n,this.filteredDepthSlot.texture,t,i),this.parallaxPass&&this.parallaxPass.setStaticUniforms(n,this.config,e.videoWidth,e.videoHeight),this.recalculateViewportLayout())}initGPUResources(){const e=this.gl;e&&(this.bilateralPass=St(e,this.qualityParams.bilateralRadius),this.parallaxPass=Et(e),this.quadVao=ye(e,this.parallaxPass.program),e.disable(e.DEPTH_TEST))}onRenderFrame(){const e=this.gl,t=this.playbackVideo;if(!(!e||!this.parallaxPass||!this.quadVao)&&!(!t||t.readyState<HTMLMediaElement.HAVE_CURRENT_DATA)){if(e.useProgram(this.parallaxPass.program),e.activeTexture(e.TEXTURE0+this.videoSlot.unit),e.bindTexture(e.TEXTURE_2D,this.videoSlot.texture),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,t),this.rvfcSupported||this.onDepthUpdate(t.currentTime),this.readInput){const i=this.readInput();e.uniform2f(this.parallaxPass.uniforms.uOffset,-i.x,i.y)}e.bindVertexArray(this.quadVao),e.drawArrays(e.TRIANGLE_STRIP,0,4)}}onDepthUpdate(e){const t=this.gl;if(!t||!this.readDepth||!this.rawDepthSlot.texture||!this.bilateralPass)return;const i=this.subsampleDepth(this.readDepth(e));this.bilateralPass.execute(t,this.quadVao,this.rawDepthSlot.texture,i,this.depthWidth,this.depthHeight,this.canvas.width,this.canvas.height)}recalculateViewportLayout(){const e=this.gl;if(!e)return;const{width:t,height:i}=this.getViewportSize(),n=Math.min(window.devicePixelRatio,this.qualityParams.dprCap),r=Math.round(t*n),s=Math.round(i*n);(this.canvas.width!==r||this.canvas.height!==s)&&(this.canvas.width=r,this.canvas.height=s,e.viewport(0,0,r,s)),this.computeCoverFitUV(this.config.parallaxStrength,this.config.overscanPadding),this.parallaxPass&&this.parallaxPass.updateUvTransform(e,this.uvOffset,this.uvScale)}disposeRenderer(){this.disposeTextures(),this.disposeGPUResources(),this.gl&&(this.gl.getExtension("WEBGL_lose_context")?.loseContext(),this.gl=null)}onContextRestored(){const e=this.canvas.getContext("webgl2");e&&(this.gl=e,e.clearColor(0,0,0,1),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!0),this.initGPUResources(),this.playbackVideo&&this.depthWidth>0&&this.initialize(this.playbackVideo,this.depthWidth,this.depthHeight),this.playbackVideo&&(this.animationFrameHandle=window.requestAnimationFrame(()=>this.onRenderFrame())))}disposeTextures(){const e=this.gl;e&&this.textures.disposeAll(e)}disposeGPUResources(){const e=this.gl;e&&(this.bilateralPass&&(this.bilateralPass.dispose(e),this.bilateralPass=null),this.parallaxPass&&(this.parallaxPass.dispose(e),this.parallaxPass=null),this.quadVao&&(e.deleteVertexArray(this.quadVao),this.quadVao=null))}}const Ue=new Float32Array([-1,-1,1,-1,-1,1,1,1]);function Re(o){const e=o.createBuffer({size:Ue.byteLength,usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST,mappedAtCreation:!0});return new Float32Array(e.getMappedRange()).set(Ue),e.unmap(),e}function me(o,e){const t=o.createBuffer({size:e.byteLength,usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST,mappedAtCreation:!0});return new Float32Array(t.getMappedRange()).set(e),t.unmap(),t}function At(o,e){const t=o.createBuffer({size:e.byteLength,usage:GPUBufferUsage.INDEX|GPUBufferUsage.COPY_DST,mappedAtCreation:!0});return new Uint16Array(t.getMappedRange()).set(e),t.unmap(),t}function B(o,e){const t=Math.ceil(e/16)*16;return o.createBuffer({size:t,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST})}function Fe(o){return o.createSampler({magFilter:"linear",minFilter:"linear",addressModeU:"clamp-to-edge",addressModeV:"clamp-to-edge"})}function wt(o){return o.createSampler({magFilter:"nearest",minFilter:"nearest",addressModeU:"clamp-to-edge",addressModeV:"clamp-to-edge"})}function Le(o,e,t,i=!0){o.queue.copyExternalImageToTexture({source:t,flipY:i},{texture:e},[t.videoWidth,t.videoHeight])}const N={arrayStride:8,attributes:[{shaderLocation:0,offset:0,format:"float32x2"}]},Be={arrayStride:8,attributes:[{shaderLocation:0,offset:0,format:"float32x2"}]},Dt={arrayStride:16,attributes:[{shaderLocation:0,offset:0,format:"float32x2"},{shaderLocation:1,offset:8,format:"float32x2"}]},Ut={arrayStride:24,attributes:[{shaderLocation:0,offset:0,format:"float32x2"},{shaderLocation:1,offset:8,format:"float32x3"},{shaderLocation:2,offset:20,format:"float32"}]},Rt=`// Parallax vertex shader — fullscreen quad pass-through with cover-fit UV transform.
|
|
240
|
+
|
|
241
|
+
struct Uniforms {
|
|
242
|
+
uvOffset: vec2f,
|
|
243
|
+
uvScale: vec2f,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
247
|
+
|
|
248
|
+
struct VertexOutput {
|
|
249
|
+
@builtin(position) position: vec4f,
|
|
250
|
+
@location(0) uv: vec2f,
|
|
251
|
+
@location(1) screenUv: vec2f,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
@vertex
|
|
255
|
+
fn vs_main(@location(0) aPosition: vec2f) -> VertexOutput {
|
|
256
|
+
var out: VertexOutput;
|
|
257
|
+
let baseUv = aPosition * 0.5 + 0.5;
|
|
258
|
+
out.uv = baseUv * u.uvScale + u.uvOffset;
|
|
259
|
+
out.screenUv = baseUv;
|
|
260
|
+
out.position = vec4f(aPosition, 0.0, 1.0);
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
`,Ft=`// Parallax fragment shader — per-pixel depth-based displacement with POM, DOF, vignette.
|
|
264
|
+
// MAX_POM_STEPS is an override constant set at pipeline creation.
|
|
265
|
+
//
|
|
266
|
+
// Uses textureSampleLevel (explicit LOD 0) instead of textureSample throughout
|
|
267
|
+
// because POM ray-marching requires texture reads in non-uniform control flow
|
|
268
|
+
// (loop iterations and conditional branches that depend on texture values).
|
|
269
|
+
// WGSL requires textureSample to be called only from uniform control flow.
|
|
270
|
+
|
|
271
|
+
override MAX_POM_STEPS: i32 = 64;
|
|
272
|
+
|
|
273
|
+
struct Uniforms {
|
|
274
|
+
offset: vec2f,
|
|
275
|
+
strength: f32,
|
|
276
|
+
pomEnabled: u32, // bool as u32 (0 = false, 1 = true)
|
|
277
|
+
pomSteps: i32,
|
|
278
|
+
contrastLow: f32,
|
|
279
|
+
contrastHigh: f32,
|
|
280
|
+
verticalReduction: f32,
|
|
281
|
+
dofStart: f32,
|
|
282
|
+
dofStrength: f32,
|
|
283
|
+
imageTexelSize: vec2f,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Binding 0 is the vertex uniform buffer (uvOffset, uvScale).
|
|
287
|
+
@group(0) @binding(1) var<uniform> u: Uniforms;
|
|
288
|
+
@group(0) @binding(2) var imageTex: texture_2d<f32>;
|
|
289
|
+
@group(0) @binding(3) var imageSampler: sampler;
|
|
290
|
+
@group(0) @binding(4) var depthTex: texture_2d<f32>;
|
|
291
|
+
@group(0) @binding(5) var depthSampler: sampler;
|
|
292
|
+
|
|
293
|
+
// ---- Helper functions ----
|
|
294
|
+
|
|
295
|
+
fn edgeFade(uv: vec2f) -> f32 {
|
|
296
|
+
let margin = u.strength * 1.5;
|
|
297
|
+
let fadeX = smoothstep(0.0, margin, uv.x) * smoothstep(0.0, margin, 1.0 - uv.x);
|
|
298
|
+
let fadeY = smoothstep(0.0, margin, uv.y) * smoothstep(0.0, margin, 1.0 - uv.y);
|
|
299
|
+
return fadeX * fadeY;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fn vignette(uv: vec2f) -> f32 {
|
|
303
|
+
let dist = length(uv - 0.5) * 1.4;
|
|
304
|
+
return 1.0 - pow(dist, 2.5);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---- Displacement functions ----
|
|
308
|
+
|
|
309
|
+
fn basicDisplace(uv: vec2f) -> vec2f {
|
|
310
|
+
var depth = textureSampleLevel(depthTex, depthSampler, uv, 0.0).r;
|
|
311
|
+
depth = smoothstep(u.contrastLow, u.contrastHigh, depth);
|
|
312
|
+
var displacement = (1.0 - depth) * u.strength;
|
|
313
|
+
displacement *= edgeFade(uv);
|
|
314
|
+
var off = u.offset * displacement;
|
|
315
|
+
off.y *= u.verticalReduction;
|
|
316
|
+
return uv + off;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fn pomDisplace(uv: vec2f) -> vec2f {
|
|
320
|
+
let layerD = 1.0 / f32(u.pomSteps);
|
|
321
|
+
|
|
322
|
+
var scaledOffset = u.offset;
|
|
323
|
+
scaledOffset.y *= u.verticalReduction;
|
|
324
|
+
|
|
325
|
+
let deltaUV = scaledOffset * u.strength / f32(u.pomSteps);
|
|
326
|
+
|
|
327
|
+
var currentLayerDepth: f32 = 0.0;
|
|
328
|
+
var currentUV = uv;
|
|
329
|
+
|
|
330
|
+
let fade = edgeFade(uv);
|
|
331
|
+
|
|
332
|
+
for (var i: i32 = 0; i < MAX_POM_STEPS; i++) {
|
|
333
|
+
if (i >= u.pomSteps) { break; }
|
|
334
|
+
|
|
335
|
+
var rawDepth = textureSampleLevel(depthTex, depthSampler, currentUV, 0.0).r;
|
|
336
|
+
rawDepth = smoothstep(u.contrastLow, u.contrastHigh, rawDepth);
|
|
337
|
+
let depthAtUV = 1.0 - rawDepth;
|
|
338
|
+
|
|
339
|
+
if (currentLayerDepth > depthAtUV) {
|
|
340
|
+
let prevUV = currentUV - deltaUV;
|
|
341
|
+
let prevLayerDepth = currentLayerDepth - layerD;
|
|
342
|
+
var prevRaw = textureSampleLevel(depthTex, depthSampler, prevUV, 0.0).r;
|
|
343
|
+
prevRaw = smoothstep(u.contrastLow, u.contrastHigh, prevRaw);
|
|
344
|
+
let prevDepthAtUV = 1.0 - prevRaw;
|
|
345
|
+
|
|
346
|
+
let afterDepth = depthAtUV - currentLayerDepth;
|
|
347
|
+
let beforeDepth = prevDepthAtUV - prevLayerDepth;
|
|
348
|
+
let t = afterDepth / (afterDepth - beforeDepth);
|
|
349
|
+
|
|
350
|
+
let hitUV = mix(currentUV, prevUV, t);
|
|
351
|
+
return mix(uv, hitUV, fade);
|
|
352
|
+
}
|
|
105
353
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Basic UV displacement with edge fade.
|
|
110
|
-
*/
|
|
111
|
-
vec2 basicDisplace(vec2 uv) {
|
|
112
|
-
float depth = texture(uDepth, uv).r;
|
|
113
|
-
depth = smoothstep(uContrastLow, uContrastHigh, depth);
|
|
114
|
-
float displacement = (1.0 - depth) * uStrength;
|
|
115
|
-
displacement *= edgeFade(uv);
|
|
116
|
-
vec2 offset = uOffset * displacement;
|
|
117
|
-
offset.y *= uVerticalReduction;
|
|
118
|
-
return uv + offset;
|
|
354
|
+
currentUV += deltaUV;
|
|
355
|
+
currentLayerDepth += layerD;
|
|
119
356
|
}
|
|
120
357
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
358
|
+
return mix(uv, currentUV, fade);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---- Main ----
|
|
362
|
+
|
|
363
|
+
@fragment
|
|
364
|
+
fn fs_main(
|
|
365
|
+
@location(0) uv: vec2f,
|
|
366
|
+
@location(1) screenUv: vec2f
|
|
367
|
+
) -> @location(0) vec4f {
|
|
368
|
+
var displaced: vec2f;
|
|
369
|
+
if (u.pomEnabled != 0u) {
|
|
370
|
+
displaced = pomDisplace(uv);
|
|
371
|
+
} else {
|
|
372
|
+
displaced = basicDisplace(uv);
|
|
373
|
+
}
|
|
374
|
+
displaced = clamp(displaced, vec2f(0.0), vec2f(1.0));
|
|
375
|
+
|
|
376
|
+
var color = textureSampleLevel(imageTex, imageSampler, displaced, 0.0);
|
|
377
|
+
|
|
378
|
+
// Depth-of-field hint
|
|
379
|
+
let dofDepth = textureSampleLevel(depthTex, depthSampler, displaced, 0.0).r;
|
|
380
|
+
let dof = smoothstep(u.dofStart, 1.0, dofDepth) * u.dofStrength;
|
|
381
|
+
let ts = u.imageTexelSize;
|
|
382
|
+
let blurred = (
|
|
383
|
+
textureSampleLevel(imageTex, imageSampler, displaced + vec2f( ts.x, 0.0), 0.0) +
|
|
384
|
+
textureSampleLevel(imageTex, imageSampler, displaced + vec2f(-ts.x, 0.0), 0.0) +
|
|
385
|
+
textureSampleLevel(imageTex, imageSampler, displaced + vec2f( 0.0, ts.y), 0.0) +
|
|
386
|
+
textureSampleLevel(imageTex, imageSampler, displaced + vec2f( 0.0, -ts.y), 0.0)
|
|
387
|
+
) * 0.25;
|
|
388
|
+
color = mix(color, blurred, dof);
|
|
389
|
+
|
|
390
|
+
// Vignette (screen-space, not texture-space)
|
|
391
|
+
color = vec4f(color.rgb * vignette(screenUv), color.a);
|
|
392
|
+
|
|
393
|
+
return color;
|
|
394
|
+
}
|
|
395
|
+
`,Lt=`// Bilateral filter vertex shader — simple pass-through, no cover-fit transform.
|
|
396
|
+
|
|
397
|
+
struct VertexOutput {
|
|
398
|
+
@builtin(position) position: vec4f,
|
|
399
|
+
@location(0) uv: vec2f,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
@vertex
|
|
403
|
+
fn vs_main(@location(0) aPosition: vec2f) -> VertexOutput {
|
|
404
|
+
var out: VertexOutput;
|
|
405
|
+
out.uv = aPosition * 0.5 + 0.5;
|
|
406
|
+
out.position = vec4f(aPosition, 0.0, 1.0);
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
`,Bt=`// Bilateral filter fragment shader — edge-preserving depth smoothing.
|
|
410
|
+
// BILATERAL_RADIUS is an override constant set at pipeline creation.
|
|
411
|
+
//
|
|
412
|
+
// Uses textureSampleLevel (explicit LOD 0) instead of textureSample throughout
|
|
413
|
+
// for consistency with other WGSL shaders and to prevent non-uniform control
|
|
414
|
+
// flow issues if the shader is modified in the future.
|
|
415
|
+
|
|
416
|
+
override BILATERAL_RADIUS: i32 = 2;
|
|
417
|
+
|
|
418
|
+
struct Uniforms {
|
|
419
|
+
texelSize: vec2f,
|
|
420
|
+
spatialSigma2: f32,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
424
|
+
@group(0) @binding(1) var rawDepthTex: texture_2d<f32>;
|
|
425
|
+
@group(0) @binding(2) var rawDepthSampler: sampler;
|
|
426
|
+
|
|
427
|
+
@fragment
|
|
428
|
+
fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
|
|
429
|
+
let depthSigma2: f32 = 0.01; // 0.1^2
|
|
430
|
+
|
|
431
|
+
let center = textureSampleLevel(rawDepthTex, rawDepthSampler, uv, 0.0).r;
|
|
432
|
+
var totalWeight: f32 = 1.0;
|
|
433
|
+
var totalDepth: f32 = center;
|
|
434
|
+
|
|
435
|
+
for (var dy: i32 = -BILATERAL_RADIUS; dy <= BILATERAL_RADIUS; dy++) {
|
|
436
|
+
for (var dx: i32 = -BILATERAL_RADIUS; dx <= BILATERAL_RADIUS; dx++) {
|
|
437
|
+
if (dx == 0 && dy == 0) { continue; }
|
|
438
|
+
|
|
439
|
+
let offset = vec2f(f32(dx), f32(dy)) * u.texelSize;
|
|
440
|
+
let neighbor = textureSampleLevel(rawDepthTex, rawDepthSampler, uv + offset, 0.0).r;
|
|
441
|
+
|
|
442
|
+
let spatialDist2 = f32(dx * dx + dy * dy);
|
|
443
|
+
let depthDiff = neighbor - center;
|
|
444
|
+
let w = exp(-spatialDist2 / u.spatialSigma2 - (depthDiff * depthDiff) / depthSigma2);
|
|
445
|
+
|
|
446
|
+
totalWeight += w;
|
|
447
|
+
totalDepth += neighbor * w;
|
|
161
448
|
}
|
|
162
|
-
|
|
163
|
-
return mix(uv, currentUV, fade);
|
|
164
449
|
}
|
|
165
450
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
void main() {
|
|
169
|
-
vec2 displaced = uPomEnabled ? pomDisplace(vUv) : basicDisplace(vUv);
|
|
170
|
-
displaced = clamp(displaced, vec2(0.0), vec2(1.0));
|
|
171
|
-
|
|
172
|
-
vec4 color = texture(uImage, displaced);
|
|
173
|
-
|
|
174
|
-
// Depth-of-field hint
|
|
175
|
-
float dofDepth = texture(uDepth, displaced).r;
|
|
176
|
-
float dof = smoothstep(uDofStart, 1.0, dofDepth) * uDofStrength;
|
|
177
|
-
vec4 blurred = (
|
|
178
|
-
texture(uImage, displaced + vec2( uImageTexelSize.x, 0.0)) +
|
|
179
|
-
texture(uImage, displaced + vec2(-uImageTexelSize.x, 0.0)) +
|
|
180
|
-
texture(uImage, displaced + vec2( 0.0, uImageTexelSize.y)) +
|
|
181
|
-
texture(uImage, displaced + vec2( 0.0, -uImageTexelSize.y))
|
|
182
|
-
) * 0.25;
|
|
183
|
-
color = mix(color, blurred, dof);
|
|
184
|
-
|
|
185
|
-
// Vignette (screen-space, not texture-space)
|
|
186
|
-
color.rgb *= vignette(vScreenUv);
|
|
187
|
-
|
|
188
|
-
fragColor = color;
|
|
189
|
-
}
|
|
190
|
-
`,q={contrastLow:.05,contrastHigh:.95,verticalReduction:.5,dofStart:.6,dofStrength:.4};function Tt(n,t,e){const i=n.createShader(t);if(!i)throw new Error("Failed to create shader.");if(n.shaderSource(i,e),n.compileShader(i),!n.getShaderParameter(i,n.COMPILE_STATUS)){const r=n.getShaderInfoLog(i)??"";throw n.deleteShader(i),new Error(`Shader compilation failed:
|
|
191
|
-
${r}`)}return i}function Gt(n,t,e){const i=n.createProgram();if(!i)throw new Error("Failed to create program.");if(n.attachShader(i,t),n.attachShader(i,e),n.linkProgram(i),!n.getProgramParameter(i,n.LINK_STATUS)){const r=n.getProgramInfoLog(i)??"";throw n.deleteProgram(i),new Error(`Program linking failed:
|
|
192
|
-
${r}`)}return n.detachShader(i,t),n.detachShader(i,e),n.deleteShader(t),n.deleteShader(e),i}function jt(n,t){return{uImage:n.getUniformLocation(t,"uImage"),uDepth:n.getUniformLocation(t,"uDepth"),uOffset:n.getUniformLocation(t,"uOffset"),uStrength:n.getUniformLocation(t,"uStrength"),uPomEnabled:n.getUniformLocation(t,"uPomEnabled"),uPomSteps:n.getUniformLocation(t,"uPomSteps"),uContrastLow:n.getUniformLocation(t,"uContrastLow"),uContrastHigh:n.getUniformLocation(t,"uContrastHigh"),uVerticalReduction:n.getUniformLocation(t,"uVerticalReduction"),uDofStart:n.getUniformLocation(t,"uDofStart"),uDofStrength:n.getUniformLocation(t,"uDofStrength"),uImageTexelSize:n.getUniformLocation(t,"uImageTexelSize"),uUvOffset:n.getUniformLocation(t,"uUvOffset"),uUvScale:n.getUniformLocation(t,"uUvScale")}}class Z{static RESIZE_DEBOUNCE_MS=100;static MAX_POM_STEPS=64;canvas;gl=null;program=null;uniforms=null;vao=null;videoTexture=null;depthTexture=null;container;depthWidth=0;depthHeight=0;videoAspect=1.7777777777777777;readDepth=null;readInput=null;playbackVideo=null;onVideoFrame=null;animationFrameHandle=0;rvfcHandle=0;rvfcSupported=!1;resizeObserver=null;resizeTimer=null;uvOffset=[0,0];uvScale=[1,1];config;constructor(t,e){this.container=t,this.config={parallaxStrength:e.parallaxStrength,pomEnabled:e.pomEnabled,pomSteps:e.pomSteps,overscanPadding:e.overscanPadding,contrastLow:e.contrastLow??q.contrastLow,contrastHigh:e.contrastHigh??q.contrastHigh,verticalReduction:e.verticalReduction??q.verticalReduction,dofStart:e.dofStart??q.dofStart,dofStrength:e.dofStrength??q.dofStrength},this.canvas=document.createElement("canvas");const i=this.canvas.getContext("webgl2",{antialias:!1,alpha:!1,desynchronized:!0,powerPreference:"high-performance"});if(!i)throw new Error("WebGL 2 is not supported.");this.gl=i,"drawingBufferColorSpace"in i&&(i.drawingBufferColorSpace="srgb"),i.clearColor(0,0,0,1),i.pixelStorei(i.UNPACK_FLIP_Y_WEBGL,!0),this.container.appendChild(this.canvas),this.initGPUResources(),this.setupResizeHandling(),this.canvas.addEventListener("webglcontextlost",this.handleContextLost),this.canvas.addEventListener("webglcontextrestored",this.handleContextRestored)}initialize(t,e,i){const r=this.gl;r&&(this.disposeTextures(),this.videoAspect=t.videoWidth/t.videoHeight,this.depthWidth=e,this.depthHeight=i,this.videoTexture=r.createTexture(),r.activeTexture(r.TEXTURE0),r.bindTexture(r.TEXTURE_2D,this.videoTexture),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),this.depthTexture=r.createTexture(),r.activeTexture(r.TEXTURE1),r.bindTexture(r.TEXTURE_2D,this.depthTexture),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),r.texStorage2D(r.TEXTURE_2D,1,r.R8,e,i),this.program&&this.uniforms&&(r.useProgram(this.program),r.uniform1i(this.uniforms.uImage,0),r.uniform1i(this.uniforms.uDepth,1),r.uniform1f(this.uniforms.uStrength,this.config.parallaxStrength),r.uniform1i(this.uniforms.uPomEnabled,this.config.pomEnabled?1:0),r.uniform1i(this.uniforms.uPomSteps,this.config.pomSteps),r.uniform1f(this.uniforms.uContrastLow,this.config.contrastLow),r.uniform1f(this.uniforms.uContrastHigh,this.config.contrastHigh),r.uniform1f(this.uniforms.uVerticalReduction,this.config.verticalReduction),r.uniform1f(this.uniforms.uDofStart,this.config.dofStart),r.uniform1f(this.uniforms.uDofStrength,this.config.dofStrength),r.uniform2f(this.uniforms.uImageTexelSize,1/t.videoWidth,1/t.videoHeight)),this.recalculateViewportLayout())}start(t,e,i,r){this.stop(),this.playbackVideo=t,this.readDepth=e,this.readInput=i,this.onVideoFrame=r??null,this.rvfcSupported=Z.isRVFCSupported(),this.rvfcSupported&&(this.rvfcHandle=t.requestVideoFrameCallback(this.videoFrameLoop)),this.animationFrameHandle=window.requestAnimationFrame(this.renderLoop)}stop(){this.animationFrameHandle&&(window.cancelAnimationFrame(this.animationFrameHandle),this.animationFrameHandle=0),this.rvfcHandle&&this.playbackVideo&&(this.playbackVideo.cancelVideoFrameCallback(this.rvfcHandle),this.rvfcHandle=0),this.playbackVideo=null,this.readDepth=null,this.readInput=null,this.onVideoFrame=null,this.rvfcSupported=!1}dispose(){this.stop(),this.disposeTextures(),this.disposeGPUResources(),this.canvas.removeEventListener("webglcontextlost",this.handleContextLost),this.canvas.removeEventListener("webglcontextrestored",this.handleContextRestored),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),window.removeEventListener("resize",this.scheduleResizeRecalculate),this.resizeTimer!==null&&(window.clearTimeout(this.resizeTimer),this.resizeTimer=null)}initGPUResources(){const t=this.gl;if(!t)return;const e=Wt.replace("#version 300 es",`#version 300 es
|
|
193
|
-
#define MAX_POM_STEPS ${Z.MAX_POM_STEPS}`),i=Tt(t,t.VERTEX_SHADER,zt),r=Tt(t,t.FRAGMENT_SHADER,e);this.program=Gt(t,i,r),this.uniforms=jt(t,this.program);const o=new Float32Array([-1,-1,1,-1,-1,1,1,1]);this.vao=t.createVertexArray(),t.bindVertexArray(this.vao);const s=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,s),t.bufferData(t.ARRAY_BUFFER,o,t.STATIC_DRAW);const a=t.getAttribLocation(this.program,"aPosition");t.enableVertexAttribArray(a),t.vertexAttribPointer(a,2,t.FLOAT,!1,0,0),t.bindVertexArray(null),t.disable(t.DEPTH_TEST)}static isRVFCSupported(){return"requestVideoFrameCallback"in HTMLVideoElement.prototype}videoFrameLoop=(t,e)=>{const i=this.playbackVideo;if(!i)return;this.rvfcHandle=i.requestVideoFrameCallback(this.videoFrameLoop);const r=e.mediaTime??i.currentTime;this.updateDepthTexture(r),this.onVideoFrame&&this.onVideoFrame(r,e.presentedFrames??0)};renderLoop=()=>{this.animationFrameHandle=window.requestAnimationFrame(this.renderLoop);const t=this.gl,e=this.playbackVideo;if(!(!t||!this.program||!this.uniforms||!this.vao)&&!(!e||e.readyState<HTMLMediaElement.HAVE_CURRENT_DATA)){if(t.useProgram(this.program),t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,this.videoTexture),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,e),this.rvfcSupported||this.updateDepthTexture(e.currentTime),this.readInput){const i=this.readInput();t.uniform2f(this.uniforms.uOffset,-i.x,i.y)}t.bindVertexArray(this.vao),t.drawArrays(t.TRIANGLE_STRIP,0,4)}};updateDepthTexture(t){const e=this.gl;if(!e||!this.readDepth||!this.depthTexture)return;const i=this.readDepth(t);e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,this.depthTexture),e.texSubImage2D(e.TEXTURE_2D,0,0,0,this.depthWidth,this.depthHeight,e.RED,e.UNSIGNED_BYTE,i)}setupResizeHandling(){typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(()=>{this.scheduleResizeRecalculate()}),this.resizeObserver.observe(this.container)),window.addEventListener("resize",this.scheduleResizeRecalculate),this.recalculateViewportLayout()}scheduleResizeRecalculate=()=>{this.resizeTimer!==null&&window.clearTimeout(this.resizeTimer),this.resizeTimer=window.setTimeout(()=>{this.resizeTimer=null,this.recalculateViewportLayout()},Z.RESIZE_DEBOUNCE_MS)};recalculateViewportLayout(){const t=this.gl;if(!t)return;const{width:e,height:i}=this.getViewportSize(),r=Math.min(window.devicePixelRatio,2),o=Math.round(e*r),s=Math.round(i*r);(this.canvas.width!==o||this.canvas.height!==s)&&(this.canvas.width=o,this.canvas.height=s,t.viewport(0,0,o,s));const a=e/i,h=this.config.parallaxStrength+this.config.overscanPadding;let l=1,u=1;a>this.videoAspect?u=this.videoAspect/a:l=a/this.videoAspect;const f=1+h*2;l/=f,u/=f,this.uvOffset=[(1-l)/2,(1-u)/2],this.uvScale=[l,u],this.program&&this.uniforms&&(t.useProgram(this.program),t.uniform2f(this.uniforms.uUvOffset,this.uvOffset[0],this.uvOffset[1]),t.uniform2f(this.uniforms.uUvScale,this.uvScale[0],this.uvScale[1]))}getViewportSize(){const t=Math.max(1,Math.round(this.container.clientWidth||window.innerWidth)),e=Math.max(1,Math.round(this.container.clientHeight||window.innerHeight));return{width:t,height:e}}handleContextLost=t=>{t.preventDefault(),this.animationFrameHandle&&(window.cancelAnimationFrame(this.animationFrameHandle),this.animationFrameHandle=0)};handleContextRestored=()=>{const t=this.canvas.getContext("webgl2");t&&(this.gl=t,t.clearColor(0,0,0,1),t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,!0),this.initGPUResources(),this.playbackVideo&&this.depthWidth>0&&this.initialize(this.playbackVideo,this.depthWidth,this.depthHeight),this.playbackVideo&&(this.animationFrameHandle=window.requestAnimationFrame(this.renderLoop)))};disposeTextures(){const t=this.gl;t&&(this.videoTexture&&(t.deleteTexture(this.videoTexture),this.videoTexture=null),this.depthTexture&&(t.deleteTexture(this.depthTexture),this.depthTexture=null))}disposeGPUResources(){const t=this.gl;t&&(this.program&&(t.deleteProgram(this.program),this.program=null),this.vao&&(t.deleteVertexArray(this.vao),this.vao=null),this.uniforms=null)}}const B={parallaxX:.4,parallaxY:1,parallaxMax:30,overscan:.05,autoplay:!0,loop:!0,muted:!0};let Yt=class Lt{constructor(t,e=.08,i=.06){this.host=t,this.lerpFactor=e,this.motionLerpFactor=i,this.host.addEventListener("mousemove",this.handleMouseMove),this.host.addEventListener("mouseleave",this.resetPointerTarget),this.host.addEventListener("touchstart",this.handleTouchStart,{passive:!0}),this.host.addEventListener("touchmove",this.handleTouchMove,{passive:!0}),this.host.addEventListener("touchend",this.handleTouchEnd,{passive:!0}),this.host.addEventListener("touchcancel",this.handleTouchEnd,{passive:!0})}pointerTarget={x:0,y:0};motionTarget={x:0,y:0};smoothedOutput={x:0,y:0};usingMotionInput=!1;motionListenerAttached=!1;motionRequested=!1;touchActive=!1;touchAnchorX=0;touchAnchorY=0;lerpFactor;motionLerpFactor;static TOUCH_DRAG_RANGE=100;update(){const t=this.touchActive?this.pointerTarget:this.usingMotionInput?this.motionTarget:this.pointerTarget,e=this.usingMotionInput&&!this.touchActive?this.motionLerpFactor:this.lerpFactor;return this.smoothedOutput.x=ot(this.smoothedOutput.x,t.x,e),this.smoothedOutput.y=ot(this.smoothedOutput.y,t.y,e),this.smoothedOutput}dispose(){this.host.removeEventListener("mousemove",this.handleMouseMove),this.host.removeEventListener("mouseleave",this.resetPointerTarget),this.host.removeEventListener("touchstart",this.handleTouchStart),this.host.removeEventListener("touchmove",this.handleTouchMove),this.host.removeEventListener("touchend",this.handleTouchEnd),this.host.removeEventListener("touchcancel",this.handleTouchEnd),this.motionListenerAttached&&(window.removeEventListener("deviceorientation",this.handleDeviceOrientation),this.motionListenerAttached=!1)}handleMouseMove=t=>{const e=this.host.getBoundingClientRect(),i=(t.clientX-e.left)/e.width*2-1,r=(t.clientY-e.top)/e.height*2-1;this.pointerTarget.x=N(i,-1,1),this.pointerTarget.y=N(r,-1,1)};resetPointerTarget=()=>{this.pointerTarget.x=0,this.pointerTarget.y=0};handleTouchStart=t=>{const e=t.touches[0];e&&(this.touchActive=!0,this.touchAnchorX=e.clientX,this.touchAnchorY=e.clientY,this.pointerTarget.x=0,this.pointerTarget.y=0,this.motionRequested||(this.motionRequested=!0,this.requestMotionPermission()))};handleTouchMove=t=>{const e=t.touches[0];if(!e)return;const i=e.clientX-this.touchAnchorX,r=e.clientY-this.touchAnchorY,o=Lt.TOUCH_DRAG_RANGE;this.pointerTarget.x=N(i/o,-1,1),this.pointerTarget.y=N(r/o,-1,1)};handleTouchEnd=()=>{this.touchActive=!1,this.pointerTarget.x=0,this.pointerTarget.y=0};async requestMotionPermission(){if(typeof DeviceOrientationEvent>"u")return;const t=DeviceOrientationEvent;if(typeof t.requestPermission=="function")try{if(await t.requestPermission()!=="granted")return}catch{return}this.motionListenerAttached||(window.addEventListener("deviceorientation",this.handleDeviceOrientation),this.motionListenerAttached=!0),this.usingMotionInput=!0}handleDeviceOrientation=t=>{const e=N((t.gamma??0)/45,-1,1),i=N((t.beta??0)/45,-1,1);this.motionTarget.x=ot(this.motionTarget.x,e,this.motionLerpFactor),this.motionTarget.y=ot(this.motionTarget.y,i,this.motionLerpFactor)}};class rt extends HTMLElement{static TAG_NAME="layershift-parallax";static get observedAttributes(){return["src","depth-src","depth-meta","parallax-x","parallax-y","parallax-max","layers","overscan","autoplay","loop","muted"]}shadow;container=null;renderer=null;inputHandler=null;depthWorker=null;video=null;initialized=!1;abortController=null;loopCount=0;constructor(){super(),this.shadow=this.attachShadow({mode:"open"})}getAttrFloat(t,e){const i=this.getAttribute(t);if(i===null)return e;const r=parseFloat(i);return Number.isFinite(r)?r:e}getAttrBool(t,e){if(!this.hasAttribute(t))return e;const i=this.getAttribute(t);return!(i==="false"||i==="0")}get parallaxX(){return this.getAttrFloat("parallax-x",B.parallaxX)}get parallaxY(){return this.getAttrFloat("parallax-y",B.parallaxY)}get parallaxMax(){return this.getAttrFloat("parallax-max",B.parallaxMax)}get overscan(){return this.getAttrFloat("overscan",B.overscan)}get shouldAutoplay(){return this.getAttrBool("autoplay",B.autoplay)}get shouldLoop(){return this.getAttrBool("loop",B.loop)}get shouldMute(){return this.getAttrBool("muted",B.muted)}emit(t,e){this.dispatchEvent(new CustomEvent(t,{detail:e,bubbles:!0,composed:!0}))}attachVideoEventListeners(t){t.addEventListener("play",()=>{this.emit("layershift-parallax:play",{currentTime:t.currentTime})}),t.addEventListener("pause",()=>{this.emit("layershift-parallax:pause",{currentTime:t.currentTime})}),t.addEventListener("ended",()=>{t.loop&&(this.loopCount+=1,this.emit("layershift-parallax:loop",{loopCount:this.loopCount}))})}connectedCallback(){this.setupShadowDOM(),this.init()}disconnectedCallback(){this.dispose()}attributeChangedCallback(t,e,i){["src","depth-src","depth-meta"].includes(t)&&(this.initialized?(this.dispose(),this.setupShadowDOM(),this.init()):this.isConnected&&this.getAttribute("src")&&this.getAttribute("depth-src")&&this.getAttribute("depth-meta")&&this.init())}setupShadowDOM(){this.shadow.innerHTML="";const t=document.createElement("style");t.textContent=`
|
|
451
|
+
return vec4f(totalDepth / totalWeight, 0.0, 0.0, 1.0);
|
|
452
|
+
}
|
|
453
|
+
`,$={contrastLow:.05,contrastHigh:.95,verticalReduction:.5,dofStart:.6,dofStrength:.4},Mt={2:2.25,1:.5625},Ct=64,_t=16,Vt=16,Me=48;class Gt extends k{device;context=null;canvasFormat;config;quadBuffer=null;linearSampler=null;bilateralPipeline=null;bilateralBindGroupLayout=null;bilateralUniformBuffer=null;bilateralBindGroup=null;rawDepthTexture=null;rawDepthView=null;filteredDepthTexture=null;filteredDepthView=null;parallaxPipeline=null;parallaxBindGroupLayout=null;vertexUniformBuffer=null;fragmentUniformBuffer=null;parallaxBindGroup=null;videoTexture=null;videoTextureView=null;offsetData=new Float32Array(2);depthFlipBuffer=null;constructor(e,t,i,n){super(e),this.device=i,this.config={parallaxStrength:t.parallaxStrength,pomEnabled:t.pomEnabled,pomSteps:t.pomSteps,overscanPadding:t.overscanPadding,contrastLow:t.contrastLow??$.contrastLow,contrastHigh:t.contrastHigh??$.contrastHigh,verticalReduction:t.verticalReduction??$.verticalReduction,dofStart:t.dofStart??$.dofStart,dofStrength:t.dofStrength??$.dofStrength},this.qualityParams=De(n,t.quality),this.context=this.canvas.getContext("webgpu"),this.canvasFormat=navigator.gpu.getPreferredCanvasFormat(),this.context.configure({device:i,format:this.canvasFormat,alphaMode:"opaque"}),this.quadBuffer=Re(i),this.linearSampler=Fe(i),this.createBilateralPipeline(),this.createParallaxPipeline(),this.bilateralUniformBuffer=B(i,_t),this.vertexUniformBuffer=B(i,Vt),this.fragmentUniformBuffer=B(i,Me),i.lost.then(r=>{console.error(`WebGPU device lost (${r.reason}): ${r.message}`),this.stop()}),this.setupResizeHandling()}initialize(e,t,i){this.disposeTextures(),this.videoAspect=e.videoWidth/e.videoHeight,this.clampDepthDimensions(t,i,this.qualityParams.depthMaxDim),this.videoTexture=this.device.createTexture({size:[e.videoWidth,e.videoHeight],format:"rgba8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT}),this.videoTextureView=this.videoTexture.createView(),this.rawDepthTexture=this.device.createTexture({size:[this.depthWidth,this.depthHeight],format:"r8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST}),this.rawDepthView=this.rawDepthTexture.createView(),this.filteredDepthTexture=this.device.createTexture({size:[this.depthWidth,this.depthHeight],format:"r8unorm",usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),this.filteredDepthView=this.filteredDepthTexture.createView(),this.depthFlipBuffer=new Uint8Array(this.depthWidth*this.depthHeight);const n=Mt[this.qualityParams.bilateralRadius]??2.25;this.device.queue.writeBuffer(this.bilateralUniformBuffer,0,new Float32Array([1/this.depthWidth,1/this.depthHeight,n,0])),this.writeStaticFragmentUniforms(e.videoWidth,e.videoHeight),this.rebuildBilateralBindGroup(),this.rebuildParallaxBindGroup(),this.recalculateViewportLayout()}createBilateralPipeline(){const e=this.device;this.bilateralBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:2,visibility:GPUShaderStage.FRAGMENT,sampler:{}}]});const t=e.createPipelineLayout({bindGroupLayouts:[this.bilateralBindGroupLayout]});this.bilateralPipeline=e.createRenderPipeline({layout:t,vertex:{module:e.createShaderModule({code:Lt}),entryPoint:"vs_main",buffers:[N]},fragment:{module:e.createShaderModule({code:Bt}),entryPoint:"fs_main",targets:[{format:"r8unorm"}],constants:{BILATERAL_RADIUS:this.qualityParams.bilateralRadius}},primitive:{topology:"triangle-strip"}})}createParallaxPipeline(){const e=this.device;this.parallaxBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.VERTEX,buffer:{type:"uniform"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},{binding:2,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:3,visibility:GPUShaderStage.FRAGMENT,sampler:{}},{binding:4,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:5,visibility:GPUShaderStage.FRAGMENT,sampler:{}}]});const t=e.createPipelineLayout({bindGroupLayouts:[this.parallaxBindGroupLayout]});this.parallaxPipeline=e.createRenderPipeline({layout:t,vertex:{module:e.createShaderModule({code:Rt}),entryPoint:"vs_main",buffers:[N]},fragment:{module:e.createShaderModule({code:Ft}),entryPoint:"fs_main",targets:[{format:this.canvasFormat}],constants:{MAX_POM_STEPS:Ct}},primitive:{topology:"triangle-strip"}})}rebuildBilateralBindGroup(){!this.bilateralBindGroupLayout||!this.bilateralUniformBuffer||!this.rawDepthView||!this.linearSampler||(this.bilateralBindGroup=this.device.createBindGroup({layout:this.bilateralBindGroupLayout,entries:[{binding:0,resource:{buffer:this.bilateralUniformBuffer}},{binding:1,resource:this.rawDepthView},{binding:2,resource:this.linearSampler}]}))}rebuildParallaxBindGroup(){!this.parallaxBindGroupLayout||!this.vertexUniformBuffer||!this.fragmentUniformBuffer||!this.videoTextureView||!this.filteredDepthView||!this.linearSampler||(this.parallaxBindGroup=this.device.createBindGroup({layout:this.parallaxBindGroupLayout,entries:[{binding:0,resource:{buffer:this.vertexUniformBuffer}},{binding:1,resource:{buffer:this.fragmentUniformBuffer}},{binding:2,resource:this.videoTextureView},{binding:3,resource:this.linearSampler},{binding:4,resource:this.filteredDepthView},{binding:5,resource:this.linearSampler}]}))}writeStaticFragmentUniforms(e,t){const i=new ArrayBuffer(Me),n=new Float32Array(i),r=new Uint32Array(i),s=new Int32Array(i);n[0]=0,n[1]=0,n[2]=this.config.parallaxStrength,r[3]=this.config.pomEnabled?1:0,s[4]=this.config.pomSteps,n[5]=this.config.contrastLow,n[6]=this.config.contrastHigh,n[7]=this.config.verticalReduction,n[8]=this.config.dofStart,n[9]=this.config.dofStrength,n[10]=1/e,n[11]=1/t,this.device.queue.writeBuffer(this.fragmentUniformBuffer,0,i)}flipDepthY(e){const t=this.depthFlipBuffer,i=this.depthWidth,n=this.depthHeight;for(let r=0;r<n;r++){const s=r*i,a=(n-1-r)*i;t.set(e.subarray(s,s+i),a)}return t}onRenderFrame(){const e=this.playbackVideo;if(!this.context||!this.parallaxPipeline||!this.parallaxBindGroup||!this.quadBuffer||!e||e.readyState<HTMLMediaElement.HAVE_CURRENT_DATA)return;if(this.videoTexture&&Le(this.device,this.videoTexture,e),this.rvfcSupported||this.onDepthUpdate(e.currentTime),this.readInput){const n=this.readInput();this.offsetData[0]=-n.x,this.offsetData[1]=n.y,this.device.queue.writeBuffer(this.fragmentUniformBuffer,0,this.offsetData)}const t=this.device.createCommandEncoder(),i=t.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:0,g:0,b:0,a:1},loadOp:"clear",storeOp:"store"}]});i.setPipeline(this.parallaxPipeline),i.setBindGroup(0,this.parallaxBindGroup),i.setVertexBuffer(0,this.quadBuffer),i.draw(4),i.end(),this.device.queue.submit([t.finish()])}onDepthUpdate(e){if(!this.readDepth||!this.rawDepthTexture||!this.filteredDepthView||!this.bilateralPipeline||!this.bilateralBindGroup||!this.quadBuffer)return;const t=this.subsampleDepth(this.readDepth(e)),i=this.flipDepthY(t);this.device.queue.writeTexture({texture:this.rawDepthTexture},i,{bytesPerRow:this.depthWidth},{width:this.depthWidth,height:this.depthHeight});const n=this.device.createCommandEncoder(),r=n.beginRenderPass({colorAttachments:[{view:this.filteredDepthView,loadOp:"clear",clearValue:{r:0,g:0,b:0,a:1},storeOp:"store"}]});r.setPipeline(this.bilateralPipeline),r.setBindGroup(0,this.bilateralBindGroup),r.setVertexBuffer(0,this.quadBuffer),r.draw(4),r.end(),this.device.queue.submit([n.finish()])}recalculateViewportLayout(){if(!this.context)return;const{width:e,height:t}=this.getViewportSize(),i=Math.min(window.devicePixelRatio,this.qualityParams.dprCap),n=Math.round(e*i),r=Math.round(t*i);(this.canvas.width!==n||this.canvas.height!==r)&&(this.canvas.width=n,this.canvas.height=r),this.computeCoverFitUV(this.config.parallaxStrength,this.config.overscanPadding),this.vertexUniformBuffer&&this.device.queue.writeBuffer(this.vertexUniformBuffer,0,new Float32Array([this.uvOffset[0],this.uvOffset[1],this.uvScale[0],this.uvScale[1]]))}disposeRenderer(){this.disposeTextures(),this.bilateralUniformBuffer?.destroy(),this.bilateralUniformBuffer=null,this.vertexUniformBuffer?.destroy(),this.vertexUniformBuffer=null,this.fragmentUniformBuffer?.destroy(),this.fragmentUniformBuffer=null,this.quadBuffer?.destroy(),this.quadBuffer=null,this.linearSampler=null,this.bilateralPipeline=null,this.bilateralBindGroupLayout=null,this.bilateralBindGroup=null,this.parallaxPipeline=null,this.parallaxBindGroupLayout=null,this.parallaxBindGroup=null,this.context&&(this.context.unconfigure(),this.context=null),this.depthFlipBuffer=null}onContextRestored(){}disposeTextures(){this.videoTexture?.destroy(),this.videoTexture=null,this.videoTextureView=null,this.rawDepthTexture?.destroy(),this.rawDepthTexture=null,this.rawDepthView=null,this.filteredDepthTexture?.destroy(),this.filteredDepthTexture=null,this.filteredDepthView=null,this.bilateralBindGroup=null,this.parallaxBindGroup=null}}const It=1500;function Ot(){return typeof navigator<"u"&&"gpu"in navigator}async function Ce(o="auto"){if(o==="webgl2")return{type:"webgl2"};if(!Ot()){if(o==="webgpu")throw new Error("WebGPU not available: navigator.gpu is undefined");return{type:"webgl2"}}try{const e=await kt();if(!e){if(o==="webgpu")throw new Error("WebGPU adapter request returned null");return{type:"webgl2"}}const t=await e.requestDevice();return{type:"webgpu",adapter:e,device:t}}catch(e){if(o==="webgpu")throw e;return{type:"webgl2"}}}async function kt(){const o=navigator.gpu.requestAdapter({powerPreference:"high-performance"}),e=new Promise(t=>{setTimeout(()=>t(null),It)});return Promise.race([o,e])}class _e{abortController=null;initialized=!1;initializing=!1;element;constructor(e){this.element=e}onConnected(){this.element.setupShadowDOM(),this.tryInit()}onDisconnected(){this.cancelInit(),this.element.doDispose(),this.initialized=!1}onAttributeChanged(e,t,i){this.element.reinitAttributes.includes(e)&&t!==i&&(this.initialized?(this.cancelInit(),this.element.doDispose(),this.initialized=!1,this.element.setupShadowDOM(),this.tryInit()):this.initializing||this.tryInit())}get isInitialized(){return this.initialized}markInitialized(){this.initialized=!0,this.initializing=!1}async tryInit(){if(this.initializing)return;const e=this.element;if(!e.isConnected)return;for(const i of e.reinitAttributes)if(!e.getAttribute(i))return;this.cancelInit();const t=new AbortController;this.abortController=t,this.initializing=!0;try{if(await e.doInit(t.signal),t.signal.aborted){this.initializing=!1;return}}catch{this.initializing=!1}}cancelInit(){this.abortController?.abort(),this.abortController=null,this.initializing=!1}}const X={parallaxX:.4,parallaxY:1,parallaxMax:30,overscan:.05,autoplay:!0,loop:!0,muted:!0};let Nt=class Ke{constructor(e,t=.08,i=.06){this.host=e,this.lerpFactor=t,this.motionLerpFactor=i,this.host.addEventListener("mousemove",this.handleMouseMove),this.host.addEventListener("mouseleave",this.resetPointerTarget),this.host.addEventListener("touchstart",this.handleTouchStart,{passive:!0}),this.host.addEventListener("touchmove",this.handleTouchMove,{passive:!0}),this.host.addEventListener("touchend",this.handleTouchEnd,{passive:!0}),this.host.addEventListener("touchcancel",this.handleTouchEnd,{passive:!0})}pointerTarget={x:0,y:0};motionTarget={x:0,y:0};smoothedOutput={x:0,y:0};usingMotionInput=!1;motionListenerAttached=!1;motionRequested=!1;touchActive=!1;touchAnchorX=0;touchAnchorY=0;lerpFactor;motionLerpFactor;static TOUCH_DRAG_RANGE=100;update(){const e=this.touchActive?this.pointerTarget:this.usingMotionInput?this.motionTarget:this.pointerTarget,t=this.usingMotionInput&&!this.touchActive?this.motionLerpFactor:this.lerpFactor;return this.smoothedOutput.x=oe(this.smoothedOutput.x,e.x,t),this.smoothedOutput.y=oe(this.smoothedOutput.y,e.y,t),this.smoothedOutput}dispose(){this.host.removeEventListener("mousemove",this.handleMouseMove),this.host.removeEventListener("mouseleave",this.resetPointerTarget),this.host.removeEventListener("touchstart",this.handleTouchStart),this.host.removeEventListener("touchmove",this.handleTouchMove),this.host.removeEventListener("touchend",this.handleTouchEnd),this.host.removeEventListener("touchcancel",this.handleTouchEnd),this.motionListenerAttached&&(window.removeEventListener("deviceorientation",this.handleDeviceOrientation),this.motionListenerAttached=!1)}handleMouseMove=e=>{const t=this.host.getBoundingClientRect(),i=(e.clientX-t.left)/t.width*2-1,n=(e.clientY-t.top)/t.height*2-1;this.pointerTarget.x=j(i,-1,1),this.pointerTarget.y=j(n,-1,1)};resetPointerTarget=()=>{this.pointerTarget.x=0,this.pointerTarget.y=0};handleTouchStart=e=>{const t=e.touches[0];t&&(this.touchActive=!0,this.touchAnchorX=t.clientX,this.touchAnchorY=t.clientY,this.pointerTarget.x=0,this.pointerTarget.y=0,this.motionRequested||(this.motionRequested=!0,this.requestMotionPermission()))};handleTouchMove=e=>{const t=e.touches[0];if(!t)return;const i=t.clientX-this.touchAnchorX,n=t.clientY-this.touchAnchorY,r=Ke.TOUCH_DRAG_RANGE;this.pointerTarget.x=j(i/r,-1,1),this.pointerTarget.y=j(n/r,-1,1)};handleTouchEnd=()=>{this.touchActive=!1,this.pointerTarget.x=0,this.pointerTarget.y=0};async requestMotionPermission(){if(typeof DeviceOrientationEvent>"u")return;const e=DeviceOrientationEvent;if(typeof e.requestPermission=="function")try{if(await e.requestPermission()!=="granted")return}catch{return}this.motionListenerAttached||(window.addEventListener("deviceorientation",this.handleDeviceOrientation),this.motionListenerAttached=!0),this.usingMotionInput=!0}handleDeviceOrientation=e=>{const t=j((e.gamma??0)/45,-1,1),i=j((e.beta??0)/45,-1,1);this.motionTarget.x=oe(this.motionTarget.x,t,this.motionLerpFactor),this.motionTarget.y=oe(this.motionTarget.y,i,this.motionLerpFactor)}};class re extends HTMLElement{static TAG_NAME="layershift-parallax";static get observedAttributes(){return["src","depth-src","depth-meta","parallax-x","parallax-y","parallax-max","layers","overscan","quality","gpu-backend","autoplay","loop","muted"]}reinitAttributes=["src","depth-src","depth-meta"];shadow;container=null;renderer=null;inputHandler=null;video=null;loopCount=0;lifecycle;constructor(){super(),this.shadow=this.attachShadow({mode:"open"}),this.lifecycle=new _e(this)}getAttrFloat(e,t){const i=this.getAttribute(e);if(i===null)return t;const n=parseFloat(i);return Number.isFinite(n)?n:t}getAttrBool(e,t){if(!this.hasAttribute(e))return t;const i=this.getAttribute(e);return!(i==="false"||i==="0")}get parallaxX(){return this.getAttrFloat("parallax-x",X.parallaxX)}get parallaxY(){return this.getAttrFloat("parallax-y",X.parallaxY)}get parallaxMax(){return this.getAttrFloat("parallax-max",X.parallaxMax)}get overscan(){return this.getAttrFloat("overscan",X.overscan)}get quality(){const e=this.getAttribute("quality");if(e==="auto"||e==="high"||e==="medium"||e==="low")return e}get gpuBackend(){const e=this.getAttribute("gpu-backend");return e==="webgpu"||e==="webgl2"?e:"auto"}get shouldAutoplay(){return this.getAttrBool("autoplay",X.autoplay)}get shouldLoop(){return this.getAttrBool("loop",X.loop)}get shouldMute(){return this.getAttrBool("muted",X.muted)}emit(e,t){this.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}attachVideoEventListeners(e){e.addEventListener("play",()=>{this.emit("layershift-parallax:play",{currentTime:e.currentTime})}),e.addEventListener("pause",()=>{this.emit("layershift-parallax:pause",{currentTime:e.currentTime})}),e.addEventListener("ended",()=>{e.loop&&(this.loopCount+=1,this.emit("layershift-parallax:loop",{loopCount:this.loopCount}))})}connectedCallback(){this.lifecycle.onConnected()}disconnectedCallback(){this.lifecycle.onDisconnected()}attributeChangedCallback(e,t,i){this.lifecycle.onAttributeChanged(e,t,i)}setupShadowDOM(){this.shadow.innerHTML="";const e=document.createElement("style");e.textContent=`
|
|
194
454
|
:host {
|
|
195
455
|
display: block;
|
|
196
456
|
width: 100%;
|
|
@@ -210,517 +470,1386 @@ ${r}`)}return n.detachShader(i,t),n.detachShader(i,e),n.deleteShader(t),n.delete
|
|
|
210
470
|
width: 100%;
|
|
211
471
|
height: 100%;
|
|
212
472
|
}
|
|
213
|
-
`,this.shadow.appendChild(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
473
|
+
`,this.shadow.appendChild(e),this.container=document.createElement("div"),this.container.className="container",this.shadow.appendChild(this.container)}async doInit(e){const t=this.getAttribute("src"),i=this.getAttribute("depth-src"),n=this.getAttribute("depth-meta");if(this.container)try{const[r,s]=await Promise.all([this.createVideoElement(t),Se(i,n)]);if(e.aborted){r.remove();return}this.video=r,this.loopCount=0,this.attachVideoEventListeners(r);const a=ot(s.frames,s.meta.width,s.meta.height),l=st(a),u=this.hasAttribute("parallax-max")?this.parallaxMax/Math.max(r.videoWidth,1):l.parallaxStrength,h=this.hasAttribute("overscan")?this.overscan:l.overscanPadding,f=new be(s),c=b=>f.sample(b),g=await Ce(this.gpuBackend);if(e.aborted)return;const v={parallaxStrength:u,pomEnabled:!0,pomSteps:l.pomSteps,overscanPadding:h,quality:this.quality,contrastLow:l.contrastLow,contrastHigh:l.contrastHigh,verticalReduction:l.verticalReduction,dofStart:l.dofStart,dofStrength:l.dofStrength};g.type==="webgpu"&&g.device&&g.adapter?this.renderer=new Gt(this.container,v,g.device,g.adapter.info):this.renderer=new Pt(this.container,v),this.renderer.initialize(r,s.meta.width,s.meta.height),this.inputHandler=new Nt(this);const d=this.parallaxX,A=this.parallaxY;if(this.renderer.start(r,c,()=>{if(!this.inputHandler)return{x:0,y:0};const b=this.inputHandler.update();return{x:b.x*d,y:b.y*A}},(b,y)=>{this.emit("layershift-parallax:frame",{currentTime:b,frameNumber:y})}),this.shouldAutoplay){r.currentTime=0;try{await r.play()}catch{}}if(e.aborted)return;this.lifecycle.markInitialized(),this.emit("layershift-parallax:ready",{videoWidth:r.videoWidth,videoHeight:r.videoHeight,duration:r.duration,depthProfile:a,derivedParams:l})}catch(r){const s=r instanceof Error?r.message:"Failed to initialize.";console.error("<layershift-parallax>: Failed to initialize.",r),this.emit("layershift-parallax:error",{message:s})}}async createVideoElement(e){const t=document.createElement("video");return t.crossOrigin="anonymous",t.setAttribute("crossorigin","anonymous"),t.playsInline=!0,t.setAttribute("playsinline",""),t.setAttribute("webkit-playsinline","true"),t.muted=this.shouldMute,t.defaultMuted=this.shouldMute,this.shouldMute&&t.setAttribute("muted",""),t.loop=this.shouldLoop,t.preload="auto",t.style.display="none",t.src=e,this.shadow.appendChild(t),await new Promise((i,n)=>{if(t.readyState>=HTMLMediaElement.HAVE_METADATA){i();return}const r=()=>{a(),i()},s=()=>{a(),n(new Error("Failed to load video metadata."))},a=()=>{t.removeEventListener("loadedmetadata",r),t.removeEventListener("error",s)};t.addEventListener("loadedmetadata",r),t.addEventListener("error",s),t.load()}),t}doDispose(){this.renderer?.dispose(),this.renderer=null,this.inputHandler?.dispose(),this.inputHandler=null,this.video&&(this.video.pause(),this.video.removeAttribute("src"),this.video.load(),this.video.remove(),this.video=null),this.loopCount=0,this.container=null}}function j(o,e,t){return Math.min(t,Math.max(e,o))}function oe(o,e,t){return o+(e-o)*t}class se{gl;hasColorBufferFloat;maskFbo=null;maskTex=null;pingFbo=null;pingTex=null;pongFbo=null;pongTex=null;distFbo=null;distTex=null;_width=0;_height=0;_dirty=!0;constructor(e,t){this.gl=e,this.hasColorBufferFloat=t}get width(){return this._width}get height(){return this._height}get isDirty(){return this._dirty}get distanceTexture(){return this.distTex}get maskTexture(){return this.maskTex}markDirty(){this._dirty=!0}createResources(e,t,i){const n=this.gl;this.dispose();const r=Math.max(1,Math.round(e/i)),s=Math.max(1,Math.round(t/i));this._width=r,this._height=s;const a=(u,h,f,c)=>{const g=n.createFramebuffer();return n.bindFramebuffer(n.FRAMEBUFFER,g),n.bindTexture(n.TEXTURE_2D,u),n.texStorage2D(n.TEXTURE_2D,1,h,f,c),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MIN_FILTER,n.LINEAR),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MAG_FILTER,n.LINEAR),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_S,n.CLAMP_TO_EDGE),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_T,n.CLAMP_TO_EDGE),n.framebufferTexture2D(n.FRAMEBUFFER,n.COLOR_ATTACHMENT0,n.TEXTURE_2D,u,0),n.bindFramebuffer(n.FRAMEBUFFER,null),g};this.maskTex=n.createTexture(),this.maskFbo=a(this.maskTex,n.R8,r,s);const l=this.hasColorBufferFloat?n.RG16F:n.RGBA8;this.pingTex=n.createTexture(),this.pingFbo=a(this.pingTex,l,r,s),this.pongTex=n.createTexture(),this.pongFbo=a(this.pongTex,l,r,s),this.distTex=n.createTexture(),this.distFbo=a(this.distTex,n.RGBA8,r,s),this._dirty=!0}compute(e){const t=this.gl;if(!this.maskFbo||!this.pingFbo||!this.pongFbo||!this.distFbo)return;const i=this._width,n=this._height;if(i===0||n===0)return;t.viewport(0,0,i,n),t.disable(t.STENCIL_TEST),t.disable(t.BLEND),t.bindFramebuffer(t.FRAMEBUFFER,this.maskFbo),t.clearColor(0,0,0,1),t.clear(t.COLOR_BUFFER_BIT),t.useProgram(e.maskPass.program),t.uniform2f(e.maskPass.uniforms.uMeshScale,e.meshScaleX,e.meshScaleY),t.bindVertexArray(e.maskVao),t.drawElements(t.TRIANGLES,e.stencilIndexCount,t.UNSIGNED_SHORT,0),t.bindFramebuffer(t.FRAMEBUFFER,this.pingFbo),t.clearColor(-1,-1,0,0),t.clear(t.COLOR_BUFFER_BIT),t.useProgram(e.seedPass.program),t.activeTexture(t.TEXTURE5),t.bindTexture(t.TEXTURE_2D,this.maskTex),t.uniform1i(e.seedPass.uniforms.uMask,5),t.uniform2f(e.seedPass.uniforms.uTexelSize,1/i,1/n),t.bindVertexArray(e.quadVao),t.drawArrays(t.TRIANGLE_STRIP,0,4);const r=se.computeFloodIterations(i,n);t.useProgram(e.floodPass.program);let s=this.pingTex,a=this.pongFbo,l=this.pongTex;for(let u=0;u<r.length;u++){const h=r[u]/Math.max(i,n);t.bindFramebuffer(t.FRAMEBUFFER,a),t.activeTexture(t.TEXTURE5),t.bindTexture(t.TEXTURE_2D,s),t.uniform1i(e.floodPass.uniforms.uSeedTex,5),t.uniform1f(e.floodPass.uniforms.uStepSize,h),t.bindVertexArray(e.quadVao),t.drawArrays(t.TRIANGLE_STRIP,0,4);const f=s,c=a;s=l,a=c===this.pongFbo?this.pingFbo:this.pongFbo,l=f}t.bindFramebuffer(t.FRAMEBUFFER,this.distFbo),t.clearColor(0,0,0,1),t.clear(t.COLOR_BUFFER_BIT),t.useProgram(e.distPass.program),t.activeTexture(t.TEXTURE5),t.bindTexture(t.TEXTURE_2D,s),t.uniform1i(e.distPass.uniforms.uSeedTex,5),t.activeTexture(t.TEXTURE6),t.bindTexture(t.TEXTURE_2D,this.maskTex),t.uniform1i(e.distPass.uniforms.uMask,6),t.uniform1f(e.distPass.uniforms.uBevelWidth,e.distRange),t.bindVertexArray(e.quadVao),t.drawArrays(t.TRIANGLE_STRIP,0,4),t.activeTexture(t.TEXTURE4),t.bindTexture(t.TEXTURE_2D,this.distTex),t.bindFramebuffer(t.FRAMEBUFFER,null),this._dirty=!1}static computeFloodIterations(e,t){const i=Math.max(e,t),n=[];let r=Math.ceil(i/2);for(;r>=1;)n.push(r),r=Math.floor(r/2);return n}dispose(){const e=this.gl;this.maskTex&&(e.deleteTexture(this.maskTex),this.maskTex=null),this.maskFbo&&(e.deleteFramebuffer(this.maskFbo),this.maskFbo=null),this.pingTex&&(e.deleteTexture(this.pingTex),this.pingTex=null),this.pingFbo&&(e.deleteFramebuffer(this.pingFbo),this.pingFbo=null),this.pongTex&&(e.deleteTexture(this.pongTex),this.pongTex=null),this.pongFbo&&(e.deleteFramebuffer(this.pongFbo),this.pongFbo=null),this.distTex&&(e.deleteTexture(this.distTex),this.distTex=null),this.distFbo&&(e.deleteFramebuffer(this.distFbo),this.distFbo=null),this._width=0,this._height=0,this._dirty=!0}}const Xt=`#version 300 es
|
|
474
|
+
in vec2 aPosition;
|
|
475
|
+
uniform vec2 uMeshScale;
|
|
476
|
+
void main() {
|
|
477
|
+
gl_Position = vec4(aPosition * uMeshScale, 0.0, 1.0);
|
|
478
|
+
}
|
|
479
|
+
`,Ht=`#version 300 es
|
|
480
|
+
precision lowp float;
|
|
481
|
+
out vec4 fragColor;
|
|
482
|
+
void main() { fragColor = vec4(0.0); }
|
|
483
|
+
`,Wt=`#version 300 es
|
|
484
|
+
in vec2 aPosition;
|
|
485
|
+
uniform vec2 uMeshScale;
|
|
486
|
+
void main() {
|
|
487
|
+
gl_Position = vec4(aPosition * uMeshScale, 0.0, 1.0);
|
|
488
|
+
}
|
|
489
|
+
`,jt=`#version 300 es
|
|
490
|
+
precision lowp float;
|
|
491
|
+
out vec4 fragColor;
|
|
492
|
+
void main() { fragColor = vec4(1.0); }
|
|
493
|
+
`,zt=`#version 300 es
|
|
494
|
+
in vec2 aPosition;
|
|
495
|
+
out vec2 vUv;
|
|
496
|
+
void main() {
|
|
497
|
+
vUv = aPosition * 0.5 + 0.5;
|
|
498
|
+
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
499
|
+
}
|
|
500
|
+
`,qt=`#version 300 es
|
|
501
|
+
precision highp float;
|
|
502
|
+
uniform sampler2D uMask;
|
|
503
|
+
uniform vec2 uTexelSize;
|
|
504
|
+
in vec2 vUv;
|
|
505
|
+
out vec2 fragSeed;
|
|
506
|
+
|
|
507
|
+
void main() {
|
|
508
|
+
float center = texture(uMask, vUv).r;
|
|
509
|
+
float left = texture(uMask, vUv + vec2(-uTexelSize.x, 0.0)).r;
|
|
510
|
+
float right = texture(uMask, vUv + vec2( uTexelSize.x, 0.0)).r;
|
|
511
|
+
float up = texture(uMask, vUv + vec2(0.0, uTexelSize.y)).r;
|
|
512
|
+
float down = texture(uMask, vUv + vec2(0.0, -uTexelSize.y)).r;
|
|
513
|
+
|
|
514
|
+
bool isEdge = (step(0.5, center) != step(0.5, left)) ||
|
|
515
|
+
(step(0.5, center) != step(0.5, right)) ||
|
|
516
|
+
(step(0.5, center) != step(0.5, up)) ||
|
|
517
|
+
(step(0.5, center) != step(0.5, down));
|
|
518
|
+
|
|
519
|
+
if (isEdge) {
|
|
520
|
+
fragSeed = vUv;
|
|
521
|
+
} else {
|
|
522
|
+
fragSeed = vec2(-1.0);
|
|
218
523
|
}
|
|
524
|
+
}
|
|
525
|
+
`,Yt=`#version 300 es
|
|
526
|
+
in vec2 aPosition;
|
|
527
|
+
out vec2 vUv;
|
|
528
|
+
void main() {
|
|
529
|
+
vUv = aPosition * 0.5 + 0.5;
|
|
530
|
+
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
531
|
+
}
|
|
219
532
|
`,Zt=`#version 300 es
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
533
|
+
precision highp float;
|
|
534
|
+
uniform sampler2D uSeedTex;
|
|
535
|
+
uniform float uStepSize;
|
|
536
|
+
in vec2 vUv;
|
|
537
|
+
out vec2 fragSeed;
|
|
538
|
+
|
|
539
|
+
void main() {
|
|
540
|
+
vec2 bestSeed = texture(uSeedTex, vUv).rg;
|
|
541
|
+
float bestDist = (bestSeed.x < 0.0) ? 1.0e10 : distance(vUv, bestSeed);
|
|
542
|
+
|
|
543
|
+
for (int dy = -1; dy <= 1; dy++) {
|
|
544
|
+
for (int dx = -1; dx <= 1; dx++) {
|
|
545
|
+
if (dx == 0 && dy == 0) continue;
|
|
546
|
+
vec2 offset = vec2(float(dx), float(dy)) * uStepSize;
|
|
547
|
+
vec2 sampleUv = vUv + offset;
|
|
548
|
+
if (sampleUv.x < 0.0 || sampleUv.x > 1.0 || sampleUv.y < 0.0 || sampleUv.y > 1.0) continue;
|
|
549
|
+
vec2 neighborSeed = texture(uSeedTex, sampleUv).rg;
|
|
550
|
+
if (neighborSeed.x < 0.0) continue;
|
|
551
|
+
float d = distance(vUv, neighborSeed);
|
|
552
|
+
if (d < bestDist) {
|
|
553
|
+
bestDist = d;
|
|
554
|
+
bestSeed = neighborSeed;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
228
557
|
}
|
|
558
|
+
|
|
559
|
+
fragSeed = bestSeed;
|
|
560
|
+
}
|
|
229
561
|
`,Jt=`#version 300 es
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
562
|
+
in vec2 aPosition;
|
|
563
|
+
out vec2 vUv;
|
|
564
|
+
void main() {
|
|
565
|
+
vUv = aPosition * 0.5 + 0.5;
|
|
566
|
+
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
567
|
+
}
|
|
568
|
+
`,$t=`#version 300 es
|
|
569
|
+
precision highp float;
|
|
570
|
+
uniform sampler2D uSeedTex;
|
|
571
|
+
uniform sampler2D uMask;
|
|
572
|
+
uniform float uBevelWidth;
|
|
573
|
+
in vec2 vUv;
|
|
574
|
+
out vec4 fragDist;
|
|
575
|
+
|
|
576
|
+
void main() {
|
|
577
|
+
float mask = texture(uMask, vUv).r;
|
|
578
|
+
if (mask < 0.5) {
|
|
579
|
+
fragDist = vec4(0.0);
|
|
580
|
+
return;
|
|
239
581
|
}
|
|
582
|
+
|
|
583
|
+
vec2 seed = texture(uSeedTex, vUv).rg;
|
|
584
|
+
if (seed.x < 0.0) {
|
|
585
|
+
fragDist = vec4(1.0);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
float d = distance(vUv, seed);
|
|
590
|
+
float normalized = clamp(d / max(uBevelWidth, 0.001), 0.0, 1.0);
|
|
591
|
+
fragDist = vec4(normalized, 0.0, 0.0, 1.0);
|
|
592
|
+
}
|
|
593
|
+
`,Kt=`#version 300 es
|
|
594
|
+
in vec2 aPosition;
|
|
595
|
+
uniform vec2 uUvOffset;
|
|
596
|
+
uniform vec2 uUvScale;
|
|
597
|
+
out vec2 vUv;
|
|
598
|
+
out vec2 vScreenUv;
|
|
599
|
+
void main() {
|
|
600
|
+
vec2 baseUv = aPosition * 0.5 + 0.5;
|
|
601
|
+
vUv = baseUv * uUvScale + uUvOffset;
|
|
602
|
+
vScreenUv = baseUv;
|
|
603
|
+
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
604
|
+
}
|
|
240
605
|
`,Qt=`#version 300 es
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
606
|
+
precision highp float;
|
|
607
|
+
|
|
608
|
+
#define MAX_POM_STEPS 32
|
|
609
|
+
|
|
610
|
+
uniform sampler2D uImage;
|
|
611
|
+
uniform sampler2D uDepth;
|
|
612
|
+
uniform vec2 uOffset;
|
|
613
|
+
uniform float uStrength;
|
|
614
|
+
uniform int uPomSteps;
|
|
615
|
+
|
|
616
|
+
// Lens transform: remap depth curve for exaggerated/compressed depth feel
|
|
617
|
+
uniform float uDepthPower; // >1 = telephoto, <1 = wide-angle
|
|
618
|
+
uniform float uDepthScale; // multiplier on depth range
|
|
619
|
+
uniform float uDepthBias; // shift depth origin
|
|
620
|
+
|
|
621
|
+
// Depth-adaptive contrast
|
|
622
|
+
uniform float uContrastLow;
|
|
623
|
+
uniform float uContrastHigh;
|
|
624
|
+
uniform float uVerticalReduction;
|
|
625
|
+
|
|
626
|
+
// DOF
|
|
627
|
+
uniform float uDofStart;
|
|
628
|
+
uniform float uDofStrength;
|
|
629
|
+
uniform vec2 uImageTexelSize;
|
|
630
|
+
|
|
631
|
+
// Interior mood
|
|
632
|
+
uniform float uFogDensity; // volumetric fog bias (0 = none, 0.3 = subtle)
|
|
633
|
+
uniform vec3 uFogColor; // fog tint color
|
|
634
|
+
uniform float uColorShift; // warm/cool grading shift
|
|
635
|
+
uniform float uBrightnessBias; // overall brightness adjustment
|
|
636
|
+
|
|
637
|
+
in vec2 vUv;
|
|
638
|
+
in vec2 vScreenUv;
|
|
639
|
+
|
|
640
|
+
layout(location = 0) out vec4 fragColor;
|
|
641
|
+
layout(location = 1) out vec4 fragDepth;
|
|
642
|
+
|
|
643
|
+
// Apply lens transform to raw depth
|
|
644
|
+
float lensDepth(float raw) {
|
|
645
|
+
float d = smoothstep(uContrastLow, uContrastHigh, raw);
|
|
646
|
+
d = pow(d, uDepthPower) * uDepthScale + uDepthBias;
|
|
647
|
+
return clamp(d, 0.0, 1.0);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
float edgeFade(vec2 uv) {
|
|
651
|
+
float margin = uStrength * 1.5;
|
|
652
|
+
float fadeX = smoothstep(0.0, margin, uv.x) * smoothstep(0.0, margin, 1.0 - uv.x);
|
|
653
|
+
float fadeY = smoothstep(0.0, margin, uv.y) * smoothstep(0.0, margin, 1.0 - uv.y);
|
|
654
|
+
return fadeX * fadeY;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// POM ray-march with lens-transformed depth
|
|
658
|
+
vec2 pomDisplace(vec2 uv, out float hitDepth) {
|
|
659
|
+
float layerD = 1.0 / float(uPomSteps);
|
|
660
|
+
vec2 scaledOffset = uOffset;
|
|
661
|
+
scaledOffset.y *= uVerticalReduction;
|
|
662
|
+
vec2 deltaUV = scaledOffset * uStrength / float(uPomSteps);
|
|
663
|
+
float currentLayerDepth = 0.0;
|
|
664
|
+
vec2 currentUV = uv;
|
|
665
|
+
float fade = edgeFade(uv);
|
|
666
|
+
|
|
667
|
+
for (int i = 0; i < MAX_POM_STEPS; i++) {
|
|
668
|
+
if (i >= uPomSteps) break;
|
|
669
|
+
float raw = texture(uDepth, currentUV).r;
|
|
670
|
+
float depthAtUV = 1.0 - lensDepth(raw);
|
|
671
|
+
if (currentLayerDepth > depthAtUV) {
|
|
672
|
+
vec2 prevUV = currentUV - deltaUV;
|
|
673
|
+
float prevLayerD = currentLayerDepth - layerD;
|
|
674
|
+
float prevRaw = texture(uDepth, prevUV).r;
|
|
675
|
+
float prevDepthAtUV = 1.0 - lensDepth(prevRaw);
|
|
676
|
+
float afterD = depthAtUV - currentLayerDepth;
|
|
677
|
+
float beforeD = prevDepthAtUV - prevLayerD;
|
|
678
|
+
float t = afterD / (afterD - beforeD);
|
|
679
|
+
vec2 hitUV = mix(currentUV, prevUV, t);
|
|
680
|
+
hitDepth = mix(depthAtUV, prevDepthAtUV, t);
|
|
681
|
+
return mix(uv, hitUV, fade);
|
|
263
682
|
}
|
|
683
|
+
currentUV += deltaUV;
|
|
684
|
+
currentLayerDepth += layerD;
|
|
264
685
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
686
|
+
hitDepth = 1.0 - lensDepth(texture(uDepth, currentUV).r);
|
|
687
|
+
return mix(uv, currentUV, fade);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
void main() {
|
|
691
|
+
float hitDepth;
|
|
692
|
+
vec2 displaced = pomDisplace(vUv, hitDepth);
|
|
693
|
+
displaced = clamp(displaced, vec2(0.0), vec2(1.0));
|
|
694
|
+
|
|
695
|
+
vec4 color = texture(uImage, displaced);
|
|
696
|
+
|
|
697
|
+
// DOF: blur far objects
|
|
698
|
+
float rawDepthAtHit = texture(uDepth, displaced).r;
|
|
699
|
+
float lensD = lensDepth(rawDepthAtHit);
|
|
700
|
+
float dof = smoothstep(uDofStart, 1.0, lensD) * uDofStrength;
|
|
701
|
+
if (dof > 0.01) {
|
|
702
|
+
vec2 ts = uImageTexelSize;
|
|
703
|
+
vec4 blurred = (
|
|
704
|
+
texture(uImage, displaced + vec2( ts.x, 0.0)) +
|
|
705
|
+
texture(uImage, displaced + vec2(-ts.x, 0.0)) +
|
|
706
|
+
texture(uImage, displaced + vec2( 0.0, ts.y)) +
|
|
707
|
+
texture(uImage, displaced + vec2( 0.0, -ts.y)) +
|
|
708
|
+
texture(uImage, displaced + vec2( ts.x, ts.y)) +
|
|
709
|
+
texture(uImage, displaced + vec2(-ts.x, -ts.y)) +
|
|
710
|
+
texture(uImage, displaced + vec2( ts.x, -ts.y)) +
|
|
711
|
+
texture(uImage, displaced + vec2(-ts.x, ts.y))
|
|
712
|
+
) * 0.125;
|
|
713
|
+
color = mix(color, blurred, dof);
|
|
271
714
|
}
|
|
272
|
-
`,ee=`#version 300 es
|
|
273
|
-
precision highp float;
|
|
274
|
-
uniform sampler2D uSeedTex;
|
|
275
|
-
uniform float uStepSize;
|
|
276
|
-
in vec2 vUv;
|
|
277
|
-
out vec2 fragSeed;
|
|
278
|
-
|
|
279
|
-
void main() {
|
|
280
|
-
vec2 bestSeed = texture(uSeedTex, vUv).rg;
|
|
281
|
-
float bestDist = (bestSeed.x < 0.0) ? 1.0e10 : distance(vUv, bestSeed);
|
|
282
|
-
|
|
283
|
-
for (int dy = -1; dy <= 1; dy++) {
|
|
284
|
-
for (int dx = -1; dx <= 1; dx++) {
|
|
285
|
-
if (dx == 0 && dy == 0) continue;
|
|
286
|
-
vec2 offset = vec2(float(dx), float(dy)) * uStepSize;
|
|
287
|
-
vec2 sampleUv = vUv + offset;
|
|
288
|
-
if (sampleUv.x < 0.0 || sampleUv.x > 1.0 || sampleUv.y < 0.0 || sampleUv.y > 1.0) continue;
|
|
289
|
-
vec2 neighborSeed = texture(uSeedTex, sampleUv).rg;
|
|
290
|
-
if (neighborSeed.x < 0.0) continue;
|
|
291
|
-
float d = distance(vUv, neighborSeed);
|
|
292
|
-
if (d < bestDist) {
|
|
293
|
-
bestDist = d;
|
|
294
|
-
bestSeed = neighborSeed;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
715
|
|
|
299
|
-
|
|
716
|
+
// Volumetric fog bias: far objects fade into fog color
|
|
717
|
+
float fogFactor = smoothstep(0.3, 1.0, lensD) * uFogDensity;
|
|
718
|
+
color.rgb = mix(color.rgb, uFogColor, fogFactor);
|
|
719
|
+
|
|
720
|
+
// Color grading shift: warm near, cool far (or vice versa)
|
|
721
|
+
float gradeAmount = (lensD - 0.5) * uColorShift;
|
|
722
|
+
color.r += gradeAmount * 0.08;
|
|
723
|
+
color.b -= gradeAmount * 0.08;
|
|
724
|
+
|
|
725
|
+
// Brightness bias
|
|
726
|
+
color.rgb *= (1.0 + uBrightnessBias);
|
|
727
|
+
|
|
728
|
+
// Subtle vignette inside portal
|
|
729
|
+
float dist = length(vScreenUv - 0.5) * 1.4;
|
|
730
|
+
color.rgb *= 1.0 - pow(dist, 3.0) * 0.3;
|
|
731
|
+
|
|
732
|
+
fragColor = color;
|
|
733
|
+
// Write lens-transformed depth to second attachment for boundary effects
|
|
734
|
+
fragDepth = vec4(lensD, 0.0, 0.0, 1.0);
|
|
735
|
+
}
|
|
736
|
+
`,ei=`#version 300 es
|
|
737
|
+
in vec2 aPosition;
|
|
738
|
+
out vec2 vUv;
|
|
739
|
+
void main() {
|
|
740
|
+
vUv = aPosition * 0.5 + 0.5;
|
|
741
|
+
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
742
|
+
}
|
|
743
|
+
`,ti=`#version 300 es
|
|
744
|
+
precision highp float;
|
|
745
|
+
uniform sampler2D uInteriorColor;
|
|
746
|
+
uniform sampler2D uDistField;
|
|
747
|
+
uniform float uEdgeOcclusionWidth; // how far edge darkening extends
|
|
748
|
+
uniform float uEdgeOcclusionStrength; // how strong (0=none, 1=full black)
|
|
749
|
+
|
|
750
|
+
in vec2 vUv;
|
|
751
|
+
out vec4 fragColor;
|
|
752
|
+
|
|
753
|
+
// sRGB <-> linear conversions for correct lighting math
|
|
754
|
+
vec3 toLinear(vec3 s) {
|
|
755
|
+
return mix(s / 12.92, pow((s + 0.055) / 1.055, vec3(2.4)), step(0.04045, s));
|
|
756
|
+
}
|
|
757
|
+
vec3 toSRGB(vec3 l) {
|
|
758
|
+
return mix(l * 12.92, 1.055 * pow(l, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, l));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
void main() {
|
|
762
|
+
vec4 color = texture(uInteriorColor, vUv);
|
|
763
|
+
float dist = texture(uDistField, vUv).r; // 0=edge, 1=deep interior
|
|
764
|
+
|
|
765
|
+
// Emissive passthrough: preserve original video luminance.
|
|
766
|
+
// Only apply a subtle edge occlusion ramp to sell chamfer->interior depth.
|
|
767
|
+
vec3 linear = toLinear(color.rgb);
|
|
768
|
+
float occ = smoothstep(0.0, uEdgeOcclusionWidth, dist);
|
|
769
|
+
linear *= mix(1.0 - uEdgeOcclusionStrength, 1.0, occ);
|
|
770
|
+
|
|
771
|
+
fragColor = vec4(toSRGB(linear), color.a);
|
|
772
|
+
}
|
|
773
|
+
`,ii=`#version 300 es
|
|
774
|
+
in vec2 aPosition;
|
|
775
|
+
in vec2 aNormal;
|
|
776
|
+
uniform float uRimWidth;
|
|
777
|
+
uniform vec2 uMeshScale;
|
|
778
|
+
out vec2 vNormal;
|
|
779
|
+
out vec2 vEdgeUv; // screen-space UV for sampling FBO textures
|
|
780
|
+
out float vEdgeDist; // 0 at edge, 1 at outer extent
|
|
781
|
+
|
|
782
|
+
void main() {
|
|
783
|
+
vec2 scaledPos = aPosition * uMeshScale;
|
|
784
|
+
vec2 scaledNormal = normalize(aNormal * uMeshScale);
|
|
785
|
+
vec2 pos = scaledPos + scaledNormal * uRimWidth;
|
|
786
|
+
|
|
787
|
+
// Pass screen-space UV of this fragment for FBO sampling
|
|
788
|
+
vEdgeUv = pos * 0.5 + 0.5;
|
|
789
|
+
vNormal = scaledNormal;
|
|
790
|
+
|
|
791
|
+
// Distance from the actual edge (0) to the outer rim extent (1)
|
|
792
|
+
vEdgeDist = length(pos - scaledPos) / max(uRimWidth, 0.001);
|
|
793
|
+
|
|
794
|
+
gl_Position = vec4(pos, 0.0, 1.0);
|
|
795
|
+
}
|
|
796
|
+
`,ni=`#version 300 es
|
|
797
|
+
precision highp float;
|
|
798
|
+
|
|
799
|
+
uniform sampler2D uInteriorColor;
|
|
800
|
+
uniform sampler2D uInteriorDepth;
|
|
801
|
+
uniform sampler2D uDistField;
|
|
802
|
+
uniform float uRimIntensity;
|
|
803
|
+
uniform vec3 uRimColor;
|
|
804
|
+
uniform float uRefractionStrength;
|
|
805
|
+
uniform float uChromaticStrength;
|
|
806
|
+
uniform float uOcclusionIntensity;
|
|
807
|
+
uniform vec2 uTexelSize; // 1.0 / viewport resolution
|
|
808
|
+
|
|
809
|
+
// Volumetric edge wall
|
|
810
|
+
uniform float uEdgeThickness;
|
|
811
|
+
uniform float uEdgeSpecular;
|
|
812
|
+
uniform vec3 uEdgeColor;
|
|
813
|
+
uniform vec2 uLightDir;
|
|
814
|
+
uniform float uBevelIntensity;
|
|
815
|
+
|
|
816
|
+
in vec2 vNormal;
|
|
817
|
+
in vec2 vEdgeUv;
|
|
818
|
+
in float vEdgeDist;
|
|
819
|
+
out vec4 fragColor;
|
|
820
|
+
|
|
821
|
+
void main() {
|
|
822
|
+
// Clamp UV to valid range for texture sampling
|
|
823
|
+
vec2 sampleUv = clamp(vEdgeUv, vec2(0.001), vec2(0.999));
|
|
824
|
+
|
|
825
|
+
// Sample interior depth at this boundary location
|
|
826
|
+
float interiorDepth = texture(uInteriorDepth, sampleUv).r;
|
|
827
|
+
|
|
828
|
+
// === DEPTH-REACTIVE RIM (structural seam) ===
|
|
829
|
+
float depthReactivity = 1.0 - interiorDepth; // 1=near, 0=far
|
|
830
|
+
float rimProfile = 1.0 - smoothstep(0.0, 1.0, vEdgeDist);
|
|
831
|
+
rimProfile = pow(rimProfile, 1.5); // sharper falloff = more structural
|
|
832
|
+
|
|
833
|
+
float depthPressure = mix(0.2, 1.0, depthReactivity * depthReactivity);
|
|
834
|
+
float rim = rimProfile * depthPressure * uRimIntensity;
|
|
835
|
+
|
|
836
|
+
vec3 rimCol = uRimColor;
|
|
837
|
+
rimCol.r += depthReactivity * 0.15;
|
|
838
|
+
rimCol.g += depthReactivity * 0.05;
|
|
839
|
+
|
|
840
|
+
// === REFRACTION DISTORTION ===
|
|
841
|
+
vec2 ts = uTexelSize * 3.0;
|
|
842
|
+
float dLeft = texture(uInteriorDepth, sampleUv + vec2(-ts.x, 0.0)).r;
|
|
843
|
+
float dRight = texture(uInteriorDepth, sampleUv + vec2( ts.x, 0.0)).r;
|
|
844
|
+
float dUp = texture(uInteriorDepth, sampleUv + vec2(0.0, ts.y)).r;
|
|
845
|
+
float dDown = texture(uInteriorDepth, sampleUv + vec2(0.0, -ts.y)).r;
|
|
846
|
+
vec2 depthGradient = vec2(dRight - dLeft, dUp - dDown);
|
|
847
|
+
vec2 refractUv = sampleUv + depthGradient * uRefractionStrength * rimProfile;
|
|
848
|
+
refractUv = clamp(refractUv, vec2(0.001), vec2(0.999));
|
|
849
|
+
|
|
850
|
+
vec4 refractedColor = texture(uInteriorColor, refractUv);
|
|
851
|
+
|
|
852
|
+
// === CHROMATIC FRINGE ===
|
|
853
|
+
float chromaticAmount = uChromaticStrength * depthReactivity * rimProfile;
|
|
854
|
+
vec2 chromaticDir = vNormal * chromaticAmount;
|
|
855
|
+
float cr = texture(uInteriorColor, refractUv + chromaticDir).r;
|
|
856
|
+
float cg = refractedColor.g;
|
|
857
|
+
float cb = texture(uInteriorColor, refractUv - chromaticDir).b;
|
|
858
|
+
vec3 chromaticColor = vec3(cr, cg, cb);
|
|
859
|
+
|
|
860
|
+
// === OCCLUSION CONTACT SHADOW ===
|
|
861
|
+
float occlusionAmount = smoothstep(0.4, 0.0, interiorDepth) * uOcclusionIntensity * rimProfile;
|
|
862
|
+
|
|
863
|
+
// === VOLUMETRIC EDGE WALL ===
|
|
864
|
+
// Sample distance field to get the inner-side distance at this boundary location
|
|
865
|
+
float edgeDist = texture(uDistField, sampleUv).r;
|
|
866
|
+
float wallZone = smoothstep(uEdgeThickness, 0.0, edgeDist) * rimProfile;
|
|
867
|
+
|
|
868
|
+
// Wall lighting from distance field gradient
|
|
869
|
+
vec2 dtx = vec2(1.0) / vec2(textureSize(uDistField, 0));
|
|
870
|
+
float wdL = texture(uDistField, sampleUv + vec2(-dtx.x, 0.0)).r;
|
|
871
|
+
float wdR = texture(uDistField, sampleUv + vec2( dtx.x, 0.0)).r;
|
|
872
|
+
float wdU = texture(uDistField, sampleUv + vec2(0.0, dtx.y)).r;
|
|
873
|
+
float wdD = texture(uDistField, sampleUv + vec2(0.0, -dtx.y)).r;
|
|
874
|
+
vec2 wallNormal = vec2(wdR - wdL, wdU - wdD);
|
|
875
|
+
float wnLen = length(wallNormal);
|
|
876
|
+
if (wnLen > 0.001) wallNormal /= wnLen;
|
|
877
|
+
|
|
878
|
+
float wallSpec = pow(max(dot(wallNormal, uLightDir), 0.0), 16.0) * uEdgeSpecular;
|
|
879
|
+
vec3 wallColor = mix(refractedColor.rgb * 0.4, uEdgeColor, 0.3);
|
|
880
|
+
wallColor += vec3(wallSpec);
|
|
881
|
+
|
|
882
|
+
// === COMPOSITE ===
|
|
883
|
+
vec3 color = mix(refractedColor.rgb, chromaticColor, min(chromaticAmount * 10.0, 1.0));
|
|
884
|
+
color *= (1.0 - occlusionAmount * 0.4);
|
|
885
|
+
|
|
886
|
+
// Blend in volumetric wall
|
|
887
|
+
color = mix(color, wallColor, wallZone * uBevelIntensity);
|
|
888
|
+
|
|
889
|
+
// Add rim energy on top
|
|
890
|
+
color += rimCol * rim;
|
|
891
|
+
|
|
892
|
+
// Alpha: rim edge fades out
|
|
893
|
+
float alpha = rimProfile * max(rim, occlusionAmount + chromaticAmount * 5.0 + wallZone * 0.5);
|
|
894
|
+
alpha = clamp(alpha, 0.0, 1.0);
|
|
895
|
+
|
|
896
|
+
fragColor = vec4(color * alpha, alpha);
|
|
897
|
+
}
|
|
898
|
+
`,ri=`#version 300 es
|
|
899
|
+
in vec2 aPosition;
|
|
900
|
+
in vec3 aNormal3;
|
|
901
|
+
in float aLerpT; // 0 = inner (at silhouette), 1 = outer edge
|
|
902
|
+
uniform vec2 uMeshScale;
|
|
903
|
+
out vec3 vNormal;
|
|
904
|
+
out vec2 vScreenUv;
|
|
905
|
+
out float vLerpT;
|
|
906
|
+
|
|
907
|
+
void main() {
|
|
908
|
+
vec2 sp = aPosition * uMeshScale;
|
|
909
|
+
vNormal = aNormal3;
|
|
910
|
+
vScreenUv = sp * 0.5 + 0.5;
|
|
911
|
+
vLerpT = aLerpT;
|
|
912
|
+
gl_Position = vec4(sp, 0.0, 1.0);
|
|
913
|
+
}
|
|
914
|
+
`,oi=`#version 300 es
|
|
915
|
+
precision highp float;
|
|
916
|
+
uniform vec3 uLightDir3;
|
|
917
|
+
uniform vec3 uChamferColor;
|
|
918
|
+
uniform float uChamferAmbient;
|
|
919
|
+
uniform float uChamferSpecular;
|
|
920
|
+
uniform float uChamferShininess;
|
|
921
|
+
uniform sampler2D uInteriorColor;
|
|
922
|
+
uniform vec2 uTexelSize; // 1 / viewport resolution
|
|
923
|
+
|
|
924
|
+
in vec3 vNormal;
|
|
925
|
+
in vec2 vScreenUv;
|
|
926
|
+
in float vLerpT;
|
|
927
|
+
out vec4 fragColor;
|
|
928
|
+
|
|
929
|
+
vec3 toLinear(vec3 s) {
|
|
930
|
+
return mix(s / 12.92, pow((s + 0.055) / 1.055, vec3(2.4)), step(0.04045, s));
|
|
931
|
+
}
|
|
932
|
+
vec3 toSRGB(vec3 l) {
|
|
933
|
+
return mix(l * 12.92, 1.055 * pow(l, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, l));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Approximate gaussian blur via 13-tap poisson disc, radius scaled by vLerpT.
|
|
937
|
+
vec3 blurSample(vec2 center, float radius) {
|
|
938
|
+
// Poisson disc offsets (normalized to unit circle)
|
|
939
|
+
const vec2 offsets[12] = vec2[12](
|
|
940
|
+
vec2(-0.326, -0.406), vec2(-0.840, -0.074), vec2(-0.696, 0.457),
|
|
941
|
+
vec2(-0.203, 0.621), vec2( 0.962, -0.195), vec2( 0.473, -0.480),
|
|
942
|
+
vec2( 0.519, 0.767), vec2( 0.185, -0.893), vec2( 0.507, 0.064),
|
|
943
|
+
vec2(-0.321, -0.860), vec2(-0.791, 0.557), vec2( 0.330, 0.418)
|
|
944
|
+
);
|
|
945
|
+
vec3 sum = texture(uInteriorColor, center).rgb;
|
|
946
|
+
for (int i = 0; i < 12; i++) {
|
|
947
|
+
vec2 uv = center + offsets[i] * radius;
|
|
948
|
+
uv = clamp(uv, vec2(0.001), vec2(0.999));
|
|
949
|
+
sum += texture(uInteriorColor, uv).rgb;
|
|
300
950
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
951
|
+
return sum / 13.0;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
void main() {
|
|
955
|
+
vec3 N = normalize(vNormal);
|
|
956
|
+
vec3 L = normalize(uLightDir3);
|
|
957
|
+
vec3 V = vec3(0.0, 0.0, -1.0); // orthographic view direction
|
|
958
|
+
|
|
959
|
+
// Blinn-Phong lighting in linear space
|
|
960
|
+
float diff = max(dot(N, L), 0.0);
|
|
961
|
+
vec3 H = normalize(L + V);
|
|
962
|
+
float spec = pow(max(dot(N, H), 0.0), uChamferShininess) * uChamferSpecular;
|
|
963
|
+
|
|
964
|
+
// Sample interior video with progressive blur (sharper at inner edge)
|
|
965
|
+
vec2 uv = clamp(vScreenUv, vec2(0.001), vec2(0.999));
|
|
966
|
+
float blurRadius = vLerpT * 12.0 * length(uTexelSize);
|
|
967
|
+
vec3 videoSample = blurRadius > 0.0001
|
|
968
|
+
? blurSample(uv, blurRadius)
|
|
969
|
+
: texture(uInteriorColor, uv).rgb;
|
|
970
|
+
|
|
971
|
+
// Base color: video tinted through chamfer color (like frosted glass)
|
|
972
|
+
vec3 video = toLinear(videoSample);
|
|
973
|
+
vec3 tint = toLinear(uChamferColor);
|
|
974
|
+
// Blend: mostly video near inner edge, more tinted at outer edge
|
|
975
|
+
vec3 base = mix(video, video * tint * 3.0, vLerpT * 0.5);
|
|
976
|
+
|
|
977
|
+
// Apply Blinn-Phong
|
|
978
|
+
vec3 lit = base * (uChamferAmbient + (1.0 - uChamferAmbient) * diff) + vec3(spec);
|
|
979
|
+
fragColor = vec4(toSRGB(lit), 1.0);
|
|
980
|
+
}
|
|
981
|
+
`;function Ve(o){const e=[];let t=0;for(let i=0;i<o.length-2;i+=2){const n=o[i],r=o[i+1],s=o[i+2],a=o[i+3],l=s-n,u=a-r,h=Math.sqrt(l*l+u*u);if(h<1e-6)continue;const f=-u/h,c=l/h;e.push(n,r,f,c,n,r,-f,-c,s,a,f,c,s,a,f,c,n,r,-f,-c,s,a,-f,-c),t+=6}return{vertices:new Float32Array(e),count:t}}function Ge(o,e,t,i,n){if(i<=0)return{vertices:new Float32Array(0),count:0};const r=n*Math.PI/180,s=-Math.cos(r),a=Math.sin(r),l=[];let u=0;for(let h=0;h<e.length;h++){const f=e[h],v=((h+1<e.length?e[h+1]:o.length)-f)/2;if(v<3)continue;const d=v-1;let A=0;for(let x=0;x<d;x++){const E=f+x*2,P=o[E],U=o[E+1],R=o[E+2],C=o[E+3];A+=P*C-R*U}const b=A>=0?1:-1,y=[],p=[];for(let x=0;x<d;x++){const E=f+x*2,P=o[E+2]-o[E],U=o[E+3]-o[E+1],R=Math.sqrt(P*P+U*U);R<1e-8?(y.push(x>0?y[x-1]:0),p.push(x>0?p[x-1]:0)):(y.push(-U/R*b),p.push(P/R*b))}const m=[],S=[];for(let x=0;x<d;x++){const E=(x-1+d)%d;let P=y[E]+y[x],U=p[E]+p[x];const R=Math.sqrt(P*P+U*U);R>1e-8?(P/=R,U/=R):(P=y[x],U=p[x]),m.push(P),S.push(U)}for(let x=0;x<d;x++){const E=x,P=(x+1)%d,U=f+x*2,R=f+(x+1)%d*2,C=o[U],I=o[U+1],D=o[R],w=o[R+1],L=m[E]*a,M=S[E]*a,O=s,_=m[P]*a,Y=S[P]*a,ie=s,fe=C+m[E]*i,$e=I+S[E]*i,Ki=D+m[P]*i,Qi=w+S[P]*i;l.push(C,I,L,M,O,0),l.push(fe,$e,L,M,O,1),l.push(D,w,_,Y,ie,0),l.push(D,w,_,Y,ie,0),l.push(fe,$e,L,M,O,1),l.push(Ki,Qi,_,Y,ie,1),u+=6}}return{vertices:new Float32Array(l),count:u}}class si extends k{gl=null;stencilPass=null;maskPass=null;jfaSeedPass=null;jfaFloodPass=null;jfaDistPass=null;interiorPass=null;compositePass=null;boundaryPass=null;chamferPass=null;quadVao=null;stencilVao=null;stencilIndexCount=0;maskVao=null;boundaryVao=null;boundaryVertexCount=0;chamferVao=null;chamferVertexCount=0;textures=new Ee;videoSlot;depthSlot;interiorFbo=null;interiorColorTex=null;interiorDepthTex=null;fboWidth=0;fboHeight=0;jfa=null;hasColorBufferFloat=!1;meshAspect=1;meshScaleX=.65;meshScaleY=.65;lightDirX=-.707;lightDirY=.707;lightDir3=[-.5,.7,-.3];config;constructor(e,t){super(e),this.config={...t},this.videoSlot=this.textures.register("video"),this.depthSlot=this.textures.register("depth");const i=this.config.bevelLightAngle*Math.PI/180;this.lightDirX=Math.cos(i),this.lightDirY=Math.sin(i);const n=this.config.lightDirection,r=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);r>1e-6&&(this.lightDir3=[n[0]/r,n[1]/r,n[2]/r]);const s=this.canvas.getContext("webgl2",{antialias:!0,alpha:!0,premultipliedAlpha:!0,stencil:!0,desynchronized:!0,powerPreference:"high-performance"});if(!s)throw new Error("WebGL 2 is not supported.");this.gl=s,this.qualityParams=we(s,t.quality),"drawingBufferColorSpace"in s&&(s.drawingBufferColorSpace="srgb"),this.hasColorBufferFloat=!!s.getExtension("EXT_color_buffer_float"),s.clearColor(0,0,0,0),s.pixelStorei(s.UNPACK_FLIP_Y_WEBGL,!0),this.initGPUResources(),this.setupResizeHandling()}initialize(e,t,i,n){const r=this.gl;r&&(this.disposeTextures(),this.disposeFBO(),this.jfa&&(this.jfa.dispose(),this.jfa=null),this.disposeStencilGeometry(),this.disposeBoundaryGeometry(),this.disposeChamferGeometry(),this.videoAspect=e.videoWidth/e.videoHeight,this.meshAspect=n.aspect,this.clampDepthDimensions(t,i,this.qualityParams.depthMaxDim),this.videoSlot.texture=r.createTexture(),r.activeTexture(r.TEXTURE0+this.videoSlot.unit),r.bindTexture(r.TEXTURE_2D,this.videoSlot.texture),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),this.depthSlot.texture=r.createTexture(),r.activeTexture(r.TEXTURE0+this.depthSlot.unit),r.bindTexture(r.TEXTURE_2D,this.depthSlot.texture),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),r.texStorage2D(r.TEXTURE_2D,1,r.R8,this.depthWidth,this.depthHeight),this.uploadStencilMesh(n),this.uploadMaskMesh(n),this.uploadBoundaryMesh(n),this.uploadChamferMesh(n),this.interiorPass&&(r.useProgram(this.interiorPass.program),r.uniform1i(this.interiorPass.uniforms.uImage,0),r.uniform1i(this.interiorPass.uniforms.uDepth,1),r.uniform1f(this.interiorPass.uniforms.uStrength,this.config.parallaxStrength),r.uniform1i(this.interiorPass.uniforms.uPomSteps,this.config.pomSteps),r.uniform1f(this.interiorPass.uniforms.uDepthPower,this.config.depthPower),r.uniform1f(this.interiorPass.uniforms.uDepthScale,this.config.depthScale),r.uniform1f(this.interiorPass.uniforms.uDepthBias,this.config.depthBias),r.uniform1f(this.interiorPass.uniforms.uContrastLow,this.config.contrastLow),r.uniform1f(this.interiorPass.uniforms.uContrastHigh,this.config.contrastHigh),r.uniform1f(this.interiorPass.uniforms.uVerticalReduction,this.config.verticalReduction),r.uniform1f(this.interiorPass.uniforms.uDofStart,this.config.dofStart),r.uniform1f(this.interiorPass.uniforms.uDofStrength,this.config.dofStrength),r.uniform2f(this.interiorPass.uniforms.uImageTexelSize,1/e.videoWidth,1/e.videoHeight),r.uniform1f(this.interiorPass.uniforms.uFogDensity,this.config.fogDensity),r.uniform3f(this.interiorPass.uniforms.uFogColor,...this.config.fogColor),r.uniform1f(this.interiorPass.uniforms.uColorShift,this.config.colorShift),r.uniform1f(this.interiorPass.uniforms.uBrightnessBias,this.config.brightnessBias)),this.compositePass&&(r.useProgram(this.compositePass.program),r.uniform1i(this.compositePass.uniforms.uInteriorColor,2),r.uniform1i(this.compositePass.uniforms.uDistField,4),r.uniform1f(this.compositePass.uniforms.uEdgeOcclusionWidth,this.config.edgeOcclusionWidth),r.uniform1f(this.compositePass.uniforms.uEdgeOcclusionStrength,this.config.edgeOcclusionStrength)),this.chamferPass&&(r.useProgram(this.chamferPass.program),r.uniform3f(this.chamferPass.uniforms.uLightDir3,...this.lightDir3),r.uniform3f(this.chamferPass.uniforms.uChamferColor,...this.config.chamferColor),r.uniform1f(this.chamferPass.uniforms.uChamferAmbient,this.config.chamferAmbient),r.uniform1f(this.chamferPass.uniforms.uChamferSpecular,this.config.chamferSpecular),r.uniform1f(this.chamferPass.uniforms.uChamferShininess,this.config.chamferShininess),r.uniform1i(this.chamferPass.uniforms.uInteriorColor,2)),this.boundaryPass&&(r.useProgram(this.boundaryPass.program),r.uniform1i(this.boundaryPass.uniforms.uInteriorColor,2),r.uniform1i(this.boundaryPass.uniforms.uInteriorDepth,3),r.uniform1i(this.boundaryPass.uniforms.uDistField,4),r.uniform1f(this.boundaryPass.uniforms.uRimIntensity,this.config.rimLightIntensity),r.uniform3f(this.boundaryPass.uniforms.uRimColor,...this.config.rimLightColor),r.uniform1f(this.boundaryPass.uniforms.uRefractionStrength,this.config.refractionStrength),r.uniform1f(this.boundaryPass.uniforms.uChromaticStrength,this.config.chromaticStrength),r.uniform1f(this.boundaryPass.uniforms.uOcclusionIntensity,this.config.occlusionIntensity),r.uniform1f(this.boundaryPass.uniforms.uEdgeThickness,this.config.edgeThickness),r.uniform1f(this.boundaryPass.uniforms.uEdgeSpecular,this.config.edgeSpecular),r.uniform3f(this.boundaryPass.uniforms.uEdgeColor,...this.config.edgeColor),r.uniform2f(this.boundaryPass.uniforms.uLightDir,this.lightDirX,this.lightDirY),r.uniform1f(this.boundaryPass.uniforms.uBevelIntensity,this.config.bevelIntensity)),this.recalculateViewportLayout())}uploadStencilMesh(e){const t=this.gl;if(!t||!this.stencilPass)return;this.stencilVao=t.createVertexArray(),t.bindVertexArray(this.stencilVao);const i=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,i),t.bufferData(t.ARRAY_BUFFER,e.vertices,t.STATIC_DRAW);const n=t.getAttribLocation(this.stencilPass.program,"aPosition");t.enableVertexAttribArray(n),t.vertexAttribPointer(n,2,t.FLOAT,!1,0,0);const r=t.createBuffer();t.bindBuffer(t.ELEMENT_ARRAY_BUFFER,r),t.bufferData(t.ELEMENT_ARRAY_BUFFER,e.indices,t.STATIC_DRAW),this.stencilIndexCount=e.indices.length,t.bindVertexArray(null)}uploadMaskMesh(e){const t=this.gl;if(!t||!this.maskPass)return;this.maskVao=t.createVertexArray(),t.bindVertexArray(this.maskVao);const i=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,i),t.bufferData(t.ARRAY_BUFFER,e.vertices,t.STATIC_DRAW);const n=t.getAttribLocation(this.maskPass.program,"aPosition");t.enableVertexAttribArray(n),t.vertexAttribPointer(n,2,t.FLOAT,!1,0,0);const r=t.createBuffer();t.bindBuffer(t.ELEMENT_ARRAY_BUFFER,r),t.bufferData(t.ELEMENT_ARRAY_BUFFER,e.indices,t.STATIC_DRAW),t.bindVertexArray(null)}uploadBoundaryMesh(e){const t=this.gl;if(!t||!this.boundaryPass)return;const i=Ve(e.edgeVertices);if(i.count===0)return;this.boundaryVao=t.createVertexArray(),t.bindVertexArray(this.boundaryVao);const n=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,n),t.bufferData(t.ARRAY_BUFFER,i.vertices,t.STATIC_DRAW);const r=16,s=t.getAttribLocation(this.boundaryPass.program,"aPosition");t.enableVertexAttribArray(s),t.vertexAttribPointer(s,2,t.FLOAT,!1,r,0);const a=t.getAttribLocation(this.boundaryPass.program,"aNormal");a>=0&&(t.enableVertexAttribArray(a),t.vertexAttribPointer(a,2,t.FLOAT,!1,r,8)),this.boundaryVertexCount=i.count,t.bindVertexArray(null)}uploadChamferMesh(e){const t=this.gl;if(!t||!this.chamferPass||this.config.chamferWidth<=0)return;const i=Ge(e.edgeVertices,e.contourOffsets,e.contourIsHole,this.config.chamferWidth,this.config.chamferAngle);if(i.count===0)return;this.chamferVao=t.createVertexArray(),t.bindVertexArray(this.chamferVao);const n=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,n),t.bufferData(t.ARRAY_BUFFER,i.vertices,t.STATIC_DRAW);const r=24,s=t.getAttribLocation(this.chamferPass.program,"aPosition");t.enableVertexAttribArray(s),t.vertexAttribPointer(s,2,t.FLOAT,!1,r,0);const a=t.getAttribLocation(this.chamferPass.program,"aNormal3");a>=0&&(t.enableVertexAttribArray(a),t.vertexAttribPointer(a,3,t.FLOAT,!1,r,8));const l=t.getAttribLocation(this.chamferPass.program,"aLerpT");l>=0&&(t.enableVertexAttribArray(l),t.vertexAttribPointer(l,1,t.FLOAT,!1,r,20)),this.chamferVertexCount=i.count,t.bindVertexArray(null)}disposeChamferGeometry(){const e=this.gl;e&&(this.chamferVao&&(e.deleteVertexArray(this.chamferVao),this.chamferVao=null),this.chamferVertexCount=0)}createFBO(e,t){const i=this.gl;if(!i)return;this.disposeFBO(),this.fboWidth=e,this.fboHeight=t,this.interiorFbo=i.createFramebuffer(),i.bindFramebuffer(i.FRAMEBUFFER,this.interiorFbo),this.interiorColorTex=i.createTexture(),i.activeTexture(i.TEXTURE2),i.bindTexture(i.TEXTURE_2D,this.interiorColorTex),i.texStorage2D(i.TEXTURE_2D,1,i.RGBA8,e,t),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MAG_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE),i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,this.interiorColorTex,0),this.interiorDepthTex=i.createTexture(),i.activeTexture(i.TEXTURE3),i.bindTexture(i.TEXTURE_2D,this.interiorDepthTex),i.texStorage2D(i.TEXTURE_2D,1,i.RGBA8,e,t),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MAG_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE),i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT1,i.TEXTURE_2D,this.interiorDepthTex,0),i.drawBuffers([i.COLOR_ATTACHMENT0,i.COLOR_ATTACHMENT1]);const n=i.checkFramebufferStatus(i.FRAMEBUFFER);n!==i.FRAMEBUFFER_COMPLETE&&console.error("Interior FBO incomplete:",n),i.bindFramebuffer(i.FRAMEBUFFER,null)}createJFAResources(e,t){const i=this.gl;i&&(this.jfa||(this.jfa=new se(i,this.hasColorBufferFloat)),this.jfa.createResources(e,t,this.qualityParams.jfaDivisor))}computeDistanceField(){!this.jfa||!this.maskPass||!this.jfaSeedPass||!this.jfaFloodPass||!this.jfaDistPass||!this.maskVao||!this.quadVao||this.jfa.compute({maskPass:this.maskPass,seedPass:this.jfaSeedPass,floodPass:this.jfaFloodPass,distPass:this.jfaDistPass,maskVao:this.maskVao,quadVao:this.quadVao,meshScaleX:this.meshScaleX,meshScaleY:this.meshScaleY,stencilIndexCount:this.stencilIndexCount,distRange:Math.max(this.config.bevelWidth,this.config.edgeOcclusionWidth)})}initGPUResources(){const e=this.gl;e&&(this.stencilPass=G(e,"stencil",Xt,Ht,["uMeshScale"]),this.maskPass=G(e,"mask",Wt,jt,["uMeshScale"]),this.jfaSeedPass=G(e,"jfa-seed",zt,qt,["uMask","uTexelSize"]),this.jfaFloodPass=G(e,"jfa-flood",Yt,Zt,["uSeedTex","uStepSize"]),this.jfaDistPass=G(e,"jfa-dist",Jt,$t,["uSeedTex","uMask","uBevelWidth"]),this.interiorPass=G(e,"interior",Kt,Qt,["uImage","uDepth","uOffset","uStrength","uPomSteps","uDepthPower","uDepthScale","uDepthBias","uContrastLow","uContrastHigh","uVerticalReduction","uDofStart","uDofStrength","uImageTexelSize","uFogDensity","uFogColor","uColorShift","uBrightnessBias","uUvOffset","uUvScale"]),this.compositePass=G(e,"composite",ei,ti,["uInteriorColor","uDistField","uEdgeOcclusionWidth","uEdgeOcclusionStrength"]),this.boundaryPass=G(e,"boundary",ii,ni,["uInteriorColor","uInteriorDepth","uDistField","uRimIntensity","uRimColor","uRimWidth","uMeshScale","uRefractionStrength","uChromaticStrength","uOcclusionIntensity","uTexelSize","uEdgeThickness","uEdgeSpecular","uEdgeColor","uLightDir","uBevelIntensity"]),this.chamferPass=G(e,"chamfer",ri,oi,["uMeshScale","uLightDir3","uChamferColor","uChamferAmbient","uChamferSpecular","uChamferShininess","uInteriorColor","uTexelSize"]),this.quadVao=ye(e,this.interiorPass.program),e.disable(e.DEPTH_TEST))}onRenderFrame(){const e=this.gl,t=this.playbackVideo;if(!e||!this.interiorPass||!this.quadVao||!t||t.readyState<HTMLMediaElement.HAVE_CURRENT_DATA||!this.interiorFbo||!this.interiorColorTex||!this.interiorDepthTex)return;this.jfa?.isDirty&&this.maskVao&&(this.computeDistanceField(),e.viewport(0,0,this.canvas.width,this.canvas.height)),e.activeTexture(e.TEXTURE0+this.videoSlot.unit),e.bindTexture(e.TEXTURE_2D,this.videoSlot.texture),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,t),this.rvfcSupported||this.onDepthUpdate(t.currentTime);let i=0,n=0;if(this.readInput){const r=this.readInput();i=-r.x,n=r.y}if(e.bindFramebuffer(e.FRAMEBUFFER,this.interiorFbo),e.checkFramebufferStatus(e.FRAMEBUFFER)!==e.FRAMEBUFFER_COMPLETE){e.bindFramebuffer(e.FRAMEBUFFER,null);return}e.viewport(0,0,this.fboWidth,this.fboHeight),e.clearColor(0,0,0,1),e.clear(e.COLOR_BUFFER_BIT),e.useProgram(this.interiorPass.program),e.uniform2f(this.interiorPass.uniforms.uOffset,i,n),e.activeTexture(e.TEXTURE0+this.videoSlot.unit),e.bindTexture(e.TEXTURE_2D,this.videoSlot.texture),e.activeTexture(e.TEXTURE0+this.depthSlot.unit),e.bindTexture(e.TEXTURE_2D,this.depthSlot.texture),e.bindVertexArray(this.quadVao),e.drawArrays(e.TRIANGLE_STRIP,0,4),e.bindFramebuffer(e.FRAMEBUFFER,null),e.clearColor(0,0,0,0),e.viewport(0,0,this.canvas.width,this.canvas.height),e.clear(e.COLOR_BUFFER_BIT|e.STENCIL_BUFFER_BIT),this.stencilVao&&this.stencilPass&&this.stencilIndexCount>0&&(e.enable(e.STENCIL_TEST),e.stencilFunc(e.ALWAYS,1,255),e.stencilOp(e.KEEP,e.KEEP,e.REPLACE),e.stencilMask(255),e.colorMask(!1,!1,!1,!1),e.useProgram(this.stencilPass.program),e.bindVertexArray(this.stencilVao),e.drawElements(e.TRIANGLES,this.stencilIndexCount,e.UNSIGNED_SHORT,0),e.colorMask(!0,!0,!0,!0)),e.stencilFunc(e.EQUAL,1,255),e.stencilMask(0),e.activeTexture(e.TEXTURE2),e.bindTexture(e.TEXTURE_2D,this.interiorColorTex),e.activeTexture(e.TEXTURE3),e.bindTexture(e.TEXTURE_2D,this.interiorDepthTex),e.activeTexture(e.TEXTURE4),e.bindTexture(e.TEXTURE_2D,this.jfa?.distanceTexture??null),e.useProgram(this.compositePass.program),e.bindVertexArray(this.quadVao),e.drawArrays(e.TRIANGLE_STRIP,0,4),e.disable(e.STENCIL_TEST),this.chamferVao&&this.chamferPass&&this.chamferVertexCount>0&&(e.useProgram(this.chamferPass.program),e.uniform2f(this.chamferPass.uniforms.uMeshScale,this.meshScaleX,this.meshScaleY),e.uniform2f(this.chamferPass.uniforms.uTexelSize,1/this.canvas.width,1/this.canvas.height),e.bindVertexArray(this.chamferVao),e.drawArrays(e.TRIANGLES,0,this.chamferVertexCount)),this.boundaryVao&&this.boundaryPass&&this.boundaryVertexCount>0&&this.config.rimLightIntensity>0&&(e.enable(e.BLEND),e.blendFunc(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA),e.useProgram(this.boundaryPass.program),e.bindVertexArray(this.boundaryVao),e.drawArrays(e.TRIANGLES,0,this.boundaryVertexCount),e.disable(e.BLEND))}onDepthUpdate(e){const t=this.gl;if(!t||!this.readDepth||!this.depthSlot.texture)return;const i=this.subsampleDepth(this.readDepth(e));t.activeTexture(t.TEXTURE0+this.depthSlot.unit),t.bindTexture(t.TEXTURE_2D,this.depthSlot.texture),t.texSubImage2D(t.TEXTURE_2D,0,0,0,this.depthWidth,this.depthHeight,t.RED,t.UNSIGNED_BYTE,i)}recalculateViewportLayout(){const e=this.gl;if(!e)return;const{width:t,height:i}=this.getViewportSize(),n=Math.min(window.devicePixelRatio,this.qualityParams.dprCap),r=Math.round(t*n),s=Math.round(i*n);(this.canvas.width!==r||this.canvas.height!==s)&&(this.canvas.width=r,this.canvas.height=s,e.viewport(0,0,r,s)),(this.fboWidth!==r||this.fboHeight!==s)&&this.createFBO(r,s);const a=this.qualityParams.jfaDivisor,l=Math.max(1,Math.round(r/a)),u=Math.max(1,Math.round(s/a));(!this.jfa||this.jfa.width!==l||this.jfa.height!==u)&&this.createJFAResources(r,s),this.computeCoverFitUV(this.config.parallaxStrength,this.config.overscanPadding),this.interiorPass&&(e.useProgram(this.interiorPass.program),e.uniform2f(this.interiorPass.uniforms.uUvOffset,this.uvOffset[0],this.uvOffset[1]),e.uniform2f(this.interiorPass.uniforms.uUvScale,this.uvScale[0],this.uvScale[1]));const h=t/i,f=.65;this.meshScaleX=f,this.meshScaleY=f,h>this.meshAspect?this.meshScaleX=f*(this.meshAspect/h):this.meshScaleY=f*(h/this.meshAspect),this.stencilPass&&(e.useProgram(this.stencilPass.program),e.uniform2f(this.stencilPass.uniforms.uMeshScale,this.meshScaleX,this.meshScaleY)),this.boundaryPass&&(e.useProgram(this.boundaryPass.program),e.uniform2f(this.boundaryPass.uniforms.uMeshScale,this.meshScaleX,this.meshScaleY),e.uniform1f(this.boundaryPass.uniforms.uRimWidth,this.config.rimLightWidth),e.uniform2f(this.boundaryPass.uniforms.uTexelSize,1/r,1/s)),this.chamferPass&&(e.useProgram(this.chamferPass.program),e.uniform2f(this.chamferPass.uniforms.uMeshScale,this.meshScaleX,this.meshScaleY)),this.jfa&&this.jfa.markDirty()}onContextRestored(){const e=this.canvas.getContext("webgl2",{alpha:!0,premultipliedAlpha:!0,stencil:!0});e&&(this.gl=e,this.hasColorBufferFloat=!!e.getExtension("EXT_color_buffer_float"),e.clearColor(0,0,0,0),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!0),this.initGPUResources(),this.recalculateViewportLayout(),this.playbackVideo&&(this.animationFrameHandle=window.requestAnimationFrame(()=>this.onRenderFrame())))}disposeTextures(){const e=this.gl;e&&this.textures.disposeAll(e)}disposeFBO(){const e=this.gl;e&&(this.interiorColorTex&&(e.deleteTexture(this.interiorColorTex),this.interiorColorTex=null),this.interiorDepthTex&&(e.deleteTexture(this.interiorDepthTex),this.interiorDepthTex=null),this.interiorFbo&&(e.deleteFramebuffer(this.interiorFbo),this.interiorFbo=null),this.fboWidth=0,this.fboHeight=0)}disposeStencilGeometry(){const e=this.gl;e&&(this.stencilVao&&(e.deleteVertexArray(this.stencilVao),this.stencilVao=null),this.maskVao&&(e.deleteVertexArray(this.maskVao),this.maskVao=null),this.stencilIndexCount=0)}disposeBoundaryGeometry(){const e=this.gl;e&&(this.boundaryVao&&(e.deleteVertexArray(this.boundaryVao),this.boundaryVao=null),this.boundaryVertexCount=0)}disposeRenderer(){this.disposeTextures(),this.disposeFBO(),this.jfa&&(this.jfa.dispose(),this.jfa=null),this.disposeStencilGeometry(),this.disposeBoundaryGeometry(),this.disposeChamferGeometry(),this.disposeGPUResources(),this.gl&&(this.gl.getExtension("WEBGL_lose_context")?.loseContext(),this.gl=null)}disposeGPUResources(){const e=this.gl;if(!e)return;const t=[this.stencilPass,this.maskPass,this.jfaSeedPass,this.jfaFloodPass,this.jfaDistPass,this.interiorPass,this.compositePass,this.boundaryPass,this.chamferPass];for(const i of t)i&&i.dispose(e);this.stencilPass=null,this.maskPass=null,this.jfaSeedPass=null,this.jfaFloodPass=null,this.jfaDistPass=null,this.interiorPass=null,this.compositePass=null,this.boundaryPass=null,this.chamferPass=null,this.quadVao&&(e.deleteVertexArray(this.quadVao),this.quadVao=null)}}const ai=`// Stencil mark pass — renders the triangulated SVG mesh into the stencil buffer.
|
|
982
|
+
// Color output is zeroed; only the stencil write matters.
|
|
983
|
+
//
|
|
984
|
+
// Bind group 0, binding 0: uniform buffer with mesh NDC scale factor.
|
|
985
|
+
// Vertex input: triangle list from earcut triangulation (vec2f positions in [-1,1]).
|
|
986
|
+
// Topology: triangle-list (not strip) — mesh comes from earcut.
|
|
987
|
+
|
|
988
|
+
struct Uniforms {
|
|
989
|
+
meshScale: vec2f,
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
993
|
+
|
|
994
|
+
struct VsOutput {
|
|
995
|
+
@builtin(position) position: vec4f,
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
@vertex
|
|
999
|
+
fn vs_main(@location(0) aPosition: vec2f) -> VsOutput {
|
|
1000
|
+
var out: VsOutput;
|
|
1001
|
+
out.position = vec4f(aPosition * uniforms.meshScale, 0.0, 1.0);
|
|
1002
|
+
return out;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Fragment outputs black — the stencil buffer write is what matters,
|
|
1006
|
+
// not the color. Matches GLSL: fragColor = vec4(0.0).
|
|
1007
|
+
@fragment
|
|
1008
|
+
fn fs_main() -> @location(0) vec4f {
|
|
1009
|
+
return vec4f(0.0, 0.0, 0.0, 0.0);
|
|
1010
|
+
}
|
|
1011
|
+
`,li=`// Mask pass — renders the triangulated SVG mesh as a solid white fill.
|
|
1012
|
+
// The output texture is consumed by the JFA seed pass for edge detection.
|
|
1013
|
+
//
|
|
1014
|
+
// Bind group 0, binding 0: uniform buffer with mesh NDC scale factor.
|
|
1015
|
+
// Vertex input: triangle list from earcut triangulation (vec2f positions in [-1,1]).
|
|
1016
|
+
// Topology: triangle-list (not strip) — mesh comes from earcut.
|
|
1017
|
+
|
|
1018
|
+
struct Uniforms {
|
|
1019
|
+
meshScale: vec2f,
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
1023
|
+
|
|
1024
|
+
struct VsOutput {
|
|
1025
|
+
@builtin(position) position: vec4f,
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
@vertex
|
|
1029
|
+
fn vs_main(@location(0) aPosition: vec2f) -> VsOutput {
|
|
1030
|
+
var out: VsOutput;
|
|
1031
|
+
out.position = vec4f(aPosition * uniforms.meshScale, 0.0, 1.0);
|
|
1032
|
+
return out;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// White fill — marks interior of the portal shape in the mask texture.
|
|
1036
|
+
// Matches GLSL: fragColor = vec4(1.0).
|
|
1037
|
+
@fragment
|
|
1038
|
+
fn fs_main() -> @location(0) vec4f {
|
|
1039
|
+
return vec4f(1.0, 1.0, 1.0, 1.0);
|
|
1040
|
+
}
|
|
1041
|
+
`,ui=`// JFA seed pass — detects edges in the binary mask via 4-neighbor comparison.
|
|
1042
|
+
// Edge pixels store their own UV as the seed coordinate (RG channels).
|
|
1043
|
+
// Non-edge pixels store (-1, -1) to mark "no seed here."
|
|
1044
|
+
//
|
|
1045
|
+
// This is the initialization step for Jump Flood Algorithm distance field generation.
|
|
1046
|
+
//
|
|
1047
|
+
// Uses textureSampleLevel (explicit LOD 0) instead of textureSample throughout
|
|
1048
|
+
// for consistency with other portal WGSL shaders and to prevent non-uniform
|
|
1049
|
+
// control flow issues if the shader is modified in the future.
|
|
1050
|
+
//
|
|
1051
|
+
// Bind group 0:
|
|
1052
|
+
// binding 0 = mask texture (r8unorm or rgba8unorm, only .r is read)
|
|
1053
|
+
// binding 1 = sampler for the mask texture
|
|
1054
|
+
// binding 2 = uniform buffer { texelSize: vec2f } for neighbor offsets
|
|
1055
|
+
//
|
|
1056
|
+
// Topology: triangle-strip fullscreen quad (4 vertices, no index buffer).
|
|
1057
|
+
|
|
1058
|
+
struct Uniforms {
|
|
1059
|
+
texelSize: vec2f,
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
@group(0) @binding(0) var maskTexture: texture_2d<f32>;
|
|
1063
|
+
@group(0) @binding(1) var maskSampler: sampler;
|
|
1064
|
+
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
|
1065
|
+
|
|
1066
|
+
struct VsOutput {
|
|
1067
|
+
@builtin(position) position: vec4f,
|
|
1068
|
+
@location(0) uv: vec2f,
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Fullscreen quad: input positions are clip-space corners {-1,-1}, {1,-1}, {-1,1}, {1,1}.
|
|
1072
|
+
// UV is derived as position * 0.5 + 0.5.
|
|
1073
|
+
@vertex
|
|
1074
|
+
fn vs_main(@location(0) aPosition: vec2f) -> VsOutput {
|
|
1075
|
+
var out: VsOutput;
|
|
1076
|
+
out.uv = aPosition * 0.5 + 0.5;
|
|
1077
|
+
out.position = vec4f(aPosition, 0.0, 1.0);
|
|
1078
|
+
return out;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Edge detection: a pixel is an edge if its mask classification (above/below 0.5)
|
|
1082
|
+
// differs from any of its 4 cardinal neighbors. This matches the GLSL step()-based
|
|
1083
|
+
// comparison exactly — step(0.5, x) returns 0.0 if x < 0.5, else 1.0, so
|
|
1084
|
+
// inequality of step values means the two pixels straddle the mask boundary.
|
|
1085
|
+
@fragment
|
|
1086
|
+
fn fs_main(in: VsOutput) -> @location(0) vec2f {
|
|
1087
|
+
let center = textureSampleLevel(maskTexture, maskSampler, in.uv, 0.0).r;
|
|
1088
|
+
let left = textureSampleLevel(maskTexture, maskSampler, in.uv + vec2f(-uniforms.texelSize.x, 0.0), 0.0).r;
|
|
1089
|
+
let right = textureSampleLevel(maskTexture, maskSampler, in.uv + vec2f( uniforms.texelSize.x, 0.0), 0.0).r;
|
|
1090
|
+
let up = textureSampleLevel(maskTexture, maskSampler, in.uv + vec2f(0.0, uniforms.texelSize.y), 0.0).r;
|
|
1091
|
+
let down = textureSampleLevel(maskTexture, maskSampler, in.uv + vec2f(0.0, -uniforms.texelSize.y), 0.0).r;
|
|
1092
|
+
|
|
1093
|
+
let centerStep = step(0.5, center);
|
|
1094
|
+
let isEdge = (centerStep != step(0.5, left)) ||
|
|
1095
|
+
(centerStep != step(0.5, right)) ||
|
|
1096
|
+
(centerStep != step(0.5, up)) ||
|
|
1097
|
+
(centerStep != step(0.5, down));
|
|
1098
|
+
|
|
1099
|
+
if (isEdge) {
|
|
1100
|
+
return in.uv;
|
|
1101
|
+
} else {
|
|
1102
|
+
return vec2f(-1.0, -1.0);
|
|
307
1103
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
1104
|
+
}
|
|
1105
|
+
`,hi=`// JFA flood fill pass — propagates nearest-seed information across the texture.
|
|
1106
|
+
// Each pixel examines its 9 neighbors (including self) at the current step size
|
|
1107
|
+
// and keeps the seed coordinate that is closest in UV space.
|
|
1108
|
+
//
|
|
1109
|
+
// Run iteratively with halving step sizes (N/2, N/4, ... 1) to build
|
|
1110
|
+
// an approximate Voronoi diagram / distance field from the edge seeds.
|
|
1111
|
+
//
|
|
1112
|
+
// Uses textureSampleLevel (explicit LOD 0) instead of textureSample throughout
|
|
1113
|
+
// because the loop body has per-fragment \`continue\` statements (UV bounds check)
|
|
1114
|
+
// that create non-uniform control flow. WGSL requires textureSample to be
|
|
1115
|
+
// called only from uniform control flow.
|
|
1116
|
+
//
|
|
1117
|
+
// Bind group 0:
|
|
1118
|
+
// binding 0 = uniform buffer { texelSize: vec2f, stepSize: f32 }
|
|
1119
|
+
// binding 1 = input seed texture (rg16float or rg32float, from previous pass)
|
|
1120
|
+
// binding 2 = sampler for the input texture
|
|
1121
|
+
//
|
|
1122
|
+
// Topology: triangle-strip fullscreen quad (4 vertices, no index buffer).
|
|
1123
|
+
|
|
1124
|
+
struct Uniforms {
|
|
1125
|
+
texelSize: vec2f,
|
|
1126
|
+
stepSize: f32,
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
1130
|
+
@group(0) @binding(1) var inputTexture: texture_2d<f32>;
|
|
1131
|
+
@group(0) @binding(2) var inputSampler: sampler;
|
|
1132
|
+
|
|
1133
|
+
struct VsOutput {
|
|
1134
|
+
@builtin(position) position: vec4f,
|
|
1135
|
+
@location(0) uv: vec2f,
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
@vertex
|
|
1139
|
+
fn vs_main(@location(0) aPosition: vec2f) -> VsOutput {
|
|
1140
|
+
var out: VsOutput;
|
|
1141
|
+
out.uv = aPosition * 0.5 + 0.5;
|
|
1142
|
+
out.position = vec4f(aPosition, 0.0, 1.0);
|
|
1143
|
+
return out;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// JFA flood step: for each of 9 neighbors at stepSize offset, read the stored
|
|
1147
|
+
// seed coordinate. If valid (seed.x >= 0), compute distance to that seed.
|
|
1148
|
+
// Keep the closest seed found across all neighbors.
|
|
1149
|
+
//
|
|
1150
|
+
// Seeds with x < 0 are uninitialized (no edge seed has reached that texel yet).
|
|
1151
|
+
// Out-of-bounds sample UVs are skipped to avoid wrapping artifacts.
|
|
1152
|
+
@fragment
|
|
1153
|
+
fn fs_main(in: VsOutput) -> @location(0) vec2f {
|
|
1154
|
+
var bestSeed = textureSampleLevel(inputTexture, inputSampler, in.uv, 0.0).rg;
|
|
1155
|
+
var bestDist: f32;
|
|
1156
|
+
if (bestSeed.x < 0.0) {
|
|
1157
|
+
bestDist = 1.0e10;
|
|
1158
|
+
} else {
|
|
1159
|
+
bestDist = distance(in.uv, bestSeed);
|
|
332
1160
|
}
|
|
333
|
-
`,oe=`#version 300 es
|
|
334
|
-
in vec2 aPosition;
|
|
335
|
-
uniform vec2 uUvOffset;
|
|
336
|
-
uniform vec2 uUvScale;
|
|
337
|
-
out vec2 vUv;
|
|
338
|
-
out vec2 vScreenUv;
|
|
339
|
-
void main() {
|
|
340
|
-
vec2 baseUv = aPosition * 0.5 + 0.5;
|
|
341
|
-
vUv = baseUv * uUvScale + uUvOffset;
|
|
342
|
-
vScreenUv = baseUv;
|
|
343
|
-
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
344
|
-
}
|
|
345
|
-
`,ne=`#version 300 es
|
|
346
|
-
precision highp float;
|
|
347
1161
|
|
|
348
|
-
|
|
1162
|
+
// Iterate over the 3x3 neighbor grid at current step size.
|
|
1163
|
+
// The loop is unrolled as two nested loops over {-1, 0, 1}.
|
|
1164
|
+
for (var dy: i32 = -1; dy <= 1; dy = dy + 1) {
|
|
1165
|
+
for (var dx: i32 = -1; dx <= 1; dx = dx + 1) {
|
|
1166
|
+
// Skip center — already read above as the initial best.
|
|
1167
|
+
if (dx == 0 && dy == 0) {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
349
1170
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
uniform vec2 uOffset;
|
|
353
|
-
uniform float uStrength;
|
|
354
|
-
uniform int uPomSteps;
|
|
1171
|
+
let offset = vec2f(f32(dx), f32(dy)) * uniforms.stepSize;
|
|
1172
|
+
let sampleUv = in.uv + offset;
|
|
355
1173
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
1174
|
+
// Clamp to [0,1] — skip samples that would read outside the texture.
|
|
1175
|
+
if (sampleUv.x < 0.0 || sampleUv.x > 1.0 || sampleUv.y < 0.0 || sampleUv.y > 1.0) {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
360
1178
|
|
|
361
|
-
|
|
362
|
-
uniform float uContrastLow;
|
|
363
|
-
uniform float uContrastHigh;
|
|
364
|
-
uniform float uVerticalReduction;
|
|
1179
|
+
let neighborSeed = textureSampleLevel(inputTexture, inputSampler, sampleUv, 0.0).rg;
|
|
365
1180
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
1181
|
+
// Skip uninitialized seeds.
|
|
1182
|
+
if (neighborSeed.x < 0.0) {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
370
1185
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
in vec2 vUv;
|
|
378
|
-
in vec2 vScreenUv;
|
|
379
|
-
|
|
380
|
-
layout(location = 0) out vec4 fragColor;
|
|
381
|
-
layout(location = 1) out vec4 fragDepth;
|
|
382
|
-
|
|
383
|
-
// Apply lens transform to raw depth
|
|
384
|
-
float lensDepth(float raw) {
|
|
385
|
-
float d = smoothstep(uContrastLow, uContrastHigh, raw);
|
|
386
|
-
d = pow(d, uDepthPower) * uDepthScale + uDepthBias;
|
|
387
|
-
return clamp(d, 0.0, 1.0);
|
|
1186
|
+
let d = distance(in.uv, neighborSeed);
|
|
1187
|
+
if (d < bestDist) {
|
|
1188
|
+
bestDist = d;
|
|
1189
|
+
bestSeed = neighborSeed;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
388
1192
|
}
|
|
389
1193
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1194
|
+
return bestSeed;
|
|
1195
|
+
}
|
|
1196
|
+
`,ci=`// JFA distance pass — converts nearest-seed UV from flood fill into a normalized
|
|
1197
|
+
// distance value. Pixels outside the mask are zeroed; pixels with no valid seed
|
|
1198
|
+
// are set to 1.0 (maximum distance). Otherwise the Euclidean UV-space distance
|
|
1199
|
+
// to the seed is divided by uBevelWidth to produce a 0..1 ramp.
|
|
1200
|
+
//
|
|
1201
|
+
// Uses textureSampleLevel (explicit LOD 0) instead of textureSample throughout
|
|
1202
|
+
// because early returns based on texture data create non-uniform control flow.
|
|
1203
|
+
// WGSL requires textureSample to be called only from uniform control flow.
|
|
1204
|
+
//
|
|
1205
|
+
// Bind group 0:
|
|
1206
|
+
// binding 0 = uniform buffer { texelSize: vec2f, bevelWidth: f32 }
|
|
1207
|
+
// binding 1 = seed texture (rg16float/rg32float from JFA flood output)
|
|
1208
|
+
// binding 2 = sampler for seed texture
|
|
1209
|
+
// binding 3 = mask texture (r8unorm, only .r is read)
|
|
1210
|
+
// binding 4 = sampler for mask texture
|
|
1211
|
+
//
|
|
1212
|
+
// Topology: triangle-strip fullscreen quad (4 vertices, no index buffer).
|
|
1213
|
+
|
|
1214
|
+
struct Uniforms {
|
|
1215
|
+
texelSize: vec2f,
|
|
1216
|
+
bevelWidth: f32,
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
1220
|
+
@group(0) @binding(1) var seedTexture: texture_2d<f32>;
|
|
1221
|
+
@group(0) @binding(2) var seedSampler: sampler;
|
|
1222
|
+
@group(0) @binding(3) var maskTexture: texture_2d<f32>;
|
|
1223
|
+
@group(0) @binding(4) var maskSampler: sampler;
|
|
1224
|
+
|
|
1225
|
+
struct VsOutput {
|
|
1226
|
+
@builtin(position) position: vec4f,
|
|
1227
|
+
@location(0) uv: vec2f,
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
@vertex
|
|
1231
|
+
fn vs_main(@location(0) aPosition: vec2f) -> VsOutput {
|
|
1232
|
+
var out: VsOutput;
|
|
1233
|
+
out.uv = aPosition * 0.5 + 0.5;
|
|
1234
|
+
out.position = vec4f(aPosition, 0.0, 1.0);
|
|
1235
|
+
return out;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Distance computation: mirrors the GLSL exactly.
|
|
1239
|
+
// - Pixels outside mask (mask < 0.5) -> black (zero distance / not part of shape).
|
|
1240
|
+
// - Pixels with no valid seed (seed.x < 0) -> maximum distance (1.0 everywhere).
|
|
1241
|
+
// - Otherwise: Euclidean distance from UV to seed, normalized by bevelWidth.
|
|
1242
|
+
@fragment
|
|
1243
|
+
fn fs_main(in: VsOutput) -> @location(0) vec4f {
|
|
1244
|
+
let mask = textureSampleLevel(maskTexture, maskSampler, in.uv, 0.0).r;
|
|
1245
|
+
if (mask < 0.5) {
|
|
1246
|
+
return vec4f(0.0, 0.0, 0.0, 0.0);
|
|
395
1247
|
}
|
|
396
1248
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
vec2 scaledOffset = uOffset;
|
|
401
|
-
scaledOffset.y *= uVerticalReduction;
|
|
402
|
-
vec2 deltaUV = scaledOffset * uStrength / float(uPomSteps);
|
|
403
|
-
float currentLayerDepth = 0.0;
|
|
404
|
-
vec2 currentUV = uv;
|
|
405
|
-
float fade = edgeFade(uv);
|
|
406
|
-
|
|
407
|
-
for (int i = 0; i < MAX_POM_STEPS; i++) {
|
|
408
|
-
if (i >= uPomSteps) break;
|
|
409
|
-
float raw = texture(uDepth, currentUV).r;
|
|
410
|
-
float depthAtUV = 1.0 - lensDepth(raw);
|
|
411
|
-
if (currentLayerDepth > depthAtUV) {
|
|
412
|
-
vec2 prevUV = currentUV - deltaUV;
|
|
413
|
-
float prevLayerD = currentLayerDepth - layerD;
|
|
414
|
-
float prevRaw = texture(uDepth, prevUV).r;
|
|
415
|
-
float prevDepthAtUV = 1.0 - lensDepth(prevRaw);
|
|
416
|
-
float afterD = depthAtUV - currentLayerDepth;
|
|
417
|
-
float beforeD = prevDepthAtUV - prevLayerD;
|
|
418
|
-
float t = afterD / (afterD - beforeD);
|
|
419
|
-
vec2 hitUV = mix(currentUV, prevUV, t);
|
|
420
|
-
hitDepth = mix(depthAtUV, prevDepthAtUV, t);
|
|
421
|
-
return mix(uv, hitUV, fade);
|
|
422
|
-
}
|
|
423
|
-
currentUV += deltaUV;
|
|
424
|
-
currentLayerDepth += layerD;
|
|
425
|
-
}
|
|
426
|
-
hitDepth = 1.0 - lensDepth(texture(uDepth, currentUV).r);
|
|
427
|
-
return mix(uv, currentUV, fade);
|
|
1249
|
+
let seed = textureSampleLevel(seedTexture, seedSampler, in.uv, 0.0).rg;
|
|
1250
|
+
if (seed.x < 0.0) {
|
|
1251
|
+
return vec4f(1.0, 1.0, 1.0, 1.0);
|
|
428
1252
|
}
|
|
429
1253
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
1254
|
+
let d = distance(in.uv, seed);
|
|
1255
|
+
let normalized = clamp(d / max(uniforms.bevelWidth, 0.001), 0.0, 1.0);
|
|
1256
|
+
return vec4f(normalized, 0.0, 0.0, 1.0);
|
|
1257
|
+
}
|
|
1258
|
+
`,fi=`// Interior pass — the most complex portal shader. Renders the parallax-displaced
|
|
1259
|
+
// video scene visible through the portal opening.
|
|
1260
|
+
//
|
|
1261
|
+
// Pipeline: POM ray-march -> barrel lens distortion -> DOF blur -> volumetric fog
|
|
1262
|
+
// -> color grading -> vignette -> MRT output (color + depth).
|
|
1263
|
+
//
|
|
1264
|
+
// The vertex shader applies a cover-fit UV transform so the video fills the
|
|
1265
|
+
// viewport regardless of aspect ratio.
|
|
1266
|
+
//
|
|
1267
|
+
// Uses textureSampleLevel (explicit LOD 0) instead of textureSample throughout
|
|
1268
|
+
// because POM ray-marching requires texture reads in non-uniform control flow
|
|
1269
|
+
// (loop iterations and conditional branches that depend on texture values).
|
|
1270
|
+
// WGSL requires textureSample to be called only from uniform control flow.
|
|
1271
|
+
//
|
|
1272
|
+
// Bind group 0:
|
|
1273
|
+
// binding 0 = vertex uniform buffer (uvOffset, uvScale)
|
|
1274
|
+
// binding 1 = fragment uniform buffer (all effect parameters)
|
|
1275
|
+
// binding 2 = video (image) texture
|
|
1276
|
+
// binding 3 = video sampler
|
|
1277
|
+
// binding 4 = depth texture
|
|
1278
|
+
// binding 5 = depth sampler
|
|
1279
|
+
//
|
|
1280
|
+
// Topology: triangle-strip fullscreen quad (4 vertices, no index buffer).
|
|
1281
|
+
// Render targets: location 0 = color (rgba8unorm), location 1 = depth (r8unorm).
|
|
1282
|
+
|
|
1283
|
+
// Pipeline-overridable POM loop bound. The actual step count is controlled by
|
|
1284
|
+
// the uniform uPomSteps, but WGSL requires a compile-time loop bound.
|
|
1285
|
+
override MAX_POM_STEPS: i32 = 64;
|
|
1286
|
+
|
|
1287
|
+
struct VertexUniforms {
|
|
1288
|
+
uvOffset: vec2f,
|
|
1289
|
+
uvScale: vec2f,
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
struct FragmentUniforms {
|
|
1293
|
+
// Input / parallax control
|
|
1294
|
+
offset: vec2f, // mouse/gyro input
|
|
1295
|
+
strength: f32, // parallax displacement magnitude
|
|
1296
|
+
pomSteps: i32, // active POM step count (<= MAX_POM_STEPS)
|
|
459
1297
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1298
|
+
// Lens transform: remap depth curve for exaggerated/compressed depth feel
|
|
1299
|
+
depthPower: f32, // >1 = telephoto, <1 = wide-angle
|
|
1300
|
+
depthScale: f32, // multiplier on depth range
|
|
1301
|
+
depthBias: f32, // shift depth origin
|
|
464
1302
|
|
|
465
|
-
|
|
466
|
-
|
|
1303
|
+
// Depth-adaptive contrast
|
|
1304
|
+
contrastLow: f32,
|
|
1305
|
+
contrastHigh: f32,
|
|
1306
|
+
verticalReduction: f32,
|
|
467
1307
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1308
|
+
// DOF
|
|
1309
|
+
dofStart: f32,
|
|
1310
|
+
dofStrength: f32,
|
|
1311
|
+
imageTexelSize: vec2f,
|
|
471
1312
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
1313
|
+
// Interior mood
|
|
1314
|
+
fogDensity: f32, // volumetric fog bias (0 = none, 0.3 = subtle)
|
|
1315
|
+
fogColor: vec3f, // fog tint color
|
|
1316
|
+
colorShift: f32, // warm/cool grading shift
|
|
1317
|
+
brightnessBias: f32, // overall brightness adjustment
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
@group(0) @binding(0) var<uniform> vertUniforms: VertexUniforms;
|
|
1321
|
+
@group(0) @binding(1) var<uniform> fragUniforms: FragmentUniforms;
|
|
1322
|
+
@group(0) @binding(2) var imageTexture: texture_2d<f32>;
|
|
1323
|
+
@group(0) @binding(3) var imageSampler: sampler;
|
|
1324
|
+
@group(0) @binding(4) var depthTexture: texture_2d<f32>;
|
|
1325
|
+
@group(0) @binding(5) var depthSampler: sampler;
|
|
1326
|
+
|
|
1327
|
+
struct VsOutput {
|
|
1328
|
+
@builtin(position) position: vec4f,
|
|
1329
|
+
@location(0) uv: vec2f, // cover-fit UV for video/depth sampling
|
|
1330
|
+
@location(1) screenUv: vec2f, // raw 0..1 UV for vignette etc.
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
@vertex
|
|
1334
|
+
fn vs_main(@location(0) aPosition: vec2f) -> VsOutput {
|
|
1335
|
+
var out: VsOutput;
|
|
1336
|
+
let baseUv = aPosition * 0.5 + 0.5;
|
|
1337
|
+
out.uv = baseUv * vertUniforms.uvScale + vertUniforms.uvOffset;
|
|
1338
|
+
out.screenUv = baseUv;
|
|
1339
|
+
out.position = vec4f(aPosition, 0.0, 1.0);
|
|
1340
|
+
return out;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// --- Fragment helpers ---
|
|
1344
|
+
|
|
1345
|
+
// Apply lens transform to raw 0..1 depth value.
|
|
1346
|
+
// Contrast remap -> power curve -> scale + bias -> clamp.
|
|
1347
|
+
fn lensDepth(raw: f32) -> f32 {
|
|
1348
|
+
var d = smoothstep(fragUniforms.contrastLow, fragUniforms.contrastHigh, raw);
|
|
1349
|
+
d = pow(d, fragUniforms.depthPower) * fragUniforms.depthScale + fragUniforms.depthBias;
|
|
1350
|
+
return clamp(d, 0.0, 1.0);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Fade displacement near UV edges to avoid boundary artifacts.
|
|
1354
|
+
fn edgeFade(uv: vec2f) -> f32 {
|
|
1355
|
+
let margin = fragUniforms.strength * 1.5;
|
|
1356
|
+
let fadeX = smoothstep(0.0, margin, uv.x) * smoothstep(0.0, margin, 1.0 - uv.x);
|
|
1357
|
+
let fadeY = smoothstep(0.0, margin, uv.y) * smoothstep(0.0, margin, 1.0 - uv.y);
|
|
1358
|
+
return fadeX * fadeY;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// MRT output: color at location 0, depth at location 1.
|
|
1362
|
+
struct FragOutput {
|
|
1363
|
+
@location(0) color: vec4f,
|
|
1364
|
+
@location(1) depth: vec4f,
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
@fragment
|
|
1368
|
+
fn fs_main(in: VsOutput) -> FragOutput {
|
|
1369
|
+
// --- POM ray-march with lens-transformed depth ---
|
|
1370
|
+
let layerD = 1.0 / f32(fragUniforms.pomSteps);
|
|
1371
|
+
var scaledOffset = fragUniforms.offset;
|
|
1372
|
+
scaledOffset.y *= fragUniforms.verticalReduction;
|
|
1373
|
+
let deltaUV = scaledOffset * fragUniforms.strength / f32(fragUniforms.pomSteps);
|
|
1374
|
+
var currentLayerDepth: f32 = 0.0;
|
|
1375
|
+
var currentUV = in.uv;
|
|
1376
|
+
let fade = edgeFade(in.uv);
|
|
1377
|
+
|
|
1378
|
+
// hitDepth tracks the interpolated depth at the POM intersection point.
|
|
1379
|
+
var hitDepth: f32 = 0.0;
|
|
1380
|
+
var pomHit = false;
|
|
1381
|
+
|
|
1382
|
+
for (var i: i32 = 0; i < MAX_POM_STEPS; i = i + 1) {
|
|
1383
|
+
if (i >= fragUniforms.pomSteps) {
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
let raw = textureSampleLevel(depthTexture, depthSampler, currentUV, 0.0).r;
|
|
1387
|
+
let depthAtUV = 1.0 - lensDepth(raw);
|
|
1388
|
+
if (currentLayerDepth > depthAtUV) {
|
|
1389
|
+
// Intersection found — refine with linear interpolation between
|
|
1390
|
+
// current and previous layer.
|
|
1391
|
+
let prevUV = currentUV - deltaUV;
|
|
1392
|
+
let prevLayerD = currentLayerDepth - layerD;
|
|
1393
|
+
let prevRaw = textureSampleLevel(depthTexture, depthSampler, prevUV, 0.0).r;
|
|
1394
|
+
let prevDepthAtUV = 1.0 - lensDepth(prevRaw);
|
|
1395
|
+
let afterD = depthAtUV - currentLayerDepth;
|
|
1396
|
+
let beforeD = prevDepthAtUV - prevLayerD;
|
|
1397
|
+
let t = afterD / (afterD - beforeD);
|
|
1398
|
+
let hitUV = mix(currentUV, prevUV, t);
|
|
1399
|
+
hitDepth = mix(depthAtUV, prevDepthAtUV, t);
|
|
1400
|
+
currentUV = mix(in.uv, hitUV, fade);
|
|
1401
|
+
pomHit = true;
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
currentUV = currentUV + deltaUV;
|
|
1405
|
+
currentLayerDepth = currentLayerDepth + layerD;
|
|
499
1406
|
}
|
|
500
1407
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
// Emissive passthrough: preserve original video luminance.
|
|
506
|
-
// Only apply a subtle edge occlusion ramp to sell chamfer→interior depth.
|
|
507
|
-
vec3 linear = toLinear(color.rgb);
|
|
508
|
-
float occ = smoothstep(0.0, uEdgeOcclusionWidth, dist);
|
|
509
|
-
linear *= mix(1.0 - uEdgeOcclusionStrength, 1.0, occ);
|
|
510
|
-
|
|
511
|
-
fragColor = vec4(toSRGB(linear), color.a);
|
|
512
|
-
}
|
|
513
|
-
`,he=`#version 300 es
|
|
514
|
-
in vec2 aPosition;
|
|
515
|
-
in vec2 aNormal;
|
|
516
|
-
uniform float uRimWidth;
|
|
517
|
-
uniform vec2 uMeshScale;
|
|
518
|
-
out vec2 vNormal;
|
|
519
|
-
out vec2 vEdgeUv; // screen-space UV for sampling FBO textures
|
|
520
|
-
out float vEdgeDist; // 0 at edge, 1 at outer extent
|
|
521
|
-
|
|
522
|
-
void main() {
|
|
523
|
-
vec2 scaledPos = aPosition * uMeshScale;
|
|
524
|
-
vec2 scaledNormal = normalize(aNormal * uMeshScale);
|
|
525
|
-
vec2 pos = scaledPos + scaledNormal * uRimWidth;
|
|
526
|
-
|
|
527
|
-
// Pass screen-space UV of this fragment for FBO sampling
|
|
528
|
-
vEdgeUv = pos * 0.5 + 0.5;
|
|
529
|
-
vNormal = scaledNormal;
|
|
530
|
-
|
|
531
|
-
// Distance from the actual edge (0) to the outer rim extent (1)
|
|
532
|
-
vEdgeDist = length(pos - scaledPos) / max(uRimWidth, 0.001);
|
|
533
|
-
|
|
534
|
-
gl_Position = vec4(pos, 0.0, 1.0);
|
|
535
|
-
}
|
|
536
|
-
`,le=`#version 300 es
|
|
537
|
-
precision highp float;
|
|
538
|
-
|
|
539
|
-
uniform sampler2D uInteriorColor;
|
|
540
|
-
uniform sampler2D uInteriorDepth;
|
|
541
|
-
uniform sampler2D uDistField;
|
|
542
|
-
uniform float uRimIntensity;
|
|
543
|
-
uniform vec3 uRimColor;
|
|
544
|
-
uniform float uRefractionStrength;
|
|
545
|
-
uniform float uChromaticStrength;
|
|
546
|
-
uniform float uOcclusionIntensity;
|
|
547
|
-
uniform vec2 uTexelSize; // 1.0 / viewport resolution
|
|
548
|
-
|
|
549
|
-
// Volumetric edge wall
|
|
550
|
-
uniform float uEdgeThickness;
|
|
551
|
-
uniform float uEdgeSpecular;
|
|
552
|
-
uniform vec3 uEdgeColor;
|
|
553
|
-
uniform vec2 uLightDir;
|
|
554
|
-
uniform float uBevelIntensity;
|
|
555
|
-
|
|
556
|
-
in vec2 vNormal;
|
|
557
|
-
in vec2 vEdgeUv;
|
|
558
|
-
in float vEdgeDist;
|
|
559
|
-
out vec4 fragColor;
|
|
560
|
-
|
|
561
|
-
void main() {
|
|
562
|
-
// Clamp UV to valid range for texture sampling
|
|
563
|
-
vec2 sampleUv = clamp(vEdgeUv, vec2(0.001), vec2(0.999));
|
|
564
|
-
|
|
565
|
-
// Sample interior depth at this boundary location
|
|
566
|
-
float interiorDepth = texture(uInteriorDepth, sampleUv).r;
|
|
567
|
-
|
|
568
|
-
// === DEPTH-REACTIVE RIM (structural seam) ===
|
|
569
|
-
float depthReactivity = 1.0 - interiorDepth; // 1=near, 0=far
|
|
570
|
-
float rimProfile = 1.0 - smoothstep(0.0, 1.0, vEdgeDist);
|
|
571
|
-
rimProfile = pow(rimProfile, 1.5); // sharper falloff = more structural
|
|
572
|
-
|
|
573
|
-
float depthPressure = mix(0.2, 1.0, depthReactivity * depthReactivity);
|
|
574
|
-
float rim = rimProfile * depthPressure * uRimIntensity;
|
|
575
|
-
|
|
576
|
-
vec3 rimCol = uRimColor;
|
|
577
|
-
rimCol.r += depthReactivity * 0.15;
|
|
578
|
-
rimCol.g += depthReactivity * 0.05;
|
|
579
|
-
|
|
580
|
-
// === REFRACTION DISTORTION ===
|
|
581
|
-
vec2 ts = uTexelSize * 3.0;
|
|
582
|
-
float dLeft = texture(uInteriorDepth, sampleUv + vec2(-ts.x, 0.0)).r;
|
|
583
|
-
float dRight = texture(uInteriorDepth, sampleUv + vec2( ts.x, 0.0)).r;
|
|
584
|
-
float dUp = texture(uInteriorDepth, sampleUv + vec2(0.0, ts.y)).r;
|
|
585
|
-
float dDown = texture(uInteriorDepth, sampleUv + vec2(0.0, -ts.y)).r;
|
|
586
|
-
vec2 depthGradient = vec2(dRight - dLeft, dUp - dDown);
|
|
587
|
-
vec2 refractUv = sampleUv + depthGradient * uRefractionStrength * rimProfile;
|
|
588
|
-
refractUv = clamp(refractUv, vec2(0.001), vec2(0.999));
|
|
589
|
-
|
|
590
|
-
vec4 refractedColor = texture(uInteriorColor, refractUv);
|
|
591
|
-
|
|
592
|
-
// === CHROMATIC FRINGE ===
|
|
593
|
-
float chromaticAmount = uChromaticStrength * depthReactivity * rimProfile;
|
|
594
|
-
vec2 chromaticDir = vNormal * chromaticAmount;
|
|
595
|
-
float cr = texture(uInteriorColor, refractUv + chromaticDir).r;
|
|
596
|
-
float cg = refractedColor.g;
|
|
597
|
-
float cb = texture(uInteriorColor, refractUv - chromaticDir).b;
|
|
598
|
-
vec3 chromaticColor = vec3(cr, cg, cb);
|
|
599
|
-
|
|
600
|
-
// === OCCLUSION CONTACT SHADOW ===
|
|
601
|
-
float occlusionAmount = smoothstep(0.4, 0.0, interiorDepth) * uOcclusionIntensity * rimProfile;
|
|
602
|
-
|
|
603
|
-
// === VOLUMETRIC EDGE WALL ===
|
|
604
|
-
// Sample distance field to get the inner-side distance at this boundary location
|
|
605
|
-
float edgeDist = texture(uDistField, sampleUv).r;
|
|
606
|
-
float wallZone = smoothstep(uEdgeThickness, 0.0, edgeDist) * rimProfile;
|
|
607
|
-
|
|
608
|
-
// Wall lighting from distance field gradient
|
|
609
|
-
vec2 dtx = vec2(1.0) / vec2(textureSize(uDistField, 0));
|
|
610
|
-
float wdL = texture(uDistField, sampleUv + vec2(-dtx.x, 0.0)).r;
|
|
611
|
-
float wdR = texture(uDistField, sampleUv + vec2( dtx.x, 0.0)).r;
|
|
612
|
-
float wdU = texture(uDistField, sampleUv + vec2(0.0, dtx.y)).r;
|
|
613
|
-
float wdD = texture(uDistField, sampleUv + vec2(0.0, -dtx.y)).r;
|
|
614
|
-
vec2 wallNormal = vec2(wdR - wdL, wdU - wdD);
|
|
615
|
-
float wnLen = length(wallNormal);
|
|
616
|
-
if (wnLen > 0.001) wallNormal /= wnLen;
|
|
617
|
-
|
|
618
|
-
float wallSpec = pow(max(dot(wallNormal, uLightDir), 0.0), 16.0) * uEdgeSpecular;
|
|
619
|
-
vec3 wallColor = mix(refractedColor.rgb * 0.4, uEdgeColor, 0.3);
|
|
620
|
-
wallColor += vec3(wallSpec);
|
|
621
|
-
|
|
622
|
-
// === COMPOSITE ===
|
|
623
|
-
vec3 color = mix(refractedColor.rgb, chromaticColor, min(chromaticAmount * 10.0, 1.0));
|
|
624
|
-
color *= (1.0 - occlusionAmount * 0.4);
|
|
625
|
-
|
|
626
|
-
// Blend in volumetric wall
|
|
627
|
-
color = mix(color, wallColor, wallZone * uBevelIntensity);
|
|
628
|
-
|
|
629
|
-
// Add rim energy on top
|
|
630
|
-
color += rimCol * rim;
|
|
631
|
-
|
|
632
|
-
// Alpha: rim edge fades out
|
|
633
|
-
float alpha = rimProfile * max(rim, occlusionAmount + chromaticAmount * 5.0 + wallZone * 0.5);
|
|
634
|
-
alpha = clamp(alpha, 0.0, 1.0);
|
|
635
|
-
|
|
636
|
-
fragColor = vec4(color * alpha, alpha);
|
|
637
|
-
}
|
|
638
|
-
`,ce=`#version 300 es
|
|
639
|
-
in vec2 aPosition;
|
|
640
|
-
in vec3 aNormal3;
|
|
641
|
-
in float aLerpT; // 0 = inner (at silhouette), 1 = outer edge
|
|
642
|
-
uniform vec2 uMeshScale;
|
|
643
|
-
out vec3 vNormal;
|
|
644
|
-
out vec2 vScreenUv;
|
|
645
|
-
out float vLerpT;
|
|
646
|
-
|
|
647
|
-
void main() {
|
|
648
|
-
vec2 sp = aPosition * uMeshScale;
|
|
649
|
-
vNormal = aNormal3;
|
|
650
|
-
vScreenUv = sp * 0.5 + 0.5;
|
|
651
|
-
vLerpT = aLerpT;
|
|
652
|
-
gl_Position = vec4(sp, 0.0, 1.0);
|
|
1408
|
+
// No intersection — use the last marched position.
|
|
1409
|
+
if (!pomHit) {
|
|
1410
|
+
hitDepth = 1.0 - lensDepth(textureSampleLevel(depthTexture, depthSampler, currentUV, 0.0).r);
|
|
1411
|
+
currentUV = mix(in.uv, currentUV, fade);
|
|
653
1412
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1413
|
+
|
|
1414
|
+
let displaced = clamp(currentUV, vec2f(0.0), vec2f(1.0));
|
|
1415
|
+
var color = textureSampleLevel(imageTexture, imageSampler, displaced, 0.0);
|
|
1416
|
+
|
|
1417
|
+
// --- DOF: blur far objects ---
|
|
1418
|
+
let rawDepthAtHit = textureSampleLevel(depthTexture, depthSampler, displaced, 0.0).r;
|
|
1419
|
+
let lensD = lensDepth(rawDepthAtHit);
|
|
1420
|
+
let dof = smoothstep(fragUniforms.dofStart, 1.0, lensD) * fragUniforms.dofStrength;
|
|
1421
|
+
if (dof > 0.01) {
|
|
1422
|
+
let ts = fragUniforms.imageTexelSize;
|
|
1423
|
+
let blurred = (
|
|
1424
|
+
textureSampleLevel(imageTexture, imageSampler, displaced + vec2f( ts.x, 0.0), 0.0) +
|
|
1425
|
+
textureSampleLevel(imageTexture, imageSampler, displaced + vec2f(-ts.x, 0.0), 0.0) +
|
|
1426
|
+
textureSampleLevel(imageTexture, imageSampler, displaced + vec2f( 0.0, ts.y), 0.0) +
|
|
1427
|
+
textureSampleLevel(imageTexture, imageSampler, displaced + vec2f( 0.0, -ts.y), 0.0) +
|
|
1428
|
+
textureSampleLevel(imageTexture, imageSampler, displaced + vec2f( ts.x, ts.y), 0.0) +
|
|
1429
|
+
textureSampleLevel(imageTexture, imageSampler, displaced + vec2f(-ts.x, -ts.y), 0.0) +
|
|
1430
|
+
textureSampleLevel(imageTexture, imageSampler, displaced + vec2f( ts.x, -ts.y), 0.0) +
|
|
1431
|
+
textureSampleLevel(imageTexture, imageSampler, displaced + vec2f(-ts.x, ts.y), 0.0)
|
|
1432
|
+
) * 0.125;
|
|
1433
|
+
color = mix(color, blurred, dof);
|
|
674
1434
|
}
|
|
675
1435
|
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1436
|
+
// --- Volumetric fog: far objects fade into fog color ---
|
|
1437
|
+
let fogFactor = smoothstep(0.3, 1.0, lensD) * fragUniforms.fogDensity;
|
|
1438
|
+
color = vec4f(mix(color.rgb, fragUniforms.fogColor, fogFactor), color.a);
|
|
1439
|
+
|
|
1440
|
+
// --- Color grading: warm near, cool far (or vice versa) ---
|
|
1441
|
+
let gradeAmount = (lensD - 0.5) * fragUniforms.colorShift;
|
|
1442
|
+
color = vec4f(
|
|
1443
|
+
color.r + gradeAmount * 0.08,
|
|
1444
|
+
color.g,
|
|
1445
|
+
color.b - gradeAmount * 0.08,
|
|
1446
|
+
color.a,
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
// --- Brightness bias ---
|
|
1450
|
+
color = vec4f(color.rgb * (1.0 + fragUniforms.brightnessBias), color.a);
|
|
1451
|
+
|
|
1452
|
+
// --- Subtle vignette inside portal ---
|
|
1453
|
+
let dist = length(in.screenUv - 0.5) * 1.4;
|
|
1454
|
+
color = vec4f(color.rgb * (1.0 - pow(dist, 3.0) * 0.3), color.a);
|
|
1455
|
+
|
|
1456
|
+
var out: FragOutput;
|
|
1457
|
+
out.color = color;
|
|
1458
|
+
// Write lens-transformed depth to second attachment for boundary effects.
|
|
1459
|
+
out.depth = vec4f(lensD, 0.0, 0.0, 1.0);
|
|
1460
|
+
return out;
|
|
1461
|
+
}
|
|
1462
|
+
`,di=`// Composite pass — composites the interior FBO color over the background,
|
|
1463
|
+
// applying a subtle edge occlusion ramp driven by the JFA distance field.
|
|
1464
|
+
//
|
|
1465
|
+
// The interior video luminance is preserved (emissive passthrough). The only
|
|
1466
|
+
// modification is a smooth darkening near the portal edge to sell the
|
|
1467
|
+
// chamfer-to-interior depth transition.
|
|
1468
|
+
//
|
|
1469
|
+
// sRGB conversions ensure occlusion math happens in linear space.
|
|
1470
|
+
//
|
|
1471
|
+
// Uses textureSampleLevel (explicit LOD 0) instead of textureSample throughout
|
|
1472
|
+
// for consistency with other portal WGSL shaders and to prevent non-uniform
|
|
1473
|
+
// control flow issues if the shader is modified in the future.
|
|
1474
|
+
//
|
|
1475
|
+
// Bind group 0:
|
|
1476
|
+
// binding 0 = uniform buffer { edgeOcclusionWidth: f32, edgeOcclusionStrength: f32 }
|
|
1477
|
+
// binding 1 = interior color texture (rgba8unorm from interior pass)
|
|
1478
|
+
// binding 2 = interior color sampler
|
|
1479
|
+
// binding 3 = distance field texture (r8unorm from JFA distance pass)
|
|
1480
|
+
// binding 4 = distance field sampler
|
|
1481
|
+
//
|
|
1482
|
+
// Topology: triangle-strip fullscreen quad (4 vertices, no index buffer).
|
|
1483
|
+
|
|
1484
|
+
struct Uniforms {
|
|
1485
|
+
edgeOcclusionWidth: f32, // how far edge darkening extends (in dist-field units)
|
|
1486
|
+
edgeOcclusionStrength: f32, // how strong (0 = none, 1 = full black at edge)
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
1490
|
+
@group(0) @binding(1) var interiorColorTexture: texture_2d<f32>;
|
|
1491
|
+
@group(0) @binding(2) var interiorColorSampler: sampler;
|
|
1492
|
+
@group(0) @binding(3) var distFieldTexture: texture_2d<f32>;
|
|
1493
|
+
@group(0) @binding(4) var distFieldSampler: sampler;
|
|
1494
|
+
|
|
1495
|
+
struct VsOutput {
|
|
1496
|
+
@builtin(position) position: vec4f,
|
|
1497
|
+
@location(0) uv: vec2f,
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
@vertex
|
|
1501
|
+
fn vs_main(@location(0) aPosition: vec2f) -> VsOutput {
|
|
1502
|
+
var out: VsOutput;
|
|
1503
|
+
out.uv = aPosition * 0.5 + 0.5;
|
|
1504
|
+
out.position = vec4f(aPosition, 0.0, 1.0);
|
|
1505
|
+
return out;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// --- sRGB <-> linear conversions for correct lighting math ---
|
|
1509
|
+
|
|
1510
|
+
fn toLinear(s: vec3f) -> vec3f {
|
|
1511
|
+
// Per-channel: if s <= 0.04045, linear = s/12.92
|
|
1512
|
+
// else linear = ((s + 0.055) / 1.055)^2.4
|
|
1513
|
+
return mix(
|
|
1514
|
+
s / 12.92,
|
|
1515
|
+
pow((s + 0.055) / 1.055, vec3f(2.4)),
|
|
1516
|
+
step(vec3f(0.04045), s),
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
fn toSRGB(l: vec3f) -> vec3f {
|
|
1521
|
+
// Per-channel: if l <= 0.0031308, srgb = l * 12.92
|
|
1522
|
+
// else srgb = 1.055 * l^(1/2.4) - 0.055
|
|
1523
|
+
return mix(
|
|
1524
|
+
l * 12.92,
|
|
1525
|
+
1.055 * pow(l, vec3f(1.0 / 2.4)) - 0.055,
|
|
1526
|
+
step(vec3f(0.0031308), l),
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
@fragment
|
|
1531
|
+
fn fs_main(in: VsOutput) -> @location(0) vec4f {
|
|
1532
|
+
let color = textureSampleLevel(interiorColorTexture, interiorColorSampler, in.uv, 0.0);
|
|
1533
|
+
let dist = textureSampleLevel(distFieldTexture, distFieldSampler, in.uv, 0.0).r; // 0=edge, 1=deep interior
|
|
1534
|
+
|
|
1535
|
+
// Emissive passthrough: preserve original video luminance.
|
|
1536
|
+
// Only apply a subtle edge occlusion ramp to sell chamfer->interior depth.
|
|
1537
|
+
var linear = toLinear(color.rgb);
|
|
1538
|
+
let occ = smoothstep(0.0, uniforms.edgeOcclusionWidth, dist);
|
|
1539
|
+
linear *= mix(1.0 - uniforms.edgeOcclusionStrength, 1.0, occ);
|
|
1540
|
+
|
|
1541
|
+
return vec4f(toSRGB(linear), color.a);
|
|
1542
|
+
}
|
|
1543
|
+
`,pi=`// Boundary effects pass — renders the visual boundary between the portal interior
|
|
1544
|
+
// and the surrounding scene. Produces depth-reactive rim lighting, refraction
|
|
1545
|
+
// distortion, chromatic aberration fringe, contact occlusion shadows, and a
|
|
1546
|
+
// volumetric edge wall effect.
|
|
1547
|
+
//
|
|
1548
|
+
// The vertex shader takes the extruded boundary mesh (position + 2D normal) and
|
|
1549
|
+
// pushes vertices outward along their normals by uRimWidth to create the
|
|
1550
|
+
// boundary ribbon geometry.
|
|
1551
|
+
//
|
|
1552
|
+
// Uses textureSampleLevel (explicit LOD 0) instead of textureSample throughout
|
|
1553
|
+
// for consistency with other portal WGSL shaders and to prevent non-uniform
|
|
1554
|
+
// control flow issues if the shader is modified in the future.
|
|
1555
|
+
//
|
|
1556
|
+
// Bind group 0:
|
|
1557
|
+
// binding 0 = uniform buffer (all effect parameters)
|
|
1558
|
+
// binding 1 = interior color texture (rgba8unorm from interior pass)
|
|
1559
|
+
// binding 2 = interior color sampler
|
|
1560
|
+
// binding 3 = interior depth texture (r8unorm from interior pass MRT)
|
|
1561
|
+
// binding 4 = interior depth sampler
|
|
1562
|
+
// binding 5 = distance field texture (r8unorm from JFA distance pass)
|
|
1563
|
+
// binding 6 = distance field sampler
|
|
1564
|
+
//
|
|
1565
|
+
// Vertex input: position (vec2f) + normal (vec2f) per boundary vertex.
|
|
1566
|
+
// Topology: triangle-strip or triangle-list (boundary ribbon mesh).
|
|
1567
|
+
|
|
1568
|
+
struct Uniforms {
|
|
1569
|
+
// Vertex uniforms
|
|
1570
|
+
rimWidth: f32,
|
|
1571
|
+
meshScale: vec2f,
|
|
1572
|
+
|
|
1573
|
+
// Fragment uniforms — rim
|
|
1574
|
+
rimIntensity: f32,
|
|
1575
|
+
rimColor: vec3f,
|
|
1576
|
+
|
|
1577
|
+
// Fragment uniforms — refraction / chromatic
|
|
1578
|
+
refractionStrength: f32,
|
|
1579
|
+
chromaticStrength: f32,
|
|
1580
|
+
occlusionIntensity: f32,
|
|
1581
|
+
texelSize: vec2f, // 1.0 / viewport resolution
|
|
1582
|
+
|
|
1583
|
+
// Fragment uniforms — volumetric edge wall
|
|
1584
|
+
edgeThickness: f32,
|
|
1585
|
+
edgeSpecular: f32,
|
|
1586
|
+
edgeColor: vec3f,
|
|
1587
|
+
lightDir: vec2f,
|
|
1588
|
+
bevelIntensity: f32,
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
1592
|
+
@group(0) @binding(1) var interiorColorTexture: texture_2d<f32>;
|
|
1593
|
+
@group(0) @binding(2) var interiorColorSampler: sampler;
|
|
1594
|
+
@group(0) @binding(3) var interiorDepthTexture: texture_2d<f32>;
|
|
1595
|
+
@group(0) @binding(4) var interiorDepthSampler: sampler;
|
|
1596
|
+
@group(0) @binding(5) var distFieldTexture: texture_2d<f32>;
|
|
1597
|
+
@group(0) @binding(6) var distFieldSampler: sampler;
|
|
1598
|
+
|
|
1599
|
+
struct VsOutput {
|
|
1600
|
+
@builtin(position) position: vec4f,
|
|
1601
|
+
@location(0) normal: vec2f,
|
|
1602
|
+
@location(1) edgeUv: vec2f, // screen-space UV for sampling FBO textures
|
|
1603
|
+
@location(2) edgeDist: f32, // 0 at edge, 1 at outer extent
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
@vertex
|
|
1607
|
+
fn vs_main(
|
|
1608
|
+
@location(0) aPosition: vec2f,
|
|
1609
|
+
@location(1) aNormal: vec2f,
|
|
1610
|
+
) -> VsOutput {
|
|
1611
|
+
let scaledPos = aPosition * uniforms.meshScale;
|
|
1612
|
+
let scaledNormal = normalize(aNormal * uniforms.meshScale);
|
|
1613
|
+
let pos = scaledPos + scaledNormal * uniforms.rimWidth;
|
|
1614
|
+
|
|
1615
|
+
var out: VsOutput;
|
|
1616
|
+
// Pass screen-space UV of this fragment for FBO sampling.
|
|
1617
|
+
out.edgeUv = pos * 0.5 + 0.5;
|
|
1618
|
+
out.normal = scaledNormal;
|
|
1619
|
+
// Distance from the actual edge (0) to the outer rim extent (1).
|
|
1620
|
+
out.edgeDist = length(pos - scaledPos) / max(uniforms.rimWidth, 0.001);
|
|
1621
|
+
out.position = vec4f(pos, 0.0, 1.0);
|
|
1622
|
+
return out;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
@fragment
|
|
1626
|
+
fn fs_main(in: VsOutput) -> @location(0) vec4f {
|
|
1627
|
+
// Clamp UV to valid range for texture sampling.
|
|
1628
|
+
let sampleUv = clamp(in.edgeUv, vec2f(0.001), vec2f(0.999));
|
|
1629
|
+
|
|
1630
|
+
// Sample interior depth at this boundary location.
|
|
1631
|
+
let interiorDepth = textureSampleLevel(interiorDepthTexture, interiorDepthSampler, sampleUv, 0.0).r;
|
|
1632
|
+
|
|
1633
|
+
// === DEPTH-REACTIVE RIM (structural seam) ===
|
|
1634
|
+
let depthReactivity = 1.0 - interiorDepth; // 1=near, 0=far
|
|
1635
|
+
var rimProfile = 1.0 - smoothstep(0.0, 1.0, in.edgeDist);
|
|
1636
|
+
rimProfile = pow(rimProfile, 1.5); // sharper falloff = more structural
|
|
1637
|
+
|
|
1638
|
+
let depthPressure = mix(0.2, 1.0, depthReactivity * depthReactivity);
|
|
1639
|
+
let rim = rimProfile * depthPressure * uniforms.rimIntensity;
|
|
1640
|
+
|
|
1641
|
+
var rimCol = uniforms.rimColor;
|
|
1642
|
+
rimCol.r += depthReactivity * 0.15;
|
|
1643
|
+
rimCol.g += depthReactivity * 0.05;
|
|
1644
|
+
|
|
1645
|
+
// === REFRACTION DISTORTION ===
|
|
1646
|
+
let ts = uniforms.texelSize * 3.0;
|
|
1647
|
+
let dLeft = textureSampleLevel(interiorDepthTexture, interiorDepthSampler, sampleUv + vec2f(-ts.x, 0.0), 0.0).r;
|
|
1648
|
+
let dRight = textureSampleLevel(interiorDepthTexture, interiorDepthSampler, sampleUv + vec2f( ts.x, 0.0), 0.0).r;
|
|
1649
|
+
let dUp = textureSampleLevel(interiorDepthTexture, interiorDepthSampler, sampleUv + vec2f(0.0, ts.y), 0.0).r;
|
|
1650
|
+
let dDown = textureSampleLevel(interiorDepthTexture, interiorDepthSampler, sampleUv + vec2f(0.0, -ts.y), 0.0).r;
|
|
1651
|
+
let depthGradient = vec2f(dRight - dLeft, dUp - dDown);
|
|
1652
|
+
let refractUv = clamp(
|
|
1653
|
+
sampleUv + depthGradient * uniforms.refractionStrength * rimProfile,
|
|
1654
|
+
vec2f(0.001),
|
|
1655
|
+
vec2f(0.999),
|
|
1656
|
+
);
|
|
1657
|
+
|
|
1658
|
+
let refractedColor = textureSampleLevel(interiorColorTexture, interiorColorSampler, refractUv, 0.0);
|
|
1659
|
+
|
|
1660
|
+
// === CHROMATIC FRINGE ===
|
|
1661
|
+
let chromaticAmount = uniforms.chromaticStrength * depthReactivity * rimProfile;
|
|
1662
|
+
let chromaticDir = in.normal * chromaticAmount;
|
|
1663
|
+
let cr = textureSampleLevel(interiorColorTexture, interiorColorSampler, refractUv + chromaticDir, 0.0).r;
|
|
1664
|
+
let cg = refractedColor.g;
|
|
1665
|
+
let cb = textureSampleLevel(interiorColorTexture, interiorColorSampler, refractUv - chromaticDir, 0.0).b;
|
|
1666
|
+
let chromaticColor = vec3f(cr, cg, cb);
|
|
1667
|
+
|
|
1668
|
+
// === OCCLUSION CONTACT SHADOW ===
|
|
1669
|
+
let occlusionAmount = smoothstep(0.4, 0.0, interiorDepth) * uniforms.occlusionIntensity * rimProfile;
|
|
1670
|
+
|
|
1671
|
+
// === VOLUMETRIC EDGE WALL ===
|
|
1672
|
+
// Sample distance field to get the inner-side distance at this boundary location.
|
|
1673
|
+
let edgeDist = textureSampleLevel(distFieldTexture, distFieldSampler, sampleUv, 0.0).r;
|
|
1674
|
+
let wallZone = smoothstep(uniforms.edgeThickness, 0.0, edgeDist) * rimProfile;
|
|
1675
|
+
|
|
1676
|
+
// Wall lighting from distance field gradient.
|
|
1677
|
+
let distDims = vec2f(textureDimensions(distFieldTexture, 0));
|
|
1678
|
+
let dtx = vec2f(1.0) / distDims;
|
|
1679
|
+
let wdL = textureSampleLevel(distFieldTexture, distFieldSampler, sampleUv + vec2f(-dtx.x, 0.0), 0.0).r;
|
|
1680
|
+
let wdR = textureSampleLevel(distFieldTexture, distFieldSampler, sampleUv + vec2f( dtx.x, 0.0), 0.0).r;
|
|
1681
|
+
let wdU = textureSampleLevel(distFieldTexture, distFieldSampler, sampleUv + vec2f(0.0, dtx.y), 0.0).r;
|
|
1682
|
+
let wdD = textureSampleLevel(distFieldTexture, distFieldSampler, sampleUv + vec2f(0.0, -dtx.y), 0.0).r;
|
|
1683
|
+
var wallNormal = vec2f(wdR - wdL, wdU - wdD);
|
|
1684
|
+
let wnLen = length(wallNormal);
|
|
1685
|
+
if (wnLen > 0.001) {
|
|
1686
|
+
wallNormal = wallNormal / wnLen;
|
|
692
1687
|
}
|
|
693
1688
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1689
|
+
let wallSpec = pow(max(dot(wallNormal, uniforms.lightDir), 0.0), 16.0) * uniforms.edgeSpecular;
|
|
1690
|
+
var wallColor = mix(refractedColor.rgb * 0.4, uniforms.edgeColor, 0.3);
|
|
1691
|
+
wallColor += vec3f(wallSpec);
|
|
1692
|
+
|
|
1693
|
+
// === COMPOSITE ===
|
|
1694
|
+
var color = mix(refractedColor.rgb, chromaticColor, min(chromaticAmount * 10.0, 1.0));
|
|
1695
|
+
color *= (1.0 - occlusionAmount * 0.4);
|
|
1696
|
+
|
|
1697
|
+
// Blend in volumetric wall.
|
|
1698
|
+
color = mix(color, wallColor, wallZone * uniforms.bevelIntensity);
|
|
1699
|
+
|
|
1700
|
+
// Add rim energy on top.
|
|
1701
|
+
color += rimCol * rim;
|
|
1702
|
+
|
|
1703
|
+
// Alpha: rim edge fades out.
|
|
1704
|
+
var alpha = rimProfile * max(rim, occlusionAmount + chromaticAmount * 5.0 + wallZone * 0.5);
|
|
1705
|
+
alpha = clamp(alpha, 0.0, 1.0);
|
|
1706
|
+
|
|
1707
|
+
return vec4f(color * alpha, alpha);
|
|
1708
|
+
}
|
|
1709
|
+
`,mi=`// Chamfer pass — renders the beveled ring around the portal opening with
|
|
1710
|
+
// Blinn-Phong lighting and a frosted glass effect. The interior video is
|
|
1711
|
+
// sampled through the chamfer with a progressive poisson disc blur (sharper
|
|
1712
|
+
// at the inner edge, more blurred toward the outer edge), then tinted through
|
|
1713
|
+
// the chamfer color to simulate frosted glass.
|
|
1714
|
+
//
|
|
1715
|
+
// Uses textureSampleLevel (explicit LOD 0) instead of textureSample throughout
|
|
1716
|
+
// because the blur function is called from a per-fragment conditional branch
|
|
1717
|
+
// (blurRadius varies per vertex). WGSL requires textureSample to be called
|
|
1718
|
+
// only from uniform control flow.
|
|
1719
|
+
//
|
|
1720
|
+
// Bind group 0:
|
|
1721
|
+
// binding 0 = uniform buffer (lighting + chamfer parameters)
|
|
1722
|
+
// binding 1 = interior color texture (rgba8unorm from interior pass)
|
|
1723
|
+
// binding 2 = interior color sampler
|
|
1724
|
+
//
|
|
1725
|
+
// Vertex input: position (vec2f) + normal3 (vec3f) + lerpT (f32).
|
|
1726
|
+
// Topology: triangle-strip or triangle-list (chamfer ring mesh).
|
|
1727
|
+
|
|
1728
|
+
struct Uniforms {
|
|
1729
|
+
lightDir3: vec3f,
|
|
1730
|
+
chamferColor: vec3f,
|
|
1731
|
+
chamferAmbient: f32,
|
|
1732
|
+
chamferSpecular: f32,
|
|
1733
|
+
chamferShininess: f32,
|
|
1734
|
+
meshScale: vec2f,
|
|
1735
|
+
texelSize: vec2f, // 1 / viewport resolution
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
1739
|
+
@group(0) @binding(1) var interiorColorTexture: texture_2d<f32>;
|
|
1740
|
+
@group(0) @binding(2) var interiorColorSampler: sampler;
|
|
1741
|
+
|
|
1742
|
+
struct VsOutput {
|
|
1743
|
+
@builtin(position) position: vec4f,
|
|
1744
|
+
@location(0) normal: vec3f,
|
|
1745
|
+
@location(1) screenUv: vec2f,
|
|
1746
|
+
@location(2) lerpT: f32,
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
@vertex
|
|
1750
|
+
fn vs_main(
|
|
1751
|
+
@location(0) aPosition: vec2f,
|
|
1752
|
+
@location(1) aNormal3: vec3f,
|
|
1753
|
+
@location(2) aLerpT: f32,
|
|
1754
|
+
) -> VsOutput {
|
|
1755
|
+
let sp = aPosition * uniforms.meshScale;
|
|
1756
|
+
|
|
1757
|
+
var out: VsOutput;
|
|
1758
|
+
out.normal = aNormal3;
|
|
1759
|
+
out.screenUv = sp * 0.5 + 0.5;
|
|
1760
|
+
out.lerpT = aLerpT;
|
|
1761
|
+
out.position = vec4f(sp, 0.0, 1.0);
|
|
1762
|
+
return out;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// --- sRGB <-> linear conversions for correct lighting math ---
|
|
1766
|
+
|
|
1767
|
+
fn toLinear(s: vec3f) -> vec3f {
|
|
1768
|
+
return mix(
|
|
1769
|
+
s / 12.92,
|
|
1770
|
+
pow((s + 0.055) / 1.055, vec3f(2.4)),
|
|
1771
|
+
step(vec3f(0.04045), s),
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
fn toSRGB(l: vec3f) -> vec3f {
|
|
1776
|
+
return mix(
|
|
1777
|
+
l * 12.92,
|
|
1778
|
+
1.055 * pow(l, vec3f(1.0 / 2.4)) - 0.055,
|
|
1779
|
+
step(vec3f(0.0031308), l),
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Approximate gaussian blur via 13-tap poisson disc, radius scaled by lerpT.
|
|
1784
|
+
// Poisson disc offsets are normalized to the unit circle.
|
|
1785
|
+
fn blurSample(center: vec2f, radius: f32) -> vec3f {
|
|
1786
|
+
// 12 poisson disc offsets — same values as the GLSL const array.
|
|
1787
|
+
let o0 = vec2f(-0.326, -0.406);
|
|
1788
|
+
let o1 = vec2f(-0.840, -0.074);
|
|
1789
|
+
let o2 = vec2f(-0.696, 0.457);
|
|
1790
|
+
let o3 = vec2f(-0.203, 0.621);
|
|
1791
|
+
let o4 = vec2f( 0.962, -0.195);
|
|
1792
|
+
let o5 = vec2f( 0.473, -0.480);
|
|
1793
|
+
let o6 = vec2f( 0.519, 0.767);
|
|
1794
|
+
let o7 = vec2f( 0.185, -0.893);
|
|
1795
|
+
let o8 = vec2f( 0.507, 0.064);
|
|
1796
|
+
let o9 = vec2f(-0.321, -0.860);
|
|
1797
|
+
let o10 = vec2f(-0.791, 0.557);
|
|
1798
|
+
let o11 = vec2f( 0.330, 0.418);
|
|
1799
|
+
|
|
1800
|
+
// Center tap.
|
|
1801
|
+
var sum = textureSampleLevel(interiorColorTexture, interiorColorSampler, center, 0.0).rgb;
|
|
1802
|
+
|
|
1803
|
+
// 12 offset taps. Each UV is clamped to avoid edge sampling artifacts.
|
|
1804
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o0 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1805
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o1 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1806
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o2 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1807
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o3 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1808
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o4 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1809
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o5 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1810
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o6 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1811
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o7 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1812
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o8 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1813
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o9 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1814
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o10 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1815
|
+
sum += textureSampleLevel(interiorColorTexture, interiorColorSampler, clamp(center + o11 * radius, vec2f(0.001), vec2f(0.999)), 0.0).rgb;
|
|
1816
|
+
|
|
1817
|
+
return sum / 13.0;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
@fragment
|
|
1821
|
+
fn fs_main(in: VsOutput) -> @location(0) vec4f {
|
|
1822
|
+
let N = normalize(in.normal);
|
|
1823
|
+
let L = normalize(uniforms.lightDir3);
|
|
1824
|
+
let V = vec3f(0.0, 0.0, -1.0); // orthographic view direction
|
|
1825
|
+
|
|
1826
|
+
// Blinn-Phong lighting in linear space.
|
|
1827
|
+
let diff = max(dot(N, L), 0.0);
|
|
1828
|
+
let H = normalize(L + V);
|
|
1829
|
+
let spec = pow(max(dot(N, H), 0.0), uniforms.chamferShininess) * uniforms.chamferSpecular;
|
|
1830
|
+
|
|
1831
|
+
// Sample interior video with progressive blur (sharper at inner edge).
|
|
1832
|
+
let uv = clamp(in.screenUv, vec2f(0.001), vec2f(0.999));
|
|
1833
|
+
let blurRadius = in.lerpT * 12.0 * length(uniforms.texelSize);
|
|
1834
|
+
|
|
1835
|
+
var videoSample: vec3f;
|
|
1836
|
+
if (blurRadius > 0.0001) {
|
|
1837
|
+
videoSample = blurSample(uv, blurRadius);
|
|
1838
|
+
} else {
|
|
1839
|
+
videoSample = textureSampleLevel(interiorColorTexture, interiorColorSampler, uv, 0.0).rgb;
|
|
720
1840
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
${r}`)}return n.detachShader(i,t),n.detachShader(i,e),n.deleteShader(t),n.deleteShader(e),i}function fe(n){const t=[];let e=0;for(let i=0;i<n.length-2;i+=2){const r=n[i],o=n[i+1],s=n[i+2],a=n[i+3],h=s-r,l=a-o,u=Math.sqrt(h*h+l*l);if(u<1e-6)continue;const f=-l/u,c=h/u;t.push(r,o,f,c,r,o,-f,-c,s,a,f,c,s,a,f,c,r,o,-f,-c,s,a,-f,-c),e+=6}return{vertices:new Float32Array(t),count:e}}function de(n,t,e,i,r){if(i<=0)return{vertices:new Float32Array(0),count:0};const o=r*Math.PI/180,s=-Math.cos(o),a=Math.sin(o),h=[];let l=0;for(let u=0;u<t.length;u++){const f=t[u],m=((u+1<t.length?t[u+1]:n.length)-f)/2;if(m<3)continue;const g=m-1;let U=0;for(let x=0;x<g;x++){const A=f+x*2,S=n[A],F=n[A+1],P=n[A+2],_=n[A+3];U+=S*_-P*F}const y=U>=0?1:-1,E=[],p=[];for(let x=0;x<g;x++){const A=f+x*2,S=n[A+2]-n[A],F=n[A+3]-n[A+1],P=Math.sqrt(S*S+F*F);P<1e-8?(E.push(x>0?E[x-1]:0),p.push(x>0?p[x-1]:0)):(E.push(-F/P*y),p.push(S/P*y))}const v=[],T=[];for(let x=0;x<g;x++){const A=(x-1+g)%g;let S=E[A]+E[x],F=p[A]+p[x];const P=Math.sqrt(S*S+F*F);P>1e-8?(S/=P,F/=P):(S=E[x],F=p[x]),v.push(S),T.push(F)}for(let x=0;x<g;x++){const A=x,S=(x+1)%g,F=f+x*2,P=f+(x+1)%g*2,_=n[F],k=n[F+1],D=n[P],R=n[P+1],C=v[A]*a,M=T[A]*a,H=s,I=v[S]*a,G=T[S]*a,tt=s,ct=_+v[A]*i,wt=k+T[A]*i,Ne=D+v[S]*i,ze=R+T[S]*i;h.push(_,k,C,M,H,0),h.push(ct,wt,C,M,H,1),h.push(D,R,I,G,tt,0),h.push(D,R,I,G,tt,0),h.push(ct,wt,C,M,H,1),h.push(Ne,ze,I,G,tt,1),l+=6}}return{vertices:new Float32Array(h),count:l}}class ft{static RESIZE_DEBOUNCE_MS=100;canvas;gl=null;container;stencilProgram=null;maskProgram=null;jfaSeedProgram=null;jfaFloodProgram=null;jfaDistProgram=null;interiorProgram=null;compositeProgram=null;boundaryProgram=null;chamferProgram=null;stencilUniforms={};maskUniforms={};jfaSeedUniforms={};jfaFloodUniforms={};jfaDistUniforms={};interiorUniforms={};compositeUniforms={};boundaryUniforms={};chamferUniforms={};quadVao=null;stencilVao=null;stencilIndexCount=0;maskVao=null;boundaryVao=null;boundaryVertexCount=0;chamferVao=null;chamferVertexCount=0;videoTexture=null;depthTexture=null;interiorFbo=null;interiorColorTex=null;interiorDepthTex=null;fboWidth=0;fboHeight=0;maskFbo=null;maskTex=null;jfaPingFbo=null;jfaPingTex=null;jfaPongFbo=null;jfaPongTex=null;distFbo=null;distTex=null;jfaWidth=0;jfaHeight=0;distFieldDirty=!0;depthWidth=0;depthHeight=0;videoAspect=1.7777777777777777;meshAspect=1;meshScaleX=.65;meshScaleY=.65;readDepth=null;readInput=null;playbackVideo=null;onVideoFrame=null;animationFrameHandle=0;rvfcHandle=0;rvfcSupported=!1;resizeObserver=null;resizeTimer=null;uvOffset=[0,0];uvScale=[1,1];lightDirX=-.707;lightDirY=.707;lightDir3=[-.5,.7,-.3];config;constructor(t,e){this.container=t,this.config={...e};const i=this.config.bevelLightAngle*Math.PI/180;this.lightDirX=Math.cos(i),this.lightDirY=Math.sin(i);const r=this.config.lightDirection,o=Math.sqrt(r[0]*r[0]+r[1]*r[1]+r[2]*r[2]);o>1e-6&&(this.lightDir3=[r[0]/o,r[1]/o,r[2]/o]),this.canvas=document.createElement("canvas");const s=this.canvas.getContext("webgl2",{antialias:!0,alpha:!0,premultipliedAlpha:!0,stencil:!0,desynchronized:!0,powerPreference:"high-performance"});if(!s)throw new Error("WebGL 2 is not supported.");this.gl=s,"drawingBufferColorSpace"in s&&(s.drawingBufferColorSpace="srgb"),s.clearColor(0,0,0,0),s.pixelStorei(s.UNPACK_FLIP_Y_WEBGL,!0),this.container.appendChild(this.canvas),this.initGPUResources(),this.setupResizeHandling(),this.canvas.addEventListener("webglcontextlost",this.handleContextLost),this.canvas.addEventListener("webglcontextrestored",this.handleContextRestored)}initialize(t,e,i,r){const o=this.gl;o&&(this.disposeTextures(),this.disposeFBO(),this.disposeJFA(),this.disposeStencilGeometry(),this.disposeBoundaryGeometry(),this.disposeChamferGeometry(),this.videoAspect=t.videoWidth/t.videoHeight,this.meshAspect=r.aspect,this.depthWidth=e,this.depthHeight=i,this.videoTexture=o.createTexture(),o.activeTexture(o.TEXTURE0),o.bindTexture(o.TEXTURE_2D,this.videoTexture),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MIN_FILTER,o.LINEAR),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MAG_FILTER,o.LINEAR),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_S,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_T,o.CLAMP_TO_EDGE),this.depthTexture=o.createTexture(),o.activeTexture(o.TEXTURE1),o.bindTexture(o.TEXTURE_2D,this.depthTexture),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MIN_FILTER,o.LINEAR),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MAG_FILTER,o.LINEAR),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_S,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_T,o.CLAMP_TO_EDGE),o.texStorage2D(o.TEXTURE_2D,1,o.R8,e,i),this.uploadStencilMesh(r),this.uploadMaskMesh(r),this.uploadBoundaryMesh(r),this.uploadChamferMesh(r),this.interiorProgram&&(o.useProgram(this.interiorProgram),o.uniform1i(this.interiorUniforms.uImage,0),o.uniform1i(this.interiorUniforms.uDepth,1),o.uniform1f(this.interiorUniforms.uStrength,this.config.parallaxStrength),o.uniform1i(this.interiorUniforms.uPomSteps,this.config.pomSteps),o.uniform1f(this.interiorUniforms.uDepthPower,this.config.depthPower),o.uniform1f(this.interiorUniforms.uDepthScale,this.config.depthScale),o.uniform1f(this.interiorUniforms.uDepthBias,this.config.depthBias),o.uniform1f(this.interiorUniforms.uContrastLow,this.config.contrastLow),o.uniform1f(this.interiorUniforms.uContrastHigh,this.config.contrastHigh),o.uniform1f(this.interiorUniforms.uVerticalReduction,this.config.verticalReduction),o.uniform1f(this.interiorUniforms.uDofStart,this.config.dofStart),o.uniform1f(this.interiorUniforms.uDofStrength,this.config.dofStrength),o.uniform2f(this.interiorUniforms.uImageTexelSize,1/t.videoWidth,1/t.videoHeight),o.uniform1f(this.interiorUniforms.uFogDensity,this.config.fogDensity),o.uniform3f(this.interiorUniforms.uFogColor,...this.config.fogColor),o.uniform1f(this.interiorUniforms.uColorShift,this.config.colorShift),o.uniform1f(this.interiorUniforms.uBrightnessBias,this.config.brightnessBias)),this.compositeProgram&&(o.useProgram(this.compositeProgram),o.uniform1i(this.compositeUniforms.uInteriorColor,2),o.uniform1i(this.compositeUniforms.uDistField,4),o.uniform1f(this.compositeUniforms.uEdgeOcclusionWidth,this.config.edgeOcclusionWidth),o.uniform1f(this.compositeUniforms.uEdgeOcclusionStrength,this.config.edgeOcclusionStrength)),this.chamferProgram&&(o.useProgram(this.chamferProgram),o.uniform3f(this.chamferUniforms.uLightDir3,...this.lightDir3),o.uniform3f(this.chamferUniforms.uChamferColor,...this.config.chamferColor),o.uniform1f(this.chamferUniforms.uChamferAmbient,this.config.chamferAmbient),o.uniform1f(this.chamferUniforms.uChamferSpecular,this.config.chamferSpecular),o.uniform1f(this.chamferUniforms.uChamferShininess,this.config.chamferShininess),o.uniform1i(this.chamferUniforms.uInteriorColor,2)),this.boundaryProgram&&(o.useProgram(this.boundaryProgram),o.uniform1i(this.boundaryUniforms.uInteriorColor,2),o.uniform1i(this.boundaryUniforms.uInteriorDepth,3),o.uniform1i(this.boundaryUniforms.uDistField,4),o.uniform1f(this.boundaryUniforms.uRimIntensity,this.config.rimLightIntensity),o.uniform3f(this.boundaryUniforms.uRimColor,...this.config.rimLightColor),o.uniform1f(this.boundaryUniforms.uRefractionStrength,this.config.refractionStrength),o.uniform1f(this.boundaryUniforms.uChromaticStrength,this.config.chromaticStrength),o.uniform1f(this.boundaryUniforms.uOcclusionIntensity,this.config.occlusionIntensity),o.uniform1f(this.boundaryUniforms.uEdgeThickness,this.config.edgeThickness),o.uniform1f(this.boundaryUniforms.uEdgeSpecular,this.config.edgeSpecular),o.uniform3f(this.boundaryUniforms.uEdgeColor,...this.config.edgeColor),o.uniform2f(this.boundaryUniforms.uLightDir,this.lightDirX,this.lightDirY),o.uniform1f(this.boundaryUniforms.uBevelIntensity,this.config.bevelIntensity)),this.recalculateViewportLayout())}uploadStencilMesh(t){const e=this.gl;if(!e||!this.stencilProgram)return;this.stencilVao=e.createVertexArray(),e.bindVertexArray(this.stencilVao);const i=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,i),e.bufferData(e.ARRAY_BUFFER,t.vertices,e.STATIC_DRAW);const r=e.getAttribLocation(this.stencilProgram,"aPosition");e.enableVertexAttribArray(r),e.vertexAttribPointer(r,2,e.FLOAT,!1,0,0);const o=e.createBuffer();e.bindBuffer(e.ELEMENT_ARRAY_BUFFER,o),e.bufferData(e.ELEMENT_ARRAY_BUFFER,t.indices,e.STATIC_DRAW),this.stencilIndexCount=t.indices.length,e.bindVertexArray(null)}uploadMaskMesh(t){const e=this.gl;if(!e||!this.maskProgram)return;this.maskVao=e.createVertexArray(),e.bindVertexArray(this.maskVao);const i=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,i),e.bufferData(e.ARRAY_BUFFER,t.vertices,e.STATIC_DRAW);const r=e.getAttribLocation(this.maskProgram,"aPosition");e.enableVertexAttribArray(r),e.vertexAttribPointer(r,2,e.FLOAT,!1,0,0);const o=e.createBuffer();e.bindBuffer(e.ELEMENT_ARRAY_BUFFER,o),e.bufferData(e.ELEMENT_ARRAY_BUFFER,t.indices,e.STATIC_DRAW),e.bindVertexArray(null)}uploadBoundaryMesh(t){const e=this.gl;if(!e||!this.boundaryProgram)return;const i=fe(t.edgeVertices);if(i.count===0)return;this.boundaryVao=e.createVertexArray(),e.bindVertexArray(this.boundaryVao);const r=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,r),e.bufferData(e.ARRAY_BUFFER,i.vertices,e.STATIC_DRAW);const o=16,s=e.getAttribLocation(this.boundaryProgram,"aPosition");e.enableVertexAttribArray(s),e.vertexAttribPointer(s,2,e.FLOAT,!1,o,0);const a=e.getAttribLocation(this.boundaryProgram,"aNormal");a>=0&&(e.enableVertexAttribArray(a),e.vertexAttribPointer(a,2,e.FLOAT,!1,o,8)),this.boundaryVertexCount=i.count,e.bindVertexArray(null)}uploadChamferMesh(t){const e=this.gl;if(!e||!this.chamferProgram||this.config.chamferWidth<=0)return;const i=de(t.edgeVertices,t.contourOffsets,t.contourIsHole,this.config.chamferWidth,this.config.chamferAngle);if(i.count===0)return;this.chamferVao=e.createVertexArray(),e.bindVertexArray(this.chamferVao);const r=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,r),e.bufferData(e.ARRAY_BUFFER,i.vertices,e.STATIC_DRAW);const o=24,s=e.getAttribLocation(this.chamferProgram,"aPosition");e.enableVertexAttribArray(s),e.vertexAttribPointer(s,2,e.FLOAT,!1,o,0);const a=e.getAttribLocation(this.chamferProgram,"aNormal3");a>=0&&(e.enableVertexAttribArray(a),e.vertexAttribPointer(a,3,e.FLOAT,!1,o,8));const h=e.getAttribLocation(this.chamferProgram,"aLerpT");h>=0&&(e.enableVertexAttribArray(h),e.vertexAttribPointer(h,1,e.FLOAT,!1,o,20)),this.chamferVertexCount=i.count,e.bindVertexArray(null)}disposeChamferGeometry(){const t=this.gl;t&&(this.chamferVao&&(t.deleteVertexArray(this.chamferVao),this.chamferVao=null),this.chamferVertexCount=0)}createFBO(t,e){const i=this.gl;if(!i)return;this.disposeFBO(),this.fboWidth=t,this.fboHeight=e,this.interiorFbo=i.createFramebuffer(),i.bindFramebuffer(i.FRAMEBUFFER,this.interiorFbo),this.interiorColorTex=i.createTexture(),i.activeTexture(i.TEXTURE2),i.bindTexture(i.TEXTURE_2D,this.interiorColorTex),i.texStorage2D(i.TEXTURE_2D,1,i.RGBA8,t,e),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MAG_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE),i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,this.interiorColorTex,0),this.interiorDepthTex=i.createTexture(),i.activeTexture(i.TEXTURE3),i.bindTexture(i.TEXTURE_2D,this.interiorDepthTex),i.texStorage2D(i.TEXTURE_2D,1,i.RGBA8,t,e),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MAG_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE),i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT1,i.TEXTURE_2D,this.interiorDepthTex,0),i.drawBuffers([i.COLOR_ATTACHMENT0,i.COLOR_ATTACHMENT1]);const r=i.checkFramebufferStatus(i.FRAMEBUFFER);r!==i.FRAMEBUFFER_COMPLETE&&console.error("Interior FBO incomplete:",r),i.bindFramebuffer(i.FRAMEBUFFER,null)}createJFAResources(t,e){const i=this.gl;if(!i)return;this.disposeJFA();const r=Math.max(1,Math.round(t/2)),o=Math.max(1,Math.round(e/2));this.jfaWidth=r,this.jfaHeight=o;const s=(a,h,l,u)=>{const f=i.createFramebuffer();return i.bindFramebuffer(i.FRAMEBUFFER,f),i.bindTexture(i.TEXTURE_2D,a),i.texStorage2D(i.TEXTURE_2D,1,h,l,u),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MAG_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE),i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,a,0),i.bindFramebuffer(i.FRAMEBUFFER,null),f};this.maskTex=i.createTexture(),this.maskFbo=s(this.maskTex,i.R8,r,o),this.jfaPingTex=i.createTexture(),this.jfaPingFbo=s(this.jfaPingTex,i.RG16F,r,o),this.jfaPongTex=i.createTexture(),this.jfaPongFbo=s(this.jfaPongTex,i.RG16F,r,o),this.distTex=i.createTexture(),this.distFbo=s(this.distTex,i.RGBA8,r,o),this.distFieldDirty=!0}computeDistanceField(){const t=this.gl;if(!t||!this.maskFbo||!this.maskVao||!this.quadVao||!this.jfaPingFbo||!this.jfaPongFbo||!this.distFbo)return;const e=this.jfaWidth,i=this.jfaHeight;if(e===0||i===0)return;t.viewport(0,0,e,i),t.disable(t.STENCIL_TEST),t.disable(t.BLEND),t.bindFramebuffer(t.FRAMEBUFFER,this.maskFbo),t.clearColor(0,0,0,1),t.clear(t.COLOR_BUFFER_BIT),t.useProgram(this.maskProgram),t.uniform2f(this.maskUniforms.uMeshScale,this.meshScaleX,this.meshScaleY),t.bindVertexArray(this.maskVao),t.drawElements(t.TRIANGLES,this.stencilIndexCount,t.UNSIGNED_SHORT,0),t.bindFramebuffer(t.FRAMEBUFFER,this.jfaPingFbo),t.clearColor(-1,-1,0,0),t.clear(t.COLOR_BUFFER_BIT),t.useProgram(this.jfaSeedProgram),t.activeTexture(t.TEXTURE5),t.bindTexture(t.TEXTURE_2D,this.maskTex),t.uniform1i(this.jfaSeedUniforms.uMask,5),t.uniform2f(this.jfaSeedUniforms.uTexelSize,1/e,1/i),t.bindVertexArray(this.quadVao),t.drawArrays(t.TRIANGLE_STRIP,0,4);const r=Math.max(e,i),o=[];let s=Math.ceil(r/2);for(;s>=1;)o.push(s),s=Math.floor(s/2);t.useProgram(this.jfaFloodProgram);let a=this.jfaPingTex,h=this.jfaPongFbo,l=this.jfaPongTex;for(let f=0;f<o.length;f++){const c=o[f]/Math.max(e,i);t.bindFramebuffer(t.FRAMEBUFFER,h),t.activeTexture(t.TEXTURE5),t.bindTexture(t.TEXTURE_2D,a),t.uniform1i(this.jfaFloodUniforms.uSeedTex,5),t.uniform1f(this.jfaFloodUniforms.uStepSize,c),t.bindVertexArray(this.quadVao),t.drawArrays(t.TRIANGLE_STRIP,0,4);const d=a,m=h;a=l,h=m===this.jfaPongFbo?this.jfaPingFbo:this.jfaPongFbo,l=d}t.bindFramebuffer(t.FRAMEBUFFER,this.distFbo),t.clearColor(0,0,0,1),t.clear(t.COLOR_BUFFER_BIT),t.useProgram(this.jfaDistProgram),t.activeTexture(t.TEXTURE5),t.bindTexture(t.TEXTURE_2D,a),t.uniform1i(this.jfaDistUniforms.uSeedTex,5),t.activeTexture(t.TEXTURE6),t.bindTexture(t.TEXTURE_2D,this.maskTex),t.uniform1i(this.jfaDistUniforms.uMask,6);const u=Math.max(this.config.bevelWidth,this.config.edgeOcclusionWidth);t.uniform1f(this.jfaDistUniforms.uBevelWidth,u),t.bindVertexArray(this.quadVao),t.drawArrays(t.TRIANGLE_STRIP,0,4),t.activeTexture(t.TEXTURE4),t.bindTexture(t.TEXTURE_2D,this.distTex),t.bindFramebuffer(t.FRAMEBUFFER,null),this.distFieldDirty=!1}start(t,e,i,r){this.stop(),this.playbackVideo=t,this.readDepth=e,this.readInput=i,this.onVideoFrame=r??null,this.rvfcSupported="requestVideoFrameCallback"in HTMLVideoElement.prototype,this.rvfcSupported&&(this.rvfcHandle=t.requestVideoFrameCallback(this.videoFrameLoop)),this.animationFrameHandle=window.requestAnimationFrame(this.renderLoop)}stop(){this.animationFrameHandle&&(window.cancelAnimationFrame(this.animationFrameHandle),this.animationFrameHandle=0),this.rvfcHandle&&this.playbackVideo&&(this.playbackVideo.cancelVideoFrameCallback(this.rvfcHandle),this.rvfcHandle=0),this.playbackVideo=null,this.readDepth=null,this.readInput=null,this.onVideoFrame=null,this.rvfcSupported=!1}dispose(){this.stop(),this.disposeTextures(),this.disposeFBO(),this.disposeJFA(),this.disposeStencilGeometry(),this.disposeBoundaryGeometry(),this.disposeChamferGeometry(),this.disposeGPUResources(),this.canvas.removeEventListener("webglcontextlost",this.handleContextLost),this.canvas.removeEventListener("webglcontextrestored",this.handleContextRestored),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),window.removeEventListener("resize",this.scheduleResizeRecalculate),this.resizeTimer!==null&&(window.clearTimeout(this.resizeTimer),this.resizeTimer=null)}initGPUResources(){const t=this.gl;if(!t)return;this.stencilProgram=O(t,L(t,t.VERTEX_SHADER,qt),L(t,t.FRAGMENT_SHADER,Zt)),this.stencilUniforms={uMeshScale:t.getUniformLocation(this.stencilProgram,"uMeshScale")},this.maskProgram=O(t,L(t,t.VERTEX_SHADER,$t),L(t,t.FRAGMENT_SHADER,Jt)),this.maskUniforms={uMeshScale:t.getUniformLocation(this.maskProgram,"uMeshScale")},this.jfaSeedProgram=O(t,L(t,t.VERTEX_SHADER,Kt),L(t,t.FRAGMENT_SHADER,Qt)),this.jfaSeedUniforms=this.getUniforms(this.jfaSeedProgram,["uMask","uTexelSize"]),this.jfaFloodProgram=O(t,L(t,t.VERTEX_SHADER,te),L(t,t.FRAGMENT_SHADER,ee)),this.jfaFloodUniforms=this.getUniforms(this.jfaFloodProgram,["uSeedTex","uStepSize"]),this.jfaDistProgram=O(t,L(t,t.VERTEX_SHADER,ie),L(t,t.FRAGMENT_SHADER,re)),this.jfaDistUniforms=this.getUniforms(this.jfaDistProgram,["uSeedTex","uMask","uBevelWidth"]),this.interiorProgram=O(t,L(t,t.VERTEX_SHADER,oe),L(t,t.FRAGMENT_SHADER,ne)),this.interiorUniforms=this.getUniforms(this.interiorProgram,["uImage","uDepth","uOffset","uStrength","uPomSteps","uDepthPower","uDepthScale","uDepthBias","uContrastLow","uContrastHigh","uVerticalReduction","uDofStart","uDofStrength","uImageTexelSize","uFogDensity","uFogColor","uColorShift","uBrightnessBias","uUvOffset","uUvScale"]),this.compositeProgram=O(t,L(t,t.VERTEX_SHADER,se),L(t,t.FRAGMENT_SHADER,ae)),this.compositeUniforms=this.getUniforms(this.compositeProgram,["uInteriorColor","uDistField","uEdgeOcclusionWidth","uEdgeOcclusionStrength"]),this.boundaryProgram=O(t,L(t,t.VERTEX_SHADER,he),L(t,t.FRAGMENT_SHADER,le)),this.boundaryUniforms=this.getUniforms(this.boundaryProgram,["uInteriorColor","uInteriorDepth","uDistField","uRimIntensity","uRimColor","uRimWidth","uMeshScale","uRefractionStrength","uChromaticStrength","uOcclusionIntensity","uTexelSize","uEdgeThickness","uEdgeSpecular","uEdgeColor","uLightDir","uBevelIntensity"]),this.chamferProgram=O(t,L(t,t.VERTEX_SHADER,ce),L(t,t.FRAGMENT_SHADER,ue)),this.chamferUniforms=this.getUniforms(this.chamferProgram,["uMeshScale","uLightDir3","uChamferColor","uChamferAmbient","uChamferSpecular","uChamferShininess","uInteriorColor","uTexelSize"]);const e=new Float32Array([-1,-1,1,-1,-1,1,1,1]);this.quadVao=t.createVertexArray(),t.bindVertexArray(this.quadVao);const i=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,i),t.bufferData(t.ARRAY_BUFFER,e,t.STATIC_DRAW);const r=t.getAttribLocation(this.interiorProgram,"aPosition");t.enableVertexAttribArray(r),t.vertexAttribPointer(r,2,t.FLOAT,!1,0,0),t.bindVertexArray(null),t.disable(t.DEPTH_TEST)}getUniforms(t,e){const i=this.gl,r={};for(const o of e)r[o]=i.getUniformLocation(t,o);return r}videoFrameLoop=(t,e)=>{const i=this.playbackVideo;if(!i)return;this.rvfcHandle=i.requestVideoFrameCallback(this.videoFrameLoop);const r=e.mediaTime??i.currentTime;this.updateDepthTexture(r),this.onVideoFrame&&this.onVideoFrame(r,e.presentedFrames??0)};renderLoop=()=>{this.animationFrameHandle=window.requestAnimationFrame(this.renderLoop);const t=this.gl,e=this.playbackVideo;if(!t||!this.interiorProgram||!this.quadVao||!e||e.readyState<HTMLMediaElement.HAVE_CURRENT_DATA||!this.interiorFbo||!this.interiorColorTex||!this.interiorDepthTex)return;this.distFieldDirty&&this.maskVao&&this.distFbo&&(this.computeDistanceField(),t.viewport(0,0,this.canvas.width,this.canvas.height)),t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,this.videoTexture),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,e),this.rvfcSupported||this.updateDepthTexture(e.currentTime);let i=0,r=0;if(this.readInput){const o=this.readInput();i=-o.x,r=o.y}t.bindFramebuffer(t.FRAMEBUFFER,this.interiorFbo),t.viewport(0,0,this.fboWidth,this.fboHeight),t.clearColor(0,0,0,1),t.clear(t.COLOR_BUFFER_BIT),t.useProgram(this.interiorProgram),t.uniform2f(this.interiorUniforms.uOffset,i,r),t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,this.videoTexture),t.activeTexture(t.TEXTURE1),t.bindTexture(t.TEXTURE_2D,this.depthTexture),t.bindVertexArray(this.quadVao),t.drawArrays(t.TRIANGLE_STRIP,0,4),t.bindFramebuffer(t.FRAMEBUFFER,null),t.clearColor(0,0,0,0),t.viewport(0,0,this.canvas.width,this.canvas.height),t.clear(t.COLOR_BUFFER_BIT|t.STENCIL_BUFFER_BIT),this.stencilVao&&this.stencilProgram&&this.stencilIndexCount>0&&(t.enable(t.STENCIL_TEST),t.stencilFunc(t.ALWAYS,1,255),t.stencilOp(t.KEEP,t.KEEP,t.REPLACE),t.stencilMask(255),t.colorMask(!1,!1,!1,!1),t.useProgram(this.stencilProgram),t.bindVertexArray(this.stencilVao),t.drawElements(t.TRIANGLES,this.stencilIndexCount,t.UNSIGNED_SHORT,0),t.colorMask(!0,!0,!0,!0)),t.stencilFunc(t.EQUAL,1,255),t.stencilMask(0),t.activeTexture(t.TEXTURE2),t.bindTexture(t.TEXTURE_2D,this.interiorColorTex),t.activeTexture(t.TEXTURE3),t.bindTexture(t.TEXTURE_2D,this.interiorDepthTex),t.activeTexture(t.TEXTURE4),t.bindTexture(t.TEXTURE_2D,this.distTex),t.useProgram(this.compositeProgram),t.bindVertexArray(this.quadVao),t.drawArrays(t.TRIANGLE_STRIP,0,4),t.disable(t.STENCIL_TEST),this.chamferVao&&this.chamferProgram&&this.chamferVertexCount>0&&(t.useProgram(this.chamferProgram),t.uniform2f(this.chamferUniforms.uMeshScale,this.meshScaleX,this.meshScaleY),t.uniform2f(this.chamferUniforms.uTexelSize,1/this.canvas.width,1/this.canvas.height),t.bindVertexArray(this.chamferVao),t.drawArrays(t.TRIANGLES,0,this.chamferVertexCount)),this.boundaryVao&&this.boundaryProgram&&this.boundaryVertexCount>0&&this.config.rimLightIntensity>0&&(t.enable(t.BLEND),t.blendFunc(t.SRC_ALPHA,t.ONE_MINUS_SRC_ALPHA),t.useProgram(this.boundaryProgram),t.bindVertexArray(this.boundaryVao),t.drawArrays(t.TRIANGLES,0,this.boundaryVertexCount),t.disable(t.BLEND))};updateDepthTexture(t){const e=this.gl;if(!e||!this.readDepth||!this.depthTexture)return;const i=this.readDepth(t);e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,this.depthTexture),e.texSubImage2D(e.TEXTURE_2D,0,0,0,this.depthWidth,this.depthHeight,e.RED,e.UNSIGNED_BYTE,i)}setupResizeHandling(){typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(()=>{this.scheduleResizeRecalculate()}),this.resizeObserver.observe(this.container)),window.addEventListener("resize",this.scheduleResizeRecalculate),this.recalculateViewportLayout()}scheduleResizeRecalculate=()=>{this.resizeTimer!==null&&window.clearTimeout(this.resizeTimer),this.resizeTimer=window.setTimeout(()=>{this.resizeTimer=null,this.recalculateViewportLayout()},ft.RESIZE_DEBOUNCE_MS)};recalculateViewportLayout(){const t=this.gl;if(!t)return;const{width:e,height:i}=this.getViewportSize(),r=Math.min(window.devicePixelRatio,2),o=Math.round(e*r),s=Math.round(i*r);(this.canvas.width!==o||this.canvas.height!==s)&&(this.canvas.width=o,this.canvas.height=s,t.viewport(0,0,o,s)),(this.fboWidth!==o||this.fboHeight!==s)&&this.createFBO(o,s);const a=Math.max(1,Math.round(o/2)),h=Math.max(1,Math.round(s/2));(this.jfaWidth!==a||this.jfaHeight!==h)&&this.createJFAResources(o,s);const l=e/i,u=this.config.parallaxStrength+this.config.overscanPadding;let f=1,c=1;l>this.videoAspect?c=this.videoAspect/l:f=l/this.videoAspect;const d=1+u*2;f/=d,c/=d,this.uvOffset=[(1-f)/2,(1-c)/2],this.uvScale=[f,c],this.interiorProgram&&(t.useProgram(this.interiorProgram),t.uniform2f(this.interiorUniforms.uUvOffset,this.uvOffset[0],this.uvOffset[1]),t.uniform2f(this.interiorUniforms.uUvScale,this.uvScale[0],this.uvScale[1]));const m=.65;this.meshScaleX=m,this.meshScaleY=m,l>this.meshAspect?this.meshScaleX=m*(this.meshAspect/l):this.meshScaleY=m*(l/this.meshAspect),this.stencilProgram&&(t.useProgram(this.stencilProgram),t.uniform2f(this.stencilUniforms.uMeshScale,this.meshScaleX,this.meshScaleY)),this.boundaryProgram&&(t.useProgram(this.boundaryProgram),t.uniform2f(this.boundaryUniforms.uMeshScale,this.meshScaleX,this.meshScaleY),t.uniform1f(this.boundaryUniforms.uRimWidth,this.config.rimLightWidth),t.uniform2f(this.boundaryUniforms.uTexelSize,1/o,1/s)),this.chamferProgram&&(t.useProgram(this.chamferProgram),t.uniform2f(this.chamferUniforms.uMeshScale,this.meshScaleX,this.meshScaleY)),this.distFieldDirty=!0}getViewportSize(){const t=Math.max(1,Math.round(this.container.clientWidth||window.innerWidth)),e=Math.max(1,Math.round(this.container.clientHeight||window.innerHeight));return{width:t,height:e}}handleContextLost=t=>{t.preventDefault(),this.animationFrameHandle&&(window.cancelAnimationFrame(this.animationFrameHandle),this.animationFrameHandle=0)};handleContextRestored=()=>{const t=this.canvas.getContext("webgl2",{alpha:!0,premultipliedAlpha:!0,stencil:!0});t&&(this.gl=t,t.clearColor(0,0,0,0),t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,!0),this.initGPUResources(),this.playbackVideo&&(this.animationFrameHandle=window.requestAnimationFrame(this.renderLoop)))};disposeTextures(){const t=this.gl;t&&(this.videoTexture&&(t.deleteTexture(this.videoTexture),this.videoTexture=null),this.depthTexture&&(t.deleteTexture(this.depthTexture),this.depthTexture=null))}disposeFBO(){const t=this.gl;t&&(this.interiorColorTex&&(t.deleteTexture(this.interiorColorTex),this.interiorColorTex=null),this.interiorDepthTex&&(t.deleteTexture(this.interiorDepthTex),this.interiorDepthTex=null),this.interiorFbo&&(t.deleteFramebuffer(this.interiorFbo),this.interiorFbo=null),this.fboWidth=0,this.fboHeight=0)}disposeJFA(){const t=this.gl;t&&(this.maskTex&&(t.deleteTexture(this.maskTex),this.maskTex=null),this.maskFbo&&(t.deleteFramebuffer(this.maskFbo),this.maskFbo=null),this.jfaPingTex&&(t.deleteTexture(this.jfaPingTex),this.jfaPingTex=null),this.jfaPingFbo&&(t.deleteFramebuffer(this.jfaPingFbo),this.jfaPingFbo=null),this.jfaPongTex&&(t.deleteTexture(this.jfaPongTex),this.jfaPongTex=null),this.jfaPongFbo&&(t.deleteFramebuffer(this.jfaPongFbo),this.jfaPongFbo=null),this.distTex&&(t.deleteTexture(this.distTex),this.distTex=null),this.distFbo&&(t.deleteFramebuffer(this.distFbo),this.distFbo=null),this.jfaWidth=0,this.jfaHeight=0,this.distFieldDirty=!0)}disposeStencilGeometry(){const t=this.gl;t&&(this.stencilVao&&(t.deleteVertexArray(this.stencilVao),this.stencilVao=null),this.maskVao&&(t.deleteVertexArray(this.maskVao),this.maskVao=null),this.stencilIndexCount=0)}disposeBoundaryGeometry(){const t=this.gl;t&&(this.boundaryVao&&(t.deleteVertexArray(this.boundaryVao),this.boundaryVao=null),this.boundaryVertexCount=0)}disposeGPUResources(){const t=this.gl;t&&(this.stencilProgram&&(t.deleteProgram(this.stencilProgram),this.stencilProgram=null),this.maskProgram&&(t.deleteProgram(this.maskProgram),this.maskProgram=null),this.jfaSeedProgram&&(t.deleteProgram(this.jfaSeedProgram),this.jfaSeedProgram=null),this.jfaFloodProgram&&(t.deleteProgram(this.jfaFloodProgram),this.jfaFloodProgram=null),this.jfaDistProgram&&(t.deleteProgram(this.jfaDistProgram),this.jfaDistProgram=null),this.interiorProgram&&(t.deleteProgram(this.interiorProgram),this.interiorProgram=null),this.compositeProgram&&(t.deleteProgram(this.compositeProgram),this.compositeProgram=null),this.boundaryProgram&&(t.deleteProgram(this.boundaryProgram),this.boundaryProgram=null),this.chamferProgram&&(t.deleteProgram(this.chamferProgram),this.chamferProgram=null),this.quadVao&&(t.deleteVertexArray(this.quadVao),this.quadVao=null),this.stencilUniforms={},this.maskUniforms={},this.jfaSeedUniforms={},this.jfaFloodUniforms={},this.jfaDistUniforms={},this.interiorUniforms={},this.compositeUniforms={},this.boundaryUniforms={},this.chamferUniforms={})}}async function me(n){const t=await fetch(n);if(!t.ok)throw new Error(`Failed to fetch SVG: ${t.status} ${t.statusText}`);const e=await t.text();return pe(e)}function pe(n){const i=new DOMParser().parseFromString(n,"image/svg+xml").querySelector("svg");if(!i)throw new Error("No <svg> element found in document.");const r=ge(i);if(r.length===0)throw new Error("No path data found in SVG.");let o=1/0,s=1/0,a=-1/0,h=-1/0;for(const D of r)for(let R=0;R<D.length;R+=2)o=Math.min(o,D[R]),a=Math.max(a,D[R]),s=Math.min(s,D[R+1]),h=Math.max(h,D[R+1]);const l=a-o,u=h-s,f=(o+a)/2,c=(s+h)/2,d=2/Math.max(l,u),m=l/u,g=r.map(D=>{const R=[];for(let C=0;C<D.length;C+=2)R.push((D[C]-f)*d),R.push(-((D[C+1]-c)*d));return R}),U=Se(g),y=[],E=[];for(const D of U){const{flatCoords:R,holeIndices:C}=Ae(D),M=Re(R,C),H=y.length/2;for(const I of M)E.push(I+H);for(const I of R)y.push(I)}const p=y,v=E,T=[],x=[],A=[],S=At(g);for(let D=0;D<g.length;D++){const R=g[D];x.push(T.length),A.push(S[D]);for(let C=0;C<R.length;C++)T.push(R[C]);R.length>=2&&T.push(R[0],R[1])}let F=1/0,P=1/0,_=-1/0,k=-1/0;for(let D=0;D<p.length;D+=2)F=Math.min(F,p[D]),_=Math.max(_,p[D]),P=Math.min(P,p[D+1]),k=Math.max(k,p[D+1]);return{vertices:new Float32Array(p),indices:new Uint16Array(v),edgeVertices:new Float32Array(T),contourOffsets:x,contourIsHole:A,bounds:{minX:F,maxX:_,minY:P,maxY:k},aspect:m}}function ge(n){const t=[];return n.querySelectorAll("path").forEach(h=>{const l=h.getAttribute("d");if(!l)return;const u=Te(l);t.push(...u)}),n.querySelectorAll("polygon").forEach(h=>{const l=h.getAttribute("points");if(!l)return;const u=Et(l);u.length>=6&&t.push(u)}),n.querySelectorAll("polyline").forEach(h=>{const l=h.getAttribute("points");if(!l)return;const u=Et(l);u.length>=6&&t.push(u)}),n.querySelectorAll("rect").forEach(h=>{const l=parseFloat(h.getAttribute("x")||"0"),u=parseFloat(h.getAttribute("y")||"0"),f=parseFloat(h.getAttribute("width")||"0"),c=parseFloat(h.getAttribute("height")||"0");f>0&&c>0&&t.push([l,u,l+f,u,l+f,u+c,l,u+c])}),n.querySelectorAll("circle").forEach(h=>{const l=parseFloat(h.getAttribute("cx")||"0"),u=parseFloat(h.getAttribute("cy")||"0"),f=parseFloat(h.getAttribute("r")||"0");f>0&&t.push(ve(l,u,f))}),n.querySelectorAll("ellipse").forEach(h=>{const l=parseFloat(h.getAttribute("cx")||"0"),u=parseFloat(h.getAttribute("cy")||"0"),f=parseFloat(h.getAttribute("rx")||"0"),c=parseFloat(h.getAttribute("ry")||"0");f>0&&c>0&&t.push(xe(l,u,f,c))}),t}function Et(n){const t=[],e=n.trim().split(/[\s,]+/);for(let i=0;i<e.length-1;i+=2){const r=parseFloat(e[i]),o=parseFloat(e[i+1]);Number.isFinite(r)&&Number.isFinite(o)&&t.push(r,o)}return t}function ve(n,t,e,i=64){const r=[];for(let o=0;o<i;o++){const s=2*Math.PI*o/i;r.push(n+e*Math.cos(s),t+e*Math.sin(s))}return r}function xe(n,t,e,i,r=64){const o=[];for(let s=0;s<r;s++){const a=2*Math.PI*s/r;o.push(n+e*Math.cos(a),t+i*Math.sin(a))}return o}function Te(n){const t=[];let e=[],i=0,r=0,o=0,s=0,a=0,h=0,l="";const u=Ee(n);let f=0;function c(){return f>=u.length?0:parseFloat(u[f++])}for(;f<u.length;){const d=u[f];let m;/^[a-zA-Z]$/.test(d)?(m=d,f++):m=l==="M"?"L":l==="m"?"l":l;const g=m===m.toLowerCase();switch(m.toUpperCase()){case"M":{e.length>0&&t.push(e),e=[];const y=c()+(g?i:0),E=c()+(g?r:0);i=y,r=E,o=y,s=E,e.push(i,r),a=i,h=r;break}case"L":{i=c()+(g?i:0),r=c()+(g?r:0),e.push(i,r),a=i,h=r;break}case"H":{i=c()+(g?i:0),e.push(i,r),a=i,h=r;break}case"V":{r=c()+(g?r:0),e.push(i,r),a=i,h=r;break}case"C":{const y=c()+(g?i:0),E=c()+(g?r:0),p=c()+(g?i:0),v=c()+(g?r:0),T=c()+(g?i:0),x=c()+(g?r:0);$(e,i,r,y,E,p,v,T,x),i=T,r=x,a=p,h=v;break}case"S":{const y=2*i-a,E=2*r-h,p=c()+(g?i:0),v=c()+(g?r:0),T=c()+(g?i:0),x=c()+(g?r:0);$(e,i,r,y,E,p,v,T,x),i=T,r=x,a=p,h=v;break}case"Q":{const y=c()+(g?i:0),E=c()+(g?r:0),p=c()+(g?i:0),v=c()+(g?r:0);bt(e,i,r,y,E,p,v),i=p,r=v,a=y,h=E;break}case"T":{const y=2*i-a,E=2*r-h,p=c()+(g?i:0),v=c()+(g?r:0);bt(e,i,r,y,E,p,v),i=p,r=v,a=y,h=E;break}case"A":{const y=c(),E=c(),p=c(),v=c(),T=c(),x=c()+(g?i:0),A=c()+(g?r:0);ye(e,i,r,y,E,p,!!v,!!T,x,A),i=x,r=A,a=i,h=r;break}case"Z":{i=o,r=s,e.length>0&&t.push(e),e=[],a=i,h=r;break}default:f++;break}l=m}return e.length>=6&&t.push(e),t}function Ee(n){const t=[],e=/([a-zA-Z])|([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/g;let i;for(;(i=e.exec(n))!==null;)t.push(i[0]);return t}const be=.5;function $(n,t,e,i,r,o,s,a,h,l=0){if(l>12){n.push(a,h);return}const u=a-t,f=h-e,c=Math.sqrt(u*u+f*f);if(c<1e-6){n.push(a,h);return}const d=Math.abs((i-a)*f-(r-h)*u)/c,m=Math.abs((o-a)*f-(s-h)*u)/c;if(d+m<be){n.push(a,h);return}const g=(t+i)/2,U=(e+r)/2,y=(i+o)/2,E=(r+s)/2,p=(o+a)/2,v=(s+h)/2,T=(g+y)/2,x=(U+E)/2,A=(y+p)/2,S=(E+v)/2,F=(T+A)/2,P=(x+S)/2;$(n,t,e,g,U,T,x,F,P,l+1),$(n,F,P,A,S,p,v,a,h,l+1)}function bt(n,t,e,i,r,o,s){const a=t+.6666666666666666*(i-t),h=e+2/3*(r-e),l=o+2/3*(i-o),u=s+2/3*(r-s);$(n,t,e,a,h,l,u,o,s)}function ye(n,t,e,i,r,o,s,a,h,l){if(i===0||r===0){n.push(h,l);return}let u=Math.abs(i),f=Math.abs(r);const c=o*Math.PI/180,d=Math.cos(c),m=Math.sin(c),g=(t-h)/2,U=(e-l)/2,y=d*g+m*U,E=-m*g+d*U;let p=y*y/(u*u)+E*E/(f*f);if(p>1){const M=Math.sqrt(p);u*=M,f*=M,p=1}const v=u*u,T=f*f,x=y*y,A=E*E;let S=Math.max(0,(v*T-v*A-T*x)/(v*A+T*x));S=Math.sqrt(S),s===a&&(S=-S);const F=S*(u*E)/f,P=S*-(f*y)/u,_=d*F-m*P+(t+h)/2,k=m*F+d*P+(e+l)/2,D=yt(1,0,(y-F)/u,(E-P)/f);let R=yt((y-F)/u,(E-P)/f,(-y-F)/u,(-E-P)/f);!a&&R>0&&(R-=2*Math.PI),a&&R<0&&(R+=2*Math.PI);const C=Math.max(4,Math.ceil(Math.abs(R)/(Math.PI/16)));for(let M=1;M<=C;M++){const H=D+M/C*R,I=Math.cos(H),G=Math.sin(H),tt=d*u*I-m*f*G+_,ct=m*u*I+d*f*G+k;n.push(tt,ct)}}function yt(n,t,e,i){const r=n*i-t*e<0?-1:1,o=n*e+t*i,s=Math.sqrt(n*n+t*t),a=Math.sqrt(e*e+i*i),h=o/(s*a);return r*Math.acos(Math.max(-1,Math.min(1,h)))}function Ae(n){const t=[],e=[];for(let i=0;i<n.length;i++){i>0&&e.push(t.length/2);for(const r of n[i])t.push(r)}return{flatCoords:t,holeIndices:e}}function At(n){const t=n.length,e=n.map(r=>Math.abs(St(r))),i=new Array(t).fill(!1);for(let r=0;r<t;r++){let o=0;const s=n[r][0],a=n[r][1];for(let h=0;h<t;h++)r!==h&&e[h]>e[r]&&Rt(s,a,n[h])&&o++;i[r]=o%2===1}return i}function Se(n){if(n.length<=1)return[n];const t=At(n),e=n.map((s,a)=>{const h=St(s);return{index:a,contour:s,area:h,isOuter:!t[a]}}),i=e.filter(s=>s.isOuter),r=e.filter(s=>!s.isOuter);if(i.length===0)return n.map(s=>[s]);const o=i.map(s=>({outer:s.contour,holes:[]}));for(const s of r){const a=s.contour[0],h=s.contour[1];let l=-1,u=1/0;for(let f=0;f<i.length;f++)if(Rt(a,h,i[f].contour)){const c=Math.abs(i[f].area);c<u&&(u=c,l=f)}l>=0?o[l].holes.push(s.contour):o.push({outer:s.contour,holes:[]})}return o.map(s=>[s.outer,...s.holes])}function St(n){let t=0;const e=n.length;for(let i=0;i<e;i+=2){const r=n[i],o=n[i+1],s=n[(i+2)%e],a=n[(i+3)%e];t+=r*a-s*o}return t/2}function Rt(n,t,e){let i=!1;const r=e.length;for(let o=0,s=r-2;o<r;s=o,o+=2){const a=e[o],h=e[o+1],l=e[s],u=e[s+1];h>t!=u>t&&n<(l-a)*(t-h)/(u-h)+a&&(i=!i)}return i}function Re(n,t,e=2){const i=t&&t.length>0,r=i?t[0]*e:n.length;let o=Dt(n,0,r,e,!0);const s=[];if(!o||o.next===o.prev)return s;i&&(o=we(n,t,o,e));let a=1/0,h=1/0,l=-1/0,u=-1/0,f=0;if(n.length>80*e){for(let c=0;c<r;c+=e){const d=n[c],m=n[c+1];d<a&&(a=d),m<h&&(h=m),d>l&&(l=d),m>u&&(u=m)}f=Math.max(l-a,u-h),f=f!==0?32767/f:0}return J(o,s,e,a,h,f,0),s}function Dt(n,t,e,i,r){let o=null;if(r===Be(n,t,e,i)>0)for(let s=t;s<e;s+=i)o=Pt(s,n[s],n[s+1],o);else for(let s=e-i;s>=t;s-=i)o=Pt(s,n[s],n[s+1],o);return o&&nt(o,o.next)&&(Q(o),o=o.next),o?(o.next.prev=o,o.prev.next=o,o.next):null}function X(n,t){t||(t=n);let e=n,i;do if(i=!1,!e.steiner&&(nt(e,e.next)||w(e.prev,e,e.next)===0)){if(Q(e),e=t=e.prev,e===e.next)break;i=!0}else e=e.next;while(i||e!==t);return t}function J(n,t,e,i,r,o,s){if(!n)return;!s&&o&&_e(n,i,r,o);let a=n,h,l;for(;n.prev!==n.next;){if(h=n.prev,l=n.next,o?Fe(n,i,r,o):De(n)){t.push(h.i/e,n.i/e,l.i/e),Q(n),n=l.next,a=l.next;continue}if(n=l,n===a){s?s===1?(n=Ue(X(n),t,e),J(n,t,e,i,r,o,2)):s===2&&Pe(n,t,e,i,r,o):J(X(n),t,e,i,r,o,1);break}}}function De(n){const t=n.prev,e=n,i=n.next;if(w(t,e,i)>=0)return!1;const r=t.x,o=e.x,s=i.x,a=t.y,h=e.y,l=i.y,u=r<o?r<s?r:s:o<s?o:s,f=a<h?a<l?a:l:h<l?h:l,c=r>o?r>s?r:s:o>s?o:s,d=a>h?a>l?a:l:h>l?h:l;let m=i.next;for(;m!==t;){if(m.x>=u&&m.x<=c&&m.y>=f&&m.y<=d&&z(r,a,o,h,s,l,m.x,m.y)&&w(m.prev,m,m.next)>=0)return!1;m=m.next}return!0}function Fe(n,t,e,i){const r=n.prev,o=n,s=n.next;if(w(r,o,s)>=0)return!1;const a=r.x,h=o.x,l=s.x,u=r.y,f=o.y,c=s.y,d=a<h?a<l?a:l:h<l?h:l,m=u<f?u<c?u:c:f<c?f:c,g=a>h?a>l?a:l:h>l?h:l,U=u>f?u>c?u:c:f>c?f:c,y=dt(d,m,t,e,i),E=dt(g,U,t,e,i);let p=n.prevZ,v=n.nextZ;for(;p&&p.z>=y&&v&&v.z<=E;){if(p.x>=d&&p.x<=g&&p.y>=m&&p.y<=U&&p!==r&&p!==s&&z(a,u,h,f,l,c,p.x,p.y)&&w(p.prev,p,p.next)>=0||(p=p.prevZ,v.x>=d&&v.x<=g&&v.y>=m&&v.y<=U&&v!==r&&v!==s&&z(a,u,h,f,l,c,v.x,v.y)&&w(v.prev,v,v.next)>=0))return!1;v=v.nextZ}for(;p&&p.z>=y;){if(p.x>=d&&p.x<=g&&p.y>=m&&p.y<=U&&p!==r&&p!==s&&z(a,u,h,f,l,c,p.x,p.y)&&w(p.prev,p,p.next)>=0)return!1;p=p.prevZ}for(;v&&v.z<=E;){if(v.x>=d&&v.x<=g&&v.y>=m&&v.y<=U&&v!==r&&v!==s&&z(a,u,h,f,l,c,v.x,v.y)&&w(v.prev,v,v.next)>=0)return!1;v=v.nextZ}return!0}function Ue(n,t,e){let i=n;do{const r=i.prev,o=i.next.next;!nt(r,o)&&Ft(r,i,i.next,o)&&K(r,o)&&K(o,r)&&(t.push(r.i/e,i.i/e,o.i/e),Q(i),Q(i.next),i=n=o),i=i.next}while(i!==n);return X(i)}function Pe(n,t,e,i,r,o){let s=n;do{let a=s.next.next;for(;a!==s.prev;){if(s.i!==a.i&&Oe(s,a)){let h=Ut(s,a);s=X(s,s.next),h=X(h,h.next),J(s,t,e,i,r,o,0),J(h,t,e,i,r,o,0);return}a=a.next}s=s.next}while(s!==n)}function we(n,t,e,i){const r=[];for(let o=0;o<t.length;o++){const s=t[o]*i,a=o<t.length-1?t[o+1]*i:n.length,h=Dt(n,s,a,i,!1);h&&(h===h.next&&(h.steiner=!0),r.push(Ve(h)))}r.sort((o,s)=>o.x-s.x);for(const o of r)e=Le(o,e);return e}function Le(n,t){const e=Ce(n,t);if(!e)return t;const i=Ut(e,n);return X(i,i.next),X(e,e.next)}function Ce(n,t){let e=t;const i=n.x,r=n.y;let o=-1/0,s=null;do{if(r<=e.y&&r>=e.next.y&&e.next.y!==e.y){const f=e.x+(r-e.y)/(e.next.y-e.y)*(e.next.x-e.x);if(f<=i&&f>o&&(o=f,s=e.x<e.next.x?e:e.next,f===i))return s}e=e.next}while(e!==t);if(!s)return null;const a=s,h=s.x,l=s.y;let u=1/0;e=s;do{if(i>=e.x&&e.x>=h&&i!==e.x&&z(r<l?i:o,r,h,l,r<l?o:i,r,e.x,e.y)){const f=Math.abs(r-e.y)/(i-e.x);K(e,n)&&(f<u||f===u&&(e.x>s.x||Me(s,e)))&&(s=e,u=f)}e=e.next}while(e!==a);return s}function Me(n,t){return w(n.prev,n,t.prev)<0&&w(t.next,n,n.next)<0}function _e(n,t,e,i){let r=n;do r.z===0&&(r.z=dt(r.x,r.y,t,e,i)),r.prevZ=r.prev,r.nextZ=r.next,r=r.next;while(r!==n);r.prevZ.nextZ=null,r.prevZ=null,Ie(r)}function Ie(n){let t=1,e;do{let i=n;n=null;let r=null;for(e=0;i;){e++;let o=i,s=0;for(let h=0;h<t&&(s++,o=o.nextZ,!!o);h++);let a=t;for(;s>0||a>0&&o;){let h;s!==0&&(a===0||!o||i.z<=o.z)?(h=i,i=i.nextZ,s--):(h=o,o=o.nextZ,a--),r?r.nextZ=h:n=h,h.prevZ=r,r=h}i=o}r.nextZ=null,t*=2}while(e>1);return n}function dt(n,t,e,i,r){let o=(n-e)*r|0,s=(t-i)*r|0;return o=(o|o<<8)&16711935,o=(o|o<<4)&252645135,o=(o|o<<2)&858993459,o=(o|o<<1)&1431655765,s=(s|s<<8)&16711935,s=(s|s<<4)&252645135,s=(s|s<<2)&858993459,s=(s|s<<1)&1431655765,o|s<<1}function Ve(n){let t=n,e=n;do(t.x<e.x||t.x===e.x&&t.y<e.y)&&(e=t),t=t.next;while(t!==n);return e}function z(n,t,e,i,r,o,s,a){return(r-s)*(t-a)-(n-s)*(o-a)>=0&&(n-s)*(i-a)-(e-s)*(t-a)>=0&&(e-s)*(o-a)-(r-s)*(i-a)>=0}function Oe(n,t){return n.next.i!==t.i&&n.prev.i!==t.i&&!ke(n,t)&&(K(n,t)&&K(t,n)&&He(n,t)&&(w(n.prev,n,t.prev)!==0||w(n,t.prev,t)!==0)||nt(n,t)&&w(n.prev,n,n.next)>0&&w(t.prev,t,t.next)>0)}function w(n,t,e){return(t.y-n.y)*(e.x-t.x)-(t.x-n.x)*(e.y-t.y)}function nt(n,t){return n.x===t.x&&n.y===t.y}function Ft(n,t,e,i){const r=at(w(n,t,e)),o=at(w(n,t,i)),s=at(w(e,i,n)),a=at(w(e,i,t));return!!(r!==o&&s!==a||r===0&&st(n,e,t)||o===0&&st(n,i,t)||s===0&&st(e,n,i)||a===0&&st(e,t,i))}function st(n,t,e){return t.x<=Math.max(n.x,e.x)&&t.x>=Math.min(n.x,e.x)&&t.y<=Math.max(n.y,e.y)&&t.y>=Math.min(n.y,e.y)}function at(n){return n>0?1:n<0?-1:0}function ke(n,t){let e=n;do{if(e.i!==n.i&&e.next.i!==n.i&&e.i!==t.i&&e.next.i!==t.i&&Ft(e,e.next,n,t))return!0;e=e.next}while(e!==n);return!1}function K(n,t){return w(n.prev,n,n.next)<0?w(n,t,n.next)>=0&&w(n,n.prev,t)>=0:w(n,t,n.prev)<0||w(n,n.next,t)<0}function He(n,t){let e=n,i=!1;const r=(n.x+t.x)/2,o=(n.y+t.y)/2;do e.y>o!=e.next.y>o&&e.next.y!==e.y&&r<(e.next.x-e.x)*(o-e.y)/(e.next.y-e.y)+e.x&&(i=!i),e=e.next;while(e!==n);return i}function Ut(n,t){const e=mt(n.i,n.x,n.y),i=mt(t.i,t.x,t.y),r=n.next,o=t.prev;return n.next=t,t.prev=n,e.next=r,r.prev=e,i.next=e,e.prev=i,o.next=i,i.prev=o,i}function Pt(n,t,e,i){const r=mt(n,t,e);return i?(r.next=i.next,r.prev=i,i.next.prev=r,i.next=r):(r.prev=r,r.next=r),r}function Q(n){n.next.prev=n.prev,n.prev.next=n.next,n.prevZ&&(n.prevZ.nextZ=n.nextZ),n.nextZ&&(n.nextZ.prevZ=n.prevZ)}function mt(n,t,e){return{i:n,x:t,y:e,prev:null,next:null,z:0,prevZ:null,nextZ:null,steiner:!1}}function Be(n,t,e,i){let r=0;for(let o=t,s=e-i;o<e;o+=i)r+=(n[s]-n[o])*(n[o+1]+n[s+1]),s=o;return r}const b={parallaxX:.4,parallaxY:.8,parallaxMax:30,overscan:.06,pomSteps:16,rimIntensity:.6,rimColor:"#ffffff",rimWidth:.025,refractionStrength:.015,chromaticStrength:.008,occlusionIntensity:.4,depthPower:.7,depthScale:1.2,depthBias:-.05,fogDensity:.15,fogColor:"#1a1a2e",colorShift:.6,brightnessBias:.05,contrastLow:.02,contrastHigh:.98,verticalReduction:.5,dofStart:.5,dofStrength:.5,bevelIntensity:.5,bevelWidth:.04,bevelDarkening:.2,bevelDesaturation:.12,bevelLightAngle:135,edgeThickness:.01,edgeSpecular:.35,edgeColor:"#a0a0a0",chamferWidth:.025,chamferAngle:45,chamferColor:"#262630",chamferAmbient:.12,chamferSpecular:.3,chamferShininess:24,edgeOcclusionWidth:.03,edgeOcclusionStrength:.2,lightDirection:"-0.5,0.7,-0.3",autoplay:!0,loop:!0,muted:!0};class pt{constructor(t,e=.08,i=.06){this.host=t,this.lerpFactor=e,this.motionLerpFactor=i,this.host.addEventListener("mousemove",this.handleMouseMove),this.host.addEventListener("mouseleave",this.resetPointerTarget),this.host.addEventListener("touchstart",this.handleTouchStart,{passive:!0}),this.host.addEventListener("touchmove",this.handleTouchMove,{passive:!0}),this.host.addEventListener("touchend",this.handleTouchEnd,{passive:!0}),this.host.addEventListener("touchcancel",this.handleTouchEnd,{passive:!0})}pointerTarget={x:0,y:0};motionTarget={x:0,y:0};smoothedOutput={x:0,y:0};usingMotionInput=!1;motionListenerAttached=!1;motionRequested=!1;touchActive=!1;touchAnchorX=0;touchAnchorY=0;lerpFactor;motionLerpFactor;static TOUCH_DRAG_RANGE=100;update(){const t=this.touchActive?this.pointerTarget:this.usingMotionInput?this.motionTarget:this.pointerTarget,e=this.usingMotionInput&&!this.touchActive?this.motionLerpFactor:this.lerpFactor;return this.smoothedOutput.x=lt(this.smoothedOutput.x,t.x,e),this.smoothedOutput.y=lt(this.smoothedOutput.y,t.y,e),this.smoothedOutput}dispose(){this.host.removeEventListener("mousemove",this.handleMouseMove),this.host.removeEventListener("mouseleave",this.resetPointerTarget),this.host.removeEventListener("touchstart",this.handleTouchStart),this.host.removeEventListener("touchmove",this.handleTouchMove),this.host.removeEventListener("touchend",this.handleTouchEnd),this.host.removeEventListener("touchcancel",this.handleTouchEnd),this.motionListenerAttached&&(window.removeEventListener("deviceorientation",this.handleDeviceOrientation),this.motionListenerAttached=!1)}handleMouseMove=t=>{const e=this.host.getBoundingClientRect(),i=(t.clientX-e.left)/e.width*2-1,r=(t.clientY-e.top)/e.height*2-1;this.pointerTarget.x=W(i,-1,1),this.pointerTarget.y=W(r,-1,1)};resetPointerTarget=()=>{this.pointerTarget.x=0,this.pointerTarget.y=0};handleTouchStart=t=>{const e=t.touches[0];e&&(this.touchActive=!0,this.touchAnchorX=e.clientX,this.touchAnchorY=e.clientY,this.pointerTarget.x=0,this.pointerTarget.y=0,this.motionRequested||(this.motionRequested=!0,this.requestMotionPermission()))};handleTouchMove=t=>{const e=t.touches[0];if(!e)return;const i=e.clientX-this.touchAnchorX,r=e.clientY-this.touchAnchorY,o=pt.TOUCH_DRAG_RANGE;this.pointerTarget.x=W(i/o,-1,1),this.pointerTarget.y=W(r/o,-1,1)};handleTouchEnd=()=>{this.touchActive=!1,this.pointerTarget.x=0,this.pointerTarget.y=0};async requestMotionPermission(){if(typeof DeviceOrientationEvent>"u")return;const t=DeviceOrientationEvent;if(typeof t.requestPermission=="function")try{if(await t.requestPermission()!=="granted")return}catch{return}this.motionListenerAttached||(window.addEventListener("deviceorientation",this.handleDeviceOrientation),this.motionListenerAttached=!0),this.usingMotionInput=!0}handleDeviceOrientation=t=>{const e=W((t.gamma??0)/45,-1,1),i=W((t.beta??0)/45,-1,1);this.motionTarget.x=lt(this.motionTarget.x,e,this.motionLerpFactor),this.motionTarget.y=lt(this.motionTarget.y,i,this.motionLerpFactor)}}class ht extends HTMLElement{static TAG_NAME="layershift-portal";static get observedAttributes(){return["src","depth-src","depth-meta","logo-src","parallax-x","parallax-y","parallax-max","overscan","pom-steps","rim-intensity","rim-color","rim-width","refraction-strength","chromatic-strength","occlusion-intensity","depth-power","depth-scale","depth-bias","fog-density","fog-color","color-shift","brightness-bias","contrast-low","contrast-high","vertical-reduction","dof-start","dof-strength","bevel-intensity","bevel-width","bevel-darkening","bevel-desaturation","bevel-light-angle","edge-thickness","edge-specular","edge-color","chamfer-width","chamfer-angle","chamfer-color","chamfer-ambient","chamfer-specular","chamfer-shininess","edge-occlusion-width","edge-occlusion-strength","light-direction","autoplay","loop","muted"]}shadow;container=null;renderer=null;inputHandler=null;depthWorker=null;video=null;mesh=null;initialized=!1;abortController=null;loopCount=0;constructor(){super(),this.shadow=this.attachShadow({mode:"open"})}getAttrFloat(t,e){const i=this.getAttribute(t);if(i===null)return e;const r=parseFloat(i);return Number.isFinite(r)?r:e}getAttrBool(t,e){if(!this.hasAttribute(t))return e;const i=this.getAttribute(t);return!(i==="false"||i==="0")}getAttrColor(t,e){const i=this.getAttribute(t)??e;return Xe(i)}getAttrVec3(t,e){const r=(this.getAttribute(t)??e).split(",").map(s=>parseFloat(s.trim()));if(r.length>=3&&r.every(Number.isFinite))return[r[0],r[1],r[2]];const o=e.split(",").map(s=>parseFloat(s.trim()));return[o[0],o[1],o[2]]}get parallaxX(){return this.getAttrFloat("parallax-x",b.parallaxX)}get parallaxY(){return this.getAttrFloat("parallax-y",b.parallaxY)}get parallaxMax(){return this.getAttrFloat("parallax-max",b.parallaxMax)}get overscan(){return this.getAttrFloat("overscan",b.overscan)}get pomSteps(){return this.getAttrFloat("pom-steps",b.pomSteps)}get rimIntensity(){return this.getAttrFloat("rim-intensity",b.rimIntensity)}get rimWidth(){return this.getAttrFloat("rim-width",b.rimWidth)}get rimColor(){return this.getAttrColor("rim-color",b.rimColor)}get refractionStrength(){return this.getAttrFloat("refraction-strength",b.refractionStrength)}get chromaticStrength(){return this.getAttrFloat("chromatic-strength",b.chromaticStrength)}get occlusionIntensity(){return this.getAttrFloat("occlusion-intensity",b.occlusionIntensity)}get depthPower(){return this.getAttrFloat("depth-power",b.depthPower)}get depthScale(){return this.getAttrFloat("depth-scale",b.depthScale)}get depthBias(){return this.getAttrFloat("depth-bias",b.depthBias)}get fogDensity(){return this.getAttrFloat("fog-density",b.fogDensity)}get fogColor(){return this.getAttrColor("fog-color",b.fogColor)}get colorShift(){return this.getAttrFloat("color-shift",b.colorShift)}get brightnessBias(){return this.getAttrFloat("brightness-bias",b.brightnessBias)}get contrastLow(){return this.getAttrFloat("contrast-low",b.contrastLow)}get contrastHigh(){return this.getAttrFloat("contrast-high",b.contrastHigh)}get verticalReduction(){return this.getAttrFloat("vertical-reduction",b.verticalReduction)}get dofStart(){return this.getAttrFloat("dof-start",b.dofStart)}get dofStrength(){return this.getAttrFloat("dof-strength",b.dofStrength)}get bevelIntensity(){return this.getAttrFloat("bevel-intensity",b.bevelIntensity)}get bevelWidth(){return this.getAttrFloat("bevel-width",b.bevelWidth)}get bevelDarkening(){return this.getAttrFloat("bevel-darkening",b.bevelDarkening)}get bevelDesaturation(){return this.getAttrFloat("bevel-desaturation",b.bevelDesaturation)}get bevelLightAngle(){return this.getAttrFloat("bevel-light-angle",b.bevelLightAngle)}get edgeThickness(){return this.getAttrFloat("edge-thickness",b.edgeThickness)}get edgeSpecular(){return this.getAttrFloat("edge-specular",b.edgeSpecular)}get edgeColor(){return this.getAttrColor("edge-color",b.edgeColor)}get chamferWidth(){return this.getAttrFloat("chamfer-width",b.chamferWidth)}get chamferAngle(){return this.getAttrFloat("chamfer-angle",b.chamferAngle)}get chamferColor(){return this.getAttrColor("chamfer-color",b.chamferColor)}get chamferAmbient(){return this.getAttrFloat("chamfer-ambient",b.chamferAmbient)}get chamferSpecular(){return this.getAttrFloat("chamfer-specular",b.chamferSpecular)}get chamferShininess(){return this.getAttrFloat("chamfer-shininess",b.chamferShininess)}get edgeOcclusionWidth(){return this.getAttrFloat("edge-occlusion-width",b.edgeOcclusionWidth)}get edgeOcclusionStrength(){return this.getAttrFloat("edge-occlusion-strength",b.edgeOcclusionStrength)}get lightDirection3(){return this.getAttrVec3("light-direction",b.lightDirection)}get shouldAutoplay(){return this.getAttrBool("autoplay",b.autoplay)}get shouldLoop(){return this.getAttrBool("loop",b.loop)}get shouldMute(){return this.getAttrBool("muted",b.muted)}emit(t,e){this.dispatchEvent(new CustomEvent(t,{detail:e,bubbles:!0,composed:!0}))}attachVideoEventListeners(t){t.addEventListener("play",()=>{this.emit("layershift-portal:play",{currentTime:t.currentTime})}),t.addEventListener("pause",()=>{this.emit("layershift-portal:pause",{currentTime:t.currentTime})}),t.addEventListener("ended",()=>{t.loop&&(this.loopCount+=1,this.emit("layershift-portal:loop",{loopCount:this.loopCount}))})}connectedCallback(){this.setupShadowDOM(),this.init()}disconnectedCallback(){this.dispose()}attributeChangedCallback(t,e,i){["src","depth-src","depth-meta","logo-src"].includes(t)&&(this.initialized?(this.dispose(),this.setupShadowDOM(),this.init()):this.isConnected&&this.getAttribute("src")&&this.getAttribute("depth-src")&&this.getAttribute("depth-meta")&&this.getAttribute("logo-src")&&this.init())}setupShadowDOM(){this.shadow.innerHTML="";const t=document.createElement("style");t.textContent=`
|
|
1841
|
+
|
|
1842
|
+
// Base color: video tinted through chamfer color (like frosted glass).
|
|
1843
|
+
let video = toLinear(videoSample);
|
|
1844
|
+
let tint = toLinear(uniforms.chamferColor);
|
|
1845
|
+
// Blend: mostly video near inner edge, more tinted at outer edge.
|
|
1846
|
+
let base = mix(video, video * tint * 3.0, in.lerpT * 0.5);
|
|
1847
|
+
|
|
1848
|
+
// Apply Blinn-Phong.
|
|
1849
|
+
let lit = base * (uniforms.chamferAmbient + (1.0 - uniforms.chamferAmbient) * diff) + vec3f(spec);
|
|
1850
|
+
return vec4f(toSRGB(lit), 1.0);
|
|
1851
|
+
}
|
|
1852
|
+
`,gi=64,vi=16,xi=16,Ie=96,bi=16,Oe=112,ke=64,Si=16,Ti=16,yi=16;class Ei extends k{device;context=null;canvasFormat;config;quadBuffer=null;linearSampler=null;nearestSampler=null;interiorPipeline=null;interiorBindGroupLayout=null;stencilMarkPipeline=null;stencilMarkBindGroupLayout=null;compositePipeline=null;compositeBindGroupLayout=null;chamferPipeline=null;chamferBindGroupLayout=null;boundaryPipeline=null;boundaryBindGroupLayout=null;maskPipeline=null;maskBindGroupLayout=null;jfaSeedPipeline=null;jfaSeedBindGroupLayout=null;jfaFloodPipeline=null;jfaFloodBindGroupLayout=null;jfaDistPipeline=null;jfaDistBindGroupLayout=null;meshScaleUniformBuffer=null;interiorVertexUniformBuffer=null;interiorFragmentUniformBuffer=null;compositeUniformBuffer=null;boundaryUniformBuffer=null;chamferUniformBuffer=null;jfaSeedUniformBuffer=null;jfaFloodUniformBuffer=null;jfaDistUniformBuffer=null;interiorBindGroup=null;stencilMarkBindGroup=null;compositeBindGroup=null;chamferBindGroup=null;boundaryBindGroup=null;maskBindGroup=null;jfaSeedBindGroup=null;jfaDistBindGroup=null;videoTexture=null;videoTextureView=null;depthTexture=null;depthTextureView=null;interiorColorTexture=null;interiorColorView=null;interiorDepthTexture=null;interiorDepthView=null;fboWidth=0;fboHeight=0;depthStencilTexture=null;depthStencilView=null;dsWidth=0;dsHeight=0;jfaMaskTexture=null;jfaMaskView=null;jfaPingTexture=null;jfaPingView=null;jfaPongTexture=null;jfaPongView=null;jfaDistTexture=null;jfaDistView=null;jfaWidth=0;jfaHeight=0;jfaDirty=!0;stencilVertexBuffer=null;stencilIndexBuffer=null;stencilIndexCount=0;boundaryVertexBuffer=null;boundaryVertexCount=0;chamferVertexBuffer=null;chamferVertexCount=0;meshAspect=1;meshScaleX=.65;meshScaleY=.65;lightDirX=-.707;lightDirY=.707;lightDir3=[-.5,.7,-.3];depthFlipBuffer=null;offsetData=new Float32Array(2);constructor(e,t,i,n){super(e),this.device=i,this.config={...t},this.qualityParams=De(n,t.quality);const r=this.config.bevelLightAngle*Math.PI/180;this.lightDirX=Math.cos(r),this.lightDirY=Math.sin(r);const s=this.config.lightDirection,a=Math.sqrt(s[0]*s[0]+s[1]*s[1]+s[2]*s[2]);a>1e-6&&(this.lightDir3=[s[0]/a,s[1]/a,s[2]/a]),this.context=this.canvas.getContext("webgpu"),this.canvasFormat=navigator.gpu.getPreferredCanvasFormat(),this.context.configure({device:i,format:this.canvasFormat,alphaMode:"premultiplied"}),this.quadBuffer=Re(i),this.linearSampler=Fe(i),this.nearestSampler=wt(i),this.createInteriorPipeline(),this.createStencilMarkPipeline(),this.createCompositePipeline(),this.createChamferPipeline(),this.createBoundaryPipeline(),this.createMaskPipeline(),this.createJFASeedPipeline(),this.createJFAFloodPipeline(),this.createJFADistPipeline(),this.meshScaleUniformBuffer=B(i,vi),this.interiorVertexUniformBuffer=B(i,xi),this.interiorFragmentUniformBuffer=B(i,Ie),this.compositeUniformBuffer=B(i,bi),this.boundaryUniformBuffer=B(i,Oe),this.chamferUniformBuffer=B(i,ke),this.jfaSeedUniformBuffer=B(i,Si),this.jfaFloodUniformBuffer=B(i,Ti),this.jfaDistUniformBuffer=B(i,yi),i.lost.then(l=>{console.error(`WebGPU device lost (${l.reason}): ${l.message}`),this.stop()}),this.setupResizeHandling()}initialize(e,t,i,n){this.disposeTextures(),this.disposeGeometryBuffers(),this.videoAspect=e.videoWidth/e.videoHeight,this.meshAspect=n.aspect,this.clampDepthDimensions(t,i,this.qualityParams.depthMaxDim),this.videoTexture=this.device.createTexture({size:[e.videoWidth,e.videoHeight],format:"rgba8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT}),this.videoTextureView=this.videoTexture.createView(),this.depthTexture=this.device.createTexture({size:[this.depthWidth,this.depthHeight],format:"r8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST}),this.depthTextureView=this.depthTexture.createView(),this.depthFlipBuffer=new Uint8Array(this.depthWidth*this.depthHeight),this.uploadStencilMesh(n),this.uploadBoundaryMesh(n),this.uploadChamferMesh(n),this.writeStaticInteriorUniforms(e.videoWidth,e.videoHeight),this.writeStaticCompositeUniforms(),this.writeStaticChamferUniforms(),this.writeStaticBoundaryUniforms(),this.recalculateViewportLayout()}createInteriorPipeline(){const e=this.device;this.interiorBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.VERTEX,buffer:{type:"uniform"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},{binding:2,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:3,visibility:GPUShaderStage.FRAGMENT,sampler:{}},{binding:4,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:5,visibility:GPUShaderStage.FRAGMENT,sampler:{}}]});const t=e.createShaderModule({code:fi});this.interiorPipeline=e.createRenderPipeline({layout:e.createPipelineLayout({bindGroupLayouts:[this.interiorBindGroupLayout]}),vertex:{module:t,entryPoint:"vs_main",buffers:[N]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:"rgba8unorm"},{format:"r8unorm"}],constants:{MAX_POM_STEPS:gi}},primitive:{topology:"triangle-strip"}})}createStencilMarkPipeline(){const e=this.device;this.stencilMarkBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.VERTEX,buffer:{type:"uniform"}}]});const t=e.createShaderModule({code:ai});this.stencilMarkPipeline=e.createRenderPipeline({layout:e.createPipelineLayout({bindGroupLayouts:[this.stencilMarkBindGroupLayout]}),vertex:{module:t,entryPoint:"vs_main",buffers:[Be]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:this.canvasFormat,writeMask:0}]},primitive:{topology:"triangle-list"},depthStencil:{format:"depth24plus-stencil8",depthWriteEnabled:!1,depthCompare:"always",stencilFront:{compare:"always",passOp:"replace",failOp:"keep",depthFailOp:"keep"},stencilBack:{compare:"always",passOp:"replace",failOp:"keep",depthFailOp:"keep"},stencilReadMask:255,stencilWriteMask:255}})}createCompositePipeline(){const e=this.device;this.compositeBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:2,visibility:GPUShaderStage.FRAGMENT,sampler:{}},{binding:3,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:4,visibility:GPUShaderStage.FRAGMENT,sampler:{}}]});const t=e.createShaderModule({code:di});this.compositePipeline=e.createRenderPipeline({layout:e.createPipelineLayout({bindGroupLayouts:[this.compositeBindGroupLayout]}),vertex:{module:t,entryPoint:"vs_main",buffers:[N]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:this.canvasFormat}]},primitive:{topology:"triangle-strip"},depthStencil:{format:"depth24plus-stencil8",depthWriteEnabled:!1,depthCompare:"always",stencilFront:{compare:"equal",passOp:"keep",failOp:"keep",depthFailOp:"keep"},stencilBack:{compare:"equal",passOp:"keep",failOp:"keep",depthFailOp:"keep"},stencilReadMask:255,stencilWriteMask:0}})}createChamferPipeline(){const e=this.device;this.chamferBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.VERTEX|GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:2,visibility:GPUShaderStage.FRAGMENT,sampler:{}}]});const t=e.createShaderModule({code:mi});this.chamferPipeline=e.createRenderPipeline({layout:e.createPipelineLayout({bindGroupLayouts:[this.chamferBindGroupLayout]}),vertex:{module:t,entryPoint:"vs_main",buffers:[Ut]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:this.canvasFormat}]},primitive:{topology:"triangle-list"},depthStencil:{format:"depth24plus-stencil8",depthWriteEnabled:!1,depthCompare:"always",stencilFront:{compare:"always",passOp:"keep",failOp:"keep",depthFailOp:"keep"},stencilBack:{compare:"always",passOp:"keep",failOp:"keep",depthFailOp:"keep"},stencilReadMask:0,stencilWriteMask:0}})}createBoundaryPipeline(){const e=this.device;this.boundaryBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.VERTEX|GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:2,visibility:GPUShaderStage.FRAGMENT,sampler:{}},{binding:3,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:4,visibility:GPUShaderStage.FRAGMENT,sampler:{}},{binding:5,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:6,visibility:GPUShaderStage.FRAGMENT,sampler:{}}]});const t=e.createShaderModule({code:pi});this.boundaryPipeline=e.createRenderPipeline({layout:e.createPipelineLayout({bindGroupLayouts:[this.boundaryBindGroupLayout]}),vertex:{module:t,entryPoint:"vs_main",buffers:[Dt]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:this.canvasFormat,blend:{color:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha",operation:"add"},alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"}}}]},primitive:{topology:"triangle-list"},depthStencil:{format:"depth24plus-stencil8",depthWriteEnabled:!1,depthCompare:"always",stencilFront:{compare:"always",passOp:"keep",failOp:"keep",depthFailOp:"keep"},stencilBack:{compare:"always",passOp:"keep",failOp:"keep",depthFailOp:"keep"},stencilReadMask:0,stencilWriteMask:0}})}createMaskPipeline(){const e=this.device;this.maskBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.VERTEX,buffer:{type:"uniform"}}]});const t=e.createShaderModule({code:li});this.maskPipeline=e.createRenderPipeline({layout:e.createPipelineLayout({bindGroupLayouts:[this.maskBindGroupLayout]}),vertex:{module:t,entryPoint:"vs_main",buffers:[Be]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:"r8unorm"}]},primitive:{topology:"triangle-list"}})}createJFASeedPipeline(){const e=this.device;this.jfaSeedBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,sampler:{}},{binding:2,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}}]});const t=e.createShaderModule({code:ui});this.jfaSeedPipeline=e.createRenderPipeline({layout:e.createPipelineLayout({bindGroupLayouts:[this.jfaSeedBindGroupLayout]}),vertex:{module:t,entryPoint:"vs_main",buffers:[N]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:"rg16float"}]},primitive:{topology:"triangle-strip"}})}createJFAFloodPipeline(){const e=this.device;this.jfaFloodBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:2,visibility:GPUShaderStage.FRAGMENT,sampler:{}}]});const t=e.createShaderModule({code:hi});this.jfaFloodPipeline=e.createRenderPipeline({layout:e.createPipelineLayout({bindGroupLayouts:[this.jfaFloodBindGroupLayout]}),vertex:{module:t,entryPoint:"vs_main",buffers:[N]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:"rg16float"}]},primitive:{topology:"triangle-strip"}})}createJFADistPipeline(){const e=this.device;this.jfaDistBindGroupLayout=e.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:2,visibility:GPUShaderStage.FRAGMENT,sampler:{}},{binding:3,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:"float"}},{binding:4,visibility:GPUShaderStage.FRAGMENT,sampler:{}}]});const t=e.createShaderModule({code:ci});this.jfaDistPipeline=e.createRenderPipeline({layout:e.createPipelineLayout({bindGroupLayouts:[this.jfaDistBindGroupLayout]}),vertex:{module:t,entryPoint:"vs_main",buffers:[N]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:"rgba8unorm"}]},primitive:{topology:"triangle-strip"}})}uploadStencilMesh(e){this.stencilVertexBuffer=me(this.device,e.vertices),this.stencilIndexBuffer=At(this.device,e.indices),this.stencilIndexCount=e.indices.length}uploadBoundaryMesh(e){const t=Ve(e.edgeVertices);t.count!==0&&(this.boundaryVertexBuffer=me(this.device,t.vertices),this.boundaryVertexCount=t.count)}uploadChamferMesh(e){if(this.config.chamferWidth<=0)return;const t=Ge(e.edgeVertices,e.contourOffsets,e.contourIsHole,this.config.chamferWidth,this.config.chamferAngle);t.count!==0&&(this.chamferVertexBuffer=me(this.device,t.vertices),this.chamferVertexCount=t.count)}writeStaticInteriorUniforms(e,t){const i=new ArrayBuffer(Ie),n=new Float32Array(i),r=new Int32Array(i);n[0]=0,n[1]=0,n[2]=this.config.parallaxStrength,r[3]=this.config.pomSteps,n[4]=this.config.depthPower,n[5]=this.config.depthScale,n[6]=this.config.depthBias,n[7]=this.config.contrastLow,n[8]=this.config.contrastHigh,n[9]=this.config.verticalReduction,n[10]=this.config.dofStart,n[11]=this.config.dofStrength,n[12]=1/e,n[13]=1/t,n[14]=this.config.fogDensity,n[16]=this.config.fogColor[0],n[17]=this.config.fogColor[1],n[18]=this.config.fogColor[2],n[19]=this.config.colorShift,n[20]=this.config.brightnessBias,this.device.queue.writeBuffer(this.interiorFragmentUniformBuffer,0,i)}writeStaticCompositeUniforms(){this.device.queue.writeBuffer(this.compositeUniformBuffer,0,new Float32Array([this.config.edgeOcclusionWidth,this.config.edgeOcclusionStrength,0,0]))}writeStaticChamferUniforms(){const e=new ArrayBuffer(ke),t=new Float32Array(e);t[0]=this.lightDir3[0],t[1]=this.lightDir3[1],t[2]=this.lightDir3[2],t[4]=this.config.chamferColor[0],t[5]=this.config.chamferColor[1],t[6]=this.config.chamferColor[2],t[7]=this.config.chamferAmbient,t[8]=this.config.chamferSpecular,t[9]=this.config.chamferShininess,t[10]=this.meshScaleX,t[11]=this.meshScaleY,t[12]=0,t[13]=0,this.device.queue.writeBuffer(this.chamferUniformBuffer,0,e)}writeStaticBoundaryUniforms(){const e=new ArrayBuffer(Oe),t=new Float32Array(e);t[0]=this.config.rimLightWidth,t[2]=this.meshScaleX,t[3]=this.meshScaleY,t[4]=this.config.rimLightIntensity,t[8]=this.config.rimLightColor[0],t[9]=this.config.rimLightColor[1],t[10]=this.config.rimLightColor[2],t[11]=this.config.refractionStrength,t[12]=this.config.chromaticStrength,t[13]=this.config.occlusionIntensity,t[14]=0,t[15]=0,t[16]=this.config.edgeThickness,t[17]=this.config.edgeSpecular,t[20]=this.config.edgeColor[0],t[21]=this.config.edgeColor[1],t[22]=this.config.edgeColor[2],t[24]=this.lightDirX,t[25]=this.lightDirY,t[26]=this.config.bevelIntensity,this.device.queue.writeBuffer(this.boundaryUniformBuffer,0,e)}rebuildInteriorBindGroup(){!this.interiorBindGroupLayout||!this.interiorVertexUniformBuffer||!this.interiorFragmentUniformBuffer||!this.videoTextureView||!this.depthTextureView||!this.linearSampler||(this.interiorBindGroup=this.device.createBindGroup({layout:this.interiorBindGroupLayout,entries:[{binding:0,resource:{buffer:this.interiorVertexUniformBuffer}},{binding:1,resource:{buffer:this.interiorFragmentUniformBuffer}},{binding:2,resource:this.videoTextureView},{binding:3,resource:this.linearSampler},{binding:4,resource:this.depthTextureView},{binding:5,resource:this.linearSampler}]}))}rebuildStencilMarkBindGroup(){!this.stencilMarkBindGroupLayout||!this.meshScaleUniformBuffer||(this.stencilMarkBindGroup=this.device.createBindGroup({layout:this.stencilMarkBindGroupLayout,entries:[{binding:0,resource:{buffer:this.meshScaleUniformBuffer}}]}))}rebuildCompositeBindGroup(){if(!this.compositeBindGroupLayout||!this.compositeUniformBuffer||!this.interiorColorView||!this.linearSampler)return;const e=this.jfaDistView??this.createFallbackTextureView("rgba8unorm");this.compositeBindGroup=this.device.createBindGroup({layout:this.compositeBindGroupLayout,entries:[{binding:0,resource:{buffer:this.compositeUniformBuffer}},{binding:1,resource:this.interiorColorView},{binding:2,resource:this.linearSampler},{binding:3,resource:e},{binding:4,resource:this.linearSampler}]})}rebuildChamferBindGroup(){!this.chamferBindGroupLayout||!this.chamferUniformBuffer||!this.interiorColorView||!this.linearSampler||(this.chamferBindGroup=this.device.createBindGroup({layout:this.chamferBindGroupLayout,entries:[{binding:0,resource:{buffer:this.chamferUniformBuffer}},{binding:1,resource:this.interiorColorView},{binding:2,resource:this.linearSampler}]}))}rebuildBoundaryBindGroup(){if(!this.boundaryBindGroupLayout||!this.boundaryUniformBuffer||!this.interiorColorView||!this.interiorDepthView||!this.linearSampler)return;const e=this.jfaDistView??this.createFallbackTextureView("rgba8unorm");this.boundaryBindGroup=this.device.createBindGroup({layout:this.boundaryBindGroupLayout,entries:[{binding:0,resource:{buffer:this.boundaryUniformBuffer}},{binding:1,resource:this.interiorColorView},{binding:2,resource:this.linearSampler},{binding:3,resource:this.interiorDepthView},{binding:4,resource:this.linearSampler},{binding:5,resource:e},{binding:6,resource:this.linearSampler}]})}rebuildMaskBindGroup(){!this.maskBindGroupLayout||!this.meshScaleUniformBuffer||(this.maskBindGroup=this.device.createBindGroup({layout:this.maskBindGroupLayout,entries:[{binding:0,resource:{buffer:this.meshScaleUniformBuffer}}]}))}rebuildJFASeedBindGroup(){!this.jfaSeedBindGroupLayout||!this.jfaMaskView||!this.linearSampler||!this.jfaSeedUniformBuffer||(this.jfaSeedBindGroup=this.device.createBindGroup({layout:this.jfaSeedBindGroupLayout,entries:[{binding:0,resource:this.jfaMaskView},{binding:1,resource:this.linearSampler},{binding:2,resource:{buffer:this.jfaSeedUniformBuffer}}]}))}rebuildJFADistBindGroup(){if(!this.jfaDistBindGroupLayout||!this.jfaDistUniformBuffer||!this.jfaMaskView||!this.linearSampler)return;const e=this.jfaPingView??this.jfaPongView;e&&(this.jfaDistBindGroup=this.device.createBindGroup({layout:this.jfaDistBindGroupLayout,entries:[{binding:0,resource:{buffer:this.jfaDistUniformBuffer}},{binding:1,resource:e},{binding:2,resource:this.nearestSampler},{binding:3,resource:this.jfaMaskView},{binding:4,resource:this.linearSampler}]}))}rebuildBindGroups(){this.rebuildInteriorBindGroup(),this.rebuildStencilMarkBindGroup(),this.rebuildCompositeBindGroup(),this.rebuildChamferBindGroup(),this.rebuildBoundaryBindGroup(),this.rebuildMaskBindGroup(),this.rebuildJFASeedBindGroup(),this.rebuildJFADistBindGroup()}createInteriorFBO(e,t){this.disposeInteriorFBO(),this.fboWidth=e,this.fboHeight=t,this.interiorColorTexture=this.device.createTexture({size:[e,t],format:"rgba8unorm",usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),this.interiorColorView=this.interiorColorTexture.createView(),this.interiorDepthTexture=this.device.createTexture({size:[e,t],format:"r8unorm",usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),this.interiorDepthView=this.interiorDepthTexture.createView()}createDepthStencilTexture(e,t){this.disposeDepthStencilTexture(),this.dsWidth=e,this.dsHeight=t,this.depthStencilTexture=this.device.createTexture({size:[e,t],format:"depth24plus-stencil8",usage:GPUTextureUsage.RENDER_ATTACHMENT}),this.depthStencilView=this.depthStencilTexture.createView()}createJFAResources(e,t){this.disposeJFAResources();const i=this.qualityParams.jfaDivisor,n=Math.max(1,Math.round(e/i)),r=Math.max(1,Math.round(t/i));this.jfaWidth=n,this.jfaHeight=r,this.jfaMaskTexture=this.device.createTexture({size:[n,r],format:"r8unorm",usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),this.jfaMaskView=this.jfaMaskTexture.createView(),this.jfaPingTexture=this.device.createTexture({size:[n,r],format:"rg16float",usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),this.jfaPingView=this.jfaPingTexture.createView(),this.jfaPongTexture=this.device.createTexture({size:[n,r],format:"rg16float",usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),this.jfaPongView=this.jfaPongTexture.createView(),this.jfaDistTexture=this.device.createTexture({size:[n,r],format:"rgba8unorm",usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),this.jfaDistView=this.jfaDistTexture.createView(),this.jfaDirty=!0}computeDistanceField(e){if(!this.maskPipeline||!this.jfaSeedPipeline||!this.jfaFloodPipeline||!this.jfaDistPipeline||!this.stencilVertexBuffer||!this.stencilIndexBuffer||!this.quadBuffer||!this.jfaMaskView||!this.jfaPingView||!this.jfaPongView||!this.jfaDistView||!this.maskBindGroup||!this.jfaSeedBindGroup||!this.jfaFloodBindGroupLayout||!this.jfaDistBindGroupLayout||!this.nearestSampler||!this.linearSampler||!this.jfaMaskView)return;const t=this.jfaWidth,i=this.jfaHeight;if(t===0||i===0)return;{const u=e.beginRenderPass({colorAttachments:[{view:this.jfaMaskView,clearValue:{r:0,g:0,b:0,a:1},loadOp:"clear",storeOp:"store"}]});u.setPipeline(this.maskPipeline),u.setBindGroup(0,this.maskBindGroup),u.setVertexBuffer(0,this.stencilVertexBuffer),u.setIndexBuffer(this.stencilIndexBuffer,"uint16"),u.drawIndexed(this.stencilIndexCount),u.end()}this.device.queue.writeBuffer(this.jfaSeedUniformBuffer,0,new Float32Array([1/t,1/i,0,0])),this.rebuildJFASeedBindGroup();{const u=e.beginRenderPass({colorAttachments:[{view:this.jfaPingView,clearValue:{r:-1,g:-1,b:0,a:0},loadOp:"clear",storeOp:"store"}]});u.setPipeline(this.jfaSeedPipeline),u.setBindGroup(0,this.jfaSeedBindGroup),u.setVertexBuffer(0,this.quadBuffer),u.draw(4),u.end()}const n=se.computeFloodIterations(t,i);let r=this.jfaPingView,s=this.jfaPongView;for(let u=0;u<n.length;u++){const h=n[u]/Math.max(t,i);this.device.queue.writeBuffer(this.jfaFloodUniformBuffer,0,new Float32Array([1/t,1/i,h,0]));const f=this.device.createBindGroup({layout:this.jfaFloodBindGroupLayout,entries:[{binding:0,resource:{buffer:this.jfaFloodUniformBuffer}},{binding:1,resource:r},{binding:2,resource:this.nearestSampler}]}),c=e.beginRenderPass({colorAttachments:[{view:s,loadOp:"clear",clearValue:{r:-1,g:-1,b:0,a:0},storeOp:"store"}]});c.setPipeline(this.jfaFloodPipeline),c.setBindGroup(0,f),c.setVertexBuffer(0,this.quadBuffer),c.draw(4),c.end();const g=r;r=s,s=g}const a=Math.max(this.config.bevelWidth,this.config.edgeOcclusionWidth);this.device.queue.writeBuffer(this.jfaDistUniformBuffer,0,new Float32Array([1/t,1/i,a,0]));const l=this.device.createBindGroup({layout:this.jfaDistBindGroupLayout,entries:[{binding:0,resource:{buffer:this.jfaDistUniformBuffer}},{binding:1,resource:r},{binding:2,resource:this.nearestSampler},{binding:3,resource:this.jfaMaskView},{binding:4,resource:this.linearSampler}]});{const u=e.beginRenderPass({colorAttachments:[{view:this.jfaDistView,clearValue:{r:0,g:0,b:0,a:1},loadOp:"clear",storeOp:"store"}]});u.setPipeline(this.jfaDistPipeline),u.setBindGroup(0,l),u.setVertexBuffer(0,this.quadBuffer),u.draw(4),u.end()}this.jfaDirty=!1,this.rebuildCompositeBindGroup(),this.rebuildBoundaryBindGroup()}flipDepthY(e){const t=this.depthFlipBuffer,i=this.depthWidth,n=this.depthHeight;for(let r=0;r<n;r++){const s=r*i,a=(n-1-r)*i;t.set(e.subarray(s,s+i),a)}return t}fallbackTexture=null;createFallbackTextureView(e){return this.fallbackTexture||(this.fallbackTexture=this.device.createTexture({size:[1,1],format:e,usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST}),this.device.queue.writeTexture({texture:this.fallbackTexture},new Uint8Array([0,0,0,0]),{bytesPerRow:4},{width:1,height:1})),this.fallbackTexture.createView()}onRenderFrame(){const e=this.playbackVideo;if(!this.context||!this.interiorPipeline||!this.interiorBindGroup||!this.quadBuffer||!e||e.readyState<HTMLMediaElement.HAVE_CURRENT_DATA||!this.interiorColorView||!this.interiorDepthView)return;if(this.videoTexture&&Le(this.device,this.videoTexture,e),this.rvfcSupported||this.onDepthUpdate(e.currentTime),this.readInput){const n=this.readInput();this.offsetData[0]=-n.x,this.offsetData[1]=n.y,this.device.queue.writeBuffer(this.interiorFragmentUniformBuffer,0,this.offsetData)}const t=this.device.createCommandEncoder();this.jfaDirty&&this.stencilVertexBuffer&&this.stencilIndexBuffer&&this.computeDistanceField(t);{const n=t.beginRenderPass({colorAttachments:[{view:this.interiorColorView,clearValue:{r:0,g:0,b:0,a:1},loadOp:"clear",storeOp:"store"},{view:this.interiorDepthView,clearValue:{r:0,g:0,b:0,a:1},loadOp:"clear",storeOp:"store"}]});n.setPipeline(this.interiorPipeline),n.setBindGroup(0,this.interiorBindGroup),n.setVertexBuffer(0,this.quadBuffer),n.draw(4),n.end()}const i=this.context.getCurrentTexture().createView();if(this.stencilMarkPipeline&&this.stencilMarkBindGroup&&this.stencilVertexBuffer&&this.stencilIndexBuffer&&this.stencilIndexCount>0&&this.depthStencilView){const n=t.beginRenderPass({colorAttachments:[{view:i,clearValue:{r:0,g:0,b:0,a:0},loadOp:"clear",storeOp:"store"}],depthStencilAttachment:{view:this.depthStencilView,stencilClearValue:0,stencilLoadOp:"clear",stencilStoreOp:"store",depthClearValue:1,depthLoadOp:"clear",depthStoreOp:"discard"}});n.setStencilReference(1),n.setPipeline(this.stencilMarkPipeline),n.setBindGroup(0,this.stencilMarkBindGroup),n.setVertexBuffer(0,this.stencilVertexBuffer),n.setIndexBuffer(this.stencilIndexBuffer,"uint16"),n.drawIndexed(this.stencilIndexCount),n.end()}if(this.compositePipeline&&this.compositeBindGroup&&this.depthStencilView){const n=t.beginRenderPass({colorAttachments:[{view:i,loadOp:"load",storeOp:"store"}],depthStencilAttachment:{view:this.depthStencilView,stencilLoadOp:"load",stencilStoreOp:"store",depthLoadOp:"load",depthStoreOp:"discard"}});n.setStencilReference(1),n.setPipeline(this.compositePipeline),n.setBindGroup(0,this.compositeBindGroup),n.setVertexBuffer(0,this.quadBuffer),n.draw(4),n.end()}if(this.chamferPipeline&&this.chamferBindGroup&&this.chamferVertexBuffer&&this.chamferVertexCount>0&&this.depthStencilView){const n=t.beginRenderPass({colorAttachments:[{view:i,loadOp:"load",storeOp:"store"}],depthStencilAttachment:{view:this.depthStencilView,stencilLoadOp:"load",stencilStoreOp:"store",depthLoadOp:"load",depthStoreOp:"discard"}});n.setPipeline(this.chamferPipeline),n.setBindGroup(0,this.chamferBindGroup),n.setVertexBuffer(0,this.chamferVertexBuffer),n.draw(this.chamferVertexCount),n.end()}if(this.boundaryPipeline&&this.boundaryBindGroup&&this.boundaryVertexBuffer&&this.boundaryVertexCount>0&&this.config.rimLightIntensity>0&&this.depthStencilView){const n=t.beginRenderPass({colorAttachments:[{view:i,loadOp:"load",storeOp:"store"}],depthStencilAttachment:{view:this.depthStencilView,stencilLoadOp:"load",stencilStoreOp:"store",depthLoadOp:"load",depthStoreOp:"discard"}});n.setPipeline(this.boundaryPipeline),n.setBindGroup(0,this.boundaryBindGroup),n.setVertexBuffer(0,this.boundaryVertexBuffer),n.draw(this.boundaryVertexCount),n.end()}this.device.queue.submit([t.finish()])}onDepthUpdate(e){if(!this.readDepth||!this.depthTexture)return;const t=this.subsampleDepth(this.readDepth(e)),i=this.flipDepthY(t);this.device.queue.writeTexture({texture:this.depthTexture},i,{bytesPerRow:this.depthWidth},{width:this.depthWidth,height:this.depthHeight})}recalculateViewportLayout(){if(!this.context)return;const{width:e,height:t}=this.getViewportSize(),i=Math.min(window.devicePixelRatio,this.qualityParams.dprCap),n=Math.round(e*i),r=Math.round(t*i);(this.canvas.width!==n||this.canvas.height!==r)&&(this.canvas.width=n,this.canvas.height=r),(this.fboWidth!==n||this.fboHeight!==r)&&this.createInteriorFBO(n,r),(this.dsWidth!==n||this.dsHeight!==r)&&this.createDepthStencilTexture(n,r);const s=this.qualityParams.jfaDivisor,a=Math.max(1,Math.round(n/s)),l=Math.max(1,Math.round(r/s));(this.jfaWidth!==a||this.jfaHeight!==l)&&this.createJFAResources(n,r),this.computeCoverFitUV(this.config.parallaxStrength,this.config.overscanPadding),this.interiorVertexUniformBuffer&&this.device.queue.writeBuffer(this.interiorVertexUniformBuffer,0,new Float32Array([this.uvOffset[0],this.uvOffset[1],this.uvScale[0],this.uvScale[1]]));const u=e/t,h=.65;this.meshScaleX=h,this.meshScaleY=h,u>this.meshAspect?this.meshScaleX=h*(this.meshAspect/u):this.meshScaleY=h*(u/this.meshAspect),this.meshScaleUniformBuffer&&this.device.queue.writeBuffer(this.meshScaleUniformBuffer,0,new Float32Array([this.meshScaleX,this.meshScaleY,0,0])),this.boundaryUniformBuffer&&(this.device.queue.writeBuffer(this.boundaryUniformBuffer,8,new Float32Array([this.meshScaleX,this.meshScaleY])),this.device.queue.writeBuffer(this.boundaryUniformBuffer,56,new Float32Array([1/n,1/r]))),this.chamferUniformBuffer&&(this.device.queue.writeBuffer(this.chamferUniformBuffer,40,new Float32Array([this.meshScaleX,this.meshScaleY])),this.device.queue.writeBuffer(this.chamferUniformBuffer,48,new Float32Array([1/n,1/r]))),this.jfaDirty=!0,this.rebuildBindGroups()}onContextRestored(){}disposeRenderer(){this.disposeTextures(),this.disposeInteriorFBO(),this.disposeDepthStencilTexture(),this.disposeJFAResources(),this.disposeGeometryBuffers(),this.meshScaleUniformBuffer?.destroy(),this.meshScaleUniformBuffer=null,this.interiorVertexUniformBuffer?.destroy(),this.interiorVertexUniformBuffer=null,this.interiorFragmentUniformBuffer?.destroy(),this.interiorFragmentUniformBuffer=null,this.compositeUniformBuffer?.destroy(),this.compositeUniformBuffer=null,this.boundaryUniformBuffer?.destroy(),this.boundaryUniformBuffer=null,this.chamferUniformBuffer?.destroy(),this.chamferUniformBuffer=null,this.jfaSeedUniformBuffer?.destroy(),this.jfaSeedUniformBuffer=null,this.jfaFloodUniformBuffer?.destroy(),this.jfaFloodUniformBuffer=null,this.jfaDistUniformBuffer?.destroy(),this.jfaDistUniformBuffer=null,this.quadBuffer?.destroy(),this.quadBuffer=null,this.linearSampler=null,this.nearestSampler=null,this.fallbackTexture?.destroy(),this.fallbackTexture=null,this.interiorPipeline=null,this.interiorBindGroupLayout=null,this.interiorBindGroup=null,this.stencilMarkPipeline=null,this.stencilMarkBindGroupLayout=null,this.stencilMarkBindGroup=null,this.compositePipeline=null,this.compositeBindGroupLayout=null,this.compositeBindGroup=null,this.chamferPipeline=null,this.chamferBindGroupLayout=null,this.chamferBindGroup=null,this.boundaryPipeline=null,this.boundaryBindGroupLayout=null,this.boundaryBindGroup=null,this.maskPipeline=null,this.maskBindGroupLayout=null,this.maskBindGroup=null,this.jfaSeedPipeline=null,this.jfaSeedBindGroupLayout=null,this.jfaSeedBindGroup=null,this.jfaFloodPipeline=null,this.jfaFloodBindGroupLayout=null,this.jfaDistPipeline=null,this.jfaDistBindGroupLayout=null,this.jfaDistBindGroup=null,this.context&&(this.context.unconfigure(),this.context=null),this.depthFlipBuffer=null}disposeTextures(){this.videoTexture?.destroy(),this.videoTexture=null,this.videoTextureView=null,this.depthTexture?.destroy(),this.depthTexture=null,this.depthTextureView=null,this.interiorBindGroup=null}disposeInteriorFBO(){this.interiorColorTexture?.destroy(),this.interiorColorTexture=null,this.interiorColorView=null,this.interiorDepthTexture?.destroy(),this.interiorDepthTexture=null,this.interiorDepthView=null,this.fboWidth=0,this.fboHeight=0,this.compositeBindGroup=null,this.chamferBindGroup=null,this.boundaryBindGroup=null}disposeDepthStencilTexture(){this.depthStencilTexture?.destroy(),this.depthStencilTexture=null,this.depthStencilView=null,this.dsWidth=0,this.dsHeight=0}disposeJFAResources(){this.jfaMaskTexture?.destroy(),this.jfaMaskTexture=null,this.jfaMaskView=null,this.jfaPingTexture?.destroy(),this.jfaPingTexture=null,this.jfaPingView=null,this.jfaPongTexture?.destroy(),this.jfaPongTexture=null,this.jfaPongView=null,this.jfaDistTexture?.destroy(),this.jfaDistTexture=null,this.jfaDistView=null,this.jfaWidth=0,this.jfaHeight=0,this.jfaDirty=!0,this.jfaSeedBindGroup=null,this.jfaDistBindGroup=null}disposeGeometryBuffers(){this.stencilVertexBuffer?.destroy(),this.stencilVertexBuffer=null,this.stencilIndexBuffer?.destroy(),this.stencilIndexBuffer=null,this.stencilIndexCount=0,this.boundaryVertexBuffer?.destroy(),this.boundaryVertexBuffer=null,this.boundaryVertexCount=0,this.chamferVertexBuffer?.destroy(),this.chamferVertexBuffer=null,this.chamferVertexCount=0,this.stencilMarkBindGroup=null,this.maskBindGroup=null}}async function Pi(o){const e=await fetch(o);if(!e.ok)throw new Error(`Failed to fetch SVG: ${e.status} ${e.statusText}`);const t=await e.text();return Ai(t)}function Ai(o){const i=new DOMParser().parseFromString(o,"image/svg+xml").querySelector("svg");if(!i)throw new Error("No <svg> element found in document.");const n=wi(i);if(n.length===0)throw new Error("No path data found in SVG.");let r=1/0,s=1/0,a=-1/0,l=-1/0;for(const D of n)for(let w=0;w<D.length;w+=2)r=Math.min(r,D[w]),a=Math.max(a,D[w]),s=Math.min(s,D[w+1]),l=Math.max(l,D[w+1]);const u=a-r,h=l-s,f=(r+a)/2,c=(s+l)/2,g=2/Math.max(u,h),v=u/h,d=n.map(D=>{const w=[];for(let L=0;L<D.length;L+=2)w.push((D[L]-f)*g),w.push(-((D[L+1]-c)*g));return w}),A=Ci(d),b=[],y=[];for(const D of A){const{flatCoords:w,holeIndices:L}=Mi(D),M=_i(w,L),O=b.length/2;for(const _ of M)y.push(_+O);for(const _ of w)b.push(_)}const p=b,m=y,S=[],x=[],E=[],P=We(d);for(let D=0;D<d.length;D++){const w=d[D];x.push(S.length),E.push(P[D]);for(let L=0;L<w.length;L++)S.push(w[L]);w.length>=2&&S.push(w[0],w[1])}let U=1/0,R=1/0,C=-1/0,I=-1/0;for(let D=0;D<p.length;D+=2)U=Math.min(U,p[D]),C=Math.max(C,p[D]),R=Math.min(R,p[D+1]),I=Math.max(I,p[D+1]);return{vertices:new Float32Array(p),indices:new Uint16Array(m),edgeVertices:new Float32Array(S),contourOffsets:x,contourIsHole:E,bounds:{minX:U,maxX:C,minY:R,maxY:I},aspect:v}}function wi(o){const e=[];return o.querySelectorAll("path").forEach(l=>{const u=l.getAttribute("d");if(!u)return;const h=Ri(u);e.push(...h)}),o.querySelectorAll("polygon").forEach(l=>{const u=l.getAttribute("points");if(!u)return;const h=Ne(u);h.length>=6&&e.push(h)}),o.querySelectorAll("polyline").forEach(l=>{const u=l.getAttribute("points");if(!u)return;const h=Ne(u);h.length>=6&&e.push(h)}),o.querySelectorAll("rect").forEach(l=>{const u=parseFloat(l.getAttribute("x")||"0"),h=parseFloat(l.getAttribute("y")||"0"),f=parseFloat(l.getAttribute("width")||"0"),c=parseFloat(l.getAttribute("height")||"0");f>0&&c>0&&e.push([u,h,u+f,h,u+f,h+c,u,h+c])}),o.querySelectorAll("circle").forEach(l=>{const u=parseFloat(l.getAttribute("cx")||"0"),h=parseFloat(l.getAttribute("cy")||"0"),f=parseFloat(l.getAttribute("r")||"0");f>0&&e.push(Di(u,h,f))}),o.querySelectorAll("ellipse").forEach(l=>{const u=parseFloat(l.getAttribute("cx")||"0"),h=parseFloat(l.getAttribute("cy")||"0"),f=parseFloat(l.getAttribute("rx")||"0"),c=parseFloat(l.getAttribute("ry")||"0");f>0&&c>0&&e.push(Ui(u,h,f,c))}),e}function Ne(o){const e=[],t=o.trim().split(/[\s,]+/);for(let i=0;i<t.length-1;i+=2){const n=parseFloat(t[i]),r=parseFloat(t[i+1]);Number.isFinite(n)&&Number.isFinite(r)&&e.push(n,r)}return e}function Di(o,e,t,i=64){const n=[];for(let r=0;r<i;r++){const s=2*Math.PI*r/i;n.push(o+t*Math.cos(s),e+t*Math.sin(s))}return n}function Ui(o,e,t,i,n=64){const r=[];for(let s=0;s<n;s++){const a=2*Math.PI*s/n;r.push(o+t*Math.cos(a),e+i*Math.sin(a))}return r}function Ri(o){const e=[];let t=[],i=0,n=0,r=0,s=0,a=0,l=0,u="";const h=Fi(o);let f=0;function c(){return f>=h.length?0:parseFloat(h[f++])}for(;f<h.length;){const g=h[f];let v;/^[a-zA-Z]$/.test(g)?(v=g,f++):v=u==="M"?"L":u==="m"?"l":u;const d=v===v.toLowerCase();switch(v.toUpperCase()){case"M":{t.length>0&&e.push(t),t=[];const b=c()+(d?i:0),y=c()+(d?n:0);i=b,n=y,r=b,s=y,t.push(i,n),a=i,l=n;break}case"L":{i=c()+(d?i:0),n=c()+(d?n:0),t.push(i,n),a=i,l=n;break}case"H":{i=c()+(d?i:0),t.push(i,n),a=i,l=n;break}case"V":{n=c()+(d?n:0),t.push(i,n),a=i,l=n;break}case"C":{const b=c()+(d?i:0),y=c()+(d?n:0),p=c()+(d?i:0),m=c()+(d?n:0),S=c()+(d?i:0),x=c()+(d?n:0);K(t,i,n,b,y,p,m,S,x),i=S,n=x,a=p,l=m;break}case"S":{const b=2*i-a,y=2*n-l,p=c()+(d?i:0),m=c()+(d?n:0),S=c()+(d?i:0),x=c()+(d?n:0);K(t,i,n,b,y,p,m,S,x),i=S,n=x,a=p,l=m;break}case"Q":{const b=c()+(d?i:0),y=c()+(d?n:0),p=c()+(d?i:0),m=c()+(d?n:0);Xe(t,i,n,b,y,p,m),i=p,n=m,a=b,l=y;break}case"T":{const b=2*i-a,y=2*n-l,p=c()+(d?i:0),m=c()+(d?n:0);Xe(t,i,n,b,y,p,m),i=p,n=m,a=b,l=y;break}case"A":{const b=c(),y=c(),p=c(),m=c(),S=c(),x=c()+(d?i:0),E=c()+(d?n:0);Bi(t,i,n,b,y,p,!!m,!!S,x,E),i=x,n=E,a=i,l=n;break}case"Z":{i=r,n=s,t.length>0&&e.push(t),t=[],a=i,l=n;break}default:f++;break}u=v}return t.length>=6&&e.push(t),e}function Fi(o){const e=[],t=/([a-zA-Z])|([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/g;let i;for(;(i=t.exec(o))!==null;)e.push(i[0]);return e}const Li=.5;function K(o,e,t,i,n,r,s,a,l,u=0){if(u>12){o.push(a,l);return}const h=a-e,f=l-t,c=Math.sqrt(h*h+f*f);if(c<1e-6){o.push(a,l);return}const g=Math.abs((i-a)*f-(n-l)*h)/c,v=Math.abs((r-a)*f-(s-l)*h)/c;if(g+v<Li){o.push(a,l);return}const d=(e+i)/2,A=(t+n)/2,b=(i+r)/2,y=(n+s)/2,p=(r+a)/2,m=(s+l)/2,S=(d+b)/2,x=(A+y)/2,E=(b+p)/2,P=(y+m)/2,U=(S+E)/2,R=(x+P)/2;K(o,e,t,d,A,S,x,U,R,u+1),K(o,U,R,E,P,p,m,a,l,u+1)}function Xe(o,e,t,i,n,r,s){const a=e+.6666666666666666*(i-e),l=t+2/3*(n-t),u=r+2/3*(i-r),h=s+2/3*(n-s);K(o,e,t,a,l,u,h,r,s)}function Bi(o,e,t,i,n,r,s,a,l,u){if(i===0||n===0){o.push(l,u);return}let h=Math.abs(i),f=Math.abs(n);const c=r*Math.PI/180,g=Math.cos(c),v=Math.sin(c),d=(e-l)/2,A=(t-u)/2,b=g*d+v*A,y=-v*d+g*A;let p=b*b/(h*h)+y*y/(f*f);if(p>1){const M=Math.sqrt(p);h*=M,f*=M,p=1}const m=h*h,S=f*f,x=b*b,E=y*y;let P=Math.max(0,(m*S-m*E-S*x)/(m*E+S*x));P=Math.sqrt(P),s===a&&(P=-P);const U=P*(h*y)/f,R=P*-(f*b)/h,C=g*U-v*R+(e+l)/2,I=v*U+g*R+(t+u)/2,D=He(1,0,(b-U)/h,(y-R)/f);let w=He((b-U)/h,(y-R)/f,(-b-U)/h,(-y-R)/f);!a&&w>0&&(w-=2*Math.PI),a&&w<0&&(w+=2*Math.PI);const L=Math.max(4,Math.ceil(Math.abs(w)/(Math.PI/16)));for(let M=1;M<=L;M++){const O=D+M/L*w,_=Math.cos(O),Y=Math.sin(O),ie=g*h*_-v*f*Y+C,fe=v*h*_+g*f*Y+I;o.push(ie,fe)}}function He(o,e,t,i){const n=o*i-e*t<0?-1:1,r=o*t+e*i,s=Math.sqrt(o*o+e*e),a=Math.sqrt(t*t+i*i),l=r/(s*a);return n*Math.acos(Math.max(-1,Math.min(1,l)))}function Mi(o){const e=[],t=[];for(let i=0;i<o.length;i++){i>0&&t.push(e.length/2);for(const n of o[i])e.push(n)}return{flatCoords:e,holeIndices:t}}function We(o){const e=o.length,t=o.map(n=>Math.abs(je(n))),i=new Array(e).fill(!1);for(let n=0;n<e;n++){let r=0;const s=o[n][0],a=o[n][1];for(let l=0;l<e;l++)n!==l&&t[l]>t[n]&&ze(s,a,o[l])&&r++;i[n]=r%2===1}return i}function Ci(o){if(o.length<=1)return[o];const e=We(o),t=o.map((s,a)=>{const l=je(s);return{index:a,contour:s,area:l,isOuter:!e[a]}}),i=t.filter(s=>s.isOuter),n=t.filter(s=>!s.isOuter);if(i.length===0)return o.map(s=>[s]);const r=i.map(s=>({outer:s.contour,holes:[]}));for(const s of n){const a=s.contour[0],l=s.contour[1];let u=-1,h=1/0;for(let f=0;f<i.length;f++)if(ze(a,l,i[f].contour)){const c=Math.abs(i[f].area);c<h&&(h=c,u=f)}u>=0?r[u].holes.push(s.contour):r.push({outer:s.contour,holes:[]})}return r.map(s=>[s.outer,...s.holes])}function je(o){let e=0;const t=o.length;for(let i=0;i<t;i+=2){const n=o[i],r=o[i+1],s=o[(i+2)%t],a=o[(i+3)%t];e+=n*a-s*r}return e/2}function ze(o,e,t){let i=!1;const n=t.length;for(let r=0,s=n-2;r<n;s=r,r+=2){const a=t[r],l=t[r+1],u=t[s],h=t[s+1];l>e!=h>e&&o<(u-a)*(e-l)/(h-l)+a&&(i=!i)}return i}function _i(o,e,t=2){const i=e&&e.length>0,n=i?e[0]*t:o.length;let r=qe(o,0,n,t,!0);const s=[];if(!r||r.next===r.prev)return s;i&&(r=ki(o,e,r,t));let a=1/0,l=1/0,u=-1/0,h=-1/0,f=0;if(o.length>80*t){for(let c=0;c<n;c+=t){const g=o[c],v=o[c+1];g<a&&(a=g),v<l&&(l=v),g>u&&(u=g),v>h&&(h=v)}f=Math.max(u-a,h-l),f=f!==0?32767/f:0}return Q(r,s,t,a,l,f,0),s}function qe(o,e,t,i,n){let r=null;if(n===Ji(o,e,t,i)>0)for(let s=e;s<t;s+=i)r=Je(s,o[s],o[s+1],r);else for(let s=t-i;s>=e;s-=i)r=Je(s,o[s],o[s+1],r);return r&&ae(r,r.next)&&(te(r),r=r.next),r?(r.next.prev=r,r.prev.next=r,r.next):null}function H(o,e){e||(e=o);let t=o,i;do if(i=!1,!t.steiner&&(ae(t,t.next)||F(t.prev,t,t.next)===0)){if(te(t),t=e=t.prev,t===t.next)break;i=!0}else t=t.next;while(i||t!==e);return e}function Q(o,e,t,i,n,r,s){if(!o)return;!s&&r&&Wi(o,i,n,r);let a=o,l,u;for(;o.prev!==o.next;){if(l=o.prev,u=o.next,r?Gi(o,i,n,r):Vi(o)){e.push(l.i/t,o.i/t,u.i/t),te(o),o=u.next,a=u.next;continue}if(o=u,o===a){s?s===1?(o=Ii(H(o),e,t),Q(o,e,t,i,n,r,2)):s===2&&Oi(o,e,t,i,n,r):Q(H(o),e,t,i,n,r,1);break}}}function Vi(o){const e=o.prev,t=o,i=o.next;if(F(e,t,i)>=0)return!1;const n=e.x,r=t.x,s=i.x,a=e.y,l=t.y,u=i.y,h=n<r?n<s?n:s:r<s?r:s,f=a<l?a<u?a:u:l<u?l:u,c=n>r?n>s?n:s:r>s?r:s,g=a>l?a>u?a:u:l>u?l:u;let v=i.next;for(;v!==e;){if(v.x>=h&&v.x<=c&&v.y>=f&&v.y<=g&&z(n,a,r,l,s,u,v.x,v.y)&&F(v.prev,v,v.next)>=0)return!1;v=v.next}return!0}function Gi(o,e,t,i){const n=o.prev,r=o,s=o.next;if(F(n,r,s)>=0)return!1;const a=n.x,l=r.x,u=s.x,h=n.y,f=r.y,c=s.y,g=a<l?a<u?a:u:l<u?l:u,v=h<f?h<c?h:c:f<c?f:c,d=a>l?a>u?a:u:l>u?l:u,A=h>f?h>c?h:c:f>c?f:c,b=ge(g,v,e,t,i),y=ge(d,A,e,t,i);let p=o.prevZ,m=o.nextZ;for(;p&&p.z>=b&&m&&m.z<=y;){if(p.x>=g&&p.x<=d&&p.y>=v&&p.y<=A&&p!==n&&p!==s&&z(a,h,l,f,u,c,p.x,p.y)&&F(p.prev,p,p.next)>=0||(p=p.prevZ,m.x>=g&&m.x<=d&&m.y>=v&&m.y<=A&&m!==n&&m!==s&&z(a,h,l,f,u,c,m.x,m.y)&&F(m.prev,m,m.next)>=0))return!1;m=m.nextZ}for(;p&&p.z>=b;){if(p.x>=g&&p.x<=d&&p.y>=v&&p.y<=A&&p!==n&&p!==s&&z(a,h,l,f,u,c,p.x,p.y)&&F(p.prev,p,p.next)>=0)return!1;p=p.prevZ}for(;m&&m.z<=y;){if(m.x>=g&&m.x<=d&&m.y>=v&&m.y<=A&&m!==n&&m!==s&&z(a,h,l,f,u,c,m.x,m.y)&&F(m.prev,m,m.next)>=0)return!1;m=m.nextZ}return!0}function Ii(o,e,t){let i=o;do{const n=i.prev,r=i.next.next;!ae(n,r)&&Ye(n,i,i.next,r)&&ee(n,r)&&ee(r,n)&&(e.push(n.i/t,i.i/t,r.i/t),te(i),te(i.next),i=o=r),i=i.next}while(i!==o);return H(i)}function Oi(o,e,t,i,n,r){let s=o;do{let a=s.next.next;for(;a!==s.prev;){if(s.i!==a.i&&qi(s,a)){let l=Ze(s,a);s=H(s,s.next),l=H(l,l.next),Q(s,e,t,i,n,r,0),Q(l,e,t,i,n,r,0);return}a=a.next}s=s.next}while(s!==o)}function ki(o,e,t,i){const n=[];for(let r=0;r<e.length;r++){const s=e[r]*i,a=r<e.length-1?e[r+1]*i:o.length,l=qe(o,s,a,i,!1);l&&(l===l.next&&(l.steiner=!0),n.push(zi(l)))}n.sort((r,s)=>r.x-s.x);for(const r of n)t=Ni(r,t);return t}function Ni(o,e){const t=Xi(o,e);if(!t)return e;const i=Ze(t,o);return H(i,i.next),H(t,t.next)}function Xi(o,e){let t=e;const i=o.x,n=o.y;let r=-1/0,s=null;do{if(n<=t.y&&n>=t.next.y&&t.next.y!==t.y){const f=t.x+(n-t.y)/(t.next.y-t.y)*(t.next.x-t.x);if(f<=i&&f>r&&(r=f,s=t.x<t.next.x?t:t.next,f===i))return s}t=t.next}while(t!==e);if(!s)return null;const a=s,l=s.x,u=s.y;let h=1/0;t=s;do{if(i>=t.x&&t.x>=l&&i!==t.x&&z(n<u?i:r,n,l,u,n<u?r:i,n,t.x,t.y)){const f=Math.abs(n-t.y)/(i-t.x);ee(t,o)&&(f<h||f===h&&(t.x>s.x||Hi(s,t)))&&(s=t,h=f)}t=t.next}while(t!==a);return s}function Hi(o,e){return F(o.prev,o,e.prev)<0&&F(e.next,o,o.next)<0}function Wi(o,e,t,i){let n=o;do n.z===0&&(n.z=ge(n.x,n.y,e,t,i)),n.prevZ=n.prev,n.nextZ=n.next,n=n.next;while(n!==o);n.prevZ.nextZ=null,n.prevZ=null,ji(n)}function ji(o){let e=1,t;do{let i=o;o=null;let n=null;for(t=0;i;){t++;let r=i,s=0;for(let l=0;l<e&&(s++,r=r.nextZ,!!r);l++);let a=e;for(;s>0||a>0&&r;){let l;s!==0&&(a===0||!r||i.z<=r.z)?(l=i,i=i.nextZ,s--):(l=r,r=r.nextZ,a--),n?n.nextZ=l:o=l,l.prevZ=n,n=l}i=r}n.nextZ=null,e*=2}while(t>1);return o}function ge(o,e,t,i,n){let r=(o-t)*n|0,s=(e-i)*n|0;return r=(r|r<<8)&16711935,r=(r|r<<4)&252645135,r=(r|r<<2)&858993459,r=(r|r<<1)&1431655765,s=(s|s<<8)&16711935,s=(s|s<<4)&252645135,s=(s|s<<2)&858993459,s=(s|s<<1)&1431655765,r|s<<1}function zi(o){let e=o,t=o;do(e.x<t.x||e.x===t.x&&e.y<t.y)&&(t=e),e=e.next;while(e!==o);return t}function z(o,e,t,i,n,r,s,a){return(n-s)*(e-a)-(o-s)*(r-a)>=0&&(o-s)*(i-a)-(t-s)*(e-a)>=0&&(t-s)*(r-a)-(n-s)*(i-a)>=0}function qi(o,e){return o.next.i!==e.i&&o.prev.i!==e.i&&!Yi(o,e)&&(ee(o,e)&&ee(e,o)&&Zi(o,e)&&(F(o.prev,o,e.prev)!==0||F(o,e.prev,e)!==0)||ae(o,e)&&F(o.prev,o,o.next)>0&&F(e.prev,e,e.next)>0)}function F(o,e,t){return(e.y-o.y)*(t.x-e.x)-(e.x-o.x)*(t.y-e.y)}function ae(o,e){return o.x===e.x&&o.y===e.y}function Ye(o,e,t,i){const n=ue(F(o,e,t)),r=ue(F(o,e,i)),s=ue(F(t,i,o)),a=ue(F(t,i,e));return!!(n!==r&&s!==a||n===0&&le(o,t,e)||r===0&&le(o,i,e)||s===0&&le(t,o,i)||a===0&&le(t,e,i))}function le(o,e,t){return e.x<=Math.max(o.x,t.x)&&e.x>=Math.min(o.x,t.x)&&e.y<=Math.max(o.y,t.y)&&e.y>=Math.min(o.y,t.y)}function ue(o){return o>0?1:o<0?-1:0}function Yi(o,e){let t=o;do{if(t.i!==o.i&&t.next.i!==o.i&&t.i!==e.i&&t.next.i!==e.i&&Ye(t,t.next,o,e))return!0;t=t.next}while(t!==o);return!1}function ee(o,e){return F(o.prev,o,o.next)<0?F(o,e,o.next)>=0&&F(o,o.prev,e)>=0:F(o,e,o.prev)<0||F(o,o.next,e)<0}function Zi(o,e){let t=o,i=!1;const n=(o.x+e.x)/2,r=(o.y+e.y)/2;do t.y>r!=t.next.y>r&&t.next.y!==t.y&&n<(t.next.x-t.x)*(r-t.y)/(t.next.y-t.y)+t.x&&(i=!i),t=t.next;while(t!==o);return i}function Ze(o,e){const t=ve(o.i,o.x,o.y),i=ve(e.i,e.x,e.y),n=o.next,r=e.prev;return o.next=e,e.prev=o,t.next=n,n.prev=t,i.next=t,t.prev=i,r.next=i,i.prev=r,i}function Je(o,e,t,i){const n=ve(o,e,t);return i?(n.next=i.next,n.prev=i,i.next.prev=n,i.next=n):(n.prev=n,n.next=n),n}function te(o){o.next.prev=o.prev,o.prev.next=o.next,o.prevZ&&(o.prevZ.nextZ=o.nextZ),o.nextZ&&(o.nextZ.prevZ=o.prevZ)}function ve(o,e,t){return{i:o,x:e,y:t,prev:null,next:null,z:0,prevZ:null,nextZ:null,steiner:!1}}function Ji(o,e,t,i){let n=0;for(let r=e,s=t-i;r<t;r+=i)n+=(o[s]-o[r])*(o[r+1]+o[s+1]),s=r;return n}const T={parallaxX:.4,parallaxY:.8,parallaxMax:30,overscan:.06,pomSteps:16,rimIntensity:.6,rimColor:"#ffffff",rimWidth:.025,refractionStrength:.015,chromaticStrength:.008,occlusionIntensity:.4,depthPower:.7,depthScale:1.2,depthBias:-.05,fogDensity:.15,fogColor:"#1a1a2e",colorShift:.6,brightnessBias:.05,contrastLow:.02,contrastHigh:.98,verticalReduction:.5,dofStart:.5,dofStrength:.5,bevelIntensity:.5,bevelWidth:.04,bevelDarkening:.2,bevelDesaturation:.12,bevelLightAngle:135,edgeThickness:.01,edgeSpecular:.35,edgeColor:"#a0a0a0",chamferWidth:.025,chamferAngle:45,chamferColor:"#262630",chamferAmbient:.12,chamferSpecular:.3,chamferShininess:24,edgeOcclusionWidth:.03,edgeOcclusionStrength:.2,lightDirection:"-0.5,0.7,-0.3",autoplay:!0,loop:!0,muted:!0};class xe{constructor(e,t=.08,i=.06){this.host=e,this.lerpFactor=t,this.motionLerpFactor=i,this.host.addEventListener("mousemove",this.handleMouseMove),this.host.addEventListener("mouseleave",this.resetPointerTarget),this.host.addEventListener("touchstart",this.handleTouchStart,{passive:!0}),this.host.addEventListener("touchmove",this.handleTouchMove,{passive:!0}),this.host.addEventListener("touchend",this.handleTouchEnd,{passive:!0}),this.host.addEventListener("touchcancel",this.handleTouchEnd,{passive:!0})}pointerTarget={x:0,y:0};motionTarget={x:0,y:0};smoothedOutput={x:0,y:0};usingMotionInput=!1;motionListenerAttached=!1;motionRequested=!1;touchActive=!1;touchAnchorX=0;touchAnchorY=0;lerpFactor;motionLerpFactor;static TOUCH_DRAG_RANGE=100;update(){const e=this.touchActive?this.pointerTarget:this.usingMotionInput?this.motionTarget:this.pointerTarget,t=this.usingMotionInput&&!this.touchActive?this.motionLerpFactor:this.lerpFactor;return this.smoothedOutput.x=ce(this.smoothedOutput.x,e.x,t),this.smoothedOutput.y=ce(this.smoothedOutput.y,e.y,t),this.smoothedOutput}dispose(){this.host.removeEventListener("mousemove",this.handleMouseMove),this.host.removeEventListener("mouseleave",this.resetPointerTarget),this.host.removeEventListener("touchstart",this.handleTouchStart),this.host.removeEventListener("touchmove",this.handleTouchMove),this.host.removeEventListener("touchend",this.handleTouchEnd),this.host.removeEventListener("touchcancel",this.handleTouchEnd),this.motionListenerAttached&&(window.removeEventListener("deviceorientation",this.handleDeviceOrientation),this.motionListenerAttached=!1)}handleMouseMove=e=>{const t=this.host.getBoundingClientRect(),i=(e.clientX-t.left)/t.width*2-1,n=(e.clientY-t.top)/t.height*2-1;this.pointerTarget.x=q(i,-1,1),this.pointerTarget.y=q(n,-1,1)};resetPointerTarget=()=>{this.pointerTarget.x=0,this.pointerTarget.y=0};handleTouchStart=e=>{const t=e.touches[0];t&&(this.touchActive=!0,this.touchAnchorX=t.clientX,this.touchAnchorY=t.clientY,this.pointerTarget.x=0,this.pointerTarget.y=0,this.motionRequested||(this.motionRequested=!0,this.requestMotionPermission()))};handleTouchMove=e=>{const t=e.touches[0];if(!t)return;const i=t.clientX-this.touchAnchorX,n=t.clientY-this.touchAnchorY,r=xe.TOUCH_DRAG_RANGE;this.pointerTarget.x=q(i/r,-1,1),this.pointerTarget.y=q(n/r,-1,1)};handleTouchEnd=()=>{this.touchActive=!1,this.pointerTarget.x=0,this.pointerTarget.y=0};async requestMotionPermission(){if(typeof DeviceOrientationEvent>"u")return;const e=DeviceOrientationEvent;if(typeof e.requestPermission=="function")try{if(await e.requestPermission()!=="granted")return}catch{return}this.motionListenerAttached||(window.addEventListener("deviceorientation",this.handleDeviceOrientation),this.motionListenerAttached=!0),this.usingMotionInput=!0}handleDeviceOrientation=e=>{const t=q((e.gamma??0)/45,-1,1),i=q((e.beta??0)/45,-1,1);this.motionTarget.x=ce(this.motionTarget.x,t,this.motionLerpFactor),this.motionTarget.y=ce(this.motionTarget.y,i,this.motionLerpFactor)}}class he extends HTMLElement{static TAG_NAME="layershift-portal";static get observedAttributes(){return["src","depth-src","depth-meta","logo-src","parallax-x","parallax-y","parallax-max","overscan","pom-steps","quality","gpu-backend","rim-intensity","rim-color","rim-width","refraction-strength","chromatic-strength","occlusion-intensity","depth-power","depth-scale","depth-bias","fog-density","fog-color","color-shift","brightness-bias","contrast-low","contrast-high","vertical-reduction","dof-start","dof-strength","bevel-intensity","bevel-width","bevel-darkening","bevel-desaturation","bevel-light-angle","edge-thickness","edge-specular","edge-color","chamfer-width","chamfer-angle","chamfer-color","chamfer-ambient","chamfer-specular","chamfer-shininess","edge-occlusion-width","edge-occlusion-strength","light-direction","autoplay","loop","muted"]}reinitAttributes=["src","depth-src","depth-meta","logo-src"];shadow;container=null;renderer=null;inputHandler=null;video=null;mesh=null;loopCount=0;lifecycle;constructor(){super(),this.shadow=this.attachShadow({mode:"open"}),this.lifecycle=new _e(this)}getAttrFloat(e,t){const i=this.getAttribute(e);if(i===null)return t;const n=parseFloat(i);return Number.isFinite(n)?n:t}getAttrBool(e,t){if(!this.hasAttribute(e))return t;const i=this.getAttribute(e);return!(i==="false"||i==="0")}getAttrColor(e,t){const i=this.getAttribute(e)??t;return $i(i)}getAttrVec3(e,t){const n=(this.getAttribute(e)??t).split(",").map(s=>parseFloat(s.trim()));if(n.length>=3&&n.every(Number.isFinite))return[n[0],n[1],n[2]];const r=t.split(",").map(s=>parseFloat(s.trim()));return[r[0],r[1],r[2]]}get parallaxX(){return this.getAttrFloat("parallax-x",T.parallaxX)}get parallaxY(){return this.getAttrFloat("parallax-y",T.parallaxY)}get parallaxMax(){return this.getAttrFloat("parallax-max",T.parallaxMax)}get overscan(){return this.getAttrFloat("overscan",T.overscan)}get pomSteps(){return this.getAttrFloat("pom-steps",T.pomSteps)}get quality(){const e=this.getAttribute("quality");if(e==="auto"||e==="high"||e==="medium"||e==="low")return e}get gpuBackend(){const e=this.getAttribute("gpu-backend");return e==="webgpu"||e==="webgl2"?e:"auto"}get rimIntensity(){return this.getAttrFloat("rim-intensity",T.rimIntensity)}get rimWidth(){return this.getAttrFloat("rim-width",T.rimWidth)}get rimColor(){return this.getAttrColor("rim-color",T.rimColor)}get refractionStrength(){return this.getAttrFloat("refraction-strength",T.refractionStrength)}get chromaticStrength(){return this.getAttrFloat("chromatic-strength",T.chromaticStrength)}get occlusionIntensity(){return this.getAttrFloat("occlusion-intensity",T.occlusionIntensity)}get depthPower(){return this.getAttrFloat("depth-power",T.depthPower)}get depthScale(){return this.getAttrFloat("depth-scale",T.depthScale)}get depthBias(){return this.getAttrFloat("depth-bias",T.depthBias)}get fogDensity(){return this.getAttrFloat("fog-density",T.fogDensity)}get fogColor(){return this.getAttrColor("fog-color",T.fogColor)}get colorShift(){return this.getAttrFloat("color-shift",T.colorShift)}get brightnessBias(){return this.getAttrFloat("brightness-bias",T.brightnessBias)}get contrastLow(){return this.getAttrFloat("contrast-low",T.contrastLow)}get contrastHigh(){return this.getAttrFloat("contrast-high",T.contrastHigh)}get verticalReduction(){return this.getAttrFloat("vertical-reduction",T.verticalReduction)}get dofStart(){return this.getAttrFloat("dof-start",T.dofStart)}get dofStrength(){return this.getAttrFloat("dof-strength",T.dofStrength)}get bevelIntensity(){return this.getAttrFloat("bevel-intensity",T.bevelIntensity)}get bevelWidth(){return this.getAttrFloat("bevel-width",T.bevelWidth)}get bevelDarkening(){return this.getAttrFloat("bevel-darkening",T.bevelDarkening)}get bevelDesaturation(){return this.getAttrFloat("bevel-desaturation",T.bevelDesaturation)}get bevelLightAngle(){return this.getAttrFloat("bevel-light-angle",T.bevelLightAngle)}get edgeThickness(){return this.getAttrFloat("edge-thickness",T.edgeThickness)}get edgeSpecular(){return this.getAttrFloat("edge-specular",T.edgeSpecular)}get edgeColor(){return this.getAttrColor("edge-color",T.edgeColor)}get chamferWidth(){return this.getAttrFloat("chamfer-width",T.chamferWidth)}get chamferAngle(){return this.getAttrFloat("chamfer-angle",T.chamferAngle)}get chamferColor(){return this.getAttrColor("chamfer-color",T.chamferColor)}get chamferAmbient(){return this.getAttrFloat("chamfer-ambient",T.chamferAmbient)}get chamferSpecular(){return this.getAttrFloat("chamfer-specular",T.chamferSpecular)}get chamferShininess(){return this.getAttrFloat("chamfer-shininess",T.chamferShininess)}get edgeOcclusionWidth(){return this.getAttrFloat("edge-occlusion-width",T.edgeOcclusionWidth)}get edgeOcclusionStrength(){return this.getAttrFloat("edge-occlusion-strength",T.edgeOcclusionStrength)}get lightDirection3(){return this.getAttrVec3("light-direction",T.lightDirection)}get shouldAutoplay(){return this.getAttrBool("autoplay",T.autoplay)}get shouldLoop(){return this.getAttrBool("loop",T.loop)}get shouldMute(){return this.getAttrBool("muted",T.muted)}emit(e,t){this.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}attachVideoEventListeners(e){e.addEventListener("play",()=>{this.emit("layershift-portal:play",{currentTime:e.currentTime})}),e.addEventListener("pause",()=>{this.emit("layershift-portal:pause",{currentTime:e.currentTime})}),e.addEventListener("ended",()=>{e.loop&&(this.loopCount+=1,this.emit("layershift-portal:loop",{loopCount:this.loopCount}))})}connectedCallback(){this.lifecycle.onConnected()}disconnectedCallback(){this.lifecycle.onDisconnected()}attributeChangedCallback(e,t,i){this.lifecycle.onAttributeChanged(e,t,i)}setupShadowDOM(){this.shadow.innerHTML="";const e=document.createElement("style");e.textContent=`
|
|
724
1853
|
:host {
|
|
725
1854
|
display: block;
|
|
726
1855
|
width: 100%;
|
|
@@ -740,4 +1869,4 @@ ${r}`)}return n.detachShader(i,t),n.detachShader(i,e),n.deleteShader(t),n.delete
|
|
|
740
1869
|
width: 100%;
|
|
741
1870
|
height: 100%;
|
|
742
1871
|
}
|
|
743
|
-
`,this.shadow.appendChild(
|
|
1872
|
+
`,this.shadow.appendChild(e),this.container=document.createElement("div"),this.container.className="container",this.shadow.appendChild(this.container)}async doInit(e){const t=this.getAttribute("src"),i=this.getAttribute("depth-src"),n=this.getAttribute("depth-meta"),r=this.getAttribute("logo-src");if(this.container)try{const[s,a,l]=await Promise.all([this.createVideoElement(t),Se(i,n),Pi(r)]);if(e.aborted){s.remove();return}this.video=s,this.mesh=l,this.loopCount=0,this.attachVideoEventListeners(s);const u=this.parallaxMax/Math.max(s.videoWidth,1),h=new be(a),f=A=>h.sample(A),c={parallaxStrength:u,overscanPadding:this.overscan,pomSteps:this.pomSteps,quality:this.quality,rimLightIntensity:this.rimIntensity,rimLightColor:this.rimColor,rimLightWidth:this.rimWidth,refractionStrength:this.refractionStrength,chromaticStrength:this.chromaticStrength,occlusionIntensity:this.occlusionIntensity,depthPower:this.depthPower,depthScale:this.depthScale,depthBias:this.depthBias,fogDensity:this.fogDensity,fogColor:this.fogColor,colorShift:this.colorShift,brightnessBias:this.brightnessBias,contrastLow:this.contrastLow,contrastHigh:this.contrastHigh,verticalReduction:this.verticalReduction,dofStart:this.dofStart,dofStrength:this.dofStrength,bevelIntensity:this.bevelIntensity,bevelWidth:this.bevelWidth,bevelDarkening:this.bevelDarkening,bevelDesaturation:this.bevelDesaturation,bevelLightAngle:this.bevelLightAngle,edgeThickness:this.edgeThickness,edgeSpecular:this.edgeSpecular,edgeColor:this.edgeColor,chamferWidth:this.chamferWidth,chamferAngle:this.chamferAngle,chamferColor:this.chamferColor,chamferAmbient:this.chamferAmbient,chamferSpecular:this.chamferSpecular,chamferShininess:this.chamferShininess,edgeOcclusionWidth:this.edgeOcclusionWidth,edgeOcclusionStrength:this.edgeOcclusionStrength,lightDirection:this.lightDirection3},g=await Ce(this.gpuBackend);if(e.aborted)return;g.type==="webgpu"&&g.device&&g.adapter?this.renderer=new Ei(this.container,c,g.device,g.adapter.info):this.renderer=new si(this.container,c),this.renderer.initialize(s,a.meta.width,a.meta.height,l),this.inputHandler=new xe(this);const v=this.parallaxX,d=this.parallaxY;if(this.renderer.start(s,f,()=>{if(!this.inputHandler)return{x:0,y:0};const A=this.inputHandler.update();return{x:A.x*v,y:A.y*d}},(A,b)=>{this.emit("layershift-portal:frame",{currentTime:A,frameNumber:b})}),this.shouldAutoplay){s.currentTime=0;try{await s.play()}catch{}}if(e.aborted)return;this.lifecycle.markInitialized(),this.emit("layershift-portal:ready",{videoWidth:s.videoWidth,videoHeight:s.videoHeight,duration:s.duration})}catch(s){const a=s instanceof Error?s.message:"Failed to initialize.";console.error("<layershift-portal>: Failed to initialize.",s),this.emit("layershift-portal:error",{message:a})}}async createVideoElement(e){const t=document.createElement("video");return t.crossOrigin="anonymous",t.setAttribute("crossorigin","anonymous"),t.playsInline=!0,t.setAttribute("playsinline",""),t.setAttribute("webkit-playsinline","true"),t.muted=this.shouldMute,t.defaultMuted=this.shouldMute,this.shouldMute&&t.setAttribute("muted",""),t.loop=this.shouldLoop,t.preload="auto",t.style.display="none",t.src=e,this.shadow.appendChild(t),await new Promise((i,n)=>{if(t.readyState>=HTMLMediaElement.HAVE_METADATA){i();return}const r=()=>{a(),i()},s=()=>{a(),n(new Error("Failed to load video metadata."))},a=()=>{t.removeEventListener("loadedmetadata",r),t.removeEventListener("error",s)};t.addEventListener("loadedmetadata",r),t.addEventListener("error",s),t.load()}),t}doDispose(){this.renderer?.dispose(),this.renderer=null,this.inputHandler?.dispose(),this.inputHandler=null,this.video&&(this.video.pause(),this.video.removeAttribute("src"),this.video.load(),this.video.remove(),this.video=null),this.mesh=null,this.loopCount=0,this.container=null}}function q(o,e,t){return Math.min(t,Math.max(e,o))}function ce(o,e,t){return o+(e-o)*t}function $i(o){const e=o.replace("#","");if(e.length===3){const t=parseInt(e[0]+e[0],16)/255,i=parseInt(e[1]+e[1],16)/255,n=parseInt(e[2]+e[2],16)/255;return[t,i,n]}if(e.length===6){const t=parseInt(e.substring(0,2),16)/255,i=parseInt(e.substring(2,4),16)/255,n=parseInt(e.substring(4,6),16)/255;return[t,i,n]}return[0,0,0]}return customElements.get(re.TAG_NAME)||customElements.define(re.TAG_NAME,re),customElements.get(he.TAG_NAME)||customElements.define(he.TAG_NAME,he),ne.LayershiftElement=re,ne.LayershiftPortalElement=he,Object.defineProperty(ne,Symbol.toStringTag,{value:"Module"}),ne})({});
|