@bitefoundry/retrieviewer 0.0.16 → 0.0.18
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/ASSET-PROVENANCE.md +11 -0
- package/ASSETS-LICENSE.md +6 -0
- package/ATTRIBUTION.md +8 -0
- package/DESKTOP-PLATFORMS.md +1 -1
- package/EMBEDDING.md +15 -9
- package/INSTALLING.md +2 -1
- package/LICENSE +16 -16
- package/NATIVE-RUNTIME.md +89 -0
- package/PREREQUISITES.md +7 -0
- package/PUPPY-MEMORY.md +70 -0
- package/README.ko.md +13 -1
- package/README.md +14 -1
- package/RELEASING.md +36 -1
- package/THIRD-PARTY-NOTICES.md +8 -0
- package/WINDOWS-COMPANION.md +15 -6
- package/braille.mjs +2 -2
- package/integrations/native-windows/CMakeLists.txt +15 -0
- package/integrations/native-windows/THIRD-PARTY-LICENSES.txt +77 -0
- package/integrations/native-windows/application.manifest +8 -0
- package/integrations/native-windows/bin/win32-x64/retrieviewer-native.exe +0 -0
- package/integrations/native-windows/desktop.mjs +45 -0
- package/integrations/native-windows/host.cpp +84 -0
- package/integrations/native-windows/manifest.json +14 -0
- package/integrations/native-windows/native-control.hpp +41 -0
- package/integrations/native-windows/native-window.hpp +128 -0
- package/integrations/native-windows/runtime.bundle.mjs +4694 -0
- package/integrations/native-windows/runtime.mjs +55 -0
- package/integrations/native-windows/sources.json +23 -0
- package/integrations/purrviewer/ART.md +289 -0
- package/integrations/purrviewer/ASSETS-LICENSE.md +21 -0
- package/integrations/purrviewer/LICENSE +24 -0
- package/integrations/purrviewer/art/ginger.bin.gz +0 -0
- package/integrations/purrviewer/art/ginger.json +1707 -0
- package/integrations/purrviewer/art/ginger.rvz +0 -0
- package/integrations/purrviewer/src/contact.mjs +63 -0
- package/integrations/purrviewer/src/habitat.mjs +41 -0
- package/integrations/purrviewer/src/life.mjs +280 -0
- package/integrations/purrviewer/src/purrviewer-core.mjs +130 -0
- package/integrations/purrviewer/src/purrviewer.mjs +37 -0
- package/integrations/purrviewer/src/walk-cycle.mjs +3 -0
- package/integrations/purrviewer/src/world.mjs +216 -0
- package/integrations/purrviewer/vendor/retrieviewer/LICENSE-CODE +21 -0
- package/integrations/purrviewer/vendor/retrieviewer/braille.mjs +73 -0
- package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-file.mjs +15 -0
- package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-indexed.mjs +78 -0
- package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-predictive.mjs +170 -0
- package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-store.mjs +114 -0
- package/integrations/windows-overlay/desktop.mjs +11 -3
- package/integrations/windows-overlay/worker.mjs +9 -2
- package/package.json +53 -13
- package/pet-indexed-manifest.json +106 -0
- package/puppy-clean.mjs +2 -2
- package/puppy-companion-comfort-q5.rvz +0 -0
- package/puppy-companion-interactions-v3-q5.rvz +0 -0
- package/puppy-companion-sprites.mjs +2 -2
- package/puppy-frame-file.mjs +15 -0
- package/puppy-frame-indexed.mjs +78 -0
- package/puppy-frame-predictive.mjs +1 -1
- package/puppy-frame-store.mjs +6 -0
- package/puppy-paper-actions-v9-q5.rvz +0 -0
- package/puppy-paper-actions.mjs +2 -2
- package/puppy-paper-approach-q5.rvz +0 -0
- package/puppy-paper-arrival-seat-q5.rvz +0 -0
- package/puppy-paper-behaviors-q5.rvz +0 -0
- package/puppy-paper-behaviors.mjs +1 -1
- package/puppy-paper-contact.mjs +2 -2
- package/puppy-paper-rear-gallop-q5.rvz +0 -0
- package/puppy-paper-seat-q5.rvz +0 -0
- package/puppy-paper-seat.mjs +2 -2
- package/puppy-paper.mjs +12 -12
- package/puppy-stride-tweens-q5.rvz +0 -0
- package/retrieviewer-cli.mjs +10 -6
- package/retrieviewer.d.mts +1 -1
- package/retrieviewer.mjs +35 -14
- package/puppy-companion-comfort-q5.bin.gz +0 -0
- package/puppy-companion-interactions-v3-q5.bin.gz +0 -0
- package/puppy-paper-actions-v9-q5.bin.gz +0 -0
- package/puppy-paper-approach-q5.bin.gz +0 -0
- package/puppy-paper-arrival-seat-q5.bin.gz +0 -0
- package/puppy-paper-behaviors-q5.bin.gz +0 -0
- package/puppy-paper-rear-gallop-q5.bin.gz +0 -0
- package/puppy-paper-seat-q5.bin.gz +0 -0
- package/puppy-stride-tweens-q5.bin.gz +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Scalar travel and camera mathematics adapted from MIT Retrieviewer
|
|
2
|
+
// puppy-paper-world.mjs; copyright (c) 2026 Retrieviewer contributors.
|
|
3
|
+
// License text is retained in vendor/retrieviewer/LICENSE-CODE.
|
|
4
|
+
// Cat values below are design choices, not measured juvenile gait data.
|
|
5
|
+
import {WALK_STRIDE} from './walk-cycle.mjs';
|
|
6
|
+
const gait = (stride, frequency) => Object.freeze({stride, frequency, speed: stride * frequency});
|
|
7
|
+
export const CAT_GAITS = Object.freeze({
|
|
8
|
+
walk: gait(WALK_STRIDE, .27/WALK_STRIDE),
|
|
9
|
+
trot: gait(.62, 2),
|
|
10
|
+
creep: gait(WALK_STRIDE, .13/WALK_STRIDE),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const GRAVITY = 9.81;
|
|
14
|
+
const DECELERATION = 1.8;
|
|
15
|
+
const ACCELERATION = Object.freeze({walk: .65, trot: 2, creep: .3});
|
|
16
|
+
const coordinate = n => Number.isFinite(n) && Math.abs(n) <= 1e6;
|
|
17
|
+
function point(x, z) {
|
|
18
|
+
if (!coordinate(x) || !coordinate(z)) throw new RangeError('Invalid cat target');
|
|
19
|
+
}
|
|
20
|
+
function options(value) {
|
|
21
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Invalid cat options');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Rest-to-rest triangle/trapezoid: both the root and phase consume this integral.
|
|
25
|
+
function travel(length, speed, acceleration) {
|
|
26
|
+
const peak = Math.min(speed, Math.sqrt(2 * length / (1 / acceleration + 1 / DECELERATION)));
|
|
27
|
+
const ta = peak / acceleration, td = peak / DECELERATION;
|
|
28
|
+
const da = peak * ta / 2, dd = peak * td / 2;
|
|
29
|
+
const tc = Math.max(0, length - da - dd) / peak;
|
|
30
|
+
const duration = ta + tc + td;
|
|
31
|
+
return {duration, at(t) {
|
|
32
|
+
if (t >= duration) return {distance: length, speed: 0};
|
|
33
|
+
if (t < ta) return {distance: acceleration * t * t / 2, speed: acceleration * t};
|
|
34
|
+
if (t < ta + tc) return {distance: da + peak * (t - ta), speed: peak};
|
|
35
|
+
const remaining = duration - t;
|
|
36
|
+
return {distance: length - DECELERATION * remaining * remaining / 2, speed: DECELERATION * remaining};
|
|
37
|
+
}};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createCatMotion(config = {}) {
|
|
41
|
+
options(config);
|
|
42
|
+
let {x = 0, z = 0} = config;
|
|
43
|
+
point(x, z);
|
|
44
|
+
let y = 0, speed = 0, distance = 0, phase = 0, heading = 0, currentGait = 'walk';
|
|
45
|
+
let active = null, pending = null, supportY=0, beltSpeed=0, beltDistance=0, wheelRadius=.6,wheelPhase=0;
|
|
46
|
+
|
|
47
|
+
function advance(ds) {
|
|
48
|
+
distance += ds;
|
|
49
|
+
phase += ds / CAT_GAITS[currentGait].stride;
|
|
50
|
+
}
|
|
51
|
+
function launch(command) {
|
|
52
|
+
if(command.kind==='wheel'){
|
|
53
|
+
wheelRadius=command.radius;wheelPhase=0;
|
|
54
|
+
active={...command,elapsed:0,covered:0};return;
|
|
55
|
+
}
|
|
56
|
+
const dx = command.x - x, dz = command.z - z, length = Math.hypot(dx, dz);
|
|
57
|
+
if (command.kind === 'go') currentGait = command.gait;
|
|
58
|
+
if (length > 0) heading = Math.atan2(dx, dz);
|
|
59
|
+
if (command.kind === 'go' && length === 0) return;
|
|
60
|
+
active = {
|
|
61
|
+
kind: command.kind, x, z, targetX: command.x, targetZ: command.z,
|
|
62
|
+
ux: length ? dx / length : 0, uz: length ? dz / length : 0,
|
|
63
|
+
length, elapsed: 0, covered: 0,
|
|
64
|
+
...(command.kind === 'pounce'
|
|
65
|
+
? (()=>{const apex=Math.max(y,command.landingY)+command.height;
|
|
66
|
+
const vy=Math.sqrt(2*GRAVITY*(apex-y));
|
|
67
|
+
return {startY:y,targetY:command.landingY,vy,
|
|
68
|
+
duration:(vy+Math.sqrt(2*GRAVITY*(apex-command.landingY)))/GRAVITY};})()
|
|
69
|
+
: {plan: travel(length, CAT_GAITS[currentGait].speed, ACCELERATION[currentGait])}),
|
|
70
|
+
};
|
|
71
|
+
if (command.kind === 'pounce') speed = length / active.duration;
|
|
72
|
+
}
|
|
73
|
+
function brake() {
|
|
74
|
+
if (!active || active.kind === 'pounce' || ['brake','wheel-brake'].includes(active.kind)) return;
|
|
75
|
+
if(active.kind==='wheel'){
|
|
76
|
+
const stride=active.stride,minDistance=beltSpeed*beltSpeed/2.4;
|
|
77
|
+
// At useful speed, finish on the supported cycle boundary; never freeze
|
|
78
|
+
// the gathered airborne drawing when an interaction brakes the wheel.
|
|
79
|
+
const distance=beltSpeed>.15?(Math.ceil(wheelPhase+minDistance/stride)-wheelPhase)*stride:minDistance;
|
|
80
|
+
const deceleration=distance>0?beltSpeed*beltSpeed/(2*distance):1.2;
|
|
81
|
+
active=beltSpeed>0?{kind:'wheel-brake',initialSpeed:beltSpeed,deceleration,
|
|
82
|
+
duration:beltSpeed/deceleration,elapsed:0,covered:0,stride}:null;return;
|
|
83
|
+
}
|
|
84
|
+
active = speed > 0 ? {
|
|
85
|
+
kind: 'brake', x, z, ux: active.ux, uz: active.uz,
|
|
86
|
+
initialSpeed: speed, duration: speed / DECELERATION, elapsed: 0, covered: 0,
|
|
87
|
+
} : null;
|
|
88
|
+
}
|
|
89
|
+
function request(command) {
|
|
90
|
+
if (active) {
|
|
91
|
+
pending = command;
|
|
92
|
+
brake();
|
|
93
|
+
}
|
|
94
|
+
if (!active) {
|
|
95
|
+
pending = null;
|
|
96
|
+
launch(command);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
go(targetX, targetZ, config = {}) {
|
|
101
|
+
options(config);
|
|
102
|
+
const {gait = 'walk'} = config;
|
|
103
|
+
point(targetX, targetZ);
|
|
104
|
+
if (typeof gait !== 'string' || !Object.hasOwn(CAT_GAITS, gait)) throw new RangeError('Invalid cat gait');
|
|
105
|
+
request({kind: 'go', x: targetX, z: targetZ, gait});
|
|
106
|
+
},
|
|
107
|
+
stop() {
|
|
108
|
+
pending = null;
|
|
109
|
+
brake(); // A flight must finish its one physical trajectory.
|
|
110
|
+
},
|
|
111
|
+
pounce(targetX, targetZ, config = {}) {
|
|
112
|
+
options(config);
|
|
113
|
+
const {height = .18,landingY=0} = config;
|
|
114
|
+
point(targetX, targetZ);
|
|
115
|
+
if (!Number.isFinite(height) || height <= 0 || height > 1) throw new RangeError('Invalid pounce height');
|
|
116
|
+
if(!Number.isFinite(landingY)||landingY<0||landingY>1)throw RangeError('Invalid landing support');
|
|
117
|
+
request({kind: 'pounce', x: targetX, z: targetZ, height,landingY});
|
|
118
|
+
},
|
|
119
|
+
wheel(config={}){
|
|
120
|
+
options(config);const {duration=10,speed=.65,radius=.6,stride=.32}=config;
|
|
121
|
+
if(![duration,speed,radius,stride].every(Number.isFinite)||duration<2||duration>30||speed<=0||speed>2||radius<.3||radius>1||stride<.05||stride>1)
|
|
122
|
+
throw RangeError('Invalid wheel motion');
|
|
123
|
+
request({kind:'wheel',duration,peakSpeed:speed,radius,stride});
|
|
124
|
+
},
|
|
125
|
+
step(dt) {
|
|
126
|
+
if (!Number.isFinite(dt) || dt < 0 || dt > .1) throw new RangeError('Invalid cat step');
|
|
127
|
+
let remaining = dt;
|
|
128
|
+
while (active && remaining > 0) {
|
|
129
|
+
const a = active, duration = a.plan?.duration ?? a.duration;
|
|
130
|
+
const used = Math.min(remaining, duration - a.elapsed);
|
|
131
|
+
a.elapsed = Math.min(duration, a.elapsed + used);
|
|
132
|
+
if(duration-a.elapsed<1e-10)a.elapsed=duration;
|
|
133
|
+
remaining = Math.max(0, remaining - used);
|
|
134
|
+
const done = a.elapsed >= duration;
|
|
135
|
+
let covered;
|
|
136
|
+
if(['wheel','wheel-brake'].includes(a.kind)){
|
|
137
|
+
if(a.kind==='wheel'){
|
|
138
|
+
beltSpeed=a.peakSpeed*Math.sin(Math.PI*a.elapsed/duration)**2;
|
|
139
|
+
covered=a.peakSpeed*(a.elapsed/2-duration*Math.sin(2*Math.PI*a.elapsed/duration)/(4*Math.PI));
|
|
140
|
+
}else{
|
|
141
|
+
beltSpeed=Math.max(0,a.initialSpeed-a.deceleration*a.elapsed);
|
|
142
|
+
covered=a.initialSpeed*a.elapsed-a.deceleration*a.elapsed*a.elapsed/2;
|
|
143
|
+
}
|
|
144
|
+
const ds=covered-a.covered;beltDistance+=ds;wheelPhase+=ds/a.stride;a.covered=covered;
|
|
145
|
+
if(done){beltSpeed=0;if(Math.abs(wheelPhase-Math.round(wheelPhase))<1e-9)wheelPhase=Math.round(wheelPhase);
|
|
146
|
+
active=null;if(pending){const command=pending;pending=null;launch(command);}}
|
|
147
|
+
continue;
|
|
148
|
+
}else if (a.kind === 'pounce') {
|
|
149
|
+
covered = a.length * (a.elapsed / duration);
|
|
150
|
+
speed = a.length / duration;
|
|
151
|
+
y = a.startY+a.vy*a.elapsed-GRAVITY*a.elapsed*a.elapsed/2;
|
|
152
|
+
} else if (a.kind === 'brake') {
|
|
153
|
+
covered = a.initialSpeed * a.elapsed - DECELERATION * a.elapsed * a.elapsed / 2;
|
|
154
|
+
speed = Math.max(0, a.initialSpeed - DECELERATION * a.elapsed);
|
|
155
|
+
} else {
|
|
156
|
+
const state = a.plan.at(a.elapsed);
|
|
157
|
+
covered = state.distance;
|
|
158
|
+
speed = state.speed;
|
|
159
|
+
}
|
|
160
|
+
x = a.x + a.ux * covered;
|
|
161
|
+
z = a.z + a.uz * covered;
|
|
162
|
+
advance(covered - a.covered);
|
|
163
|
+
a.covered = covered;
|
|
164
|
+
if (done) {
|
|
165
|
+
// Exact endpoint is used only when the trajectory itself has ended.
|
|
166
|
+
if (a.kind !== 'brake') { x = a.targetX; z = a.targetZ; }
|
|
167
|
+
speed = 0;
|
|
168
|
+
if(a.kind==='pounce')supportY=a.targetY;
|
|
169
|
+
y = supportY;
|
|
170
|
+
active = null;
|
|
171
|
+
if (pending) {
|
|
172
|
+
const command = pending;
|
|
173
|
+
pending = null;
|
|
174
|
+
launch(command);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
sample() {
|
|
180
|
+
return {x, y, z, speed, distance, phase, gait: currentGait, moving: active !== null, heading,
|
|
181
|
+
supportY,airborne:active?.kind==='pounce',flightProgress:active?.kind==='pounce'?active.elapsed/active.duration:0,
|
|
182
|
+
exercising:['wheel','wheel-brake'].includes(active?.kind),beltSpeed,beltDistance,wheelPhase,wheelAngle:beltDistance/wheelRadius};
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Fixed assumed 32-inch 16:9 display at .70m; fit it inside any pixel rectangle.
|
|
188
|
+
const diagonal = .0254 * 32;
|
|
189
|
+
const screenWidth = diagonal * 16 / Math.hypot(16, 9);
|
|
190
|
+
const screenHeight = diagonal * 9 / Math.hypot(16, 9);
|
|
191
|
+
const pitch = 9 * Math.PI / 180, c = Math.cos(pitch), s = Math.sin(pitch);
|
|
192
|
+
export function projectCat(x, y, z, config = {}) {
|
|
193
|
+
options(config);
|
|
194
|
+
const {width, height, canvasMeters = .60} = config;
|
|
195
|
+
if (![x, y, z, width, height, canvasMeters].every(Number.isFinite)
|
|
196
|
+
|| width <= 0 || height <= 0 || canvasMeters <= 0) throw new RangeError('Invalid cat projection');
|
|
197
|
+
const cameraY = c * (y - .45) - s * (z - 1.65);
|
|
198
|
+
const depth = -s * (y - .45) - c * (z - 1.65);
|
|
199
|
+
if (!Number.isFinite(depth) || depth <= 0) throw new RangeError('Cat point behind camera');
|
|
200
|
+
const focal = .70 * Math.min(width / screenWidth, height / screenHeight);
|
|
201
|
+
const result = {x: width / 2 + focal * x / depth, y: height / 2 - focal * cameraY / depth,
|
|
202
|
+
size: focal * canvasMeters / depth, depth};
|
|
203
|
+
if (!Object.values(result).every(Number.isFinite)) throw new RangeError('Invalid cat projection range');
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Invert the same fixed pinhole camera at the ground plane (world y=0).
|
|
208
|
+
export function unprojectCatGround(u,v,{width,height}={}){
|
|
209
|
+
if(![u,v,width,height].every(Number.isFinite)||width<=0||height<=0)throw RangeError('Invalid cat ground pixel');
|
|
210
|
+
const focal=.70*Math.min(width/screenWidth,height/screenHeight),q=(height/2-v)/focal;
|
|
211
|
+
const denominator=s-q*c;
|
|
212
|
+
if(Math.abs(denominator)<1e-10)throw RangeError('Cat ground ray at horizon');
|
|
213
|
+
const dz=-.45*(c+q*s)/denominator,depth=s*.45-c*dz;
|
|
214
|
+
if(depth<=0||!Number.isFinite(depth))throw RangeError('Cat ground behind camera');
|
|
215
|
+
return {x:(u-width/2)*depth/focal,z:dz+1.65};
|
|
216
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Retrieviewer contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Unicode 2x4 cells, the same dot numbering used by AuraDeck's Braille canvas.
|
|
2
|
+
export const BRAILLE_BITS=[[1,8],[2,16],[4,32],[64,128]];
|
|
3
|
+
const BAYER=[[0,8,2,10],[12,4,14,6],[3,11,1,9],[15,7,13,5]];
|
|
4
|
+
const clamp=v=>Math.max(0,Math.min(1,v));
|
|
5
|
+
const GLYPHS=Array.from({length:256},(_,i)=>String.fromCharCode(0x2800+i));
|
|
6
|
+
let grayTable=null,grayBrightness=0,grayContrast=0;
|
|
7
|
+
function stableBraille(rgba,width,height,brightness,contrast){
|
|
8
|
+
if(grayBrightness!==brightness||grayContrast!==contrast){
|
|
9
|
+
grayTable=Float32Array.from({length:256},(_,v)=>{
|
|
10
|
+
const luma=(.2126*v+.7152*v+.0722*v)/255;
|
|
11
|
+
return luma===0?0:clamp((Math.pow(luma,.9)*brightness-.5)*contrast+.5);
|
|
12
|
+
});
|
|
13
|
+
grayBrightness=brightness;grayContrast=contrast;
|
|
14
|
+
}
|
|
15
|
+
const table=grayTable,bytePixels=rgba instanceof Uint8Array||rgba instanceof Uint8ClampedArray,columns=Math.ceil(width/2),rows=Math.ceil(height/4),masks=new Uint8Array(columns*rows);
|
|
16
|
+
let dots=0;
|
|
17
|
+
// Stable dithering has no error diffusion: background pixels cannot affect a
|
|
18
|
+
// neighbor. Skip them before luma/hash work, and write straight to cell masks.
|
|
19
|
+
// Float32 rounding matches the original intermediate values array exactly.
|
|
20
|
+
for(let y=0;y<height;y++)for(let x=0,p=y*width*4;x<width;x++,p+=4){
|
|
21
|
+
const r=rgba[p],g=rgba[p+1],b=rgba[p+2],a=rgba[p+3];
|
|
22
|
+
if(a===0||(r===0&&g===0&&b===0))continue;
|
|
23
|
+
let value;
|
|
24
|
+
if(bytePixels&&a===255&&r===g&&g===b)value=table[r];
|
|
25
|
+
else{
|
|
26
|
+
const luma=(.2126*r+.7152*g+.0722*b)/255*(a/255);
|
|
27
|
+
value=Math.fround(luma===0?0:clamp((Math.pow(luma,.9)*brightness-.5)*contrast+.5));
|
|
28
|
+
}
|
|
29
|
+
if(value<=0)continue;
|
|
30
|
+
let n=Math.imul(x+1,0x1f123bb5)^Math.imul(y+1,0x5f356495);
|
|
31
|
+
n=Math.imul(n^(n>>>16),0x45d9f3b);n=Math.imul(n^(n>>>16),0x45d9f3b);n=(n^(n>>>16))>>>0;
|
|
32
|
+
if(value>(n+.5)/4294967296){masks[(y>>2)*columns+(x>>1)]|=BRAILLE_BITS[y&3][x&1];dots++;}
|
|
33
|
+
}
|
|
34
|
+
// Reuse one row of glyph references. Per-cell string concatenation retained
|
|
35
|
+
// thousands of rope nodes until the final join on large desktop grids.
|
|
36
|
+
const lines=new Array(rows),cells=new Array(columns);
|
|
37
|
+
for(let y=0;y<rows;y++){for(let x=0;x<columns;x++)cells[x]=GLYPHS[masks[y*columns+x]];lines[y]=cells.join('');}
|
|
38
|
+
return{text:lines.join('\n'),columns,rows,dots};
|
|
39
|
+
}
|
|
40
|
+
export function rgbaToBraille(rgba,width,height,{brightness=1,contrast=1,method='floyd'}={}){
|
|
41
|
+
if(!Number.isInteger(width)||!Number.isInteger(height)||width<1||height<1||width*height>1_000_000||rgba.length!==width*height*4)throw Error('Invalid RGBA image');
|
|
42
|
+
if(!Number.isFinite(brightness)||brightness<=0||brightness>4||!Number.isFinite(contrast)||contrast<=0||contrast>4||!['floyd','bayer','stable'].includes(method))throw Error('Invalid dithering options');
|
|
43
|
+
if(method==='stable')return stableBraille(rgba,width,height,brightness,contrast);
|
|
44
|
+
const values=new Float32Array(width*height),bits=new Uint8Array(width*height);
|
|
45
|
+
for(let i=0;i<values.length;i++){
|
|
46
|
+
const p=i*4,luma=(.2126*rgba[p]+.7152*rgba[p+1]+.0722*rgba[p+2])/255*(rgba[p+3]/255);
|
|
47
|
+
values[i]=luma===0?0:clamp((Math.pow(luma,.9)*brightness-.5)*contrast+.5);
|
|
48
|
+
}
|
|
49
|
+
if(method==='bayer'){
|
|
50
|
+
for(let y=0;y<height;y++)for(let x=0;x<width;x++)bits[y*width+x]=values[y*width+x]>(BAYER[y%4][x%4]+.5)/16?1:0;
|
|
51
|
+
}else{
|
|
52
|
+
const add=(x,y,error,weight)=>{if(x>=0&&x<width&&y<height)values[y*width+x]+=error*weight/16;};
|
|
53
|
+
for(let y=0;y<height;y++){
|
|
54
|
+
const direction=y%2?-1:1;
|
|
55
|
+
for(let k=0;k<width;k++){
|
|
56
|
+
const x=direction===1?k:width-1-k,i=y*width+x,old=values[i],value=old>=.5?1:0,error=old-value;
|
|
57
|
+
bits[i]=value;add(x+direction,y,error,7);add(x-direction,y+1,error,3);add(x,y+1,error,5);add(x+direction,y+1,error,1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const columns=Math.ceil(width/2),rows=Math.ceil(height/4),lines=[];
|
|
62
|
+
for(let cy=0;cy<rows;cy++){
|
|
63
|
+
let line='';
|
|
64
|
+
for(let cx=0;cx<columns;cx++){
|
|
65
|
+
let mask=0;for(let dy=0;dy<4;dy++)for(let dx=0;dx<2;dx++){
|
|
66
|
+
const x=cx*2+dx,y=cy*4+dy;if(x<width&&y<height&&bits[y*width+x])mask|=BRAILLE_BITS[dy][dx];
|
|
67
|
+
}
|
|
68
|
+
line+=String.fromCodePoint(0x2800+mask);
|
|
69
|
+
}
|
|
70
|
+
lines.push(line);
|
|
71
|
+
}
|
|
72
|
+
return {text:lines.join('\n'),columns,rows,dots:bits.reduce((a,b)=>a+b,0)};
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Loaded only by the default local Node host; browsers never import node:fs.
|
|
2
|
+
import {openSync,readSync,fstatSync,closeSync} from 'node:fs';
|
|
3
|
+
import {gunzipSync} from 'node:zlib';
|
|
4
|
+
import {indexedIndexLength,readIndexedIndex,createIndexedFrameStore} from './puppy-frame-indexed.mjs';
|
|
5
|
+
import {registerFrameStore} from './puppy-frame-store.mjs';
|
|
6
|
+
|
|
7
|
+
export function openFrameFile(url,options){
|
|
8
|
+
const fd=openSync(url,'r');let owned=true;
|
|
9
|
+
try{
|
|
10
|
+
const stat=fstatSync(fd);if(!stat.isFile())throw Error('Invalid frame file');
|
|
11
|
+
const read=(offset,size)=>{const out=new Uint8Array(size);let at=0;while(at<size){const n=readSync(fd,out,at,size-at,offset+at);if(!n)throw Error('Truncated frame file');at+=n;}return out;};
|
|
12
|
+
const header=read(0,20),length=indexedIndexLength(header),index=readIndexedIndex(read(0,length),stat.size);
|
|
13
|
+
const store=registerFrameStore(createIndexedFrameStore(index,{...options,read,expand:(b,n)=>gunzipSync(b,{maxOutputLength:Math.max(1,n)}),close:()=>closeSync(fd)}));owned=false;return store;
|
|
14
|
+
}finally{if(owned)closeSync(fd);}
|
|
15
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// RVZ5: the approved RVQ5 rows, independently gzip-compressed for random access.
|
|
2
|
+
// Header/map match RVQ5; each 24-byte crop record stores x,y,w,h,offset,gzipLength.
|
|
3
|
+
// Offsets are relative to the end of the index. No new quantization or resampling.
|
|
4
|
+
import {unfilterRow} from './puppy-frame-predictive.mjs';
|
|
5
|
+
const HEADER=20,RECORD=24,MAX_PIXELS=16_777_216,MAX_INDEX=64*1024*1024,MAX_EXPANDED=512*1024*1024;
|
|
6
|
+
const fail=why=>{throw Error('Invalid indexed frames: '+why);};
|
|
7
|
+
const viewOf=b=>new DataView(b.buffer,b.byteOffset,b.byteLength);
|
|
8
|
+
export function indexedIndexLength(header){
|
|
9
|
+
if(!(header instanceof Uint8Array)||header.length<HEADER||String.fromCharCode(...header.subarray(0,4))!=='RVZ5')fail('header');
|
|
10
|
+
const v=viewOf(header),width=v.getUint32(4,true),height=v.getUint32(8,true),frames=v.getUint32(12,true),unique=v.getUint32(16,true);
|
|
11
|
+
if(!width||!height||width>16384||height>16384||width*height>MAX_PIXELS||!frames||!unique||unique>frames)fail('shape');
|
|
12
|
+
const length=HEADER+4*frames+RECORD*unique;
|
|
13
|
+
if(length>MAX_INDEX)fail('index size');return length;
|
|
14
|
+
}
|
|
15
|
+
export function readIndexedIndex(bytes,fileSize=bytes.length){
|
|
16
|
+
const length=indexedIndexLength(bytes);if(bytes.length<length||!Number.isSafeInteger(fileSize)||fileSize<length)fail('truncated index');
|
|
17
|
+
const v=viewOf(bytes),width=v.getUint32(4,true),height=v.getUint32(8,true),frameCount=v.getUint32(12,true),uniqueCount=v.getUint32(16,true);
|
|
18
|
+
const map=new Uint32Array(frameCount),used=new Uint8Array(uniqueCount),rects=new Uint32Array(uniqueCount*6);let payload=0,expanded=0;
|
|
19
|
+
for(let i=0;i<frameCount;i++){const u=v.getUint32(HEADER+i*4,true);if(u>=uniqueCount)fail('map');map[i]=u;used[u]=1;}
|
|
20
|
+
if(used.includes(0))fail('unreferenced frame');
|
|
21
|
+
for(let u=0;u<uniqueCount;u++){
|
|
22
|
+
const at=HEADER+frameCount*4+u*RECORD,r=Array.from({length:6},(_,i)=>v.getUint32(at+i*4,true)),[x,y,w,h,offset,size]=r;
|
|
23
|
+
if((w===0)!==(h===0)||(!w&&(x||y))||x+w>width||y+h>height)fail('rectangle');
|
|
24
|
+
if(offset!==payload||size<20)fail('offset');payload+=size;expanded+=h*(w+1);
|
|
25
|
+
if(expanded>MAX_EXPANDED)fail('expanded size');rects.set(r,u*6);
|
|
26
|
+
}
|
|
27
|
+
if(length+payload!==fileSize)fail('file size');
|
|
28
|
+
return {width,height,frameCount,uniqueCount,map,rects,indexBytes:length,expandedBytes:expanded,fileSize};
|
|
29
|
+
}
|
|
30
|
+
const display=Uint8Array.from({length:32},(_,q)=>Math.round(q*255/31));
|
|
31
|
+
|
|
32
|
+
// The host provides bounded positional reads and a synchronous gzip decoder.
|
|
33
|
+
// Only two reconstructed frames survive; compressed art stays on disk.
|
|
34
|
+
export function createIndexedFrameStore(index,{read,expand,close=()=>{},frameIndices,kernel=null}={}){
|
|
35
|
+
if(typeof read!=='function'||typeof expand!=='function')fail('reader');
|
|
36
|
+
const {width,height,frameCount,uniqueCount,map,rects,indexBytes}=index,n=width*height;
|
|
37
|
+
const retained=frameIndices===undefined?null:new Uint8Array(frameCount);
|
|
38
|
+
if(retained){if(!Array.isArray(frameIndices)||!frameIndices.length)fail('retained frames');for(const i of frameIndices){if(!Number.isInteger(i)||i<0||i>=frameCount)fail('retained frame index');retained[i]=1;}}
|
|
39
|
+
const slots=[{unique:-1,frame:null},{unique:-1,frame:null}],cur=new Uint8Array(width),prev=new Uint8Array(width);
|
|
40
|
+
let closed=false,decodes=0,hits=0,diskBytes=0;
|
|
41
|
+
return Object.freeze({width,height,frameCount,uniqueCount,byteLength:n*frameCount,residentPixelBytes:0,
|
|
42
|
+
get residentBytes(){return closed?0:map.byteLength+rects.byteLength+cur.byteLength+prev.byteLength+(retained?.byteLength??0)+slots.reduce((s,e)=>s+(e.frame?.byteLength??0),0);},
|
|
43
|
+
stats(){return {decodes,hits,cachedFrames:slots.filter(s=>s.frame).length,diskReads:decodes,diskBytes,closed};},
|
|
44
|
+
getFrame(i){
|
|
45
|
+
if(closed)throw Error('Frame store is disposed');
|
|
46
|
+
if(!Number.isInteger(i)||i<0||i>=frameCount)fail('frame index');if(retained&&!retained[i])fail('frame was not retained');
|
|
47
|
+
const u=map[i];if(slots[0].unique===u){hits++;return slots[0].frame;}if(slots[1].unique===u){hits++;slots.reverse();return slots[0].frame;}
|
|
48
|
+
const k=u*6,x=rects[k],y=rects[k+1],w=rects[k+2],h=rects[k+3],offset=rects[k+4],size=rects[k+5],expected=h*(w+1);
|
|
49
|
+
const compressed=read(indexBytes+offset,size);if(!(compressed instanceof Uint8Array)||compressed.length!==size)fail('short read');
|
|
50
|
+
const rows=expand(compressed,expected);if(!(rows instanceof Uint8Array)||rows.length!==expected)fail('expanded length');
|
|
51
|
+
// Mark the evicted slot invalid before parsing: a failed decode is never a hit.
|
|
52
|
+
const slot=slots.pop();slots.unshift(slot);slot.unique=-1;const frame=slot.frame??=new Uint8Array(n);frame.fill(0);prev.fill(0);
|
|
53
|
+
if(kernel?.decodeRows)kernel.decodeRows(frame,rows,x,y,w,h,width);else for(let r=0,at=0;r<h;r++){
|
|
54
|
+
const type=rows[at++];if(type>4)fail('filter');
|
|
55
|
+
unfilterRow(type,rows,at,cur,prev,w);at+=w;
|
|
56
|
+
for(let c=0;c<w;c++){
|
|
57
|
+
const q=cur[c];if(q>31)fail('quantized value');frame[(y+r)*width+x+c]=display[q];
|
|
58
|
+
}
|
|
59
|
+
prev.set(cur.subarray(0,w));
|
|
60
|
+
}
|
|
61
|
+
slot.unique=u;decodes++;diskBytes+=size;return frame;
|
|
62
|
+
},
|
|
63
|
+
dispose(){if(closed)return;closed=true;for(const slot of slots){slot.frame=null;slot.unique=-1;}close();},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Portable/custom loaders retain the existing in-memory Q5 representation.
|
|
68
|
+
// The default local Node loader instead reads only the index and requested crops.
|
|
69
|
+
export async function expandIndexedArchive(bytes,expand){
|
|
70
|
+
const index=readIndexedIndex(bytes),{frameCount,uniqueCount,rects,indexBytes,expandedBytes}=index;
|
|
71
|
+
const out=new Uint8Array(indexBytes+expandedBytes);out.set(bytes.subarray(0,indexBytes));out.set([82,86,81,53]);const v=viewOf(out);let offset=0;
|
|
72
|
+
for(let u=0;u<uniqueCount;u++){
|
|
73
|
+
const k=u*6,size=rects[k+3]*(rects[k+2]+1),start=indexBytes+rects[k+4],rows=await expand(bytes.subarray(start,start+rects[k+5]),size);
|
|
74
|
+
if(!(rows instanceof Uint8Array)||rows.length!==size)fail('expanded length');
|
|
75
|
+
const at=HEADER+frameCount*4+u*RECORD;v.setUint32(at+16,offset,true);v.setUint32(at+20,size,true);out.set(rows,indexBytes+offset);offset+=size;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// 32-level predictive grayscale frames (RVQ5), a lossy VISUAL COMPARISON codec
|
|
2
|
+
// built from a lossless RVF1 store. Every pixel v becomes q=round(v*31/255) and
|
|
3
|
+
// is shown as round(q*255/31) (max error 4; 0 and 255 stay exact). The RVF1
|
|
4
|
+
// dedup map and crop rectangles are kept unchanged. Each cropped row stores a
|
|
5
|
+
// PNG filter type (0 None, 1 Sub, 2 Up, 3 Average, 4 Paeth; 1 byte per pixel)
|
|
6
|
+
// followed by w residual bytes (q - prediction) mod 256.
|
|
7
|
+
//
|
|
8
|
+
// Little-endian layout:
|
|
9
|
+
// 0 'RVQ5' 4 u32 width 8 u32 height 12 u32 logicalFrameCount 16 u32 uniqueCount
|
|
10
|
+
// 20 u32[logicalFrameCount] logical -> unique index
|
|
11
|
+
// then uniqueCount records of u32 x,y,w,h,offset,length (length = h*(w+1))
|
|
12
|
+
// then the filtered rows of every unique crop, contiguous in order.
|
|
13
|
+
// At load every crop is validated. The portable store keeps 5-bit packed indices;
|
|
14
|
+
// an optional host codec compresses each filtered crop instead (Node zlib).
|
|
15
|
+
// Neither retains the input archive. Full frames rebuild on demand (LRU of 2).
|
|
16
|
+
const MAGIC=[0x52,0x56,0x51,0x35],RVF1=[0x52,0x56,0x46,0x31],HEADER=20,RECORD=24,MAX_SIDE=16384,BITS=5;
|
|
17
|
+
|
|
18
|
+
export const quantize5=v=>Math.round(v*31/255);
|
|
19
|
+
export const expand5=q=>Math.round(q*255/31);
|
|
20
|
+
const QUANT=Uint8Array.from({length:256},(_,v)=>quantize5(v)),DISPLAY=Uint8Array.from({length:32},(_,q)=>expand5(q));
|
|
21
|
+
export const quantizedGray=Uint8Array.from({length:256},(_,v)=>DISPLAY[QUANT[v]]);
|
|
22
|
+
export const isPredictiveFrames=bytes=>bytes instanceof Uint8Array&&bytes.length>=4&&MAGIC.every((v,i)=>bytes[i]===v);
|
|
23
|
+
|
|
24
|
+
const fail=what=>{throw Error(`Invalid predictive frames: ${what}`);};
|
|
25
|
+
const paeth=(a,b,c)=>{const p=a+b-c,pa=Math.abs(p-a),pb=Math.abs(p-b),pc=Math.abs(p-c);return pa<=pb&&pa<=pc?a:pb<=pc?b:c;};
|
|
26
|
+
export function predict(type,a,b,c){
|
|
27
|
+
switch(type){case 0:return 0;case 1:return a;case 2:return b;case 3:return (a+b)>>1;case 4:return paeth(a,b,c);}
|
|
28
|
+
return fail('filter');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkShape(width,height,frameCount){
|
|
32
|
+
if(!Number.isInteger(width)||!Number.isInteger(height)||width<1||height<1||width>MAX_SIDE||height>MAX_SIDE)fail('header');
|
|
33
|
+
if(!Number.isInteger(frameCount)||frameCount<1||frameCount>0xffffffff||!Number.isSafeInteger(width*height*frameCount))fail('header');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Encode an RVF1 byte store (see puppy-frame-store.mjs). Rows pick the filter
|
|
37
|
+
// with the smallest sum of |signed residual|, lowest type on ties; `filter`
|
|
38
|
+
// forces one type for every row (tests). Deterministic.
|
|
39
|
+
export function encodePredictiveFrames(rvf1,{filter}={}){
|
|
40
|
+
if(!(rvf1 instanceof Uint8Array)||rvf1.length<HEADER||RVF1.some((v,i)=>rvf1[i]!==v))throw Error('Invalid packed frames: magic');
|
|
41
|
+
if(filter!==undefined&&!(Number.isInteger(filter)&&filter>=0&&filter<=4))fail('filter');
|
|
42
|
+
const view=new DataView(rvf1.buffer,rvf1.byteOffset,rvf1.byteLength),u32=at=>view.getUint32(at,true);
|
|
43
|
+
const width=u32(4),height=u32(8),frameCount=u32(12),unique=u32(16),records=HEADER+4*frameCount,data=records+RECORD*unique;
|
|
44
|
+
checkShape(width,height,frameCount);
|
|
45
|
+
if(rvf1.length<data)throw Error('Invalid packed frames: truncated');
|
|
46
|
+
let total=0;
|
|
47
|
+
for(let u=0;u<unique;u++)total+=u32(records+RECORD*u+12)*(u32(records+RECORD*u+8)+1);
|
|
48
|
+
const out=new Uint8Array(data+total),ov=new DataView(out.buffer);
|
|
49
|
+
out.set(rvf1.subarray(0,data));out.set(MAGIC,0);
|
|
50
|
+
const rows=Array.from({length:5},()=>new Uint8Array(width)),cur=new Uint8Array(width),prev=new Uint8Array(width);let at=data,offset=0;
|
|
51
|
+
for(let u=0;u<unique;u++){
|
|
52
|
+
const rec=records+RECORD*u,w=u32(rec+8),h=u32(rec+12),src=data+u32(rec+16);
|
|
53
|
+
if(u32(rec+20)!==w*h||src+w*h>rvf1.length)throw Error('Invalid packed frames: offset');
|
|
54
|
+
ov.setUint32(rec+16,offset,true);ov.setUint32(rec+20,h*(w+1),true);offset+=h*(w+1);
|
|
55
|
+
prev.fill(0);
|
|
56
|
+
for(let y=0;y<h;y++){
|
|
57
|
+
for(let x=0;x<w;x++)cur[x]=QUANT[rvf1[src+y*w+x]];
|
|
58
|
+
let best=filter??-1,bestCost=Infinity;
|
|
59
|
+
for(let t=filter??0;t<=(filter??4);t++){
|
|
60
|
+
const r=rows[t];let cost=0;
|
|
61
|
+
for(let x=0;x<w;x++){const v=(cur[x]-predict(t,x?cur[x-1]:0,prev[x],x?prev[x-1]:0))&255;r[x]=v;cost+=v>127?256-v:v;}
|
|
62
|
+
if(cost<bestCost){bestCost=cost;best=t;}
|
|
63
|
+
}
|
|
64
|
+
out[at++]=best;out.set(rows[best].subarray(0,w),at);at+=w;prev.set(cur.subarray(0,w));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Exact PNG inverse filter of one row into `cur` (bpp 1, integer arithmetic).
|
|
71
|
+
export function unfilterRow(type,src,at,cur,prev,w){
|
|
72
|
+
switch(type){
|
|
73
|
+
case 0:for(let x=0;x<w;x++)cur[x]=src[at+x];return;
|
|
74
|
+
case 1:for(let x=0;x<w;x++)cur[x]=(src[at+x]+(x?cur[x-1]:0))&255;return;
|
|
75
|
+
case 2:for(let x=0;x<w;x++)cur[x]=(src[at+x]+prev[x])&255;return;
|
|
76
|
+
case 3:for(let x=0;x<w;x++)cur[x]=(src[at+x]+(((x?cur[x-1]:0)+prev[x])>>1))&255;return;
|
|
77
|
+
case 4:for(let x=0;x<w;x++)cur[x]=(src[at+x]+paeth(x?cur[x-1]:0,prev[x],x?prev[x-1]:0))&255;return;
|
|
78
|
+
}
|
|
79
|
+
fail('filter');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate and unfilter. Only the returned typed arrays survive; `bytes` is
|
|
83
|
+
// not referenced by the store built from them.
|
|
84
|
+
function decode(bytes,{frameIndices,codec}={}){
|
|
85
|
+
if(codec&&(typeof codec.compress!=='function'||typeof codec.expand!=='function'))fail('codec');
|
|
86
|
+
if(!(bytes instanceof Uint8Array))fail('bytes');
|
|
87
|
+
if(!isPredictiveFrames(bytes)||bytes.length<HEADER)fail('magic');
|
|
88
|
+
const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),u32=at=>view.getUint32(at,true);
|
|
89
|
+
const width=u32(4),height=u32(8),frameCount=u32(12),unique=u32(16);
|
|
90
|
+
checkShape(width,height,frameCount);
|
|
91
|
+
if(unique<1||unique>frameCount)fail('unique count');
|
|
92
|
+
const data=HEADER+4*frameCount+RECORD*unique;
|
|
93
|
+
if(bytes.length<data)fail('truncated');
|
|
94
|
+
const retained=frameIndices===undefined?null:new Uint8Array(frameCount);
|
|
95
|
+
if(retained){
|
|
96
|
+
if(!Array.isArray(frameIndices)||!frameIndices.length)fail('retained frames');
|
|
97
|
+
for(const f of frameIndices){if(!Number.isInteger(f)||f<0||f>=frameCount)fail('retained frame index');retained[f]=1;}
|
|
98
|
+
}
|
|
99
|
+
const map=new Uint32Array(frameCount),used=new Uint8Array(unique),needed=retained?new Uint8Array(unique):null;
|
|
100
|
+
for(let f=0;f<frameCount;f++){const u=u32(HEADER+4*f);if(u>=unique)fail('map');map[f]=u;used[u]=1;if(retained?.[f])needed[u]=1;}
|
|
101
|
+
if(used.includes(0))fail('unreferenced frame');
|
|
102
|
+
// x,y,w,h,first pixel index in the packed stream
|
|
103
|
+
const rects=new Float64Array(unique*5);let total=0,pixels=0;
|
|
104
|
+
for(let u=0;u<unique;u++){
|
|
105
|
+
const at=HEADER+4*frameCount+RECORD*u,x=u32(at),y=u32(at+4),w=u32(at+8),h=u32(at+12),offset=u32(at+16),length=u32(at+20);
|
|
106
|
+
if((w===0)!==(h===0)||(w===0&&(x||y))||x+w>width||y+h>height)fail('rectangle');
|
|
107
|
+
if(length!==h*(w+1)||offset!==total)fail('offset');
|
|
108
|
+
rects.set([x,y,w,h,pixels],u*5);total+=length;if(!needed||needed[u])pixels+=w*h;
|
|
109
|
+
}
|
|
110
|
+
if(bytes.length!==data+total)fail('pixel length');
|
|
111
|
+
if(pixels*BITS>0xffffffff)fail('pixel count');
|
|
112
|
+
const packed=codec?null:new Uint8Array(Math.ceil(pixels*BITS/8)),chunks=codec?new Array(unique):null;
|
|
113
|
+
let cur=new Uint8Array(width),prev=new Uint8Array(width),at=data,pos=0;
|
|
114
|
+
for(let u=0;u<unique;u++){
|
|
115
|
+
const w=rects[u*5+2],h=rects[u*5+3],start=at;prev.fill(0);
|
|
116
|
+
for(let y=0;y<h;y++){
|
|
117
|
+
unfilterRow(bytes[at],bytes,at+1,cur,prev,w);at+=w+1;
|
|
118
|
+
for(let x=0;x<w;x++){
|
|
119
|
+
const q=cur[x];if(q>31)fail('quantized value');if(codec||(needed&&!needed[u]))continue;
|
|
120
|
+
const i=pos>>>3,s=pos&7;packed[i]|=(q<<s)&255;if(s>3)packed[i+1]|=q>>(8-s);pos+=BITS;
|
|
121
|
+
}
|
|
122
|
+
const t=prev;prev=cur;cur=t;
|
|
123
|
+
}
|
|
124
|
+
if(codec&&h&&(!needed||needed[u]))chunks[u]=Uint8Array.from(codec.compress(bytes.subarray(start,at)));
|
|
125
|
+
}
|
|
126
|
+
return{width,height,frameCount,unique,map,rects,packed,retained,chunks,codec};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createStore({width,height,frameCount,unique,map,rects,packed,retained,chunks,codec}){
|
|
130
|
+
const n=width*height,slots=[{unique:-1,frame:null},{unique:-1,frame:null}];let decodes=0,hits=0;
|
|
131
|
+
const pixelBytes=packed?.byteLength??chunks.reduce((sum,c)=>sum+(c?.byteLength??0),0);
|
|
132
|
+
const cur=codec?new Uint8Array(width):null,prev=codec?new Uint8Array(width):null;
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
width,height,frameCount,uniqueCount:unique,byteLength:n*frameCount,residentPixelBytes:pixelBytes,
|
|
135
|
+
get residentBytes(){return pixelBytes+map.byteLength+rects.byteLength+(cur?.byteLength??0)+(prev?.byteLength??0)+(retained?.byteLength??0)+slots.reduce((s,e)=>s+(e.frame?.byteLength??0),0);},
|
|
136
|
+
stats(){return{decodes,hits,cachedFrames:slots.filter(e=>e.frame).length};},
|
|
137
|
+
getFrame(index){
|
|
138
|
+
if(!Number.isInteger(index)||index<0||index>=frameCount)fail('frame index');
|
|
139
|
+
if(retained&&!retained[index])fail('frame was not retained');
|
|
140
|
+
const u=map[index];
|
|
141
|
+
if(slots[0].unique===u){hits++;return slots[0].frame;}
|
|
142
|
+
if(slots[1].unique===u){hits++;slots.reverse();return slots[0].frame;}
|
|
143
|
+
const slot=slots.pop();slots.unshift(slot);decodes++;
|
|
144
|
+
const frame=slot.frame??=new Uint8Array(n),k=u*5,x=rects[k],y=rects[k+1],w=rects[k+2],h=rects[k+3];
|
|
145
|
+
frame.fill(0);slot.unique=u;
|
|
146
|
+
if(codec){
|
|
147
|
+
if(!h)return frame;
|
|
148
|
+
const rows=codec.expand(chunks[u],h*(w+1));
|
|
149
|
+
if(rows.length!==h*(w+1))fail('expanded length');
|
|
150
|
+
prev.fill(0);
|
|
151
|
+
for(let r=0,at=0;r<h;r++,at+=w+1){
|
|
152
|
+
unfilterRow(rows[at],rows,at+1,cur,prev,w);
|
|
153
|
+
for(let c=0;c<w;c++)frame[(y+r)*width+x+c]=DISPLAY[cur[c]];
|
|
154
|
+
prev.set(cur.subarray(0,w));
|
|
155
|
+
}
|
|
156
|
+
return frame;
|
|
157
|
+
}
|
|
158
|
+
let pos=rects[k+4]*BITS;
|
|
159
|
+
for(let r=0;r<h;r++){
|
|
160
|
+
const row=(y+r)*width+x;
|
|
161
|
+
for(let c=0;c<w;c++){const i=pos>>>3,s=pos&7;let v=packed[i]>>s;if(s>3)v|=packed[i+1]<<(8-s);frame[row+c]=DISPLAY[v&31];pos+=BITS;}
|
|
162
|
+
}
|
|
163
|
+
return frame;
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Parse, validate and unpack an RVQ5 store. A returned frame is valid until two
|
|
169
|
+
// other unique frames have been requested.
|
|
170
|
+
export function readPredictiveFrames(bytes,options){return createStore(decode(bytes,options));}
|