xbintsc 0.3.69 → 0.3.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -4
- package/README.zh-CN.md +51 -2
- package/dist/src/driver/compiler.js +6 -1
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/extensions/gui/index.js +10 -2
- package/dist/src/extensions/gui/index.js.map +1 -1
- package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
- package/dist/tests/e2e/gui-advanced.test.js +261 -0
- package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
- package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
- package/dist/tests/e2e/gui-canvas.test.js +233 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
- package/dist/tests/e2e/gui-example-svg.test.d.ts +21 -0
- package/dist/tests/e2e/gui-example-svg.test.js +105 -0
- package/dist/tests/e2e/gui-example-svg.test.js.map +1 -0
- package/dist/tests/e2e/gui-example.test.d.ts +1 -1
- package/dist/tests/e2e/gui-example.test.js +2 -2
- package/dist/tests/e2e/gui-helpers.d.ts +3 -0
- package/dist/tests/e2e/gui-helpers.js +5 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-layout.test.js +73 -0
- package/dist/tests/e2e/gui-layout.test.js.map +1 -1
- package/dist/tests/e2e/gui-media.test.d.ts +8 -0
- package/dist/tests/e2e/gui-media.test.js +119 -0
- package/dist/tests/e2e/gui-media.test.js.map +1 -0
- package/dist/tests/e2e/gui-paint.test.js +40 -0
- package/dist/tests/e2e/gui-paint.test.js.map +1 -1
- package/dist/tests/e2e/gui-transform.test.d.ts +7 -0
- package/dist/tests/e2e/gui-transform.test.js +198 -0
- package/dist/tests/e2e/gui-transform.test.js.map +1 -0
- package/doc/ai/cli.md +5 -0
- package/doc/ai/extensions.md +20 -3
- package/doc/ai/zh-CN/extensions.md +14 -3
- package/doc/gui/animation.md +97 -0
- package/doc/gui/api.md +92 -0
- package/doc/gui/build.md +91 -0
- package/doc/gui/canvas.md +98 -0
- package/doc/gui/dom.md +128 -0
- package/doc/gui/input.md +25 -0
- package/doc/gui/known-issues.md +78 -0
- package/doc/gui/layout.md +108 -0
- package/doc/gui/media.md +87 -0
- package/doc/gui/rendering.md +113 -0
- package/doc/gui/runtime.md +88 -0
- package/doc/gui-scripts.md +7 -3
- package/doc/gui.md +109 -612
- package/doc/implemented.md +1 -0
- package/doc/zh-CN/gui/animation.md +93 -0
- package/doc/zh-CN/gui/api.md +88 -0
- package/doc/zh-CN/gui/build.md +84 -0
- package/doc/zh-CN/gui/canvas.md +80 -0
- package/doc/zh-CN/gui/dom.md +119 -0
- package/doc/zh-CN/gui/input.md +23 -0
- package/doc/zh-CN/gui/known-issues.md +60 -0
- package/doc/zh-CN/gui/layout.md +91 -0
- package/doc/zh-CN/gui/media.md +71 -0
- package/doc/zh-CN/gui/rendering.md +92 -0
- package/doc/zh-CN/gui/runtime.md +76 -0
- package/doc/zh-CN/gui-scripts.md +6 -2
- package/doc/zh-CN/gui.md +89 -532
- package/doc/zh-CN/implemented.md +1 -0
- package/llms.txt +15 -2
- package/package.json +1 -1
- package/runtime/ext_gui/canvas.cpp +580 -0
- package/runtime/ext_gui/canvas.h +170 -0
- package/runtime/ext_gui/canvas_internal.h +59 -0
- package/runtime/ext_gui/canvas_raster.cpp +254 -0
- package/runtime/ext_gui/document.cpp +45 -4
- package/runtime/ext_gui/document.h +15 -1
- package/runtime/ext_gui/dom_api.cpp +3 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
- package/runtime/ext_gui/dom_api_internal.h +73 -0
- package/runtime/ext_gui/dom_api_media.cpp +274 -0
- package/runtime/ext_gui/dom_api_node.cpp +6 -4
- package/runtime/ext_gui/dom_api_proto.cpp +80 -0
- package/runtime/ext_gui/gui.cpp +6 -0
- package/runtime/ext_gui/gui_canvas.cpp +95 -0
- package/runtime/ext_gui/gui_engine.h +49 -0
- package/runtime/ext_gui/gui_events.cpp +21 -5
- package/runtime/ext_gui/gui_media.cpp +193 -0
- package/runtime/ext_gui/image.cpp +31 -12
- package/runtime/ext_gui/image.h +20 -7
- package/runtime/ext_gui/layout.cpp +130 -6
- package/runtime/ext_gui/layout.h +54 -1
- package/runtime/ext_gui/layout_flex.cpp +263 -99
- package/runtime/ext_gui/layout_flow.cpp +85 -9
- package/runtime/ext_gui/layout_flow_internal.h +84 -5
- package/runtime/ext_gui/layout_inline.cpp +54 -37
- package/runtime/ext_gui/media.cpp +514 -0
- package/runtime/ext_gui/media.h +104 -0
- package/runtime/ext_gui/nanosvg.h +3278 -0
- package/runtime/ext_gui/nanosvgrast.h +1472 -0
- package/runtime/ext_gui/paint.cpp +156 -38
- package/runtime/ext_gui/paint.h +42 -6
- package/runtime/ext_gui/renderer.cpp +117 -12
- package/runtime/ext_gui/renderer.h +49 -9
- package/runtime/ext_gui/renderer_image.cpp +205 -78
- package/runtime/ext_gui/renderer_internal.h +12 -9
- package/runtime/ext_gui/renderer_text.cpp +46 -39
- package/runtime/ext_gui/style.h +21 -0
- package/runtime/ext_gui/style_print.cpp +17 -0
- package/runtime/ext_gui/style_ua.cpp +7 -0
- package/runtime/ext_gui/style_values.cpp +83 -1
- package/runtime/ext_gui/svg.cpp +182 -0
- package/runtime/ext_gui/svg.h +46 -0
- package/runtime/ext_gui/text.cpp +156 -25
- package/runtime/ext_gui/text.h +11 -2
- package/runtime/ext_gui/transform.cpp +412 -0
- package/runtime/ext_gui/transform.h +112 -0
- package/runtime/ext_gui/window.cpp +10 -5
- package/scripts/build-gui.ts +148 -6
- package/scripts/check-file-length.ts +5 -1
- package/scripts/install-gui-deps.sh +28 -0
- package/src/driver/compiler.ts +6 -1
- package/src/extensions/gui/index.ts +10 -2
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end tests for `<video>` / `<audio>` support in the `gui` extension.
|
|
3
|
+
*
|
|
4
|
+
* These exercise the real decoding path (vendored FFmpeg + SDL audio) and the
|
|
5
|
+
* DOM media API. The fixtures live in `examples/resources`; when they are
|
|
6
|
+
* absent (a trim checkout) the suite is skipped rather than failing.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { dirname, join, resolve } from "node:path";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
14
|
+
import { build } from "../../src/driver/compiler.js";
|
|
15
|
+
import { createDefaultRegistry } from "../../src/extensions/registry.js";
|
|
16
|
+
import { guiExtension } from "../../src/extensions/gui/index.js";
|
|
17
|
+
import { createGuiHarness, guiAvailable } from "./gui-helpers.js";
|
|
18
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
19
|
+
const resourceDir = join(root, "examples", "resources");
|
|
20
|
+
const moviePath = join(resourceDir, "movie.mp4");
|
|
21
|
+
const soundPath = join(resourceDir, "sounds.mp3");
|
|
22
|
+
const exampleDir = join(root, "examples", "gui");
|
|
23
|
+
/** A local path safe to embed in an HTML attribute (no percent-encoding: the
|
|
24
|
+
* media loader hands the string straight to FFmpeg). */
|
|
25
|
+
function mediaSrc(absolutePath) {
|
|
26
|
+
return absolutePath.split("\\").join("/");
|
|
27
|
+
}
|
|
28
|
+
const resourcesAvailable = existsSync(moviePath) && existsSync(soundPath);
|
|
29
|
+
describe.skipIf(!guiAvailable || !resourcesAvailable)("gui extension — media", () => {
|
|
30
|
+
const harness = createGuiHarness();
|
|
31
|
+
beforeAll(() => harness.setup());
|
|
32
|
+
afterAll(() => harness.cleanup());
|
|
33
|
+
it("decodes video/audio and drives playback through the DOM API", () => {
|
|
34
|
+
const movie = mediaSrc(moviePath);
|
|
35
|
+
const sound = mediaSrc(soundPath);
|
|
36
|
+
const { value, stdout } = harness.compileAndRun("media", `
|
|
37
|
+
import { createWindow, run } from "gui";
|
|
38
|
+
|
|
39
|
+
const win = createWindow({ title: "media", width: 400, height: 300 });
|
|
40
|
+
win.on("ready", () => {
|
|
41
|
+
win.loadHTML(\`<html><body style="margin:0">
|
|
42
|
+
<video id="v" src="${movie}" autoplay muted></video>
|
|
43
|
+
<audio id="a" src="${sound}"></audio>
|
|
44
|
+
</body></html>\`);
|
|
45
|
+
|
|
46
|
+
const v: any = win.document.getElementById("v");
|
|
47
|
+
const a: any = win.document.getElementById("a");
|
|
48
|
+
let ticks = 0;
|
|
49
|
+
|
|
50
|
+
v.addEventListener("loadedmetadata", () => {
|
|
51
|
+
console.log("duration=" + (v.duration > 0) + ":" + (a.duration > 0));
|
|
52
|
+
console.log("dims=" + (v.videoWidth > 0) + ":" + (v.videoHeight > 0));
|
|
53
|
+
console.log("ready=" + v.readyState);
|
|
54
|
+
v.pause();
|
|
55
|
+
console.log("paused=" + v.paused);
|
|
56
|
+
v.currentTime = 2.5;
|
|
57
|
+
console.log("seek=" + (v.currentTime >= 2.4));
|
|
58
|
+
v.volume = 0.25;
|
|
59
|
+
console.log("volume=" + v.volume);
|
|
60
|
+
v.muted = true;
|
|
61
|
+
console.log("muted=" + v.muted);
|
|
62
|
+
v.play();
|
|
63
|
+
console.log("playing=" + (v.paused === false));
|
|
64
|
+
});
|
|
65
|
+
v.addEventListener("error", () => console.log("video-error"));
|
|
66
|
+
a.addEventListener("error", () => console.log("audio-error"));
|
|
67
|
+
v.addEventListener("timeupdate", () => {
|
|
68
|
+
ticks = ticks + 1;
|
|
69
|
+
if (ticks >= 2) {
|
|
70
|
+
console.log("advanced=" + (v.currentTime > 2.4));
|
|
71
|
+
win.close();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
run();
|
|
77
|
+
`, { autocloseMs: 8000 });
|
|
78
|
+
expect(stdout).not.toContain("video-error");
|
|
79
|
+
expect(stdout).not.toContain("audio-error");
|
|
80
|
+
expect(value("duration")).toBe("true:true");
|
|
81
|
+
expect(value("dims")).toBe("true:true");
|
|
82
|
+
expect(value("paused")).toBe("true");
|
|
83
|
+
expect(value("seek")).toBe("true");
|
|
84
|
+
expect(value("volume")).toBe("0.25");
|
|
85
|
+
expect(value("muted")).toBe("true");
|
|
86
|
+
expect(value("playing")).toBe("true");
|
|
87
|
+
expect(value("advanced")).toBe("true");
|
|
88
|
+
});
|
|
89
|
+
it("runs the media example from the repository root", () => {
|
|
90
|
+
const workdir = mkdtempSync(join(tmpdir(), "xbintsc-media-"));
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(join(workdir, "media.html"), readFileSync(join(exampleDir, "media.html"), "utf8"));
|
|
93
|
+
writeFileSync(join(workdir, "media.ts"), readFileSync(join(exampleDir, "media.ts"), "utf8"));
|
|
94
|
+
const extensions = createDefaultRegistry().register(guiExtension);
|
|
95
|
+
const result = build(join(workdir, "media.ts"), {
|
|
96
|
+
emit: "exe",
|
|
97
|
+
outDir: join(workdir, "out"),
|
|
98
|
+
cacheDir: join(workdir, ".cache"),
|
|
99
|
+
extensions,
|
|
100
|
+
});
|
|
101
|
+
expect(result.diagnostics.filter((diagnostic) => diagnostic.category === "error")).toEqual([]);
|
|
102
|
+
// The example's `src` paths are relative to the working directory.
|
|
103
|
+
const executed = spawnSync(result.outputPath, [], {
|
|
104
|
+
cwd: root,
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
env: { ...process.env, XT_GUI_AUTOCLOSE_MS: "8000" },
|
|
107
|
+
});
|
|
108
|
+
expect(executed.status, executed.stderr).toBe(0);
|
|
109
|
+
expect(executed.stdout).toMatch(/video = \d+x\d+ duration=\d/);
|
|
110
|
+
expect(executed.stdout).toMatch(/audio duration=\d/);
|
|
111
|
+
expect(executed.stdout).toContain("play paused=false");
|
|
112
|
+
expect(executed.stdout).toContain("closed");
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=gui-media.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gui-media.test.js","sourceRoot":"","sources":["../../../tests/e2e/gui-media.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -41,6 +41,46 @@ describe.skipIf(!guiAvailable)("gui extension — paint", () => {
|
|
|
41
41
|
expect(stdout).toContain(`image x=0.0 y=0.0 w=40.0 h=40.0 src=${src}`);
|
|
42
42
|
expect(stdout).toContain(`image x=0.0 y=40.0 w=4.0 h=4.0 src=${src}`);
|
|
43
43
|
});
|
|
44
|
+
it("sizes and paints an SVG (intrinsic and explicit)", () => {
|
|
45
|
+
// An SVG is a vector source: the image loader detects it and rasterises it
|
|
46
|
+
// at the drawn size instead of failing like `stb_image` alone would.
|
|
47
|
+
const src = harness.writeAsset("logo.svg", `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="6" viewBox="0 0 8 6">` +
|
|
48
|
+
`<rect width="8" height="6" fill="#ff0000"/>` +
|
|
49
|
+
`<circle cx="4" cy="3" r="2" fill="#0000ff"/></svg>`);
|
|
50
|
+
const { stdout, stderr } = harness.compileAndRun("svg", `
|
|
51
|
+
import { createWindow, run } from "gui";
|
|
52
|
+
|
|
53
|
+
const HTML = \`
|
|
54
|
+
<html><head><style>
|
|
55
|
+
body { margin: 0; }
|
|
56
|
+
#scaled { width: 64px; height: 48px; }
|
|
57
|
+
</style></head><body>
|
|
58
|
+
<img id="natural" src="${src}">
|
|
59
|
+
<img id="scaled" src="${src}">
|
|
60
|
+
</body></html>
|
|
61
|
+
\`;
|
|
62
|
+
|
|
63
|
+
const win = createWindow({ title: "svg", width: 400, height: 300 });
|
|
64
|
+
win.on("ready", () => {
|
|
65
|
+
const natural = win.getBoundingClientRect("#natural");
|
|
66
|
+
console.log("natural=" + natural.width + "x" + natural.height);
|
|
67
|
+
const scaled = win.getBoundingClientRect("#scaled");
|
|
68
|
+
console.log("scaled=" + scaled.width + "x" + scaled.height);
|
|
69
|
+
console.log(win.paintList().trim());
|
|
70
|
+
win.close();
|
|
71
|
+
});
|
|
72
|
+
win.loadHTML(HTML);
|
|
73
|
+
run();
|
|
74
|
+
`);
|
|
75
|
+
// Intrinsic size comes from the SVG's width/height; the explicit CSS size
|
|
76
|
+
// wins for the second image.
|
|
77
|
+
expect(stdout).toContain("natural=8x6");
|
|
78
|
+
expect(stdout).toContain("scaled=64x48");
|
|
79
|
+
expect(stdout).toContain(`image x=0.0 y=0.0 w=8.0 h=6.0 src=${src}`);
|
|
80
|
+
expect(stdout).toContain(`image x=0.0 y=6.0 w=64.0 h=48.0 src=${src}`);
|
|
81
|
+
// The SVG must actually rasterise, not just lay out.
|
|
82
|
+
expect(stderr).not.toContain("could not decode image");
|
|
83
|
+
});
|
|
44
84
|
it("builds a display list of backgrounds and borders", () => {
|
|
45
85
|
const { value, stdout } = harness.compileAndRun("paint", `
|
|
46
86
|
import { createWindow, run } from "gui";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gui-paint.test.js","sourceRoot":"","sources":["../../../tests/e2e/gui-paint.test.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gui-paint.test.js","sourceRoot":"","sources":["../../../tests/e2e/gui-paint.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end tests for the `gui` extension — CSS `transform` and
|
|
3
|
+
* `transform-origin`: parsing/computed style, the matrix the paint list carries,
|
|
4
|
+
* transformed geometry (`getBoundingClientRect`), hit testing, and animating a
|
|
5
|
+
* transform through a CSS transition.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end tests for the `gui` extension — CSS `transform` and
|
|
3
|
+
* `transform-origin`: parsing/computed style, the matrix the paint list carries,
|
|
4
|
+
* transformed geometry (`getBoundingClientRect`), hit testing, and animating a
|
|
5
|
+
* transform through a CSS transition.
|
|
6
|
+
*/
|
|
7
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
8
|
+
import { createGuiHarness, guiAvailable } from "./gui-helpers.js";
|
|
9
|
+
const harness = createGuiHarness();
|
|
10
|
+
describe.skipIf(!guiAvailable)("gui extension — transform", () => {
|
|
11
|
+
beforeAll(() => harness.setup());
|
|
12
|
+
afterAll(() => harness.cleanup());
|
|
13
|
+
it("composes translate/rotate/scale into one matrix", () => {
|
|
14
|
+
const { stdout, value } = harness.compileAndRun("transform-matrix", `
|
|
15
|
+
import { createWindow, run } from "gui";
|
|
16
|
+
|
|
17
|
+
const HTML = \`
|
|
18
|
+
<html><head><style>
|
|
19
|
+
body { margin: 0; }
|
|
20
|
+
#moved {
|
|
21
|
+
position: absolute; left: 100px; top: 100px; width: 100px; height: 50px;
|
|
22
|
+
background-color: #ff0000;
|
|
23
|
+
transform-origin: 0 0;
|
|
24
|
+
transform: rotate(90deg) translate(10px, 0px);
|
|
25
|
+
}
|
|
26
|
+
#grew {
|
|
27
|
+
position: absolute; left: 100px; top: 200px; width: 40px; height: 20px;
|
|
28
|
+
background-color: #00ff00;
|
|
29
|
+
transform: scale(2, 3);
|
|
30
|
+
}
|
|
31
|
+
</style></head><body>
|
|
32
|
+
<div id="moved"></div><div id="grew"></div>
|
|
33
|
+
</body></html>
|
|
34
|
+
\`;
|
|
35
|
+
|
|
36
|
+
const win = createWindow({ title: "transform", width: 400, height: 300 });
|
|
37
|
+
win.on("ready", () => {
|
|
38
|
+
console.log("computed=" + win.computedStyle("#moved", "transform"));
|
|
39
|
+
console.log("origin=" + win.computedStyle("#moved", "transform-origin"));
|
|
40
|
+
console.log("default-origin=" + win.computedStyle("#grew", "transform-origin"));
|
|
41
|
+
const moved = win.getBoundingClientRect("#moved");
|
|
42
|
+
console.log("moved=" + moved.x.toFixed(1) + "," + moved.y.toFixed(1) + "," +
|
|
43
|
+
moved.width.toFixed(1) + "," + moved.height.toFixed(1));
|
|
44
|
+
const grew = win.getBoundingClientRect("#grew");
|
|
45
|
+
console.log("grew=" + grew.x.toFixed(1) + "," + grew.y.toFixed(1) + "," +
|
|
46
|
+
grew.width.toFixed(1) + "," + grew.height.toFixed(1));
|
|
47
|
+
console.log(win.paintList().trim());
|
|
48
|
+
win.close();
|
|
49
|
+
});
|
|
50
|
+
win.loadHTML(HTML);
|
|
51
|
+
run();
|
|
52
|
+
`);
|
|
53
|
+
expect(value("computed")).toBe("rotate(90deg) translate(10px, 0px)");
|
|
54
|
+
expect(value("origin")).toBe("0px 0px");
|
|
55
|
+
// `transform-origin` defaults to the centre of the border box.
|
|
56
|
+
expect(value("default-origin")).toBe("50% 50%");
|
|
57
|
+
/* rotate(90deg) about the top-left corner turns the 100x50 box into a
|
|
58
|
+
* 50x100 one; the local `translate(10px, 0)` moves it 10px down. */
|
|
59
|
+
expect(value("moved")).toBe("50.0,110.0,50.0,100.0");
|
|
60
|
+
// `scale(2, 3)` about the centre: 40x20 -> 80x60, centred on the same point.
|
|
61
|
+
expect(value("grew")).toBe("80.0,180.0,80.0,60.0");
|
|
62
|
+
// Paint keeps the untransformed layout geometry and carries the matrix.
|
|
63
|
+
expect(stdout).toContain("rect x=100.0 y=100.0 w=100.0 h=50.0 r=0.0 color=#ff0000ff transform=matrix(");
|
|
64
|
+
expect(stdout).toContain("transform=matrix(2,0,0,3,-120,-420)");
|
|
65
|
+
});
|
|
66
|
+
it("parses the 3d spellings and drops unsupported functions", () => {
|
|
67
|
+
const { stdout, value } = harness.compileAndRun("transform-parse", `
|
|
68
|
+
import { createWindow, run } from "gui";
|
|
69
|
+
|
|
70
|
+
const HTML = \`
|
|
71
|
+
<html><head><style>
|
|
72
|
+
body { margin: 0; }
|
|
73
|
+
#a {
|
|
74
|
+
position: absolute; left: 0px; top: 0px; width: 40px; height: 20px;
|
|
75
|
+
background-color: #ff0000; transform-origin: 0 0;
|
|
76
|
+
transform: translate3d(10px, 4px, 0) scale3d(2, 3, 1) rotateZ(0.5turn);
|
|
77
|
+
}
|
|
78
|
+
#b {
|
|
79
|
+
position: absolute; left: 0px; top: 100px; width: 40px; height: 20px;
|
|
80
|
+
background-color: #00ff00; transform: skewX(10deg);
|
|
81
|
+
}
|
|
82
|
+
#c {
|
|
83
|
+
position: absolute; left: 100px; top: 100px; width: 100px; height: 50px;
|
|
84
|
+
background-color: #0000ff; transform-origin: 0 0;
|
|
85
|
+
transform: translate(50%, 50%);
|
|
86
|
+
}
|
|
87
|
+
#d { transform-origin: top; }
|
|
88
|
+
#e { transform-origin: bottom right; }
|
|
89
|
+
#f { transform-origin: 10px; }
|
|
90
|
+
</style></head><body><div id="a"></div><div id="b"></div><div id="c"></div>
|
|
91
|
+
<div id="d"></div><div id="e"></div><div id="f"></div></body></html>
|
|
92
|
+
\`;
|
|
93
|
+
|
|
94
|
+
const win = createWindow({ title: "parse", width: 400, height: 300 });
|
|
95
|
+
win.on("ready", () => {
|
|
96
|
+
console.log("a=" + win.computedStyle("#a", "transform"));
|
|
97
|
+
console.log("b=" + win.computedStyle("#b", "transform"));
|
|
98
|
+
console.log("c=" + win.computedStyle("#c", "transform"));
|
|
99
|
+
/* translate(50%, 50%) resolves against the box: 50px, 25px. */
|
|
100
|
+
console.log("c-rect=" + win.getBoundingClientRect("#c").x.toFixed(1) + "," +
|
|
101
|
+
win.getBoundingClientRect("#c").y.toFixed(1));
|
|
102
|
+
/* A vertical keyword keeps x centred; bottom-right is right-bottom. */
|
|
103
|
+
console.log("d-origin=" + win.computedStyle("#d", "transform-origin"));
|
|
104
|
+
console.log("e-origin=" + win.computedStyle("#e", "transform-origin"));
|
|
105
|
+
console.log("f-origin=" + win.computedStyle("#f", "transform-origin"));
|
|
106
|
+
console.log(win.paintList().trim());
|
|
107
|
+
win.close();
|
|
108
|
+
});
|
|
109
|
+
win.loadHTML(HTML);
|
|
110
|
+
run();
|
|
111
|
+
`);
|
|
112
|
+
// `translate3d`/`scale3d` keep their x/y and drop `z`; `rotateZ` is `rotate`.
|
|
113
|
+
expect(value("a")).toBe("translate(10px, 4px) scale(2, 3) rotate(180deg)");
|
|
114
|
+
// An unsupported function invalidates the whole declaration.
|
|
115
|
+
expect(value("b")).toBe("none");
|
|
116
|
+
// Percentages stay unresolved in the computed value and resolve in layout.
|
|
117
|
+
expect(value("c")).toBe("translate(50%, 50%)");
|
|
118
|
+
expect(value("c-rect")).toBe("150.0,125.0");
|
|
119
|
+
expect(value("d-origin")).toBe("50% 0%");
|
|
120
|
+
expect(value("e-origin")).toBe("100% 100%");
|
|
121
|
+
expect(value("f-origin")).toBe("10px 50%");
|
|
122
|
+
expect(stdout).toContain("transform=matrix(1,0,0,1,50,25)");
|
|
123
|
+
});
|
|
124
|
+
it("hit tests through the transform", () => {
|
|
125
|
+
const { value } = harness.compileAndRun("transform-hit", `
|
|
126
|
+
import { createWindow, run } from "gui";
|
|
127
|
+
|
|
128
|
+
const HTML = \`
|
|
129
|
+
<html><head><style>
|
|
130
|
+
body { margin: 0; }
|
|
131
|
+
#box {
|
|
132
|
+
position: absolute; left: 100px; top: 100px; width: 100px; height: 50px;
|
|
133
|
+
background-color: #ff0000;
|
|
134
|
+
transform-origin: 0 0;
|
|
135
|
+
transform: rotate(90deg);
|
|
136
|
+
}
|
|
137
|
+
</style></head><body><div id="box"></div></body></html>
|
|
138
|
+
\`;
|
|
139
|
+
|
|
140
|
+
const win = createWindow({ title: "hit", width: 400, height: 300 });
|
|
141
|
+
win.on("ready", () => {
|
|
142
|
+
/* The rotated box covers x 50..150, y 100..200 and no longer covers the
|
|
143
|
+
* area it occupied before the rotation (x 100..200). */
|
|
144
|
+
console.log("rotated=" + win.hitTest(60, 190));
|
|
145
|
+
console.log("vacated=" + win.hitTest(190, 110));
|
|
146
|
+
win.close();
|
|
147
|
+
});
|
|
148
|
+
win.loadHTML(HTML);
|
|
149
|
+
run();
|
|
150
|
+
`);
|
|
151
|
+
expect(value("rotated")).toBe("div#box");
|
|
152
|
+
expect(value("vacated")).toBe("");
|
|
153
|
+
});
|
|
154
|
+
it("animates `transform` through a CSS transition", () => {
|
|
155
|
+
const { value } = harness.compileAndRun("transform-transition", `
|
|
156
|
+
import { createWindow, run } from "gui";
|
|
157
|
+
|
|
158
|
+
const HTML = \`
|
|
159
|
+
<html><head><style>
|
|
160
|
+
body { margin: 0; }
|
|
161
|
+
#box {
|
|
162
|
+
position: absolute; left: 20px; top: 200px; width: 60px; height: 30px;
|
|
163
|
+
background-color: #0000ff;
|
|
164
|
+
transform: translateX(0px) rotate(0deg);
|
|
165
|
+
transition: transform 400ms linear;
|
|
166
|
+
}
|
|
167
|
+
#box:hover { transform: translateX(40px) rotate(90deg); }
|
|
168
|
+
</style></head><body><div id="box"></div></body></html>
|
|
169
|
+
\`;
|
|
170
|
+
|
|
171
|
+
const win = createWindow({ title: "transition", width: 400, height: 300 });
|
|
172
|
+
win.on("ready", () => {
|
|
173
|
+
console.log("start=" + win.computedStyle("#box", "transform"));
|
|
174
|
+
win.sendEvent("mousemove", { x: 50, y: 215 });
|
|
175
|
+
win.advance(100);
|
|
176
|
+
console.log("q1=" + win.computedStyle("#box", "transform"));
|
|
177
|
+
win.advance(200);
|
|
178
|
+
console.log("q3=" + win.computedStyle("#box", "transform"));
|
|
179
|
+
win.advance(200);
|
|
180
|
+
console.log("end=" + win.computedStyle("#box", "transform"));
|
|
181
|
+
const end = win.getBoundingClientRect("#box");
|
|
182
|
+
console.log("end-rect=" + end.x.toFixed(1) + "," + end.y.toFixed(1) + "," +
|
|
183
|
+
end.width.toFixed(1) + "," + end.height.toFixed(1));
|
|
184
|
+
win.close();
|
|
185
|
+
});
|
|
186
|
+
win.loadHTML(HTML);
|
|
187
|
+
run();
|
|
188
|
+
`);
|
|
189
|
+
expect(value("start")).toBe("translateX(0px) rotate(0deg)");
|
|
190
|
+
// 100/400 and 300/400 of the way between the two lists, linearly.
|
|
191
|
+
expect(value("q1")).toBe("translateX(10px) rotate(22.5deg)");
|
|
192
|
+
expect(value("q3")).toBe("translateX(30px) rotate(67.5deg)");
|
|
193
|
+
expect(value("end")).toBe("translateX(40px) rotate(90deg)");
|
|
194
|
+
// Moved 40px right and rotated a quarter turn about its centre.
|
|
195
|
+
expect(value("end-rect")).toBe("75.0,185.0,30.0,60.0");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
//# sourceMappingURL=gui-transform.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gui-transform.test.js","sourceRoot":"","sources":["../../../tests/e2e/gui-transform.test.ts"],"names":[],"mappings":"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"}
|
package/doc/ai/cli.md
CHANGED
|
@@ -46,6 +46,11 @@ Parsing details that occasionally surprise:
|
|
|
46
46
|
`run`, or is a positional for the other commands.
|
|
47
47
|
- `-o` consumes the next argument unconditionally; the other value flags leave
|
|
48
48
|
the value unset when the next argument starts with `-`.
|
|
49
|
+
- `-o`/`output` may name a directory that does not exist yet: the driver creates
|
|
50
|
+
the parent directories of the output path. (`--out` likewise creates its
|
|
51
|
+
directory.) Without that, an explicit path such as `-o dist/app.exe` where
|
|
52
|
+
`dist/` was missing used to fail inside the linker, since clang — not xbintsc —
|
|
53
|
+
is what writes the executable.
|
|
49
54
|
|
|
50
55
|
## Project config
|
|
51
56
|
|
package/doc/ai/extensions.md
CHANGED
|
@@ -67,10 +67,27 @@ import { createWindow, run } from "gui";
|
|
|
67
67
|
|
|
68
68
|
It is a self-contained GPU-accelerated renderer (own HTML parser, CSS cascade,
|
|
69
69
|
layout and compositor) — not a system WebView. It ships as a per-platform
|
|
70
|
-
prebuilt `gui.a`/`gui.lib` because it is C++.
|
|
70
|
+
prebuilt `gui.a`/`gui.lib` because it is C++. `<img>`, `<video>` and `<audio>`
|
|
71
|
+
really decode: a minimal static FFmpeg is bundled into the archive, and the DOM
|
|
72
|
+
media API (`play`/`pause`/`currentTime`/`volume`/…) plus the
|
|
73
|
+
`loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` events let a
|
|
74
|
+
program drive playback. There are no built-in controls — draw your own chrome
|
|
75
|
+
with elements and CSS (see `examples/gui/media.ts`). Video frames and audio PCM
|
|
76
|
+
are decoded on the main thread. `<canvas>` is supported too: `getContext("2d")`
|
|
77
|
+
returns a context whose fills, paths, text and `drawImage` are rasterised by a
|
|
78
|
+
self-written CPU rasteriser, and element handles expose `canvas` drawing plus
|
|
79
|
+
`width`/`height`. CSS `transform`/`transform-origin` work as paint-time
|
|
80
|
+
effects, so a box (and the text, images and children in it) can be translated,
|
|
81
|
+
scaled and rotated without reflowing anything — animate it per frame from
|
|
82
|
+
`requestAnimationFrame` or with `transition: transform` (see
|
|
83
|
+
`examples/gui/pelican-svg`). If that archive is missing for
|
|
71
84
|
your platform, the build fails with an actionable message; design and roadmap
|
|
72
|
-
live in [../gui.md](../gui.md)
|
|
73
|
-
[
|
|
85
|
+
live in [../gui.md](../gui.md) (overview; the engine is documented by function
|
|
86
|
+
under [`doc/gui/`](../gui/)), and the scripting surface in
|
|
87
|
+
[../gui-scripts.md](../gui-scripts.md). Media playback is the one part that
|
|
88
|
+
Windows does not get: the Windows `gui.a` is built without FFmpeg and media
|
|
89
|
+
slots are inert. See [../gui/canvas.md](../gui/canvas.md) for the canvas API and
|
|
90
|
+
its limits.
|
|
74
91
|
|
|
75
92
|
## Third-party npm packages
|
|
76
93
|
|
|
@@ -64,9 +64,20 @@ import { createWindow, run } from "gui";
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
它是自带渲染引擎的 GPU 加速实现(自己的 HTML 解析器、CSS 级联、布局与合成器),不是
|
|
67
|
-
系统 WebView
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
系统 WebView。`<img>`、`<video>`、`<audio>` 都是真正解码:归档内打入一个最小化的
|
|
68
|
+
静态 FFmpeg,DOM 媒体 API(`play`/`pause`/`currentTime`/`volume`/…)与
|
|
69
|
+
`loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` 事件可让程序驱动播放。
|
|
70
|
+
没有内置控件——用元素和 CSS 自绘控件(见 `examples/gui/media.ts`);视频帧与音频 PCM
|
|
71
|
+
都在主线程解码。也支持 `<canvas>`:`getContext("2d")` 返回的上下文,其填充、路径、
|
|
72
|
+
文本与 `drawImage` 由自研 CPU 光栅器处理,元素句柄还暴露 `width`/`height`。CSS
|
|
73
|
+
`transform`/`transform-origin` 作为绘制期效果可用,因此一个盒子(以及其中的文本、
|
|
74
|
+
图像与子元素)可以被平移、缩放与旋转而不触发任何重排——可以用
|
|
75
|
+
`requestAnimationFrame` 逐帧驱动,或写成 `transition: transform`(见
|
|
76
|
+
`examples/gui/pelican-svg`)。因为主体是 C++,它以各平台预编译的 `gui.a`/`gui.lib` 形式分发。如果该
|
|
77
|
+
平台的归档不存在,构建会以明确的提示失败;设计与路线图见 [../../zh-CN/gui.md](../../zh-CN/gui.md)(总览;引擎按功能拆分在 [`doc/zh-CN/gui/`](../../zh-CN/gui/) 下),
|
|
78
|
+
脚本接口见 [../../zh-CN/gui-scripts.md](../../zh-CN/gui-scripts.md)。媒体播放是
|
|
79
|
+
Windows 唯一不支持的:Windows 的 `gui.a` 不构建 FFmpeg,媒体槽位为空操作。Canvas API
|
|
80
|
+
及其限制见 [../../zh-CN/gui/canvas.md](../../zh-CN/gui/canvas.md)。
|
|
70
81
|
|
|
71
82
|
## 第三方 npm 包
|
|
72
83
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Animation: frames and transitions
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
## Animation frames (M10)
|
|
7
|
+
|
|
8
|
+
`requestAnimationFrame` runs a callback once on the next frame; the callback
|
|
9
|
+
receives the frame timestamp (ms) and may mutate the DOM, which is restyled and
|
|
10
|
+
repainted in the same frame. Re-queue from inside the callback to animate:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const win = createWindow({ title: "anim", width: 320, height: 240 });
|
|
14
|
+
let n = 0;
|
|
15
|
+
const tick = (t: number) => {
|
|
16
|
+
win.document.getElementById("label").textContent = String(n++);
|
|
17
|
+
if (n < 120) win.requestAnimationFrame(tick); // id returned; cancelAnimationFrame(id) drops it
|
|
18
|
+
};
|
|
19
|
+
win.on("ready", () => win.requestAnimationFrame(tick));
|
|
20
|
+
win.loadHTML("<div id='label'>0</div>");
|
|
21
|
+
run();
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## CSS transitions (M6b)
|
|
25
|
+
|
|
26
|
+
`XtDocument` runs CSS transitions between the *target* computed styles (the
|
|
27
|
+
cascade for the current `:hover`/`:focus` state) and the *displayed* styles used
|
|
28
|
+
for layout and paint. When a state change alters a transitioned property, a
|
|
29
|
+
running transition is recorded and re-applied every frame until it finishes;
|
|
30
|
+
the engine advances the clock with real frame deltas (`XtDocument::advance`),
|
|
31
|
+
and `win.advance(ms)` lets tests step it deterministically.
|
|
32
|
+
|
|
33
|
+
- Supported properties: `background-color`, `color`, `border-color`,
|
|
34
|
+
`border-radius` and `transform`; `transition: all` covers them. A
|
|
35
|
+
layout-affecting property (width, position, …) is still not animated — that
|
|
36
|
+
would reflow every frame — but `transform` is paint-time, so animating it costs
|
|
37
|
+
one matrix per animated box per frame ([Layout](./layout.md#transforms-m15)).
|
|
38
|
+
- Both the `transition` shorthand and the `transition-property` / `-duration` /
|
|
39
|
+
`-delay` / `-timing-function` longhands are parsed; time values accept `s` and
|
|
40
|
+
`ms`; timing functions are `linear`, `ease`, `ease-in`, `ease-out` and
|
|
41
|
+
`ease-in-out` (`ease` is a smoothstep approximation).
|
|
42
|
+
- Retargeting mid-flight starts a new transition from the current interpolated
|
|
43
|
+
value, so reversing a hover animates smoothly from wherever it was.
|
|
44
|
+
- `computedStyle()` reports the displayed (interpolated) value, so transitions
|
|
45
|
+
are directly observable in tests.
|
|
46
|
+
|
|
47
|
+
`transform` lists interpolate **element-wise** (`document.cpp`,
|
|
48
|
+
`xt_transform_interpolate`), which is what makes the common pair
|
|
49
|
+
`translateY(-4px) rotate(-0.35deg)` <-> `none` glide instead of jumping: the
|
|
50
|
+
missing side is filled with identity ops of the same kind. Two lists whose shapes
|
|
51
|
+
or units do not line up (`rotate(10deg)` vs `rotate(10rad)`, or a different number
|
|
52
|
+
of functions) are not interpolated at all — the transition snaps to the target,
|
|
53
|
+
which is CSS's discrete fallback rather than a wrong-looking sweep.
|
|
54
|
+
|
|
55
|
+
## CSS transforms (M15)
|
|
56
|
+
|
|
57
|
+
`transform` and `transform-origin` are supported as *paint-time* effects
|
|
58
|
+
(`translate`/`translate3d`/`translateX`/`translateY`,
|
|
59
|
+
`scale`/`scale3d`/`scaleX`/`scaleY`, `rotate`/`rotateZ`, `matrix`; the `3d`
|
|
60
|
+
spellings' `z` argument is parsed and dropped, since the engine is 2D;
|
|
61
|
+
`deg`/`rad`/`grad`/`turn`; `%`, `px`, `em`, `rem`, `vw`, `vh`). Layout
|
|
62
|
+
is not affected: geometry stays where flow put it, and only the painted result
|
|
63
|
+
moves, so a size, a scroll position or a sibling's position never depends on an
|
|
64
|
+
animation frame. See [Layout](./layout.md#transforms-m15) for the composition
|
|
65
|
+
rules and [Rendering](./rendering.md) for how it reaches the GPU.
|
|
66
|
+
|
|
67
|
+
Two ways to drive one:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// 1. Per frame — a pose from the frame clock. Nothing reflows: the string is
|
|
71
|
+
// parsed into matrices and the display list picks them up in the same frame.
|
|
72
|
+
const tick = (t: number): void => {
|
|
73
|
+
const s = t / 1000;
|
|
74
|
+
art.style.setProperty(
|
|
75
|
+
"transform",
|
|
76
|
+
"translateY(" + (Math.sin(s * 2.4) * 5).toFixed(2) + "px) " +
|
|
77
|
+
"rotate(" + (Math.sin(s * 1.15) * 1.1).toFixed(2) + "deg)",
|
|
78
|
+
);
|
|
79
|
+
win.requestAnimationFrame(tick);
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```css
|
|
84
|
+
/* 2. From a state change — a transitioned transform, like any other property. */
|
|
85
|
+
#caption {
|
|
86
|
+
transform-origin: 50% 100%;
|
|
87
|
+
transition: transform 260ms ease-out;
|
|
88
|
+
}
|
|
89
|
+
#caption:hover { transform: translateY(-2px) scale(1.03); }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Both are exercised by `examples/gui/pelican-svg` (per-frame sway plus a
|
|
93
|
+
hover transition on the caption) and pinned by
|
|
94
|
+
`tests/e2e/gui-transform.test.ts`.
|
|
95
|
+
|
|
96
|
+
Still to do for animation: `@keyframes` animations and `cubic-bezier(...)`.
|
|
97
|
+
|