@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 +17 -2
- package/dist/kernel.d.ts +2 -1
- package/dist/kernel.js +19 -5
- package/dist/worker-kernel.js +19 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
# @liquid-glass-forge/cpu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Bounded JavaScript glass rendering with optional worker execution.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](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,
|
|
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
|
-
|
|
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/dist/worker-kernel.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|