@liquid-glass-forge/cpu 0.0.0-stage → 0.1.0-rc.1

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.
@@ -0,0 +1,187 @@
1
+ /** Data contracts only: no browser access or resource acquisition at import. */
2
+ export const CONTRACT_VERSION = 1;
3
+ export const OPTICAL_MODEL = 'rounded-refraction-v1';
4
+ export const MAX_PIXELS = 4_000_000;
5
+ export const MAX_SURFACES = 16;
6
+ export class ContractError extends TypeError {
7
+ code;
8
+ constructor(message, code = 'INVALID_CONTRACT') {
9
+ super(message);
10
+ this.name = 'ContractError';
11
+ this.code = code;
12
+ }
13
+ }
14
+ const byteViewBrand = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), Symbol.toStringTag).get;
15
+ const bufferSize = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength').get;
16
+ function record(value, label) {
17
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
18
+ throw new ContractError(`${label} must be an object`);
19
+ return value;
20
+ }
21
+ function finite(value, low, high, label) {
22
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < low || value > high) {
23
+ throw new ContractError(`${label} must be finite in [${low}, ${high}]`);
24
+ }
25
+ }
26
+ function revision(value, label) {
27
+ if (!Number.isSafeInteger(value) || value < 0)
28
+ throw new ContractError(`${label} must be a nonnegative safe integer`);
29
+ }
30
+ function dimensions(width, height) {
31
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1
32
+ || width * height > MAX_PIXELS)
33
+ throw new ContractError(`Dimensions must be positive safe integers totaling at most ${MAX_PIXELS} pixels`);
34
+ }
35
+ function version(value) {
36
+ if (value !== CONTRACT_VERSION)
37
+ throw new ContractError('Unsupported contractVersion');
38
+ }
39
+ function keys(value, allowed, label) {
40
+ for (const key of Object.keys(value))
41
+ if (!allowed.includes(key))
42
+ throw new ContractError(`Unknown ${label} field: ${key}`);
43
+ }
44
+ /** Validates without copying, transferring, closing, freezing or mutating pixel storage. */
45
+ export function validateSource(input) {
46
+ const source = record(input, 'source');
47
+ keys(source, ['kind', 'contractVersion', 'width', 'height', 'revision', 'data', 'ownership', 'colorSpace', 'alphaMode'], 'source');
48
+ version(source.contractVersion);
49
+ if (source.kind !== 'rgba8' || source.colorSpace !== 'srgb' || source.alphaMode !== 'straight') {
50
+ throw new ContractError('Only straight-alpha sRGB RGBA8 sources are supported');
51
+ }
52
+ dimensions(source.width, source.height);
53
+ revision(source.revision, 'source.revision');
54
+ if (source.ownership !== 'borrowed' && source.ownership !== 'transfer')
55
+ throw new ContractError('Explicit source ownership is required');
56
+ const bytes = source.data;
57
+ let attachedBufferSize;
58
+ try {
59
+ attachedBufferSize = bufferSize.call(bytes?.buffer);
60
+ }
61
+ catch {
62
+ throw new ContractError('Source pixels must use an unshared ArrayBuffer');
63
+ }
64
+ if (!ArrayBuffer.isView(bytes) || !['Uint8Array', 'Uint8ClampedArray'].includes(byteViewBrand.call(bytes))
65
+ || bytes.byteLength !== source.width * source.height * 4) {
66
+ throw new ContractError('Source data must be an attached, unshared RGBA8 byte view of exactly width * height * 4 bytes');
67
+ }
68
+ if (source.ownership === 'transfer' && (bytes.byteOffset !== 0 || bytes.byteLength !== attachedBufferSize)) {
69
+ throw new ContractError('Transfer ownership requires an entire dedicated buffer');
70
+ }
71
+ }
72
+ export function createSource(input) {
73
+ const value = record(input, 'source');
74
+ keys(value, ['kind', 'contractVersion', 'width', 'height', 'revision', 'data', 'ownership', 'colorSpace', 'alphaMode'], 'source');
75
+ if (value.kind !== undefined && value.kind !== 'rgba8')
76
+ throw new ContractError('Unsupported source kind');
77
+ const source = { kind: 'rgba8', contractVersion: value.contractVersion, width: value.width, height: value.height,
78
+ revision: value.revision, data: value.data, ownership: value.ownership, colorSpace: value.colorSpace, alphaMode: value.alphaMode };
79
+ validateSource(source);
80
+ return Object.freeze(source);
81
+ }
82
+ /** Creates an owned metadata snapshot; it never performs rendering or transfers pixels. */
83
+ export function createFrame(input, source) {
84
+ validateSource(source);
85
+ const frame = record(input, 'frame');
86
+ keys(frame, ['contractVersion', 'generation', 'revision', 'sourceRevision', 'surfaces'], 'frame');
87
+ version(frame.contractVersion);
88
+ revision(frame.generation, 'frame.generation');
89
+ revision(frame.revision, 'frame.revision');
90
+ revision(frame.sourceRevision, 'frame.sourceRevision');
91
+ if (frame.sourceRevision !== source.revision)
92
+ throw new ContractError('Frame refers to a stale source revision', 'STALE_SOURCE');
93
+ if (!Array.isArray(frame.surfaces) || frame.surfaces.length < 1 || frame.surfaces.length > MAX_SURFACES) {
94
+ throw new ContractError(`A frame requires 1..${MAX_SURFACES} surfaces`);
95
+ }
96
+ const ids = new Set();
97
+ let pixels = 0;
98
+ const surfaces = Array.from(frame.surfaces, (entry) => {
99
+ const surface = record(entry, 'surface');
100
+ keys(surface, ['id', 'width', 'height', 'radius', 'bevel', 'strength', 'sourceRect'], 'surface');
101
+ if (typeof surface.id !== 'string' || surface.id.length < 1 || surface.id.length > 128 || ids.has(surface.id)) {
102
+ throw new ContractError('Surface IDs must be nonempty, distinct strings of at most 128 characters');
103
+ }
104
+ ids.add(surface.id);
105
+ dimensions(surface.width, surface.height);
106
+ const width = surface.width, height = surface.height;
107
+ pixels += width * height;
108
+ if (pixels > MAX_PIXELS)
109
+ throw new ContractError('Total output pixels exceed the frame budget');
110
+ const radius = surface.radius === undefined ? Math.min(width, height) / 4 : surface.radius;
111
+ const bevel = surface.bevel === undefined ? Math.min(width, height) / 5 : surface.bevel;
112
+ const strength = surface.strength === undefined ? 12 : surface.strength;
113
+ finite(radius, 0, Math.min(width, height) / 2, 'radius');
114
+ finite(bevel, Number.MIN_VALUE, 1_000_000, 'bevel');
115
+ finite(strength, 0, 1_000_000, 'strength');
116
+ const rect = record(surface.sourceRect === undefined ? { x: 0, y: 0, width: source.width, height: source.height } : surface.sourceRect, 'sourceRect');
117
+ keys(rect, ['x', 'y', 'width', 'height'], 'sourceRect');
118
+ finite(rect.x, 0, source.width, 'sourceRect.x');
119
+ finite(rect.y, 0, source.height, 'sourceRect.y');
120
+ finite(rect.width, Number.MIN_VALUE, source.width, 'sourceRect.width');
121
+ finite(rect.height, Number.MIN_VALUE, source.height, 'sourceRect.height');
122
+ if (rect.x + rect.width > source.width || rect.y + rect.height > source.height)
123
+ throw new ContractError('Source rectangle must fit within the source');
124
+ return Object.freeze({ id: surface.id, width, height, radius, bevel, strength,
125
+ sourceRect: Object.freeze({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }) });
126
+ });
127
+ return Object.freeze({ contractVersion: CONTRACT_VERSION, opticalModel: OPTICAL_MODEL,
128
+ generation: frame.generation, revision: frame.revision, sourceRevision: frame.sourceRevision,
129
+ surfaces: Object.freeze(surfaces) });
130
+ }
131
+ /** Compare generation and source revision before accepting any future async result. */
132
+ export function isCurrentFrame(frame, current) {
133
+ return frame.generation === current.generation && frame.revision === current.revision && frame.sourceRevision === current.sourceRevision;
134
+ }
135
+ /** Pure lifecycle contract; no async initialization, scheduling or resource management is performed. */
136
+ export function transitionLifecycle(state, event) {
137
+ const states = ['new', 'initializing', 'ready', 'paused', 'failed', 'destroyed'];
138
+ if (!states.includes(state))
139
+ throw new ContractError('Unknown lifecycle state');
140
+ if (event === 'destroy')
141
+ return 'destroyed';
142
+ if (state === 'new' && event === 'initialize')
143
+ return 'initializing';
144
+ if (state === 'initializing' && event === 'resolve')
145
+ return 'ready';
146
+ if (state === 'ready' && event === 'pause')
147
+ return 'paused';
148
+ if (state === 'paused' && event === 'resume')
149
+ return 'ready';
150
+ if (['initializing', 'ready', 'paused'].includes(state) && event === 'fail')
151
+ return 'failed';
152
+ throw new ContractError(`Invalid lifecycle transition: ${state} -> ${event}`);
153
+ }
154
+ export function decodeSrgb(encoded) {
155
+ finite(encoded, 0, 1, 'encoded sRGB');
156
+ return encoded <= 0.04045 ? encoded / 12.92 : ((encoded + 0.055) / 1.055) ** 2.4;
157
+ }
158
+ export function encodeSrgb(linear) {
159
+ finite(linear, 0, 1, 'linear sRGB');
160
+ if (linear === 0 || linear === 1)
161
+ return linear;
162
+ return linear <= 0.0031308 ? 12.92 * linear : 1.055 * linear ** (1 / 2.4) - 0.055;
163
+ }
164
+ /** Reference interpolation: straight sRGB inputs -> linear premultiplied blend -> straight sRGB. */
165
+ export function interpolateRgba(corners, x, y) {
166
+ finite(x, 0, 1, 'x');
167
+ finite(y, 0, 1, 'y');
168
+ if (!Array.isArray(corners) || corners.length !== 4)
169
+ throw new ContractError('Four RGBA corners are required');
170
+ const weights = [(1 - x) * (1 - y), x * (1 - y), (1 - x) * y, x * y];
171
+ const channels = [0, 0, 0, 0];
172
+ Array.from(corners).forEach((corner, index) => {
173
+ if (!Array.isArray(corner) || corner.length !== 4)
174
+ throw new ContractError('RGBA colors require four normalized channels');
175
+ for (const value of corner)
176
+ finite(value, 0, 1, 'RGBA channel');
177
+ const alpha = corner[3], weight = weights[index];
178
+ for (let c = 0; c < 3; c++)
179
+ channels[c] = channels[c] + decodeSrgb(corner[c]) * alpha * weight;
180
+ channels[3] = channels[3] + alpha * weight;
181
+ });
182
+ const alpha = channels[3];
183
+ if (alpha === 0)
184
+ return Object.freeze([0, 0, 0, 0]);
185
+ return Object.freeze([encodeSrgb(Math.min(1, channels[0] / alpha)), encodeSrgb(Math.min(1, channels[1] / alpha)),
186
+ encodeSrgb(Math.min(1, channels[2] / alpha)), alpha]);
187
+ }