@liquid-glass-forge/cpu 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
1
  # @liquid-glass-forge/cpu
2
2
 
3
- Experimental preview. API and device/performance qualification remain open; iPad is unqualified.
3
+ Bounded JavaScript glass rendering with optional worker execution.
4
4
 
5
- Experimental preview JavaScript CPU optics, version `0.1.0-rc.1`.
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-5.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/cpu@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 JavaScript CPU optics, version `0.1.0-rc.2`.
6
21
  Uses explicit core RGBA8 sources. Imports do not acquire browser resources.
7
22
  `prepareCpuSource` snapshots pixels into private linear premultiplied storage;
8
23
  `renderCpu` implements `rounded-refraction-v1`, optionally extending it with
package/dist/kernel.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { PixelSource, SurfaceInput } from '@liquid-glass-forge/core';
2
2
  export declare const CPU_MATERIAL_MODEL: 'rounded-material-v1';
3
+ export declare const CPU_LENS_MODEL: 'refractive-lens-v2';
3
4
  export interface CpuEffects {
4
- readonly model: typeof CPU_MATERIAL_MODEL;
5
+ readonly model: typeof CPU_MATERIAL_MODEL | typeof CPU_LENS_MODEL;
5
6
  readonly blurRadius?: number;
6
7
  readonly dispersion?: number;
7
8
  readonly tint?: readonly [number, number, number, number];
package/dist/kernel.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ContractError, MAX_PIXELS, decodeSrgb, encodeSrgb, validateSource } from '@liquid-glass-forge/core';
2
2
  export const CPU_MATERIAL_MODEL = 'rounded-material-v1';
3
+ export const CPU_LENS_MODEL = 'refractive-lens-v2';
3
4
  const storage = new WeakMap();
4
5
  const ENCODING_STEPS = 4096;
5
6
  export const CPU_LOOKUP_BYTES = 12296;
