@liquid-glass-forge/webgl 0.1.0-rc.1 → 0.1.0-rc.2

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 CHANGED
@@ -1,8 +1,23 @@
1
- # Liquid Glass Forge WebGL
1
+ # @liquid-glass-forge/webgl
2
2
 
3
- Experimental preview. API and device/performance qualification remain open; iPad is unqualified.
3
+ WebGL2 glass refraction and a shared atlas for compositing optical surfaces.
4
4
 
5
- Experimental preview package, `0.1.0-rc.1`. Shared persistent WebGL2 atlas,
5
+ [![A live glass lens bends the lettering beneath it](https://raw.githubusercontent.com/namipsg/liquid-glass-forge/main/docs/assets/glass-playground.gif)](https://namipsg.github.io/liquid-glass-forge/#playground)
6
+
7
+ [Live showcase](https://namipsg.github.io/liquid-glass-forge/#playground) · [Examples](https://namipsg.github.io/liquid-glass-forge/examples.html) · [Documentation](https://github.com/namipsg/liquid-glass-forge/blob/main/docs/PHASE-6.md) · [Support](https://namipsg.github.io/liquid-glass-forge/support.html) · [Install by framework](https://github.com/namipsg/liquid-glass-forge/blob/main/docs/QUICKSTART.md)
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @liquid-glass-forge/webgl@0.1.0-rc.2
13
+ ```
14
+
15
+ Keep the Forge packages at matching versions. This is an experimental preview.
16
+ The [support guide](https://namipsg.github.io/liquid-glass-forge/support.html) describes browser and device qualification.
17
+
18
+ ## Usage
19
+
20
+ Experimental preview package, `0.1.0-rc.2`. Shared persistent WebGL2 atlas,
6
21
  versioned optics, GPU blur and synchronous Canvas2D tile presentation.
7
22
 
8
23
  Use `WebGLAtlas` from `@liquid-glass-forge/webgl` for owned Canvas2D targets.
package/dist/atlas.js CHANGED
@@ -257,7 +257,7 @@ export class WebGLAtlas {
257
257
  throw new WebGLError('ATTRIBUTES_UNSUPPORTED', 'Required context attributes not granted');
258
258
  this.#maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
259
259
  this.#maxViewport = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
260
- this.#optics = this.#program(OPTICS, ['Source', 'Encoding', 'Size', 'Origin', 'Rect', 'Radius', 'Bevel', 'Strength', 'Prepared', 'QuantizeUp', 'SourceLinear', 'Calibration', 'Dispersion', 'Tint', 'Light', 'Rim', 'Specular']);
260
+ this.#optics = this.#program(OPTICS, ['Source', 'Encoding', 'Size', 'Origin', 'Rect', 'Radius', 'Bevel', 'Strength', 'Prepared', 'QuantizeUp', 'SourceLinear', 'Calibration', 'Dispersion', 'Tint', 'Light', 'Rim', 'Specular', 'LensProfile']);
261
261
  this.#blur = this.#program(BLUR, ['Source', 'Encoding', 'Axis', 'Radius', 'Prepared', 'SourceLinear']);
262
262
  this.#source = this.#texture();
263
263
  this.#encoding = this.#texture();
@@ -541,6 +541,7 @@ export class WebGLAtlas {
541
541
  gl.uniform1i(u.SourceLinear, this.#sourceLinear ? 1 : 0);
542
542
  gl.uniform1i(u.Prepared, e.blur ? 1 : 0);
543
543
  gl.uniform1i(u.QuantizeUp, this.#quantizeUp ? 1 : 0);
544
+ gl.uniform1i(u.LensProfile, e.lens ? 1 : 0);
544
545
  gl.uniform1f(u.Dispersion, e.dispersion);
545
546
  gl.uniform4f(u.Tint, ...e.tint);
546
547
  gl.uniform2f(u.Light, ...e.light);
package/dist/dom.d.ts CHANGED
@@ -111,13 +111,15 @@ export declare class OpticalRenderer implements GlassRenderer {
111
111
  effectiveHeight: number | undefined;
112
112
  qualityReduced: boolean;
113
113
  reasons: readonly string[];
114
- quality: Required<{
114
+ quality: Required<Omit<{
115
115
  readonly maxPixels?: number;
116
116
  readonly maxWidth?: number;
117
117
  readonly maxHeight?: number;
118
118
  readonly maxUpdatesPerSecond?: number;
119
119
  readonly maxSourcePixels?: number;
120
- }>;
120
+ readonly cadence?: 'balanced' | 'interactive';
121
+ }, "cadence">>;
122
+ cadencePolicy?: "interactive";
121
123
  timings: (import("@liquid-glass-forge/cpu").CpuTimings & {
122
124
  readonly captureMs: number;
123
125
  readonly rateWaitMs: number;
package/dist/effects.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export interface WebGLEffects {
2
- readonly model: 'rounded-material-v1';
2
+ readonly model: 'rounded-material-v1' | 'refractive-lens-v2';
3
3
  readonly blurRadius?: number;
4
4
  readonly dispersion?: number;
5
5
  readonly tint?: readonly [number, number, number, number];
@@ -8,6 +8,7 @@ export interface WebGLEffects {
8
8
  readonly light?: readonly [number, number];
9
9
  }
10
10
  export declare function effects(input?: WebGLEffects): Readonly<{
11
+ lens: boolean;
11
12
  blur: number;
12
13
  dispersion: number;
13
14
  rim: number;
package/dist/effects.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { decodeSrgb, ContractError } from '@liquid-glass-forge/core';
2
2
  export function effects(input) {
3
3
  if (input !== undefined && (!input || typeof input !== 'object' || Array.isArray(input) ||
4
- Reflect.ownKeys(input).some(k => typeof k !== 'string' || !['model', 'blurRadius', 'dispersion', 'tint', 'rim', 'specular', 'light'].includes(k)) || input.model !== 'rounded-material-v1'))
4
+ Reflect.ownKeys(input).some(k => typeof k !== 'string' || !['model', 'blurRadius', 'dispersion', 'tint', 'rim', 'specular', 'light'].includes(k)) || !['rounded-material-v1', 'refractive-lens-v2'].includes(input.model)))
5
5
  throw new ContractError('Invalid WebGL material');
6
6
  const blur = input?.blurRadius === undefined ? 0 : input.blurRadius, dispersion = input?.dispersion === undefined ? 0 : input.dispersion, rim = input?.rim === undefined ? 0 : input.rim, specular = input?.specular === undefined ? 0 : input.specular;
7
7
  if (!Number.isInteger(blur) || blur < 0 || blur > 16 || ![dispersion, rim, specular].every(v => typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= 1))
@@ -13,5 +13,5 @@ export function effects(input) {
13
13
  if (!scale)
14
14
  throw new ContractError('Light must be nonzero');
15
15
  const x = light[0] / scale, y = light[1] / scale, length = Math.hypot(x, y);
16
- return Object.freeze({ blur, dispersion, rim, specular, tint: [decodeSrgb(tint[0]), decodeSrgb(tint[1]), decodeSrgb(tint[2]), tint[3]], light: [x / length, y / length] });
16
+ return Object.freeze({ lens: input?.model === 'refractive-lens-v2', blur, dispersion, rim, specular, tint: [decodeSrgb(tint[0]), decodeSrgb(tint[1]), decodeSrgb(tint[2]), tint[3]], light: [x / length, y / length] });
17
17
  }
package/dist/shaders.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export declare const VERTEX = "#version 300 es\nvoid main(){vec2 p=vec2(float((gl_VertexID<<1)&2),float(gl_VertexID&2));gl_Position=vec4(p*2.0-1.0,0,1);}";
2
- export declare const OPTICS = "#version 300 es\nprecision highp float;\nprecision highp int;\nuniform highp sampler2D uSource;\nuniform highp sampler2D uEncoding;\nuniform vec2 uSize,uOrigin,uLight;\nuniform vec4 uRect,uTint;\nuniform float uRadius,uBevel,uStrength,uDispersion,uRim,uSpecular;\nuniform bool uPrepared,uQuantizeUp,uSourceLinear,uCalibration;\nout vec4 result;\nfloat encodeByte(float v){int i=min(int(v*4096.0),4095);vec3 e=texelFetch(uEncoding,ivec2(i&63,i>>6),0).rgb;return e.r+(v>=e.b?1.0:(v<e.g?-1.0:0.0));}\nfloat decodeByte(float v){return v<=.04045?v/12.92:pow((v+.055)/1.055,2.4);}\nvec4 texel(ivec2 p){vec4 c=texelFetch(uSource,p,0);return uPrepared?c:(uSourceLinear?vec4(c.rgb*c.a,c.a):vec4(vec3(decodeByte(c.r),decodeByte(c.g),decodeByte(c.b))*c.a,c.a));}\nvec4 sampleAt(vec2 p){ivec2 s=textureSize(uSource,0);p=clamp(p,vec2(0.0),vec2(float(s.x-1),float(s.y-1)));ivec2 lo=ivec2(floor(p)),hi=min(lo+1,s-1);vec2 t=fract(p);\nif(t.x==0.0&&t.y==0.0)return texel(lo);\nreturn (texel(lo)*(1.0-t.x)+texel(ivec2(hi.x,lo.y))*t.x)*(1.0-t.y)+(texel(ivec2(lo.x,hi.y))*(1.0-t.x)+texel(hi)*t.x)*t.y;}\nvoid main(){\nif(uCalibration){vec4 c=texelFetch(uSource,ivec2(int(gl_FragCoord.x),0),0);float n=floor(c.r*65535.0+.5);result=vec4(floor(n/256.0)/255.0,mod(n,256.0)/255.0,0,1);return;}\nvec2 local=gl_FragCoord.xy-uOrigin,center=vec2(local.x,uSize.y-local.y),p=center-uSize*.5;\nvec2 q=abs(p)-(uSize*.5-uRadius),a=max(q,0.0);float len=length(a),d=len+min(max(q.x,q.y),0.0)-uRadius;\nfloat mask=clamp(.5-d,0.0,1.0);if(mask==0.0){result=vec4(0.0);return;}\nfloat w=clamp(1.0+d/uBevel,0.0,1.0);w*=w;\nvec2 n=w>0.0?(len>0.0?a/len*sign(p):(q.x>q.y?vec2(sign(p.x),0.0):vec2(0.0,sign(p.y)))):vec2(0.0);\nvec2 scale=uRect.zw/uSize,base=uRect.xy+center*scale-.5,delta=n*uStrength*w*scale;\n// An exact source texel with all optional passes disabled is byte identity.\n// Avoid decode/interpolate/encode work only where bilinear weights are exactly zero.\nif(!uPrepared&&!uSourceLinear&&uDispersion==0.0&&uTint.a==0.0&&uRim==0.0&&uSpecular==0.0){\n vec2 at=clamp(base+delta,vec2(0.0),vec2(float(textureSize(uSource,0).x-1),float(textureSize(uSource,0).y-1)));\n if(all(equal(fract(at),vec2(0.0)))){vec4 raw=texelFetch(uSource,ivec2(at),0);float alpha=floor(raw.a*mask*255.0+.5);ivec3 bytes=ivec3(floor(raw.rgb*255.0+.5));ivec3 premult=(bytes*int(alpha)+(uQuantizeUp?254:127))/255;result=alpha==0.0?vec4(0.0):vec4(vec3(premult)/255.0,alpha/255.0);return;}\n}\nvec4 c=sampleAt(base+delta);float alpha=floor(c.a*mask*255.0+.5);\nif(alpha==0.0){result=vec4(0.0);return;}\nvec3 rgb=c.a>0.0?c.rgb/c.a:vec3(0.0);\nif(uDispersion>0.0){vec4 r=sampleAt(base+delta*(1.0+uDispersion)),b=sampleAt(base+delta*(1.0-uDispersion));rgb.r=r.a>0.0?r.r/r.a:0.0;rgb.b=b.a>0.0?b.b/b.a:0.0;}\nif(uTint.a>0.0)rgb=mix(rgb,uTint.rgb,uTint.a);\nif(w>0.0&&(uRim>0.0||uSpecular>0.0)){float l=max(dot(n,uLight),0.0);l*=l;l*=l;l*=l;float h=clamp(w*(uRim+uSpecular*l),0.0,1.0);rgb=clamp(rgb+(1.0-rgb)*h,0.0,1.0);}\nivec3 bytes=ivec3(encodeByte(rgb.r),encodeByte(rgb.g),encodeByte(rgb.b));\nivec3 premult=(bytes*int(alpha)+(uQuantizeUp?254:127))/255;\nresult=vec4(vec3(premult)/255.0,alpha/255.0);\n}";
2
+ export declare const OPTICS = "#version 300 es\nprecision highp float;\nprecision highp int;\nuniform highp sampler2D uSource;\nuniform highp sampler2D uEncoding;\nuniform vec2 uSize,uOrigin,uLight;\nuniform vec4 uRect,uTint;\nuniform float uRadius,uBevel,uStrength,uDispersion,uRim,uSpecular;\nuniform bool uPrepared,uQuantizeUp,uSourceLinear,uCalibration,uLensProfile;\nout vec4 result;\nfloat encodeByte(float v){int i=min(int(v*4096.0),4095);vec3 e=texelFetch(uEncoding,ivec2(i&63,i>>6),0).rgb;return e.r+(v>=e.b?1.0:(v<e.g?-1.0:0.0));}\nfloat decodeByte(float v){return v<=.04045?v/12.92:pow((v+.055)/1.055,2.4);}\nvec4 texel(ivec2 p){vec4 c=texelFetch(uSource,p,0);return uPrepared?c:(uSourceLinear?vec4(c.rgb*c.a,c.a):vec4(vec3(decodeByte(c.r),decodeByte(c.g),decodeByte(c.b))*c.a,c.a));}\nvec4 sampleAt(vec2 p){ivec2 s=textureSize(uSource,0);p=clamp(p,vec2(0.0),vec2(float(s.x-1),float(s.y-1)));ivec2 lo=ivec2(floor(p)),hi=min(lo+1,s-1);vec2 t=fract(p);\nif(t.x==0.0&&t.y==0.0)return texel(lo);\nreturn (texel(lo)*(1.0-t.x)+texel(ivec2(hi.x,lo.y))*t.x)*(1.0-t.y)+(texel(ivec2(lo.x,hi.y))*(1.0-t.x)+texel(hi)*t.x)*t.y;}\nvoid main(){\nif(uCalibration){vec4 c=texelFetch(uSource,ivec2(int(gl_FragCoord.x),0),0);float n=floor(c.r*65535.0+.5);result=vec4(floor(n/256.0)/255.0,mod(n,256.0)/255.0,0,1);return;}\nvec2 local=gl_FragCoord.xy-uOrigin,center=vec2(local.x,uSize.y-local.y),p=center-uSize*.5;\nvec2 q=abs(p)-(uSize*.5-uRadius),a=max(q,0.0);float len=length(a),d=len+min(max(q.x,q.y),0.0)-uRadius;\nfloat mask=clamp(.5-d,0.0,1.0);if(mask==0.0){result=vec4(0.0);return;}\n// Keep v2's bevel outside the medial axis at small corner radii. V1 is unchanged.\nfloat bevel=uLensProfile?min(uBevel,max(uRadius,.5)):uBevel;\nfloat w=clamp(1.0+d/bevel,0.0,1.0);w*=w;\nvec2 n=w>0.0?(len>0.0?a/len*sign(p):(q.x>q.y?vec2(sign(p.x),0.0):vec2(0.0,sign(p.y)))):vec2(0.0);\nfloat displacement=w;\nif(uLensProfile&&w>0.0){float u=sqrt(w),nz=sqrt(max(0.0,1.0-u*u)),eta=2.0/3.0;\nfloat bend=sqrt(1.0-eta*eta*u*u)-eta*nz;displacement=-u*bend/(eta+bend*nz);}\nvec2 scale=uRect.zw/uSize,base=uRect.xy+center*scale-.5,delta=n*uStrength*displacement*scale;\n// An exact source texel with all optional passes disabled is byte identity.\n// Avoid decode/interpolate/encode work only where bilinear weights are exactly zero.\nif(!uPrepared&&!uSourceLinear&&uDispersion==0.0&&uTint.a==0.0&&uRim==0.0&&uSpecular==0.0){\n vec2 at=clamp(base+delta,vec2(0.0),vec2(float(textureSize(uSource,0).x-1),float(textureSize(uSource,0).y-1)));\n if(all(equal(fract(at),vec2(0.0)))){vec4 raw=texelFetch(uSource,ivec2(at),0);float alpha=floor(raw.a*mask*255.0+.5);ivec3 bytes=ivec3(floor(raw.rgb*255.0+.5));ivec3 premult=(bytes*int(alpha)+(uQuantizeUp?254:127))/255;result=alpha==0.0?vec4(0.0):vec4(vec3(premult)/255.0,alpha/255.0);return;}\n}\nvec4 c=sampleAt(base+delta);float alpha=floor(c.a*mask*255.0+.5);\nif(alpha==0.0){result=vec4(0.0);return;}\nvec3 rgb=c.a>0.0?c.rgb/c.a:vec3(0.0);\nif(uDispersion>0.0){vec4 r=sampleAt(base+delta*(1.0+uDispersion)),b=sampleAt(base+delta*(1.0-uDispersion));rgb.r=r.a>0.0?r.r/r.a:0.0;rgb.b=b.a>0.0?b.b/b.a:0.0;}\nif(uTint.a>0.0)rgb=mix(rgb,uTint.rgb,uTint.a);\nif(w>0.0&&(uRim>0.0||uSpecular>0.0)){float l=max(dot(n,uLight),0.0);l*=l;l*=l;l*=l;float h=clamp(w*(uRim+uSpecular*l),0.0,1.0);rgb=clamp(rgb+(1.0-rgb)*h,0.0,1.0);}\nivec3 bytes=ivec3(encodeByte(rgb.r),encodeByte(rgb.g),encodeByte(rgb.b));\nivec3 premult=(bytes*int(alpha)+(uQuantizeUp?254:127))/255;\nresult=vec4(vec3(premult)/255.0,alpha/255.0);\n}";
3
3
  export declare const BLUR = "#version 300 es\nprecision highp float;\nprecision highp int;\nuniform highp sampler2D uSource;\nuniform highp sampler2D uEncoding;\nuniform ivec2 uAxis;\nuniform int uRadius;\nuniform bool uPrepared,uSourceLinear;\nout vec4 result;\nfloat decodeByte(float v){return v<=.04045?v/12.92:pow((v+.055)/1.055,2.4);}\nvoid main(){ivec2 p=ivec2(gl_FragCoord.xy),size=textureSize(uSource,0);vec4 sum=vec4(0.0);\nfor(int k=-16;k<=16;k++){if(abs(k)<=uRadius){vec4 c=texelFetch(uSource,clamp(p+uAxis*k,ivec2(0),size-1),0);sum+=uPrepared?c:(uSourceLinear?vec4(c.rgb*c.a,c.a):vec4(vec3(decodeByte(c.r),decodeByte(c.g),decodeByte(c.b))*c.a,c.a));}}\nresult=sum/float(2*uRadius+1);\n}";
4
4
  export declare function encodingTextureData(): Float32Array;
package/dist/shaders.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export const VERTEX = `#version 300 es
2
2
  void main(){vec2 p=vec2(float((gl_VertexID<<1)&2),float(gl_VertexID&2));gl_Position=vec4(p*2.0-1.0,0,1);}`;
3
- // Same versioned equations as CONTRACTS.md and PHASE-5.md. The scalar oracle
3
+ // The v1 path retains CONTRACTS.md and PHASE-5.md; the opt-in v2 path
4
+ // traces an air-to-glass ray through an elliptical bevel. The scalar oracle
4
5
  // remains independent; production never obtains pixels from a GPU framebuffer.
5
6
  // Explicit float constructors avoid ANGLE/D3D11 generating an ambiguous HLSL
6
7
  // vec2_ctor_int overload for mixed integer and floating-point arguments.
@@ -12,7 +13,7 @@ uniform highp sampler2D uEncoding;
12
13
  uniform vec2 uSize,uOrigin,uLight;
13
14
  uniform vec4 uRect,uTint;
14
15
  uniform float uRadius,uBevel,uStrength,uDispersion,uRim,uSpecular;
15
- uniform bool uPrepared,uQuantizeUp,uSourceLinear,uCalibration;
16
+ uniform bool uPrepared,uQuantizeUp,uSourceLinear,uCalibration,uLensProfile;
16
17
  out vec4 result;
17
18
  float encodeByte(float v){int i=min(int(v*4096.0),4095);vec3 e=texelFetch(uEncoding,ivec2(i&63,i>>6),0).rgb;return e.r+(v>=e.b?1.0:(v<e.g?-1.0:0.0));}
18
19
  float decodeByte(float v){return v<=.04045?v/12.92:pow((v+.055)/1.055,2.4);}
@@ -25,9 +26,14 @@ if(uCalibration){vec4 c=texelFetch(uSource,ivec2(int(gl_FragCoord.x),0),0);float
25
26
  vec2 local=gl_FragCoord.xy-uOrigin,center=vec2(local.x,uSize.y-local.y),p=center-uSize*.5;
26
27
  vec2 q=abs(p)-(uSize*.5-uRadius),a=max(q,0.0);float len=length(a),d=len+min(max(q.x,q.y),0.0)-uRadius;
27
28
  float mask=clamp(.5-d,0.0,1.0);if(mask==0.0){result=vec4(0.0);return;}
28
- float w=clamp(1.0+d/uBevel,0.0,1.0);w*=w;
29
+ // Keep v2's bevel outside the medial axis at small corner radii. V1 is unchanged.
30
+ float bevel=uLensProfile?min(uBevel,max(uRadius,.5)):uBevel;
31
+ float w=clamp(1.0+d/bevel,0.0,1.0);w*=w;
29
32
  vec2 n=w>0.0?(len>0.0?a/len*sign(p):(q.x>q.y?vec2(sign(p.x),0.0):vec2(0.0,sign(p.y)))):vec2(0.0);
30
- vec2 scale=uRect.zw/uSize,base=uRect.xy+center*scale-.5,delta=n*uStrength*w*scale;
33
+ float displacement=w;
34
+ if(uLensProfile&&w>0.0){float u=sqrt(w),nz=sqrt(max(0.0,1.0-u*u)),eta=2.0/3.0;
35
+ float bend=sqrt(1.0-eta*eta*u*u)-eta*nz;displacement=-u*bend/(eta+bend*nz);}
36
+ vec2 scale=uRect.zw/uSize,base=uRect.xy+center*scale-.5,delta=n*uStrength*displacement*scale;
31
37
  // An exact source texel with all optional passes disabled is byte identity.
32
38
  // Avoid decode/interpolate/encode work only where bilinear weights are exactly zero.
33
39
  if(!uPrepared&&!uSourceLinear&&uDispersion==0.0&&uTint.a==0.0&&uRim==0.0&&uSpecular==0.0){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquid-glass-forge/webgl",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.2",
4
4
  "private": false,
5
5
  "description": "Experimental shared-atlas WebGL2 optics and optional vanilla integration",
6
6
  "type": "module",
@@ -28,10 +28,11 @@
28
28
  "README.md"
29
29
  ],
30
30
  "dependencies": {
31
- "@liquid-glass-forge/core": "0.1.0-rc.1",
32
- "@liquid-glass-forge/cpu": "0.1.0-rc.1",
33
- "@liquid-glass-forge/dom": "0.1.0-rc.1"
31
+ "@liquid-glass-forge/core": "0.1.0-rc.2",
32
+ "@liquid-glass-forge/cpu": "0.1.0-rc.2",
33
+ "@liquid-glass-forge/dom": "0.1.0-rc.2"
34
34
  },
35
+ "homepage": "https://namipsg.github.io/liquid-glass-forge/#playground",
35
36
  "repository": {
36
37
  "type": "git",
37
38
  "url": "git+https://github.com/namipsg/liquid-glass-forge.git",