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
package/test/canvas-2d.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { beforeAll, expect, test } from "bun:test";
|
|
2
2
|
import CanvasKitInit from "../usr/lib/canvaskit/canvaskit.js";
|
|
3
|
-
import { canvasContext, resetCanvas, setCanvasKit, disposeDocumentCanvases } from "../lib/renderer/canvas-2d.js";
|
|
3
|
+
import { canvasResource, canvasContext, resetCanvas, setCanvasKit, disposeDocumentCanvases } from "../lib/renderer/canvas-2d.js";
|
|
4
4
|
|
|
5
5
|
let CanvasKit;
|
|
6
6
|
|
|
@@ -38,10 +38,12 @@ test("a colour reads back stably, and one CanvasKit cannot parse is black", () =
|
|
|
38
38
|
expect(context.fillStyle).toBe("#000000");
|
|
39
39
|
expect(sample(context, "not-a-color")).toEqual([0, 0, 0, 255]);
|
|
40
40
|
expect(sample(context, "red")).toEqual([255, 0, 0, 255]);
|
|
41
|
-
|
|
41
|
+
// Like Chromium, an 8-bit premultiplied backing store rounds RGB at low alpha.
|
|
42
|
+
expect(sample(context, "rgba(1, 2, 3, 0.5)")).toEqual([2, 2, 4, 128]);
|
|
42
43
|
expect(context.getImageData(0, 0, 1, 1).data.length).toBe(4);
|
|
43
44
|
expect(context.getImageData(0, 0, 1, 1).data[3]).toBe(0);
|
|
44
|
-
expect(canvasContext(canvas(), "
|
|
45
|
+
expect(canvasContext(canvas(), "webgl2")).toBeNull();
|
|
46
|
+
expect(canvasContext(canvas(), "webgl").getParameter(0x1F01)).toBe("CanvasKit CPU");
|
|
45
47
|
});
|
|
46
48
|
|
|
47
49
|
test("the same canvas keeps one context, and changing width clears it", () => {
|
|
@@ -93,7 +95,7 @@ test("gradient and pattern facades hide native shaders while style identity and
|
|
|
93
95
|
expect(context.strokeStyle).toBe(gradient);
|
|
94
96
|
const radial = context.createRadialGradient(0, 0, 0, 1, 1, 2);
|
|
95
97
|
expect(Object.keys(radial)).toEqual(["addColorStop"]);
|
|
96
|
-
const pattern = context.createPattern(
|
|
98
|
+
const pattern = context.createPattern(node, "repeat");
|
|
97
99
|
expect(Object.keys(pattern)).toEqual(["setTransform"]);
|
|
98
100
|
for (const name of ["Td", "qf", "pe", "ie", "oe"]) expect(pattern[name]).toBeUndefined();
|
|
99
101
|
resetCanvas(node);
|
|
@@ -134,3 +136,249 @@ test("discarding a document frees its connected and detached canvas pixel buffer
|
|
|
134
136
|
CanvasKit._free = free;
|
|
135
137
|
}
|
|
136
138
|
});
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
test("CPU WebGL redraws moved geometry without painting outside its coverage", () => {
|
|
142
|
+
const node=canvas(64,64),gl=canvasContext(node,'webgl'),program=gl.createProgram();
|
|
143
|
+
for(const [type,source] of [[gl.VERTEX_SHADER,'attribute vec2 position; uniform float dx; varying vec3 color; void main(){color=vec3(1.0,0.0,0.0);gl_Position=vec4(position+vec2(dx,0.0),0.0,1.0);}'],
|
|
144
|
+
[gl.FRAGMENT_SHADER,'precision highp float; varying vec3 color; void main(){gl_FragColor=vec4(color,1.0);}']]){
|
|
145
|
+
const shader=gl.createShader(type);gl.shaderSource(shader,source);gl.compileShader(shader);
|
|
146
|
+
expect(gl.getShaderInfoLog(shader)).toBe('');gl.attachShader(program,shader);
|
|
147
|
+
}
|
|
148
|
+
gl.linkProgram(program);gl.useProgram(program);
|
|
149
|
+
gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());
|
|
150
|
+
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-.75,-.5,-.25,-.5,-.5,.5]),gl.STATIC_DRAW);
|
|
151
|
+
const a=gl.getAttribLocation(program,'position');gl.enableVertexAttribArray(a);gl.vertexAttribPointer(a,2,gl.FLOAT,false,0,0);
|
|
152
|
+
const dx=gl.getUniformLocation(program,'dx');
|
|
153
|
+
const pixel=(x,y)=>Array.from(canvasResource(node).image.readPixels(x,y,{width:1,height:1,colorType:CanvasKit.ColorType.RGBA_8888,alphaType:CanvasKit.AlphaType.Unpremul,colorSpace:CanvasKit.ColorSpace.SRGB}));
|
|
154
|
+
gl.clearColor(0,0,1,1);gl.clear(gl.COLOR_BUFFER_BIT);gl.drawArrays(gl.TRIANGLES,0,3);
|
|
155
|
+
expect(pixel(16,32)).toEqual([255,0,0,255]);expect(pixel(48,32)).toEqual([0,0,255,255]);
|
|
156
|
+
gl.uniform1f(dx,1);gl.clear(gl.COLOR_BUFFER_BIT);gl.drawArrays(gl.TRIANGLES,0,3);
|
|
157
|
+
expect(pixel(16,32)).toEqual([0,0,255,255]);expect(pixel(48,32)).toEqual([255,0,0,255]);
|
|
158
|
+
gl.uniform1f(dx,4);gl.clear(gl.COLOR_BUFFER_BIT);gl.drawArrays(gl.TRIANGLES,0,3);
|
|
159
|
+
expect(pixel(48,32)).toEqual([0,0,255,255]);expect(gl.getError()).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('CPU WebGL samples uploaded RGBA textures and preserves upload snapshots',()=>{
|
|
163
|
+
const node=canvas(16,16),gl=canvasContext(node,'webgl'),p=gl.createProgram();
|
|
164
|
+
for(const [kind,source] of [[gl.VERTEX_SHADER,'attribute vec2 p; varying vec2 uv; void main(){gl_Position=vec4(p,0.,1.);uv=(p+1.)*.5;}'],[gl.FRAGMENT_SHADER,'precision highp float; varying vec2 uv; uniform sampler2D image; void main(){gl_FragColor=texture2D(image,uv);}']]){
|
|
165
|
+
const s=gl.createShader(kind);gl.shaderSource(s,source);gl.compileShader(s);expect(gl.getShaderInfoLog(s)).toBe('');gl.attachShader(p,s);
|
|
166
|
+
}
|
|
167
|
+
gl.linkProgram(p);gl.useProgram(p);gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);gl.enableVertexAttribArray(0);gl.vertexAttribPointer(0,2,gl.FLOAT,false,0,0);
|
|
168
|
+
gl.bindTexture(gl.TEXTURE_2D,gl.createTexture());gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST);
|
|
169
|
+
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,1,2,0,gl.RGBA,gl.UNSIGNED_BYTE,new Uint8Array([255,0,0,255,0,0,255,255]));gl.drawArrays(gl.TRIANGLES,0,6);
|
|
170
|
+
const read=y=>Array.from(canvasResource(node).image.readPixels(8,y,{width:1,height:1,colorType:CanvasKit.ColorType.RGBA_8888,alphaType:CanvasKit.AlphaType.Unpremul,colorSpace:CanvasKit.ColorSpace.SRGB}));
|
|
171
|
+
expect(read(2)).toEqual([0,0,255,255]);expect(read(13)).toEqual([255,0,0,255]);expect(gl.getError()).toBe(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('cached background shading follows moving depth occluders and LEQUAL preserves coplanar copies',()=>{
|
|
175
|
+
const node=canvas(32,32),gl=canvasContext(node,'webgl'),p=gl.createProgram();
|
|
176
|
+
for(const [kind,source]of[[gl.VERTEX_SHADER,'attribute vec2 p;uniform float dx;uniform float z;void main(){gl_Position=vec4(p+vec2(dx,0.),z,1.);}'],[gl.FRAGMENT_SHADER,'precision highp float;uniform vec4 color;void main(){gl_FragColor=color;}']]){
|
|
177
|
+
const sh=gl.createShader(kind);gl.shaderSource(sh,source);gl.compileShader(sh);expect(gl.getShaderInfoLog(sh)).toBe('');gl.attachShader(p,sh);
|
|
178
|
+
}
|
|
179
|
+
gl.linkProgram(p);gl.useProgram(p);const buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.enableVertexAttribArray(0);gl.vertexAttribPointer(0,2,gl.FLOAT,false,0,0);
|
|
180
|
+
gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL);
|
|
181
|
+
const dx=gl.getUniformLocation(p,'dx'),z=gl.getUniformLocation(p,'z'),color=gl.getUniformLocation(p,'color');
|
|
182
|
+
const quad=(left,right,offset,depth,rgba)=>{gl.uniform1f(dx,offset);gl.uniform1f(z,depth);gl.uniform4fv(color,rgba);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([left,-1,right,-1,left,1,left,1,right,-1,right,1]),gl.STATIC_DRAW);gl.drawArrays(gl.TRIANGLES,0,6);};
|
|
183
|
+
const pixel=x=>Array.from(canvasResource(node).image.readPixels(x,16,{width:1,height:1,colorType:CanvasKit.ColorType.RGBA_8888,alphaType:CanvasKit.AlphaType.Unpremul,colorSpace:CanvasKit.ColorSpace.SRGB}));
|
|
184
|
+
for(const offset of[0,1,0]){
|
|
185
|
+
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
|
|
186
|
+
quad(-1,0,offset,-.123456789,[1,0,0,1]);quad(-1,1,0,.3,[0,0,1,1]);
|
|
187
|
+
expect(pixel(offset?24:8)).toEqual([255,0,0,255]);expect(pixel(offset?8:24)).toEqual([0,0,255,255]);
|
|
188
|
+
}
|
|
189
|
+
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
|
|
190
|
+
quad(-1,1,0,.123456789,[1,0,0,1]);quad(-1,1,0,.123456789,[0,1,0,1]);
|
|
191
|
+
const image=canvasResource(node).image.readPixels(0,0,{width:32,height:32,colorType:CanvasKit.ColorType.RGBA_8888,alphaType:CanvasKit.AlphaType.Unpremul,colorSpace:CanvasKit.ColorSpace.SRGB});
|
|
192
|
+
for(let i=0;i<image.length;i+=4)expect(Array.from(image.slice(i,i+4))).toEqual([0,255,0,255]);expect(gl.getError()).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('texture alpha is unpremultiplied in both numeric and CanvasKit fragment paths',()=>{
|
|
196
|
+
for(const native of[false,true]){
|
|
197
|
+
const node=canvas(8,8),gl=canvasContext(node,'webgl'),p=gl.createProgram();
|
|
198
|
+
for(const [kind,source]of[[gl.VERTEX_SHADER,'attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}'],[gl.FRAGMENT_SHADER,`precision highp float;uniform sampler2D image;void main(){vec4 c=texture2D(image,vec2(.5));${native?'for(int i=0;i<1;i++){c.r+=0.;}':''}gl_FragColor=c;}`]]){
|
|
199
|
+
const sh=gl.createShader(kind);gl.shaderSource(sh,source);gl.compileShader(sh);expect(gl.getShaderInfoLog(sh)).toBe('');gl.attachShader(p,sh);
|
|
200
|
+
}
|
|
201
|
+
gl.linkProgram(p);gl.useProgram(p);gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);gl.enableVertexAttribArray(0);gl.vertexAttribPointer(0,2,gl.FLOAT,false,0,0);
|
|
202
|
+
gl.bindTexture(gl.TEXTURE_2D,gl.createTexture());gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,1,1,0,gl.RGBA,gl.UNSIGNED_BYTE,new Uint8Array([200,100,50,128]));
|
|
203
|
+
gl.clearColor(0,0,1,1);gl.clear(gl.COLOR_BUFFER_BIT);gl.drawArrays(gl.TRIANGLES,0,6);
|
|
204
|
+
const pixel=canvasResource(node).image.readPixels(2,2,{width:1,height:1,colorType:CanvasKit.ColorType.RGBA_8888,alphaType:CanvasKit.AlphaType.Unpremul,colorSpace:CanvasKit.ColorSpace.SRGB});
|
|
205
|
+
for(let i=0;i<4;i++)expect(Math.abs(pixel[i]-[100,50,152,255][i])).toBeLessThanOrEqual(1);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('patterns snapshot actual image sources and survive source drawing and bitmap resets',()=>{
|
|
210
|
+
const document={},source={...canvas(2,2),ownerDocument:document},target={...canvas(4,4),ownerDocument:document};
|
|
211
|
+
const src=canvasContext(source,'2d'),dst=canvasContext(target,'2d');
|
|
212
|
+
try{
|
|
213
|
+
src.fillStyle='red';src.fillRect(0,0,2,2);
|
|
214
|
+
const pattern=dst.createPattern(source,'repeat');
|
|
215
|
+
src.fillStyle='blue';src.fillRect(0,0,2,2);resetCanvas(source);
|
|
216
|
+
dst.fillStyle=pattern;dst.fillRect(0,0,4,4);
|
|
217
|
+
expect([...dst.getImageData(3,3,1,1).data]).toEqual([255,0,0,255]);
|
|
218
|
+
resetCanvas(target);dst.fillStyle=pattern;dst.fillRect(0,0,4,4);
|
|
219
|
+
expect([...dst.getImageData(3,3,1,1).data]).toEqual([255,0,0,255]);
|
|
220
|
+
expect(()=>dst.createPattern({},'repeat')).toThrow(TypeError);
|
|
221
|
+
expect(()=>dst.createPattern(source,'invalid')).toThrow();
|
|
222
|
+
}finally{disposeDocumentCanvases(document);}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('pattern sampling follows the drawing context and survives save/restore and reset',()=>{
|
|
226
|
+
const source=canvas(2,1),src=canvasContext(source,'2d');
|
|
227
|
+
src.fillStyle='red';src.fillRect(0,0,1,1);
|
|
228
|
+
src.fillStyle='blue';src.fillRect(1,0,1,1);
|
|
229
|
+
const target=canvas(8,2),ctx=canvasContext(target,'2d');
|
|
230
|
+
const pattern=src.createPattern(source,'repeat');
|
|
231
|
+
pattern.setTransform({a:4,b:0,c:0,d:1,e:0,f:0});
|
|
232
|
+
ctx.fillStyle=pattern;
|
|
233
|
+
expect(ctx.imageSmoothingEnabled).toBe(true);
|
|
234
|
+
expect(ctx.imageSmoothingQuality).toBe('low');
|
|
235
|
+
ctx.imageSmoothingEnabled=false;
|
|
236
|
+
ctx.save();ctx.imageSmoothingEnabled=true;ctx.imageSmoothingQuality='high';ctx.restore();
|
|
237
|
+
expect(ctx.imageSmoothingEnabled).toBe(false);
|
|
238
|
+
expect(ctx.imageSmoothingQuality).toBe('low');
|
|
239
|
+
ctx.fillRect(0,0,8,1);
|
|
240
|
+
expect([...ctx.getImageData(3,0,1,1).data]).toEqual([255,0,0,255]);
|
|
241
|
+
ctx.imageSmoothingEnabled=true;ctx.fillRect(0,1,8,1);
|
|
242
|
+
const blended=ctx.getImageData(3,1,1,1).data;
|
|
243
|
+
expect(blended[0]).toBeGreaterThan(0);expect(blended[2]).toBeGreaterThan(0);
|
|
244
|
+
resetCanvas(target);
|
|
245
|
+
expect(ctx.imageSmoothingEnabled).toBe(true);
|
|
246
|
+
expect(ctx.imageSmoothingQuality).toBe('low');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('drawImage honors smoothing and zero destination width',()=>{
|
|
250
|
+
const source=canvas(2,1),src=canvasContext(source,'2d');
|
|
251
|
+
src.fillStyle='red';src.fillRect(0,0,1,1);src.fillStyle='blue';src.fillRect(1,0,1,1);
|
|
252
|
+
const target=canvas(8,3),ctx=canvasContext(target,'2d',{resolveImage:canvasResource});
|
|
253
|
+
ctx.imageSmoothingEnabled=false;ctx.drawImage(source,0,0,8,1);
|
|
254
|
+
expect([...ctx.getImageData(3,0,1,1).data]).toEqual([255,0,0,255]);
|
|
255
|
+
ctx.imageSmoothingEnabled=true;ctx.drawImage(source,0,1,8,1);
|
|
256
|
+
const blended=ctx.getImageData(3,1,1,1).data;
|
|
257
|
+
expect(blended[0]).toBeGreaterThan(0);expect(blended[2]).toBeGreaterThan(0);
|
|
258
|
+
ctx.drawImage(source,0,2,0,1);
|
|
259
|
+
expect([...ctx.getImageData(0,2,1,1).data]).toEqual([0,0,0,0]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('pixel-aligned pattern blits match shader fills including clips, alpha and repetition limits',()=>{
|
|
263
|
+
const source=canvas(3,3),src=canvasContext(source,'2d');
|
|
264
|
+
src.fillStyle='rgba(200,40,20,.5)';src.fillRect(0,0,3,3);
|
|
265
|
+
src.fillStyle='blue';src.fillRect(1,1,1,1);
|
|
266
|
+
for(const repetition of ['repeat','repeat-x','repeat-y','no-repeat']){
|
|
267
|
+
const outputs=[];
|
|
268
|
+
for(const smooth of [false,true]){
|
|
269
|
+
const target=canvas(8,6),ctx=canvasContext(target,'2d');
|
|
270
|
+
ctx.fillStyle='green';ctx.fillRect(0,0,8,6);
|
|
271
|
+
ctx.imageSmoothingEnabled=smooth;ctx.globalAlpha=.6;
|
|
272
|
+
const pattern=ctx.createPattern(source,repetition);
|
|
273
|
+
pattern.setTransform({a:1,b:0,c:0,d:1,e:-1,f:1});ctx.fillStyle=pattern;
|
|
274
|
+
ctx.save();ctx.beginPath();ctx.rect(1,1,6,4);ctx.clip();ctx.translate(1,-1);
|
|
275
|
+
ctx.fillRect(8,7,-12,-10);ctx.restore();
|
|
276
|
+
outputs.push([...ctx.getImageData(0,0,8,6).data]);
|
|
277
|
+
}
|
|
278
|
+
expect(outputs[0]).toEqual(outputs[1]);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('a failed Canvas context constructor releases its raster and restores the factory',()=>{
|
|
283
|
+
const makeCanvas=CanvasKit.MakeCanvas,makeSurface=CanvasKit.MakeSurface,free=CanvasKit.Free;
|
|
284
|
+
let released=0;
|
|
285
|
+
CanvasKit.Free=allocation=>{released++;return free(allocation);};
|
|
286
|
+
CanvasKit.MakeCanvas=(width,height)=>{CanvasKit.MakeSurface(width,height);throw Error('context construction failed');};
|
|
287
|
+
try{
|
|
288
|
+
expect(()=>canvasContext(canvas(4,4),'2d')).toThrow('context construction failed');
|
|
289
|
+
expect(released).toBe(1);
|
|
290
|
+
expect(CanvasKit.MakeSurface).toBe(makeSurface);
|
|
291
|
+
}finally{CanvasKit.MakeCanvas=makeCanvas;CanvasKit.Free=free;}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('drawImage ignores fill color and shaders while preserving globalAlpha and fill state',()=>{
|
|
295
|
+
const document={},source={ownerDocument:document,width:2,height:1},target={ownerDocument:document,width:2,height:1};
|
|
296
|
+
const src=canvasContext(source,'2d'),ctx=canvasContext(target,'2d',{resolveImage:canvasResource});
|
|
297
|
+
try{
|
|
298
|
+
src.fillStyle='red';src.fillRect(0,0,2,1);
|
|
299
|
+
const gradient=ctx.createLinearGradient(0,0,2,0);gradient.addColorStop(0,'transparent');gradient.addColorStop(1,'blue');
|
|
300
|
+
const pattern=ctx.createPattern(source,'repeat');
|
|
301
|
+
for(const style of ['rgba(0,0,255,0)', 'rgba(0,0,255,.25)',gradient,pattern]){
|
|
302
|
+
ctx.clearRect(0,0,2,1);ctx.fillStyle=style;const before=ctx.fillStyle;ctx.globalAlpha=1;ctx.drawImage(source,0,0);
|
|
303
|
+
expect([...ctx.getImageData(0,0,1,1).data]).toEqual([255,0,0,255]);expect(ctx.fillStyle).toBe(before);
|
|
304
|
+
ctx.clearRect(0,0,2,1);ctx.globalAlpha=.5;ctx.drawImage(source,0,0,2,1);
|
|
305
|
+
expect([...ctx.getImageData(0,0,1,1).data]).toEqual([255,0,0,128]);
|
|
306
|
+
ctx.save();ctx.fillStyle='green';ctx.restore();ctx.clearRect(0,0,2,1);ctx.drawImage(source,0,0);
|
|
307
|
+
expect([...ctx.getImageData(0,0,1,1).data]).toEqual([255,0,0,128]);
|
|
308
|
+
}
|
|
309
|
+
}finally{disposeDocumentCanvases(document)}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('image paints keep blend state across restore without constructing fill-pattern shaders',()=>{
|
|
313
|
+
const document={},source={ownerDocument:document,width:2,height:1},target={ownerDocument:document,width:2,height:1};
|
|
314
|
+
const src=canvasContext(source,'2d'),ctx=canvasContext(target,'2d',{resolveImage:canvasResource});
|
|
315
|
+
const make=CanvasKit.Image.prototype.makeShaderOptions;let shaders=0;
|
|
316
|
+
try{
|
|
317
|
+
src.fillStyle='red';src.fillRect(0,0,2,1);ctx.fillStyle=ctx.createPattern(source,'repeat');
|
|
318
|
+
CanvasKit.Image.prototype.makeShaderOptions=function(...args){shaders++;return make.apply(this,args)};
|
|
319
|
+
for(let i=0;i<3;i++){ctx.save();ctx.globalAlpha=.5;ctx.globalCompositeOperation='copy';ctx.drawImage(source,0,0);ctx.restore();}
|
|
320
|
+
expect(shaders).toBe(0);
|
|
321
|
+
expect([...ctx.getImageData(0,0,1,1).data]).toEqual([255,0,0,128]);
|
|
322
|
+
ctx.globalAlpha=1;ctx.globalCompositeOperation='source-over';ctx.drawImage(source,0,0);
|
|
323
|
+
expect([...ctx.getImageData(0,0,1,1).data]).toEqual([255,0,0,255]);
|
|
324
|
+
}finally{CanvasKit.Image.prototype.makeShaderOptions=make;disposeDocumentCanvases(document)}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('roundRect draws rounded corners, mirrors negative dimensions and rejects invalid radii',()=>{
|
|
328
|
+
const context=canvasContext(canvas(40,40),'2d');
|
|
329
|
+
expect('roundRect' in context).toBe(true);
|
|
330
|
+
context.fillStyle='red';context.beginPath();context.roundRect(2,2,30,30,8);context.fill();
|
|
331
|
+
expect(context.getImageData(2,2,1,1).data[3]).toBe(0);
|
|
332
|
+
expect([...context.getImageData(17,17,1,1).data]).toEqual([255,0,0,255]);
|
|
333
|
+
expect(context.getImageData(17,3,1,1).data[3]).toBeGreaterThan(200);
|
|
334
|
+
context.clearRect(0,0,40,40);context.beginPath();context.roundRect(32,2,-30,30,[{x:8,y:12},0,0,0]);context.fill();
|
|
335
|
+
expect(context.getImageData(31,2,1,1).data[3]).toBe(0);
|
|
336
|
+
expect(context.getImageData(2,2,1,1).data[3]).toBeGreaterThan(200);
|
|
337
|
+
expect(()=>context.roundRect(0,0,20,20,-1)).toThrow(RangeError);
|
|
338
|
+
expect(()=>context.roundRect(0,0,20,20,[])).toThrow(RangeError);
|
|
339
|
+
expect(()=>context.roundRect(0,0,20,20,[1,2,3,4,5])).toThrow(RangeError);
|
|
340
|
+
context.clearRect(0,0,40,40);context.beginPath();context.roundRect(2,2,10,10,50);context.fill();
|
|
341
|
+
expect(context.getImageData(7,7,1,1).data[3]).toBeGreaterThan(200);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('cached radial rectangles preserve transforms, clip, alpha and mutable stops',()=>{
|
|
345
|
+
for(const scale of [1,1.5]){
|
|
346
|
+
const a=canvas(40,40),b=canvas(40,40),ca=canvasContext(a,'2d'),cb=canvasContext(b,'2d');
|
|
347
|
+
const gradients=[];
|
|
348
|
+
for(const c of [ca,cb]){
|
|
349
|
+
c.translate(.25,.5);c.scale(scale,scale);
|
|
350
|
+
c.beginPath();c.rect(1,2,20,18);c.clip();
|
|
351
|
+
const g=c.createRadialGradient(9,10,0,9,10,20);
|
|
352
|
+
g.addColorStop(0,'red');g.addColorStop(1,'transparent');c.fillStyle=g;gradients.push(g);
|
|
353
|
+
}
|
|
354
|
+
for(let i=0;i<3;i++){
|
|
355
|
+
if(i===2)for(const g of gradients)g.addColorStop(.5,'blue');
|
|
356
|
+
for(const c of [ca,cb]){c.clearRect(-5,-5,60,60);c.globalAlpha=.4;}
|
|
357
|
+
ca.fillRect(.5,1.25,22,20);
|
|
358
|
+
cb.beginPath();cb.rect(.5,1.25,22,20);cb.fill();
|
|
359
|
+
const x=ca.getImageData(0,0,40,40).data,y=cb.getImageData(0,0,40,40).data;
|
|
360
|
+
for(let j=0;j<x.length;j+=4){
|
|
361
|
+
expect(Math.abs(x[j+3]-y[j+3])).toBeLessThanOrEqual(1);
|
|
362
|
+
for(let k=0;k<3;k++)expect(Math.abs(x[j+k]*x[j+3]/255-y[j+k]*y[j+3]/255)).toBeLessThanOrEqual(2);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for(const node of [a,b]){node.width=1;resetCanvas(node);}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('vertical gradient strips match full shading under scaling and clipping',()=>{
|
|
370
|
+
for(const scale of [1,1.5]){
|
|
371
|
+
const nodes=[canvas(36,36),canvas(36,36)],contexts=nodes.map(n=>canvasContext(n,'2d'));
|
|
372
|
+
for(const c of contexts){
|
|
373
|
+
c.scale(scale,scale);c.beginPath();c.rect(2,3,18,17);c.clip();
|
|
374
|
+
const g=c.createLinearGradient(0,0,0,24);g.addColorStop(0,'red');g.addColorStop(.4,'rgba(0,255,0,.4)');g.addColorStop(1,'blue');c.fillStyle=g;c.globalAlpha=.6;
|
|
375
|
+
}
|
|
376
|
+
contexts[0].fillRect(0,0,24,24);contexts[1].beginPath();contexts[1].rect(0,0,24,24);contexts[1].fill();
|
|
377
|
+
const [x,y]=contexts.map(c=>c.getImageData(0,0,36,36).data);
|
|
378
|
+
for(let j=0;j<x.length;j+=4){
|
|
379
|
+
expect(Math.abs(x[j+3]-y[j+3])).toBeLessThanOrEqual(1);
|
|
380
|
+
for(let k=0;k<3;k++)expect(Math.abs(x[j+k]*x[j+3]/255-y[j+k]*y[j+3]/255)).toBeLessThanOrEqual(2);
|
|
381
|
+
}
|
|
382
|
+
for(const n of nodes){n.width=1;resetCanvas(n);}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
test('a slow earlier layout does not throttle later Canvas-only frames',async()=>{
|
|
3
|
+
const p=Bun.spawn([process.execPath,new URL('./fixtures/canvas-frame-pacing.js',import.meta.url).pathname],{stdout:'pipe',stderr:'pipe'});
|
|
4
|
+
const [exit,stderr,stdout]=await Promise.all([p.exited,new Response(p.stderr).text(),new Response(p.stdout).text()]);
|
|
5
|
+
expect({exit,stderr}).toEqual({exit:0,stderr:''});
|
|
6
|
+
expect(JSON.parse(stdout).layoutsDuringAnimation).toBe(0);
|
|
7
|
+
},15000);
|
package/test/cdp-viewers.test.js
CHANGED
|
@@ -41,3 +41,39 @@ for(const direct of [false,true])test(`independent viewer ACK, stop and disconne
|
|
|
41
41
|
}
|
|
42
42
|
}finally{for(const c of clients)c.ws.close();server.stop(true);}
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
test('a key-up received during slow key-down IPC is not overwritten by its completion',async()=>{
|
|
46
|
+
const calls=[];let release;
|
|
47
|
+
const server=CdpServer.create({press:(key,options)=>{
|
|
48
|
+
calls.push([key,options.eventType]);
|
|
49
|
+
if(calls.length===1)return new Promise(resolve=>{release=resolve});
|
|
50
|
+
}}).listen(0);
|
|
51
|
+
let client;
|
|
52
|
+
try{
|
|
53
|
+
client=await connect(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);
|
|
54
|
+
const {targetId}=await client.send('Target.createTarget',{url:'about:blank'});
|
|
55
|
+
const {sessionId}=await client.send('Target.attachToTarget',{targetId,flatten:true});
|
|
56
|
+
const send=type=>client.send('Input.dispatchKeyEvent',{type,key:'w',code:'KeyW'},sessionId);
|
|
57
|
+
const down=send('keyDown');await until(()=>calls.length===1);
|
|
58
|
+
await send('keyUp');release();await down;
|
|
59
|
+
await send('keyDown');await send('keyUp');
|
|
60
|
+
expect(calls).toEqual([['w','keydown'],['w','keyup'],['w','keydown'],['w','keyup']]);
|
|
61
|
+
}finally{release?.();client?.ws.close();server.stop(true);}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('CDP keeps independent held keys and matches releases even when modifiers change',async()=>{
|
|
65
|
+
const calls=[];
|
|
66
|
+
const server=CdpServer.create({press:async(key,options)=>calls.push([key,options.eventType])}).listen(0);
|
|
67
|
+
let client;
|
|
68
|
+
try{
|
|
69
|
+
client=await connect(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);
|
|
70
|
+
const {targetId}=await client.send('Target.createTarget',{url:'about:blank'});
|
|
71
|
+
const {sessionId}=await client.send('Target.attachToTarget',{targetId,flatten:true});
|
|
72
|
+
const send=(type,key,modifiers=0)=>client.send('Input.dispatchKeyEvent',{type,key,code:'Key'+key.toUpperCase(),modifiers},sessionId);
|
|
73
|
+
await send('keyDown','a');await send('keyDown','b',1);await send('keyUp','a');
|
|
74
|
+
await send('char','b',1); // A's release must not make B's char a second press.
|
|
75
|
+
await client.send('Input.dispatchKeyEvent',{type:'char',text:'b'},sessionId); // code may be omitted
|
|
76
|
+
await send('keyUp','b',0);await send('keyDown','b',1);await send('keyUp','b');
|
|
77
|
+
expect(calls).toEqual([['a','keydown'],['b','keydown'],['a','keyup'],['b','keyup'],['b','keydown'],['b','keyup']]);
|
|
78
|
+
}finally{client?.ws.close();server.stop(true);}
|
|
79
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
diff --git a/games/breakout/index.html b/games/breakout/index.html
|
|
2
|
+
index 3fc45d2..1b11750 100644
|
|
3
|
+
--- a/games/breakout/index.html
|
|
4
|
+
+++ b/games/breakout/index.html
|
|
5
|
+
@@ -240,6 +240,10 @@ canvas{display:block;width:100%;height:100%;object-fit:contain;touch-action:mani
|
|
6
|
+
let score=0,lives=MAX_LIVES;
|
|
7
|
+
let paddleX,ballX,ballY,ballVX,ballVY;
|
|
8
|
+
let bricks=[];
|
|
9
|
+
+ const staticLayer=document.createElement('canvas');
|
|
10
|
+
+ staticLayer.width=W;staticLayer.height=H;
|
|
11
|
+
+ const staticContext=staticLayer.getContext('2d');
|
|
12
|
+
+ let staticLayerDirty=true;
|
|
13
|
+
let particles=[];
|
|
14
|
+
let ballSpeed=4.5;
|
|
15
|
+
let ballLaunched=false;
|
|
16
|
+
@@ -250,6 +254,7 @@ canvas{display:block;width:100%;height:100%;object-fit:contain;touch-action:mani
|
|
17
|
+
let highScore=0;
|
|
18
|
+
|
|
19
|
+
function initBricks(){
|
|
20
|
+
+ staticLayerDirty=true;
|
|
21
|
+
bricks=[];
|
|
22
|
+
for(let r=0;r<BRICK_ROWS;r++){
|
|
23
|
+
for(let c=0;c<BRICK_COLS;c++){
|
|
24
|
+
@@ -331,10 +336,11 @@ canvas{display:block;width:100%;height:100%;object-fit:contain;touch-action:mani
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---- UPDATE ----
|
|
28
|
+
- function update(){
|
|
29
|
+
+ function update(elapsed){
|
|
30
|
+
if(state==='playing'&&mouseX!==null){
|
|
31
|
+
const target=mouseX-PADDLE_W/2;
|
|
32
|
+
- const speed=8;
|
|
33
|
+
+ // Preserve 8px/frame at 60 Hz without slowing mouse tracking at low FPS.
|
|
34
|
+
+ const speed=480*elapsed;
|
|
35
|
+
if(Math.abs(paddleX-target)>2){
|
|
36
|
+
if(paddleX<target) paddleX+=Math.min(speed,target-paddleX);
|
|
37
|
+
else paddleX-=Math.min(speed,paddleX-target);
|
|
38
|
+
@@ -386,6 +392,7 @@ canvas{display:block;width:100%;height:100%;object-fit:contain;touch-action:mani
|
|
39
|
+
const bx=b.x,by=b.y,bw=b.w,bh=b.h;
|
|
40
|
+
if(ballX+BALL_SIZE>bx&&ballX<bx+bw&&ballY+BALL_SIZE>by&&ballY<by+bh){
|
|
41
|
+
b.alive=false;
|
|
42
|
+
+ staticLayerDirty=true;
|
|
43
|
+
const cx=bx+bw/2,cy=by+bh/2;
|
|
44
|
+
emitParticles(cx,cy,12,b.color.fill);
|
|
45
|
+
sfxBrick(330+b.row*60);
|
|
46
|
+
@@ -462,7 +469,7 @@ canvas{display:block;width:100%;height:100%;object-fit:contain;touch-action:mani
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- DRAW ----
|
|
50
|
+
- function draw(){
|
|
51
|
+
+ function redrawStaticLayer(ctx){
|
|
52
|
+
ctx.clearRect(0,0,W,H);
|
|
53
|
+
|
|
54
|
+
// Grid dots
|
|
55
|
+
@@ -490,6 +497,13 @@ canvas{display:block;width:100%;height:100%;object-fit:contain;touch-action:mani
|
|
56
|
+
ctx.restore();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
+ }
|
|
60
|
+
+
|
|
61
|
+
+ function draw(){
|
|
62
|
+
+ if(staticLayerDirty){redrawStaticLayer(staticContext);staticLayerDirty=false;}
|
|
63
|
+
+ ctx.clearRect(0,0,W,H);
|
|
64
|
+
+ ctx.drawImage(staticLayer,0,0);
|
|
65
|
+
+
|
|
66
|
+
// Paddle
|
|
67
|
+
ctx.save();
|
|
68
|
+
ctx.shadowColor='rgba(255,107,107,0.5)';
|
|
69
|
+
@@ -605,8 +619,12 @@ canvas{display:block;width:100%;height:100%;object-fit:contain;touch-action:mani
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- LOOP ----
|
|
73
|
+
- function loop(){
|
|
74
|
+
- update();
|
|
75
|
+
+ let lastFrameTime=null;
|
|
76
|
+
+ function loop(time=performance.now()){
|
|
77
|
+
+ // Bound catch-up after a background tab or a long suspended frame.
|
|
78
|
+
+ const elapsed=lastFrameTime===null?1/60:Math.min(0.1,Math.max(0,(time-lastFrameTime)/1000));
|
|
79
|
+
+ lastFrameTime=time;
|
|
80
|
+
+ update(elapsed);
|
|
81
|
+
updateParticles();
|
|
82
|
+
draw();
|
|
83
|
+
animId=requestAnimationFrame(loop);
|
|
84
|
+
diff --git a/games/mine/index.html b/games/mine/index.html
|
|
85
|
+
index 01e972d..91b0e32 100644
|
|
86
|
+
--- a/games/mine/index.html
|
|
87
|
+
+++ b/games/mine/index.html
|
|
88
|
+
@@ -415,6 +415,8 @@ board.addEventListener('click',e=>{
|
|
89
|
+
|
|
90
|
+
board.addEventListener('contextmenu',e=>{
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
+ if(paused||over||won)return;
|
|
93
|
+
+ const g=getCell(e);if(g)toggleFlag(g.row,g.col);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
board.addEventListener('mousedown',e=>{
|
|
97
|
+
diff --git a/games/puzzle15/index.html b/games/puzzle15/index.html
|
|
98
|
+
index 42f278e..5b0fa02 100644
|
|
99
|
+
--- a/games/puzzle15/index.html
|
|
100
|
+
+++ b/games/puzzle15/index.html
|
|
101
|
+
@@ -188,7 +188,7 @@ h1{
|
|
102
|
+
function findTile(val){
|
|
103
|
+
for(let r=0;r<gridSize;r++)
|
|
104
|
+
for(let c=0;c<gridSize;c++)
|
|
105
|
+
- if(grid[r][c]===val)return{r,c};
|
|
106
|
+
+ if(grid[r][c]===val)return{row:r,col:c};
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
diff --git a/games/snake/index.html b/games/snake/index.html
|
|
111
|
+
index 2f6919e..af005eb 100644
|
|
112
|
+
--- a/games/snake/index.html
|
|
113
|
+
+++ b/games/snake/index.html
|
|
114
|
+
@@ -166,8 +166,7 @@ canvas{display:block;width:100%;height:100%;image-rendering:pixelated;touch-acti
|
|
115
|
+
|
|
116
|
+
// ---- DIRECTIONS ----
|
|
117
|
+
const UP={x:0,y:-1},DOWN={x:0,y:1},LEFT={x:-1,y:0},RIGHT={x:1,y:0};
|
|
118
|
+
- const opposite={};
|
|
119
|
+
- opposite[UP]=DOWN;opposite[DOWN]=UP;opposite[LEFT]=RIGHT;opposite[RIGHT]=LEFT;
|
|
120
|
+
+ const opposite=new Map([[UP,DOWN],[DOWN,UP],[LEFT,RIGHT],[RIGHT,LEFT]]);
|
|
121
|
+
|
|
122
|
+
// ---- PARTICLES ----
|
|
123
|
+
let particles=[];
|
|
124
|
+
@@ -684,8 +683,8 @@ canvas{display:block;width:100%;height:100%;image-rendering:pixelated;touch-acti
|
|
125
|
+
|
|
126
|
+
// ---- INPUT ----
|
|
127
|
+
function setDirection(dir){
|
|
128
|
+
- if(!opposite[dir])return;
|
|
129
|
+
- if(dir===opposite[nextDirection])return;
|
|
130
|
+
+ if(!opposite.has(dir))return;
|
|
131
|
+
+ if(dir===opposite.get(direction))return;
|
|
132
|
+
nextDirection=dir;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
diff --git a/games/space/index.html b/games/space/index.html
|
|
136
|
+
index b980c91..d567d68 100644
|
|
137
|
+
--- a/games/space/index.html
|
|
138
|
+
+++ b/games/space/index.html
|
|
139
|
+
@@ -1113,13 +1113,6 @@ function setupTouch(c){
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
-canvas.addEventListener('mousemove',e=>{
|
|
144
|
+
-if(game.state!=='playing'||paused||!game.player.alive)return;
|
|
145
|
+
-const rect=canvas.getBoundingClientRect();
|
|
146
|
+
-const x=(e.clientX-rect.left)/canvas.clientWidth*W;
|
|
147
|
+
-game.player.x=clamp(x,game.player.w/2+5,W-game.player.w/2-5);
|
|
148
|
+
-});
|
|
149
|
+
-
|
|
150
|
+
document.addEventListener('visibilitychange',()=>{
|
|
151
|
+
if(document.hidden&&game&&game.state==='playing')paused=true;
|
|
152
|
+
});
|
|
153
|
+
@@ -1152,6 +1145,12 @@ function loop(){update();draw();rafId=requestAnimationFrame(loop)}
|
|
154
|
+
canvas=document.getElementById('c');
|
|
155
|
+
canvas.width=W;canvas.height=H;
|
|
156
|
+
ctx=canvas.getContext('2d');
|
|
157
|
+
+canvas.addEventListener('mousemove',e=>{
|
|
158
|
+
+if(game.state!=='playing'||paused||!game.player.alive)return;
|
|
159
|
+
+const rect=canvas.getBoundingClientRect();
|
|
160
|
+
+const x=(e.clientX-rect.left)/canvas.clientWidth*W;
|
|
161
|
+
+game.player.x=clamp(x,game.player.w/2+5,W-game.player.w/2-5);
|
|
162
|
+
+});
|
|
163
|
+
setupTouch(canvas);
|
|
164
|
+
try{audio.init()}catch(e){}
|
|
165
|
+
resize();
|
|
166
|
+
diff --git a/games/typing/index.html b/games/typing/index.html
|
|
167
|
+
index 17bf35b..bad31b2 100644
|
|
168
|
+
--- a/games/typing/index.html
|
|
169
|
+
+++ b/games/typing/index.html
|
|
170
|
+
@@ -634,6 +634,7 @@ document.getElementById('pauseOverlay').addEventListener('click',e=>{if(e.target
|
|
171
|
+
// ===== Keyboard =====
|
|
172
|
+
document.addEventListener('keydown',e=>{
|
|
173
|
+
if(e.repeat)return;const k=e.key;
|
|
174
|
+
+if(k!=='Escape'&&(e.target.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)))return;
|
|
175
|
+
if((k==='p'||k==='P'||k==='Escape')&&isRunning){e.preventDefault();if(isPaused)resumeGame();else pauseGame();return}
|
|
176
|
+
if(k==='m'||k==='M'){e.preventDefault();if(!audioCtx)initAudio();toggleMute();return}
|
|
177
|
+
if((k===' '||k==='Enter')&&!isRunning){e.preventDefault();if(!audioCtx)initAudio();startGame();return}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Classic-Games and Hextris compatibility checks
|
|
2
|
+
|
|
3
|
+
Run these commands from the Buninu Browser repository. No npm packages or
|
|
4
|
+
native additions are needed. Games stay in a separate checkout, outside the
|
|
5
|
+
browser package:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
git clone --no-hardlinks https://github.com/KoRifCan/Classic-Games.git ../Classic-Games
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The tested upstream revision is
|
|
12
|
+
`83f36f3687814c1f8e7b79f581f6fc69da72ba5e`.
|
|
13
|
+
|
|
14
|
+
## Upstream game fixes
|
|
15
|
+
|
|
16
|
+
`0001-fix-game-initialization.patch` fixes six bugs in the game sources,
|
|
17
|
+
separately from browser compatibility. Apply it once:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
git -C ../Classic-Games apply ../buninu-browser/test/classic-games-patches/0001-fix-game-initialization.patch
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- Space Shooter registers a `mousemove` listener before assigning `canvas`.
|
|
24
|
+
Move registration after canvas initialization. The original also throws
|
|
25
|
+
in Chromium.
|
|
26
|
+
- 15-Puzzle's `findTile()` returns `{r,c}`, but callers use `row/col`.
|
|
27
|
+
Return the names those callers expect; the original shuffle cannot finish.
|
|
28
|
+
- Minesweeper advertises right-click flagging, but its `contextmenu` handler
|
|
29
|
+
only prevents the menu. Connect that handler to its existing `toggleFlag()`;
|
|
30
|
+
Ctrl/Meta-click and touch flagging retain their existing paths.
|
|
31
|
+
- Snake uses direction objects as ordinary object keys, so all four entries
|
|
32
|
+
collide as `"[object Object]"` and left turns are always rejected. Use a
|
|
33
|
+
`Map` and compare against the last committed direction; rapid inputs must
|
|
34
|
+
not queue a reversal into the body before the next movement tick.
|
|
35
|
+
|
|
36
|
+
- Typing Test handles P/M shortcuts on `document` even while editing its
|
|
37
|
+
text field, cancelling ordinary typing. Ignore letter shortcuts in editable
|
|
38
|
+
controls while preserving Escape pause/resume and shortcuts outside them.
|
|
39
|
+
|
|
40
|
+
- Breakout limits mouse-follow movement to 8 pixels per animation frame,
|
|
41
|
+
making the paddle lag at low FPS. Use 480 pixels per second, preserving
|
|
42
|
+
its 60 Hz speed; cap elapsed time at 100 ms to avoid a large resume jump.
|
|
43
|
+
Ball and particle physics retain their existing behavior. Cache the dot
|
|
44
|
+
background and brick shadows/gradients in an offscreen Canvas; invalidate
|
|
45
|
+
it on board initialization and each destroyed brick, retaining the same pixels.
|
|
46
|
+
|
|
47
|
+
These patches never replace browser behavior with a game-specific exception.
|
|
48
|
+
The hosted upstream originals will still contain their own bugs until their
|
|
49
|
+
maintainer fixes them.
|
|
50
|
+
|
|
51
|
+
## Run all sixteen desktop scenarios
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
bun test/manual-classic-games.js ../Classic-Games buninu /tmp/classic-games
|
|
55
|
+
# Optional reference run, if Chromium headless shell is installed:
|
|
56
|
+
bun test/manual-classic-games.js ../Classic-Games chromium /tmp/classic-games-chromium
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
To browse the local patched games interactively through the default Casty frontend:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
bun index.js file://"$PWD/../Classic-Games/index.html"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The runner serves the checkout on an automatically assigned loopback port,
|
|
66
|
+
launches the browser, and closes its server and browser when finished. It
|
|
67
|
+
writes `report.json` and initial/played PNG screenshots per game. A failed
|
|
68
|
+
scenario, page error, or CDP exception causes a nonzero exit status. Restrict
|
|
69
|
+
a diagnostic run with `CLASSIC_GAMES=memory,simon` if needed.
|
|
70
|
+
|
|
71
|
+
The browser remains in its default SES/seccomp configuration. No host-JS
|
|
72
|
+
bypass, game physics changes, score assignment, or game-state function calls
|
|
73
|
+
are used. Flappy Bird's Canvas text, Snake's rendered head-eye positions, and Simon's light sequence are observed
|
|
74
|
+
through bounded text logging / a MutationObserver; these record output and
|
|
75
|
+
do not change game decisions.
|
|
76
|
+
|
|
77
|
+
| Game | Interaction verified |
|
|
78
|
+
|---|---|
|
|
79
|
+
| Tetris | Move/rotate, hard-drop score, pause/resume input |
|
|
80
|
+
| Snake | All four directions turn correctly; direct reversal is rejected |
|
|
81
|
+
| 2048 | Move/spawn tiles and undo restores the board |
|
|
82
|
+
| Flappy Bird | Space starts, draws the score, and a second flap remains alive |
|
|
83
|
+
| Space Shooter | Start/fire and movement change the canvas |
|
|
84
|
+
| Tic Tac Toe | Human move, AI response, new game clears marks |
|
|
85
|
+
| Memory | Flip a card, match a pair; hidden backs render correctly |
|
|
86
|
+
| Minesweeper | Set/remove a right-click flag and reveal cells |
|
|
87
|
+
| Pong | Start and paddle/ball updates |
|
|
88
|
+
| Breakout | Start, move paddle, launch ball |
|
|
89
|
+
| Dino Runner | Start and jump/runner animation |
|
|
90
|
+
| Sudoku | Start, toggle pencil notes, fill a hint |
|
|
91
|
+
| Connect 4 | Seven drops produce a four-disc win and update the round score |
|
|
92
|
+
| Typing Test | Type/advance words; P/M remain input; Escape pauses and resumes |
|
|
93
|
+
| Simon Says | Observe the first light, enter it, advance to round two |
|
|
94
|
+
| 15-Puzzle | Render fifteen tiles and slide an adjacent tile |
|
|
95
|
+
|
|
96
|
+
This is a functional compatibility check, not a completed campaign for each
|
|
97
|
+
game or an end-to-end Casty/Kitty FPS benchmark. Sudoku's synchronous puzzle
|
|
98
|
+
generation can take noticeably longer in the instrumented JS environment;
|
|
99
|
+
the manual runner gives observations enough time to finish.
|
|
100
|
+
|
|
101
|
+
## Breakout paddle timing
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
bun test/manual-breakout-paddle.js ../Classic-Games buninu
|
|
105
|
+
bun test/manual-breakout-paddle.js ../Classic-Games chromium
|
|
106
|
+
bun test/manual-breakout-static.js ../Classic-Games buninu
|
|
107
|
+
bun test/manual-breakout-static.js ../Classic-Games chromium
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This separate timing check supplies controlled RAF timestamps to the actual
|
|
111
|
+
game. At 15, 60 and 120 FPS, 400 ms must move its paddle 192 pixels. A
|
|
112
|
+
one-second frame gap must contribute at most 100 ms of catch-up movement.
|
|
113
|
+
The static-layer check compares the initial Canvas pixels and the pixels
|
|
114
|
+
after destroying a brick with the unpatched upstream source from the checkout's
|
|
115
|
+
HEAD. They must match exactly while idle brick draws fall from 60 to zero.
|
|
116
|
+
The ordinary sixteen-game runner above still uses real-time CDP input.
|
|
117
|
+
|
|
118
|
+
## Hextris
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
env NODE_TLS_REJECT_UNAUTHORIZED=0 bun test/manual-hextris.js buninu /tmp/hextris
|
|
122
|
+
# Reference; the helper also forwards the opt-out to Chromium's TLS flag:
|
|
123
|
+
env NODE_TLS_REJECT_UNAUTHORIZED=0 bun test/manual-hextris.js chromium /tmp/hextris-chromium
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The TLS environment variable is only needed if this site's certificate fails
|
|
127
|
+
validation on your system. The test checks a mouse-start, both arrow rotations,
|
|
128
|
+
pause/resume, and generated blocks, then saves `playing.png` and `report.json`.
|
|
129
|
+
Production fixes are HTML namespace-aware SVG/RCDATA parsing and preservation
|
|
130
|
+
of automatic semicolon insertion before member-access rewriting. No Hextris
|
|
131
|
+
source patch is required.
|
|
132
|
+
|
|
133
|
+
## Generic browser changes exercised
|
|
134
|
+
|
|
135
|
+
- Canvas `roundRect()` and a page-local `CanvasRenderingContext2D` prototype,
|
|
136
|
+
so native feature detection and prototype extensions work.
|
|
137
|
+
- CPU Web Audio biquad filters and fractional AudioBuffer length conversion;
|
|
138
|
+
filter tests verify both frequency response and actual filtered PCM.
|
|
139
|
+
- Orthographic `rotateX`/`rotateY` projection and backface hiding across
|
|
140
|
+
`preserve-3d` card faces. This is not full perspective/3D CSS support.
|
|
141
|
+
- Fractional Grid row tracks use a definite height, including aspect-ratio
|
|
142
|
+
sizing, so empty Simon quadrants get a usable painted and clickable area.
|
|
143
|
+
- SVG elements do not enter HTML RCDATA parsing; ASI boundaries survive
|
|
144
|
+
member-access rewriting before subsequent parsing.
|
|
145
|
+
- Unsupported scroll-driven animation properties remain absent from feature
|
|
146
|
+
detection even though ordinary CSS animation aliases now work.
|
|
147
|
+
|
|
148
|
+
## Recorded result
|
|
149
|
+
|
|
150
|
+
All sixteen desktop scenarios passed in one default-sandbox run on this
|
|
151
|
+
checkout, with no recorded page errors or CDP exceptions. The associated
|
|
152
|
+
browser regression run passed 272 tests across fourteen files; eight additional
|
|
153
|
+
piano mapping/input tests also passed. Screenshots and
|
|
154
|
+
per-game evidence from this session are in `/tmp/classic-games-final-regression/`;
|
|
155
|
+
that temporary directory is not a packaged artifact. Re-running the command
|
|
156
|
+
above recreates equivalent reports in your chosen output directory.
|