@@ -223,7 +224,7 @@ function surfaceOptions(input, source) {
223
224
  function effectOptions(input) {
224
225
  if (input !== undefined) {
225
226
  object(input, ['model', 'blurRadius', 'dispersion', 'tint', 'rim', 'specular', 'light'], 'effects');
226
- if (input.model !== CPU_MATERIAL_MODEL)
227
+ if (input.model !== CPU_MATERIAL_MODEL && input.model !== CPU_LENS_MODEL)
227
228
  throw new ContractError('Unsupported CPU material model');
228
229
  }
229
230
  const blurRadius = input?.blurRadius === undefined ? 0 : input.blurRadius, dispersion = input?.dispersion === undefined ? 0 : input.dispersion;
@@ -248,7 +249,7 @@ function effectOptions(input) {
248
249
  if (scale === 0)
249
250
  throw new ContractError('Light direction must be nonzero');
250
251
  const lx = light[0] / scale, ly = light[1] / scale, length = Math.hypot(lx, ly);
251
- return { blurRadius, dispersion, tint: [decodeSrgb(tint[0]), decodeSrgb(tint[1]), decodeSrgb(tint[2]), tint[3]], rim, specular, light: [lx / length, ly / length] };
252
+ return { lens: input?.model === CPU_LENS_MODEL, blurRadius, dispersion, tint: [decodeSrgb(tint[0]), decodeSrgb(tint[1]), decodeSrgb(tint[2]), tint[3]], rim, specular, light: [lx / length, ly / length] };
252
253
  }
253
254
  /** Validate and freeze a data-only snapshot before asynchronous execution. */
254
255
  export function snapshotCpuEffects(input) {
@@ -378,6 +379,11 @@ function renderClassifiedOpaque(prepared, surface, data, geo, encoding, output)
378
379
  export function renderCpu(prepared, input, effects, output) {
379
380
  const s = live(prepared), surface = surfaceOptions(input, prepared), e = effectOptions(effects);
380
381
  const { width, height, radius, bevel, strength, sourceRect: rect } = surface;
382
+ // A bevel wider than the corner's curvature reaches the SDF medial axis,
383
+ // where the nearest edge (and its normal) changes discontinuously. Keep the
384
+ // v2 ray profile outside that axis; a sharp corner has only subpixel bevel.
385
+ // The versioned v1 material retains its original geometry.
386
+ const opticalBevel = e.lens ? Math.min(bevel, Math.max(radius, .5)) : bevel;
381
387
  if (output === undefined)
382
388
  output = new Uint8ClampedArray(width * height * 4);
383
389
  try {
@@ -390,7 +396,7 @@ export function renderCpu(prepared, input, effects, output) {
390
396
  throw new ContractError('Invalid CPU output storage');
391
397
  const data = blurred(prepared, s, e.blurRadius), scaleX = rect.width / width, scaleY = rect.height / height;
392
398
  const encoding = getColourTables();
393
- const baseline = e.blurRadius === 0 && e.dispersion === 0 && e.tint[3] === 0 && e.rim === 0 && e.specular === 0;
399
+ const baseline = !e.lens && e.blurRadius === 0 && e.dispersion === 0 && e.tint[3] === 0 && e.rim === 0 && e.specular === 0;
394
400
  const preferResampling = baseline && s.opaque && scaleX === 2 && scaleY === 2 && Number.isInteger(rect.x) && Number.isInteger(rect.y);
395
401
  const geo = baseline ? geometryCache(s, width, height, radius, bevel, preferResampling) : undefined;
396
402
  const sourceRgb = geo?.sourceResampling ? resampledSourceRgb(prepared, data, surface, geo, encoding) : undefined;
@@ -429,7 +435,7 @@ export function renderCpu(prepared, input, effects, output) {
429
435
  }
430
436
  }
431
437
  else {
432
- const g = geometry(width, height, radius, bevel, x, y);
438
+ const g = geometry(width, height, radius, opticalBevel, x, y);
433
439
  coverage = g.coverage;
434
440
  normalX = g.normalX;
435
441
  normalY = g.normalY;
@@ -440,7 +446,15 @@ export function renderCpu(prepared, input, effects, output) {
440
446
  continue;
441
447
  }
442
448
  const baseX = rect.x + (x + .5) * scaleX - .5, baseY = rect.y + (y + .5) * scaleY - .5;
443
- const dx = normalX * strength * weight * scaleX, dy = normalY * strength * weight * scaleY;
449
+ // Opt-in v2: trace an air-to-glass ray through an elliptical bevel
450
+ // (eta = 1 / 1.5). Backdrop sampling bends inward; v1 remains unchanged.
451
+ let displacement = weight;
452
+ if (e.lens && weight > 0) {
453
+ const u = Math.sqrt(weight), nz = Math.sqrt(Math.max(0, 1 - u * u)), eta = 2 / 3;
454
+ const bend = Math.sqrt(1 - eta * eta * u * u) - eta * nz;
455
+ displacement = -u * bend / (eta + bend * nz);
456
+ }
457
+ const dx = normalX * strength * displacement * scaleX, dy = normalY * strength * displacement * scaleY;
444
458
  if (sourceRgb && dx === 0 && dy === 0) {
445
459
  output[index + 3] = Math.round(coverage * 255);
446
460
  const at = (y * width + x) * 3;
@@ -1,5 +1,6 @@
1
1
  import { ContractError, MAX_PIXELS, decodeSrgb, encodeSrgb, validateSource } from './worker-core.js';
2
2
  export const CPU_MATERIAL_MODEL = 'rounded-material-v1';
3
+ export const CPU_LENS_MODEL = 'refractive-lens-v2';
3
4
  const storage = new WeakMap();
4
5
  const ENCODING_STEPS = 4096;
5
6
  export const CPU_LOOKUP_BYTES = 12296;
@@ -223,7 +224,7 @@ function surfaceOptions(input, source) {
223
224
  function effectOptions(input) {
224
225
  if (input !== undefined) {
225
226
  object(input, ['model', 'blurRadius', 'dispersion', 'tint', 'rim', 'specular', 'light'], 'effects');
226
- if (input.model !== CPU_MATERIAL_MODEL)
227
+ if (input.model !== CPU_MATERIAL_MODEL && input.model !== CPU_LENS_MODEL)
227
228
  throw new ContractError('Unsupported CPU material model');
228
229
  }
229
230
  const blurRadius = input?.blurRadius === undefined ? 0 : input.blurRadius, dispersion = input?.dispersion === undefined ? 0 : input.dispersion;
@@ -248,7 +249,7 @@ function effectOptions(input) {
248
249
  if (scale === 0)
249
250
  throw new ContractError('Light direction must be nonzero');
250
251
  const lx = light[0] / scale, ly = light[1] / scale, length = Math.hypot(lx, ly);
251
- return { blurRadius, dispersion, tint: [decodeSrgb(tint[0]), decodeSrgb(tint[1]), decodeSrgb(tint[2]), tint[3]], rim, specular, light: [lx / length, ly / length] };
252
+ return { lens: input?.model === CPU_LENS_MODEL, blurRadius, dispersion, tint: [decodeSrgb(tint[0]), decodeSrgb(tint[1]), decodeSrgb(tint[2]), tint[3]], rim, specular, light: [lx / length, ly / length] };
252
253
  }
253
254
  /** Validate and freeze a data-only snapshot before asynchronous execution. */
254
255
  export function snapshotCpuEffects(input) {
@@ -378,6 +379,11 @@ function renderClassifiedOpaque(prepared, surface, data, geo, encoding, output)
378
379
  export function renderCpu(prepared, input, effects, output) {
379
380
  const s = live(prepared), surface = surfaceOptions(input, prepared), e = effectOptions(effects);
380
381
  const { width, height, radius, bevel, strength, sourceRect: rect } = surface;
382
+ // A bevel wider than the corner's curvature reaches the SDF medial axis,
383
+ // where the nearest edge (and its normal) changes discontinuously. Keep the
384
+ // v2 ray profile outside that axis; a sharp corner has only subpixel bevel.
385
+ // The versioned v1 material retains its original geometry.
386
+ const opticalBevel = e.lens ? Math.min(bevel, Math.max(radius, .5)) : bevel;
381
387
  if (output === undefined)
382
388
  output = new Uint8ClampedArray(width * height * 4);
383
389
  try {
@@ -390,7 +396,7 @@ export function renderCpu(prepared, input, effects, output) {
390
396
  throw new ContractError('Invalid CPU output storage');
391
397
  const data = blurred(prepared, s, e.blurRadius), scaleX = rect.width / width, scaleY = rect.height / height;
392
398
  const encoding = getColourTables();
393
- const baseline = e.blurRadius === 0 && e.dispersion === 0 && e.tint[3] === 0 && e.rim === 0 && e.specular === 0;
399
+ const baseline = !e.lens && e.blurRadius === 0 && e.dispersion === 0 && e.tint[3] === 0 && e.rim === 0 && e.specular === 0;
394
400
  const preferResampling = baseline && s.opaque && scaleX === 2 && scaleY === 2 && Number.isInteger(rect.x) && Number.isInteger(rect.y);
395
401
  const geo = baseline ? geometryCache(s, width, height, radius, bevel, preferResampling) : undefined;
396
402
  const sourceRgb = geo?.sourceResampling ? resampledSourceRgb(prepared, data, surface, geo, encoding) : undefined;
@@ -429,7 +435,7 @@ export function renderCpu(prepared, input, effects, output) {
429
435
  }
430
436
  }
431
437
  else {
432
- const g = geometry(width, height, radius, bevel, x, y);
438
+ const g = geometry(width, height, radius, opticalBevel, x, y);
433
439
  coverage = g.coverage;
434
440
  normalX = g.normalX;
435
441
  normalY = g.normalY;
@@ -440,7 +446,15 @@ export function renderCpu(prepared, input, effects, output) {
440
446
  continue;
441
447
  }
442
448
  const baseX = rect.x + (x + .5) * scaleX - .5, baseY = rect.y + (y + .5) * scaleY - .5;
443
- const dx = normalX * strength * weight * scaleX, dy = normalY * strength * weight * scaleY;
449
+ // Opt-in v2: trace an air-to-glass ray through an elliptical bevel
450
+ // (eta = 1 / 1.5). Backdrop sampling bends inward; v1 remains unchanged.
451
+ let displacement = weight;
452
+ if (e.lens && weight > 0) {
453
+ const u = Math.sqrt(weight), nz = Math.sqrt(Math.max(0, 1 - u * u)), eta = 2 / 3;
454
+ const bend = Math.sqrt(1 - eta * eta * u * u) - eta * nz;
455
+ displacement = -u * bend / (eta + bend * nz);
456
+ }
457
+ const dx = normalX * strength * displacement * scaleX, dy = normalY * strength * displacement * scaleY;
444
458
  if (sourceRgb && dx === 0 && dy === 0) {
445
459
  output[index + 3] = Math.round(coverage * 255);
446
460
  const at = (y * width + x) * 3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquid-glass-forge/cpu",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.2",
4
4
  "description": "Experimental JavaScript CPU optics for explicit Liquid Glass Forge pixel sources",
5
5
  "private": false,
6
6
  "type": "module",
@@ -26,8 +26,9 @@
26
26
  "README.md"
27
27
  ],
28
28
  "dependencies": {
29
- "@liquid-glass-forge/core": "0.1.0-rc.1"
29
+ "@liquid-glass-forge/core": "0.1.0-rc.2"
30
30
  },
31
+ "homepage": "https://namipsg.github.io/liquid-glass-forge/#playground",
31
32
  "repository": {
32
33
  "type": "git",
33
34
  "url": "git+https://github.com/namipsg/liquid-glass-forge.git",