buninu-browser 0.7.0 → 0.9.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.
- package/AGENTS.md +5 -0
- package/README.md +90 -1
- package/buninu-browser.js +1 -1
- package/frontends/wayland/README.md +35 -2
- package/frontends/wayland/client.js +8 -6
- package/frontends/wayland/clipboard.js +1 -1
- package/frontends/wayland/index.js +1 -1
- package/frontends/wayland/input.js +1 -1
- package/frontends/wayland/resource-io.js +6 -0
- package/frontends/wayland/transport-posix.js +120 -0
- package/frontends/wayland/transport-win32.js +9 -0
- package/frontends/wayland/transport.js +3 -120
- package/frontends/wayland/win32-resources.js +80 -0
- package/frontends/wayland/win32-wire.js +62 -0
- package/lib/audio/biquad.js +25 -0
- package/lib/audio/piano.js +6 -3
- package/lib/audio/playback-buffer.js +5 -5
- package/lib/audio/pulse-client.js +24 -5
- package/lib/audio/web-audio.js +570 -0
- package/lib/browser-dialog.js +126 -0
- package/lib/cdp-server.js +43 -11
- package/lib/file-picker.js +48 -0
- package/lib/headless-shell.js +34 -12
- package/lib/layout/text-layout.js +112 -19
- package/lib/media/mp4-frame-times.js +93 -0
- package/lib/paint/direct-canvas.ts +39 -0
- package/lib/paint/display-list.js +37 -12
- package/lib/paint/raster-readback.js +16 -7
- package/lib/renderer/animation-frames.js +10 -5
- package/lib/renderer/bindings.js +69 -16
- package/lib/renderer/canvas-2d.js +265 -15
- package/lib/renderer/canvas-round-rect.js +36 -0
- package/lib/renderer/css-animations.js +173 -0
- package/lib/renderer/dialog-client.js +30 -0
- package/lib/renderer/dialog-worker.js +35 -0
- package/lib/renderer/eval-environment.js +28 -0
- package/lib/renderer/file-input.js +49 -0
- package/lib/renderer/host.js +33 -4
- package/lib/renderer/html-drag.js +77 -0
- package/lib/renderer/image-loading.js +45 -2
- package/lib/renderer/main.js +4 -0
- package/lib/renderer/module-transform.js +5 -3
- package/lib/renderer/page-builtins.js +59 -0
- package/lib/renderer/page-realm.js +177 -40
- package/lib/renderer/page-renderer.js +397 -49
- package/lib/renderer/piano-input.js +3 -1
- package/lib/renderer/raster-surface.js +18 -0
- package/lib/renderer/script-rewrite.js +165 -9
- package/lib/renderer/video-presentation.js +5 -4
- package/lib/renderer/webgl/clip.js +24 -0
- package/lib/renderer/webgl/context.js +158 -0
- package/lib/renderer/webgl/cpu-fragment.js +211 -0
- package/lib/renderer/webgl/draw-cache.js +22 -0
- package/lib/renderer/webgl/pipeline.js +70 -0
- package/lib/renderer/webgl/shaders.js +58 -0
- package/lib/renderer/webgl/textures.js +43 -0
- package/lib/renderer/window-named-properties.js +46 -0
- package/lib/screenshot-cli.js +4 -7
- package/lib/style/animation-values.js +29 -0
- package/lib/style/computed-style.js +109 -13
- package/lib/style/transform.js +44 -0
- package/lib/window-size.js +10 -0
- package/package.json +1 -1
- package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
- package/patches/happy-dom/README.md +16 -0
- package/test/animation-frames.test.js +15 -0
- package/test/canvas-2d.test.js +252 -4
- package/test/canvas-frame-pacing.test.js +7 -0
- package/test/cdp-viewers.test.js +36 -0
- package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
- package/test/classic-games-patches/README.md +156 -0
- package/test/classic-games-scenarios.js +133 -0
- package/test/clipboard.test.js +16 -0
- package/test/computed-style.test.js +18 -0
- package/test/css-animation-lifecycle.test.js +28 -0
- package/test/css-animations.test.js +83 -0
- package/test/direct-canvas.test.js +39 -0
- package/test/dynamic-inline-handlers.test.js +25 -0
- package/test/eval-declarations.test.js +22 -0
- package/test/file-picker.test.js +34 -0
- package/test/fixtures/canvas-damage.js +23 -0
- package/test/fixtures/canvas-frame-pacing.js +35 -0
- package/test/fixtures/direct-canvas.js +96 -0
- package/test/fixtures/javascript-dialogs.html +8 -0
- package/test/fixtures/media-audio-lifetime.js +65 -0
- package/test/fixtures/renderer-memory.js +7 -5
- package/test/fixtures/video-black-replay.js +8 -6
- package/test/fixtures/video-slow-end.js +39 -0
- package/test/fixtures/web-audio-renderer.js +45 -0
- package/test/flex.test.js +26 -0
- package/test/grid.test.js +13 -0
- package/test/headless-shell.test.js +9 -0
- package/test/headless-window-size.test.js +28 -0
- package/test/html-rcdata.test.js +35 -0
- package/test/image-decode.test.js +18 -0
- package/test/javascript-dialog.test.js +83 -0
- package/test/manual-breakout-paddle.js +42 -0
- package/test/manual-breakout-static.js +35 -0
- package/test/manual-classic-games.js +41 -0
- package/test/manual-clumsy-bird-three-pipes.js +81 -0
- package/test/manual-clumsy-bird.js +52 -0
- package/test/manual-hextris.js +30 -0
- package/test/manual-numbered-music.js +24 -0
- package/test/manual-t-rex-runner.js +49 -0
- package/test/manual-threejs-scene.js +64 -0
- package/test/manual-webgl-benchmark.js +27 -0
- package/test/manual-webgl-cpu.html +26 -0
- package/test/media-audio-lifetime.test.js +8 -0
- package/test/mp4-frame-times.test.js +33 -0
- package/test/page-scripts.test.js +96 -1
- package/test/piano-input.test.js +15 -2
- package/test/piano.test.js +11 -2
- package/test/playback-buffer.test.js +11 -0
- package/test/pointer-drag.test.js +33 -0
- package/test/positioning.test.js +82 -0
- package/test/pulse-client.test.js +51 -2
- package/test/raster-readback.test.js +14 -0
- package/test/script-rewrite.test.js +61 -3
- package/test/video-slow-end.test.js +6 -0
- package/test/viewport-keyboard-overflow.test.js +25 -0
- package/test/wayland-win32-resources.test.js +47 -0
- package/test/wayland-win32-wire.test.js +59 -0
- package/test/web-audio-renderer.test.js +8 -0
- package/test/web-audio.test.js +264 -0
- package/test/webgl-clip.test.js +9 -0
- package/test/webgl-cpu-fragment.test.js +43 -0
- package/test/window-named-properties.test.js +41 -0
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
- package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
- package/vendor/land-renderer/LICENSE +21 -0
- package/vendor/land-renderer/README.md +14 -0
- package/vendor/land-renderer/direct-compositor.ts +443 -0
- package/vendor/land-renderer/frame.js +88 -0
- package/vendor/land-renderer/png-fast.js +85 -0
- package/vendor/land-renderer/raw-frames.js +66 -0
- package/vendor/land-renderer/render-types.ts +7 -0
- package/vendor/land-renderer/renderer.ts +134 -0
- package/vendor/land-renderer/snapshot.js +37 -0
- package/vendor/land-renderer/viewport.js +18 -0
- package/vendor/ses/dist/ses.mjs +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {createWebAudioScope} from '../lib/audio/web-audio.js';
|
|
3
|
+
|
|
4
|
+
function setup(realtime = false) {
|
|
5
|
+
const outputs=[],closed=[],errors=[];
|
|
6
|
+
const scope=createWebAudioScope({realtime,
|
|
7
|
+
audio:{
|
|
8
|
+
open(options,listener){outputs.push({options,listener,pcm:[]});return outputs.length;},
|
|
9
|
+
write(id,bytes){outputs[id-1].pcm.push(new Int16Array(bytes.buffer,bytes.byteOffset,bytes.byteLength/2).slice());},
|
|
10
|
+
close(id){closed.push(id);},
|
|
11
|
+
},reportError:error=>errors.push(String(error)),
|
|
12
|
+
});
|
|
13
|
+
const context=new scope.api.AudioContext({sampleRate:8000});
|
|
14
|
+
const pull=(frames=128)=>{
|
|
15
|
+
const output=outputs.at(-1);
|
|
16
|
+
output.listener({event:'request',bytes:frames*4});
|
|
17
|
+
return output.pcm.at(-1);
|
|
18
|
+
};
|
|
19
|
+
return {...scope,context,outputs,closed,errors,pull};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('oscillator PCM follows frequency, gain, disconnect and reconnect without duplicate connections',()=>{
|
|
23
|
+
const f=setup(),c=f.context,o=c.createOscillator(),g=c.createGain();
|
|
24
|
+
try {
|
|
25
|
+
o.frequency.setValueAtTime(500,c.currentTime);g.gain.value=.25;
|
|
26
|
+
expect(o.connect(g)).toBe(g);g.connect(c.destination);g.connect(c.destination);o.start();
|
|
27
|
+
const first=f.pull();
|
|
28
|
+
expect(first[8]).toBeCloseTo(8192,0); // Stereo frame 4: sin(pi/2) * .25.
|
|
29
|
+
for(let i=0;i<first.length;i+=2)expect(first[i]).toBe(first[i+1]);
|
|
30
|
+
o.frequency.setValueAtTime(1000,c.currentTime);
|
|
31
|
+
const second=f.pull();expect(second[4]).toBeCloseTo(8192,0);
|
|
32
|
+
o.disconnect();expect([...f.pull()].every(x=>x===0)).toBe(true);
|
|
33
|
+
o.connect(g);g.gain.value=.1;expect(f.pull()[4]).toBeCloseTo(3277,0);
|
|
34
|
+
expect(c.currentTime).toBe(512/8000);
|
|
35
|
+
expect(f.outputs).toHaveLength(1);
|
|
36
|
+
}finally{f.dispose();}
|
|
37
|
+
expect(f.closed).toEqual([1]);expect(f.errors).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('scheduled start, stop, gain ramps and ended events use the audio sample timeline',async()=>{
|
|
41
|
+
const f=setup(),c=f.context,o=c.createOscillator(),g=c.createGain();let ended=0;
|
|
42
|
+
try {
|
|
43
|
+
o.frequency.value=500;o.connect(g).connect(c.destination);
|
|
44
|
+
g.gain.setValueAtTime(0,0).linearRampToValueAtTime(1,.032);
|
|
45
|
+
o.onended=()=>ended++;o.start(.016);o.stop(.04);
|
|
46
|
+
const samples=f.pull(512);
|
|
47
|
+
expect([...samples.slice(0,128*2)].every(x=>x===0)).toBe(true);
|
|
48
|
+
expect(samples[(128+4)*2]).toBeCloseTo(Math.round(32767*132/256),0);
|
|
49
|
+
expect([...samples.slice(320*2)].every(x=>x===0)).toBe(true);
|
|
50
|
+
await Bun.sleep(10);expect(ended).toBe(1);
|
|
51
|
+
f.pull();await Bun.sleep(5);expect(ended).toBe(1);
|
|
52
|
+
expect(()=>o.start()).toThrow();
|
|
53
|
+
}finally{f.dispose();}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('suspend freezes the context and resume reopens only its own stream; close rejects later resume',async()=>{
|
|
57
|
+
const f=setup(),c=f.context,o=c.createOscillator();
|
|
58
|
+
o.connect(c.destination);o.start();f.pull();const time=c.currentTime;
|
|
59
|
+
await c.suspend();expect(c.state).toBe('suspended');expect(f.closed).toEqual([1]);
|
|
60
|
+
f.outputs[0].listener({event:'request',bytes:512});expect(c.currentTime).toBe(time);
|
|
61
|
+
await c.resume();expect(c.state).toBe('running');expect(f.outputs).toHaveLength(2);
|
|
62
|
+
f.pull();expect(c.currentTime).toBeGreaterThan(time);
|
|
63
|
+
await c.close();expect(c.state).toBe('closed');expect(f.closed).toEqual([1,2]);
|
|
64
|
+
await expect(c.resume()).rejects.toMatchObject({name:'InvalidStateError'});
|
|
65
|
+
f.dispose();expect(f.closed).toEqual([1,2]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('parameter ramps can be held and cancelled; invalid times, ports and cross-context graphs reject',()=>{
|
|
69
|
+
const f=setup(),c=f.context,o=c.createOscillator(),g=c.createGain();
|
|
70
|
+
try {
|
|
71
|
+
const p=g.gain;
|
|
72
|
+
p.setValueAtTime(0,0).linearRampToValueAtTime(1,.1).cancelAndHoldAtTime(.05);
|
|
73
|
+
f.pull(800);expect(p.value).toBeCloseTo(.5);
|
|
74
|
+
p.cancelScheduledValues(0);p.value=.25;expect(p.value).toBe(.25);
|
|
75
|
+
expect(()=>p.setValueAtTime(1,-1)).toThrow(RangeError);
|
|
76
|
+
expect(()=>p.setValueAtTime(NaN,0)).toThrow(TypeError);
|
|
77
|
+
expect(()=>p.exponentialRampToValueAtTime(0,1)).toThrow(RangeError);
|
|
78
|
+
expect(()=>o.stop()).toThrow();
|
|
79
|
+
expect(()=>o.connect(c.destination,1)).toThrow();
|
|
80
|
+
const other=new f.api.AudioContext();expect(()=>o.connect(other.destination)).toThrow();
|
|
81
|
+
}finally{f.dispose();}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('multiple sources mix in one stream and cycles cannot recurse indefinitely',()=>{
|
|
85
|
+
const f=setup(),c=f.context,a=c.createOscillator(),b=new f.api.OscillatorNode(c,{frequency:1000,detune:1200});
|
|
86
|
+
try {
|
|
87
|
+
const g=new f.api.GainNode(c,{gain:.1});
|
|
88
|
+
a.frequency.value=500;a.connect(g);b.connect(g);g.connect(c.destination);
|
|
89
|
+
a.start();b.start();const samples=f.pull();
|
|
90
|
+
expect(samples[4]).toBeCloseTo(Math.round(32767*.1*Math.sin(Math.PI/4)),0);
|
|
91
|
+
expect(f.outputs).toHaveLength(1);
|
|
92
|
+
g.connect(g);expect([...f.pull()].every(Number.isFinite)).toBe(true);
|
|
93
|
+
expect(()=>new f.api.AudioNode(c)).toThrow(TypeError);
|
|
94
|
+
expect(()=>new f.api.AudioParam()).toThrow(TypeError);
|
|
95
|
+
expect(f.api.webkitAudioContext).toBe(f.api.AudioContext);
|
|
96
|
+
}finally{f.dispose();}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('sample buffers play through the existing PCM graph with looping, rate and ended events',async()=>{
|
|
100
|
+
const f=setup(),c=f.context;
|
|
101
|
+
try{
|
|
102
|
+
const b=c.createBuffer(1,4,8000);b.copyToChannel(new Float32Array([0,.25,.5,.75]),0);
|
|
103
|
+
expect([...b.getChannelData(0)]).toEqual([0,.25,.5,.75]);expect(b.duration).toBe(.0005);
|
|
104
|
+
const copy=new Float32Array(3);b.copyFromChannel(copy,0,1);expect([...copy]).toEqual([.25,.5,.75]);
|
|
105
|
+
expect(()=>b.getChannelData(1)).toThrow();
|
|
106
|
+
const node=c.createBufferSource();let ended=0;node.buffer=b;node.onended=()=>ended++;
|
|
107
|
+
node.loop=true;node.loopStart=1/8000;node.loopEnd=3/8000;
|
|
108
|
+
node.connect(c.destination);node.start();
|
|
109
|
+
const pcm=f.pull(8);expect([...pcm.filter((_,i)=>i%2===0)]).toEqual([0,8192,16384,8192,16384,8192,16384,8192]);
|
|
110
|
+
node.stop(c.currentTime);f.pull(128);await Bun.sleep(10);expect(ended).toBe(1);
|
|
111
|
+
const faster=c.createBufferSource();faster.buffer=b;faster.playbackRate.value=2;faster.connect(c.destination);faster.start();
|
|
112
|
+
expect([...f.pull(128).filter((_,i)=>i%2===0)].filter(x=>x!==0)).toEqual([16384]);
|
|
113
|
+
expect(()=>faster.start()).toThrow();
|
|
114
|
+
}finally{f.dispose();}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('decodeAudioData detaches input, resamples samples, completes callbacks and closes the decoder',async()=>{
|
|
118
|
+
let closed=0,success=0,failures=0;
|
|
119
|
+
const scope=createWebAudioScope({realtime:false,decodeAudio:async bytes=>{
|
|
120
|
+
if(bytes[0]===0)throw Error('invalid compressed sound');
|
|
121
|
+
let first=true;return {sampleRate:16000,channels:1,nextSamples(){if(!first)return null;first=false;return new Float32Array([0,.25,.5,.75,1,.75,.5,.25]);},close(){closed++;}};
|
|
122
|
+
}});
|
|
123
|
+
const c=new scope.api.AudioContext({sampleRate:8000});
|
|
124
|
+
try{
|
|
125
|
+
const input=new Uint8Array([1]).buffer;
|
|
126
|
+
const pending=c.decodeAudioData(input,b=>{success++;expect(b.sampleRate).toBe(8000);});
|
|
127
|
+
expect(input.byteLength).toBe(0);
|
|
128
|
+
const b=await pending;expect([...b.getChannelData(0)]).toEqual([0,.5,1,.5]);expect(success).toBe(1);expect(closed).toBe(1);
|
|
129
|
+
await expect(c.decodeAudioData(new Uint8Array([0]).buffer,null,e=>{failures++;expect(e.name).toBe('EncodingError');})).rejects.toMatchObject({name:'EncodingError'});
|
|
130
|
+
expect(failures).toBe(1);
|
|
131
|
+
await expect(c.decodeAudioData(new Uint8Array([1]))).rejects.toThrow('ArrayBuffer');
|
|
132
|
+
}finally{scope.dispose();}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('BiquadFilterNode reports frequency response and filters actual PCM',async()=>{
|
|
136
|
+
const t=setup(),{context}=t;
|
|
137
|
+
try{
|
|
138
|
+
const filter=context.createBiquadFilter();filter.frequency.value=200;filter.Q.value=0;
|
|
139
|
+
expect(filter).toBeInstanceOf(t.api.BiquadFilterNode);
|
|
140
|
+
const frequencies=new Float32Array([0,100,2000,5000]),magnitude=new Float32Array(4),phase=new Float32Array(4);
|
|
141
|
+
filter.getFrequencyResponse(frequencies,magnitude,phase);
|
|
142
|
+
expect(magnitude[0]).toBeCloseTo(1,5);expect(magnitude[1]).toBeGreaterThan(.8);
|
|
143
|
+
expect(magnitude[2]).toBeLessThan(.01);expect(Number.isNaN(magnitude[3])).toBe(true);
|
|
144
|
+
expect(()=>filter.getFrequencyResponse(frequencies,new Float32Array(1),phase)).toThrow();
|
|
145
|
+
const oscillator=context.createOscillator();oscillator.frequency.value=2000;oscillator.connect(filter);filter.connect(context.destination);oscillator.start();
|
|
146
|
+
await context.resume();let filtered;
|
|
147
|
+
for(let i=0;i<12;i++)filtered=t.pull();
|
|
148
|
+
const rms=samples=>Math.sqrt(samples.reduce((sum,value)=>sum+value*value,0)/samples.length);
|
|
149
|
+
expect(rms(filtered)).toBeLessThan(300);
|
|
150
|
+
filter.type='highpass';let passed;
|
|
151
|
+
for(let i=0;i<12;i++)passed=t.pull();
|
|
152
|
+
expect(rms(passed)).toBeGreaterThan(10000);
|
|
153
|
+
}finally{await context.close();t.dispose();}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
test('live synthesis yields between bounded packets so page timers can schedule future notes', async()=>{
|
|
158
|
+
const f=setup(true),c=f.context;let timer;
|
|
159
|
+
try {
|
|
160
|
+
let next=0;
|
|
161
|
+
const schedule=()=>{while(next<c.currentTime+.1){const o=c.createOscillator();o.frequency.value=500;o.connect(c.destination);o.start(next);o.stop(next+.055);next+=.04;}};
|
|
162
|
+
schedule();timer=setInterval(schedule,25);
|
|
163
|
+
const started=performance.now();f.outputs[0].listener({event:'request',bytes:8000*.25*4});
|
|
164
|
+
expect(c.currentTime).toBeLessThanOrEqual(.08+128/c.sampleRate);
|
|
165
|
+
await Bun.sleep(170);
|
|
166
|
+
expect(c.currentTime).toBeLessThan((performance.now()-started)/1000+.10);
|
|
167
|
+
const pcm=f.outputs[0].pcm;expect(pcm.length).toBeGreaterThan(3);
|
|
168
|
+
expect(pcm.every(packet=>packet.length<=8000*.096*2)).toBe(true);
|
|
169
|
+
expect(pcm.every(packet=>packet.some(value=>value!==0))).toBe(true);
|
|
170
|
+
} finally {clearInterval(timer);f.dispose();}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
test('silent connected gain branches remain reconnectable and preserve elapsed automation',()=>{
|
|
175
|
+
const f=setup(),c=f.context;
|
|
176
|
+
try {
|
|
177
|
+
const master=c.createGain();master.connect(c.destination);
|
|
178
|
+
for(let i=0;i<300;i++){const g=c.createGain();g.gain.setValueAtTime(.1,0).exponentialRampToValueAtTime(.001,.1);g.connect(master);}
|
|
179
|
+
const gain=c.createGain();gain.gain.setValueAtTime(0,0).linearRampToValueAtTime(.5,.032);gain.connect(master);
|
|
180
|
+
expect([...f.pull(256)].every(v=>v===0)).toBe(true);
|
|
181
|
+
const o=c.createOscillator();o.frequency.value=500;o.connect(gain);o.start();
|
|
182
|
+
expect(f.pull()[8]).toBeCloseTo(Math.round(32767*.5),0);
|
|
183
|
+
o.disconnect();expect([...f.pull()].every(v=>v===0)).toBe(true);
|
|
184
|
+
o.connect(gain);expect(f.pull().some(v=>v!==0)).toBe(true);
|
|
185
|
+
}finally{f.dispose();}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
test('live audio catches up after repeated long paint tasks instead of slowing its clock',async()=>{
|
|
190
|
+
const f=setup(true),c=f.context,o=c.createOscillator();let painting;
|
|
191
|
+
try {
|
|
192
|
+
o.connect(c.destination);o.start();
|
|
193
|
+
f.outputs[0].listener({event:'request',bytes:8000*2*4});
|
|
194
|
+
const started=performance.now();
|
|
195
|
+
painting=setInterval(()=>{const end=performance.now()+140;while(performance.now()<end){}},16);
|
|
196
|
+
await Bun.sleep(700);
|
|
197
|
+
clearInterval(painting);
|
|
198
|
+
const elapsed=(performance.now()-started)/1000;
|
|
199
|
+
expect(c.currentTime).toBeGreaterThan(elapsed*.9);
|
|
200
|
+
expect(c.currentTime).toBeLessThan(elapsed+.10);
|
|
201
|
+
}finally{clearInterval(painting);f.dispose();}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('unreferenced finished filter graphs are collected after their real tails decay',async()=>{
|
|
205
|
+
const f=setup(),c=f.context;
|
|
206
|
+
let weakFilter,weakGain;
|
|
207
|
+
(()=>{
|
|
208
|
+
const source=c.createBufferSource(),filter=c.createBiquadFilter(),gain=c.createGain();
|
|
209
|
+
weakFilter=new WeakRef(filter);weakGain=new WeakRef(gain);
|
|
210
|
+
filter.type='lowpass';filter.frequency.value=100;filter.Q.value=10;
|
|
211
|
+
const buffer=c.createBuffer(1,1,c.sampleRate);buffer.getChannelData(0)[0]=1;source.buffer=buffer;
|
|
212
|
+
source.connect(filter).connect(gain).connect(c.destination);source.start();
|
|
213
|
+
})();
|
|
214
|
+
try{
|
|
215
|
+
f.pull(128);await Bun.sleep(5);Bun.gc(true);await Bun.sleep(0);
|
|
216
|
+
expect(weakFilter.deref()!==undefined).toBe(true); // tail owns its path after source ends
|
|
217
|
+
expect(f.pull(128).some(v=>v!==0)).toBe(true);
|
|
218
|
+
f.pull(8000); // let the IIR decay below float32's normal range
|
|
219
|
+
for(let i=0;i<4;i++){await Bun.sleep(5);expect(f.pull().every(v=>v===0)).toBe(true);Bun.gc(true);}
|
|
220
|
+
expect(weakFilter.deref()===undefined).toBe(true);
|
|
221
|
+
expect(weakGain.deref()===undefined).toBe(true);
|
|
222
|
+
expect(f.pull().every(v=>v===0)).toBe(true);
|
|
223
|
+
}finally{f.dispose();}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('an author-held silent filter remains reconnectable after collection of other graphs',async()=>{
|
|
227
|
+
const f=setup(),c=f.context,filter=c.createBiquadFilter();
|
|
228
|
+
filter.type='lowpass';filter.frequency.value=500;filter.connect(c.destination);
|
|
229
|
+
try{
|
|
230
|
+
f.pull(256);await Bun.sleep(5);Bun.gc(true);
|
|
231
|
+
const source=c.createOscillator();source.frequency.value=200;source.connect(filter);source.start();
|
|
232
|
+
expect(f.pull(256).some(v=>v!==0)).toBe(true);
|
|
233
|
+
source.stop();f.pull(8000);expect(f.pull().every(v=>v===0)).toBe(true);
|
|
234
|
+
const next=c.createOscillator();next.frequency.value=200;next.connect(filter);next.start();
|
|
235
|
+
expect(f.pull(256).some(v=>v!==0)).toBe(true);
|
|
236
|
+
}finally{f.dispose();}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('future scheduled voices retain intermediate processing nodes until playback',async()=>{
|
|
240
|
+
const f=setup(),c=f.context;let weak;
|
|
241
|
+
(()=>{const source=c.createOscillator(),filter=c.createBiquadFilter(),gain=c.createGain();
|
|
242
|
+
source.frequency.value=500;filter.frequency.value=1000;gain.gain.value=.25;
|
|
243
|
+
weak=new WeakRef(filter);source.connect(filter).connect(gain).connect(c.destination);source.start(.1);source.stop(.2);
|
|
244
|
+
})();
|
|
245
|
+
try{
|
|
246
|
+
for(let i=0;i<3;i++){await Bun.sleep(5);Bun.gc(true)}
|
|
247
|
+
expect(weak.deref()!==undefined).toBe(true);
|
|
248
|
+
const samples=f.pull(2048);
|
|
249
|
+
expect(samples.slice(0,800*2).every(v=>v===0)).toBe(true);
|
|
250
|
+
expect(samples.slice(800*2).some(v=>v!==0)).toBe(true);
|
|
251
|
+
}finally{f.dispose();}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('AudioParam modulation keeps its running input alive through weak input connections',async()=>{
|
|
255
|
+
const f=setup(),c=f.context,signal=c.createOscillator(),gain=c.createGain();
|
|
256
|
+
gain.gain.value=0;signal.frequency.value=500;signal.connect(gain).connect(c.destination);signal.start();
|
|
257
|
+
let mod;
|
|
258
|
+
(()=>{const oscillator=c.createOscillator();oscillator.type='square';oscillator.frequency.value=0;oscillator.connect(gain.gain);oscillator.start();mod=new WeakRef(oscillator)})();
|
|
259
|
+
try{
|
|
260
|
+
await Bun.sleep(5);Bun.gc(true);
|
|
261
|
+
expect(f.pull()[8]).toBeCloseTo(32767,0);
|
|
262
|
+
mod.deref().stop();expect(f.pull().every(v=>v===0)).toBe(true);
|
|
263
|
+
}finally{f.dispose();}
|
|
264
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{test,expect}from'bun:test';
|
|
2
|
+
import{clipTriangles}from'../lib/renderer/webgl/clip.js';
|
|
3
|
+
test('homogeneous clipping keeps indexed geometry inside and interpolates crossings',()=>{
|
|
4
|
+
const source=new Float32Array([-.5,-.5,0,1,0,.5,-.5,0,1,1,0,.5,0,1,.5]),indices=[0,1,2];
|
|
5
|
+
const inside=clipTriangles(source,indices,5);expect(inside.vertices).toBe(source);expect(inside.indices).toBe(indices);
|
|
6
|
+
source[5]=2;const clipped=clipTriangles(source,indices,5);expect(clipped.indices.length).toBe(6);
|
|
7
|
+
for(let i=0;i<clipped.vertices.length;i+=5){const v=clipped.vertices.subarray(i,i+5);expect(Math.abs(v[0])).toBeLessThanOrEqual(v[3]);expect(v[4]).toBeGreaterThanOrEqual(0);expect(v[4]).toBeLessThanOrEqual(1);}
|
|
8
|
+
source[0]=NaN;expect(clipTriangles(source,indices,5).indices).toEqual([]);
|
|
9
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {beforeAll,expect,test} from 'bun:test';
|
|
2
|
+
import init from '../usr/lib/canvaskit/canvaskit.js';
|
|
3
|
+
import {compileStage,shader,floatImage,imageShader} from '../lib/renderer/webgl/pipeline.js';
|
|
4
|
+
import {runCPUFragment} from '../lib/renderer/webgl/cpu-fragment.js';
|
|
5
|
+
let kit;
|
|
6
|
+
beforeAll(async()=>{kit=await init({wasmBinary:await Bun.file(new URL('../usr/lib/canvaskit/canvaskit.wasm',import.meta.url)).arrayBuffer()});});
|
|
7
|
+
function compare(source,values=new Map()){
|
|
8
|
+
const stage=compileStage(kit,source,'fragment');expect(stage.cpu).not.toBeNull();
|
|
9
|
+
const width=8,height=8,coverage=new Float32Array(width*height*4),varyings=stage.varyings.map(()=>new Float32Array(coverage.length));
|
|
10
|
+
for(let y=0;y<height;y++)for(let x=0;x<width;x++){const i=(y*width+x)*4;coverage.set([1,height-y-.5,.5,1],i);for(const v of varyings)v.set([(x+.5)/width,(y+.5)/height,.25,1],i);}
|
|
11
|
+
const images=[coverage,...varyings].map(v=>floatImage(kit,width,height,v)),children=images.map(i=>imageShader(kit,i)),surface=kit.MakeSurface(width,height),paint=new kit.Paint();let effect;
|
|
12
|
+
try{effect=shader(kit,stage,values,children);paint.setShader(effect);surface.getCanvas().drawPaint(paint);surface.flush();const snapshot=surface.makeImageSnapshot();let expected;try{expected=snapshot.readPixels(0,0,{width,height,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Premul,colorSpace:kit.ColorSpace.SRGB});}finally{snapshot.delete();}
|
|
13
|
+
const actual=runCPUFragment(stage,values,[],coverage,varyings,width,height,0,0,height);
|
|
14
|
+
let difference=0;for(let i=0;i<actual.length;i++)difference=Math.max(difference,Math.abs(actual[i]-expected[i]));expect(difference).toBeLessThanOrEqual(2);return actual;
|
|
15
|
+
}finally{paint.delete();effect?.delete();surface.delete();for(const c of children)c.delete();for(const i of images)i.delete();stage.effect.delete();}
|
|
16
|
+
}
|
|
17
|
+
test('numeric CPU fragment lighting and derivatives agree with CanvasKit',()=>{
|
|
18
|
+
compare('precision highp float;varying vec3 n;uniform vec3 light;void main(){vec3 N=normalize(n);float v=pow(max(dot(N,normalize(light)),0.),3.);gl_FragColor=vec4(v,abs(dFdx(n.x)),abs(dFdy(n.y)),.75);}',new Map([['light',[1,2,3]]]));
|
|
19
|
+
});
|
|
20
|
+
test('conditional expressions and logical operators do not evaluate skipped side effects',()=>{
|
|
21
|
+
const pixels=compare('precision highp float;float bump(inout float x){x+=.2;return x;}void main(){float x=0.;float a=true?bump(x):bump(x);bool b=false&&(bump(x)>0.);bool c=true||(bump(x)>0.);gl_FragColor=vec4(x,a,0.,1.);}');
|
|
22
|
+
expect([...pixels.slice(0,4)]).toEqual([51,51,0,255]);
|
|
23
|
+
});
|
|
24
|
+
test('mutable shader globals are reset between pixels',()=>{
|
|
25
|
+
const pixels=compare('precision highp float;float v=.25;void main(){v+=.25;gl_FragColor=vec4(v,0.,0.,1.);}');
|
|
26
|
+
expect(pixels[0]).toBe(128);expect(pixels.at(-4)).toBe(128);
|
|
27
|
+
});
|
|
28
|
+
test('control-flow derivatives retain the CanvasKit fallback',()=>{
|
|
29
|
+
for(const body of ['if(gl_FragCoord.x>3.)gl_FragColor=vec4(dFdx(gl_FragCoord.x));else gl_FragColor=vec4(1.);']){
|
|
30
|
+
const stage=compileStage(kit,'precision highp float;void main(){'+body+'}','fragment');expect(stage.cpu).toBeNull();stage.effect.delete();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
test('numeric vertex transforms and varyings agree with CanvasKit float output',async()=>{
|
|
34
|
+
const {runVertices}=await import('../lib/renderer/webgl/pipeline.js');
|
|
35
|
+
const stage=compileStage(kit,'attribute vec3 position;attribute vec3 normal;uniform mat4 model;uniform mat3 normals;varying vec3 v;void main(){v=normalize(normals*normal);gl_Position=model*vec4(position,1.);}','vertex');
|
|
36
|
+
try{
|
|
37
|
+
expect(stage.cpuVertex).not.toBeNull();
|
|
38
|
+
const values=new Map([['model',[2,0,0,.1,0,3,0,.2,0,0,4,.3,5,6,7,1]],['normals',[1,0,0,0,2,0,0,0,3]]]);
|
|
39
|
+
const attrs=new Map([['position',new Float32Array([1,2,3,-1,.5,2])],['normal',new Float32Array([1,2,3,3,2,1])]]);
|
|
40
|
+
const actual=runVertices(kit,stage,values,attrs,2),reference=runVertices(kit,{...stage,cpuVertex:null},values,attrs,2);
|
|
41
|
+
for(let i=0;i<actual.length;i++)expect(actual[i]).toBeCloseTo(reference[i],5);
|
|
42
|
+
}finally{stage.effect.delete();}
|
|
43
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {test, expect} from 'bun:test';
|
|
2
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
3
|
+
import {parseHTMLDocument} from '../lib/happy-dom/parser.js';
|
|
4
|
+
|
|
5
|
+
test('Window named elements are live, shadowable, and available in classic scripts and inline handlers', async () => {
|
|
6
|
+
const host = new RendererHost();
|
|
7
|
+
try {
|
|
8
|
+
await host.call('loadDocument', {url:'https://example.test/',source:`
|
|
9
|
+
<textarea id="source">hello</textarea><textarea id="result"></textarea>
|
|
10
|
+
<form name="namedForm"></form><input name="notGlobal"><div id="dupe"></div><div id="dupe"></div>
|
|
11
|
+
<button onclick="result.value=source.value.toUpperCase()">Convert</button>
|
|
12
|
+
<script>window.initial = source.value; window.group = dupe;</script>`,contentType:'text/html'});
|
|
13
|
+
expect(await host.call('evaluate', '[initial, namedForm.tagName, typeof notGlobal, dupe.length, dupe.item(0).tagName]')).toEqual(['hello','FORM','undefined',2,'DIV']);
|
|
14
|
+
expect(await host.call('evaluate', 'document.querySelector("button").click(); result.value')).toBe('HELLO');
|
|
15
|
+
expect(await host.call('evaluate', 'source.id="renamed"; [typeof source, renamed.value]')).toEqual(['undefined','hello']);
|
|
16
|
+
expect(await host.call('evaluate', 'window.renamed=42; let x=renamed;delete window.renamed;[x,renamed.value]')).toEqual([42,'hello']);
|
|
17
|
+
expect(await host.call('evaluate', 'document.querySelectorAll("#dupe")[0].remove(); [group.length, dupe.tagName]')).toEqual([1,'DIV']);
|
|
18
|
+
expect(await host.call('evaluate', 'renamed.remove(); typeof renamed')).toBe('undefined');
|
|
19
|
+
expect(await host.call('evaluate', `eval('(async()=>{\\nresult.value="first"\\nresult.value += " second"\\n})()'); result.value`)).toBe('first second');
|
|
20
|
+
} finally {host.close();}
|
|
21
|
+
},20000);
|
|
22
|
+
|
|
23
|
+
test('type selectors accept underscores, including escaped punctuation and selector lists', () => {
|
|
24
|
+
const {document} = parseHTMLDocument('<x_widget></x_widget><g:family_creation></g:family_creation>');
|
|
25
|
+
expect(document.querySelectorAll('x_widget, g\\:family_creation').length).toBe(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('deferred absolute backgrounds paint before later relative links, including after relayout', async () => {
|
|
29
|
+
const host = new RendererHost();
|
|
30
|
+
try {
|
|
31
|
+
await host.call('loadDocument',{url:'https://example.test/',contentType:'text/html',source:`
|
|
32
|
+
<style>body{margin:0}.container{position:relative}.background{position:absolute;inset:0;background:red}
|
|
33
|
+
.front{position:relative}a{display:block;height:50px;background:green}</style>
|
|
34
|
+
<div class="container"><div class="background"><div style="height:100%"></div></div><div class="front"><a href="#ok">Click</a></div></div>`});
|
|
35
|
+
for (const width of [800,600,800]) {
|
|
36
|
+
await host.call('resize',width,600,1);
|
|
37
|
+
expect(await host.call('evaluate','document.elementFromPoint(20,20).tagName')).toBe('A');
|
|
38
|
+
expect(await host.call('click',20,20)).toBeTruthy();
|
|
39
|
+
}
|
|
40
|
+
}finally{host.close();}
|
|
41
|
+
},20000);
|
|
@@ -78,7 +78,7 @@ export default <
|
|
|
78
78
|
},
|
|
79
79
|
textarea: {
|
|
80
80
|
className: 'HTMLTextAreaElement',
|
|
81
|
-
contentModel: HTMLElementConfigContentModelEnum.
|
|
81
|
+
contentModel: HTMLElementConfigContentModelEnum.rcdata
|
|
82
82
|
},
|
|
83
83
|
script: {
|
|
84
84
|
className: 'HTMLScriptElement',
|
|
@@ -499,7 +499,7 @@ export default <
|
|
|
499
499
|
},
|
|
500
500
|
title: {
|
|
501
501
|
className: 'HTMLTitleElement',
|
|
502
|
-
contentModel: HTMLElementConfigContentModelEnum.
|
|
502
|
+
contentModel: HTMLElementConfigContentModelEnum.rcdata
|
|
503
503
|
},
|
|
504
504
|
tr: {
|
|
505
505
|
className: 'HTMLTableRowElement',
|
|
@@ -326,6 +326,18 @@ export default class HTMLParser {
|
|
|
326
326
|
break;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
if (this.currentNode && this.readState === MarkupReadStateEnum.any &&
|
|
330
|
+
(<Element>this.currentNode)[PropertySymbol.namespaceURI] === NamespaceURI.html &&
|
|
331
|
+
HTMLElementConfig[(<Element>this.currentNode)[PropertySymbol.localName]]?.contentModel === HTMLElementConfigContentModelEnum.rcdata) {
|
|
332
|
+
const tag = (<Element>this.currentNode)[PropertySymbol.localName];
|
|
333
|
+
const end = new RegExp('</' + tag + '(?=[\\t\\n\\f\\r />])[^>]*>', 'gi');
|
|
334
|
+
end.lastIndex = this.markupRegExp.lastIndex;
|
|
335
|
+
const closing = end.exec(html);
|
|
336
|
+
const text = html.slice(this.markupRegExp.lastIndex, closing?.index ?? html.length);
|
|
337
|
+
this.parseRawTextElementContent(tag, text);
|
|
338
|
+
this.markupRegExp.lastIndex = closing ? end.lastIndex : html.length;
|
|
339
|
+
}
|
|
340
|
+
|
|
329
341
|
lastIndex = this.markupRegExp.lastIndex;
|
|
330
342
|
}
|
|
331
343
|
|
|
@@ -467,12 +479,14 @@ export default class HTMLParser {
|
|
|
467
479
|
|
|
468
480
|
const tagName = this.nextElement![PropertySymbol.tagName]!;
|
|
469
481
|
const lowerTagName = tagName.toLowerCase();
|
|
470
|
-
const config =
|
|
482
|
+
const config = this.nextElement![PropertySymbol.namespaceURI] === NamespaceURI.html
|
|
483
|
+
? HTMLElementConfig[lowerTagName] : undefined;
|
|
471
484
|
let previousCurrentNode: Node | null = null;
|
|
472
485
|
|
|
473
486
|
while (previousCurrentNode !== this.rootNode) {
|
|
474
487
|
const parentLowerTagName = (<Element>this.currentNode)[PropertySymbol.tagName]?.toLowerCase();
|
|
475
|
-
const parentConfig = parentLowerTagName
|
|
488
|
+
const parentConfig = parentLowerTagName && (<Element>this.currentNode)[PropertySymbol.namespaceURI] === NamespaceURI.html
|
|
489
|
+
? HTMLElementConfig[parentLowerTagName] : null;
|
|
476
490
|
|
|
477
491
|
if (previousCurrentNode === this.currentNode) {
|
|
478
492
|
throw new Error(
|
|
@@ -601,7 +615,8 @@ export default class HTMLParser {
|
|
|
601
615
|
// Raw text elements (e.g. <script>) should be appended after the raw text has been added as content to the element.
|
|
602
616
|
// <html>, <head> and <body> are special elements with context constraints. They are already available in the document.
|
|
603
617
|
if (
|
|
604
|
-
(!config || config.contentModel !== HTMLElementConfigContentModelEnum.rawText
|
|
618
|
+
(!config || (config.contentModel !== HTMLElementConfigContentModelEnum.rawText &&
|
|
619
|
+
config.contentModel !== HTMLElementConfigContentModelEnum.rcdata)) &&
|
|
605
620
|
this.nextElement !== documentElement &&
|
|
606
621
|
this.nextElement !== head &&
|
|
607
622
|
this.nextElement !== body
|
|
@@ -797,6 +812,11 @@ export default class HTMLParser {
|
|
|
797
812
|
|
|
798
813
|
// Plain text elements such as <script> and <style> should only contain text.
|
|
799
814
|
// Plain text elements should not decode entities. See #1564.
|
|
815
|
+
if (upperTagName === 'TEXTAREA' || upperTagName === 'TITLE') {
|
|
816
|
+
text = XMLEncodeUtility.decodeHTMLEntities(text.replace(/\r\n?/g, '\n').replace(/\0/g, '\uFFFD'));
|
|
817
|
+
// The tree builder ignores one leading LF in a textarea start tag.
|
|
818
|
+
if (upperTagName === 'TEXTAREA' && text.startsWith('\n')) text = text.slice(1);
|
|
819
|
+
}
|
|
800
820
|
this.currentNode, true);
|
|
801
821
|
|
|
802
822
|
const rawTextElement = this.currentNode;
|
|
@@ -46,7 +46,7 @@ const SELECTOR_GROUP_REGEXP = /(\\[0-9a-fA-F]{1,6}\s?|\\[\s\S])|(\s*[\s,+>~]\s*)
|
|
|
46
46
|
* Group 23: Pseudo element (e.g. "::after", "::-webkit-inner-spin-button").
|
|
47
47
|
*/
|
|
48
48
|
const SELECTOR_REGEXP =
|
|
49
|
-
/(\*)|((?:[a-zA-Z0-9\u00A0-\
|
|
49
|
+
/(\*)|((?:[a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|#(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\.(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*("((?:[^"\\]|\\[\s\S])*)"|'((?:[^'\\]|\\[\s\S])*)')\s*(s|i){0,1}\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*(([a-zA-Z0-9\u00A0-\uFFFF_¤£-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|:([a-zA-Z-]+)\s*\(.+\)|:([a-zA-Z-]+)|::([a-zA-Z-]+)/g;
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Selector pseudo RegExp.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dr. John (醫者小智)
|
|
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,14 @@
|
|
|
1
|
+
# Buninu Land renderer
|
|
2
|
+
|
|
3
|
+
The renderer and its runtime dependencies are copied **unchanged** from
|
|
4
|
+
`buninu-land` commit `3a42a5e315b351aa1e70459133df50541d5368e3` (MIT).
|
|
5
|
+
|
|
6
|
+
Browser integration belongs in `lib/paint/direct-canvas.ts`, not in these
|
|
7
|
+
vendored files. That adapter presents browser RGBA layers as Land surfaces;
|
|
8
|
+
the compositor's channel-symmetric arithmetic preserves RGBA when supplied
|
|
9
|
+
with an opaque RGBA background surface. No red/blue conversion is needed.
|
|
10
|
+
|
|
11
|
+
`direct-compositor.ts` is the shared composition engine, including placement,
|
|
12
|
+
cropping, scaling, opacity detection, geometry/word caches and occlusion.
|
|
13
|
+
`renderer.ts` and its encoding/transport dependencies are included intact;
|
|
14
|
+
browser screenshots retain the browser's existing CDP/encoding owner.
|