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
|
@@ -179,7 +179,7 @@ function buildBox(node, flags, blockified = false) {
|
|
|
179
179
|
// by its display: body { display: grid } or flex is common.
|
|
180
180
|
const display = node.type === "root" ? style?.display : node.type;
|
|
181
181
|
const flex = ["flex", "inline-flex"].includes(display)
|
|
182
|
-
? style?.flexDirection
|
|
182
|
+
? style?.flexDirection?.startsWith("column") ? "column" : "row"
|
|
183
183
|
: null;
|
|
184
184
|
const grid = ["grid", "inline-grid"].includes(display);
|
|
185
185
|
const table = ["table", "inline-table"].includes(node.type);
|
|
@@ -358,6 +358,7 @@ function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = nu
|
|
|
358
358
|
const context = Number.isInteger(style.zIndex);
|
|
359
359
|
ctx.layer = Object.freeze({
|
|
360
360
|
z: context ? style.zIndex : 0,
|
|
361
|
+
nodeId: box.node.id,
|
|
361
362
|
order: ctx.layerCount++,
|
|
362
363
|
context,
|
|
363
364
|
parent: ctx.stackingContext,
|
|
@@ -430,6 +431,7 @@ function cachedBlockResult(ctx, box, key, y, x) {
|
|
|
430
431
|
for (const layer of cached.layers) {
|
|
431
432
|
layers.push(Object.freeze({
|
|
432
433
|
z: layer.z,
|
|
434
|
+
nodeId: layer.nodeId,
|
|
433
435
|
order: ctx.layerCount++,
|
|
434
436
|
context: layer.context,
|
|
435
437
|
parent: layer.parent === -1 ? ctx.stackingContext : layers[layer.parent],
|
|
@@ -485,7 +487,7 @@ function cacheBlockResult(ctx, box, key, x, y, result, start) {
|
|
|
485
487
|
for (const layer of ordered) {
|
|
486
488
|
const parent = indices.get(layer.parent) ?? (layer.parent === start.context ? -1 : null);
|
|
487
489
|
if (parent === null) return;
|
|
488
|
-
layers.push({ z: layer.z, context: layer.context, parent });
|
|
490
|
+
layers.push({ z: layer.z, nodeId: layer.nodeId, context: layer.context, parent });
|
|
489
491
|
}
|
|
490
492
|
const indexOf = (layer) => layer === start.layer ? -1 : indices.get(layer) ?? null;
|
|
491
493
|
const boxLayers = boxes.map((record) => indexOf(record.layer));
|
|
@@ -538,7 +540,7 @@ function layoutAbsolutes(ctx, box, record) {
|
|
|
538
540
|
const [top, right, bottom, left] = (style?.inset ?? ["auto", "auto", "auto", "auto"])
|
|
539
541
|
.map((value, index) => value === "auto" ? null : resolveLength(value, index % 2 ? containing.width : containing.height));
|
|
540
542
|
const margin = style?.margin ?? ZERO_BOX;
|
|
541
|
-
|
|
543
|
+
let [marginTop, marginRight, marginBottom, marginLeft] = margin.map(numberOrZero);
|
|
542
544
|
const specifiedWidth = numberOrNull(resolveLength(style?.width, containing.width));
|
|
543
545
|
let width;
|
|
544
546
|
if (specifiedWidth == null && left != null && right != null) {
|
|
@@ -546,6 +548,19 @@ function layoutAbsolutes(ctx, box, record) {
|
|
|
546
548
|
} else {
|
|
547
549
|
width = resolveHorizontal(ctx, absolute, 0, containing.width - (left ?? 0) - (right ?? 0), true, containing.width).width;
|
|
548
550
|
}
|
|
551
|
+
// CSS 2 §10.3.7: with both insets and width resolved, auto margins
|
|
552
|
+
// absorb the remaining space. Normal-flow margin resolution above is
|
|
553
|
+
// only used to size the box; its x is not the absolute position.
|
|
554
|
+
if (specifiedWidth != null && left != null && right != null) {
|
|
555
|
+
const remaining = containing.width-left-right-width-marginLeft-marginRight;
|
|
556
|
+
const autoLeft=margin[3]==="auto",autoRight=margin[1]==="auto";
|
|
557
|
+
if(autoLeft&&autoRight) {
|
|
558
|
+
if(remaining>=0)marginLeft=marginRight=remaining/2;
|
|
559
|
+
else if(style.direction==="rtl"){marginLeft=remaining;marginRight=0;}
|
|
560
|
+
else {marginLeft=0;marginRight=remaining;}
|
|
561
|
+
}else if(autoLeft)marginLeft=remaining;
|
|
562
|
+
else if(autoRight)marginRight=remaining;
|
|
563
|
+
}
|
|
549
564
|
const staticPosition = absolute.staticPosition ?? containing;
|
|
550
565
|
const x = left != null ? containing.x + left + marginLeft
|
|
551
566
|
: right != null ? containing.x + containing.width - right - marginRight - width
|
|
@@ -566,6 +581,14 @@ function layoutAbsolutes(ctx, box, record) {
|
|
|
566
581
|
}
|
|
567
582
|
}
|
|
568
583
|
|
|
584
|
+
/** CSS Sizing 4: a degenerate ratio behaves as auto; replaced boxes can prefer their natural ratio. */
|
|
585
|
+
function preferredAspectRatio(style, natural = null) {
|
|
586
|
+
const value = style?.aspectRatio;
|
|
587
|
+
if (!value || value.auto && natural != null) return natural;
|
|
588
|
+
const ratio = value.numerator / value.denominator;
|
|
589
|
+
return Number.isFinite(ratio) && ratio > 0 ? ratio : natural;
|
|
590
|
+
}
|
|
591
|
+
|
|
569
592
|
/**
|
|
570
593
|
* justify-content in a column flex container (Flexbox §8.2): `free` is the
|
|
571
594
|
* height left over once the items are stacked, as with a min-height or height
|
|
@@ -602,12 +625,12 @@ function justifyColumn(ctx, items, justify = "normal", free) {
|
|
|
602
625
|
}
|
|
603
626
|
|
|
604
627
|
/** Moves output already produced; records are replaced, as retained ones are frozen and shared. */
|
|
605
|
-
function shiftOutput(ctx, boxStart, fragmentStart, dx, dy) {
|
|
606
|
-
for (let index = boxStart; index <
|
|
628
|
+
function shiftOutput(ctx, boxStart, fragmentStart, dx, dy, boxEnd=ctx.boxes.length, fragmentEnd=ctx.fragments.length) {
|
|
629
|
+
for (let index = boxStart; index < boxEnd; index++) {
|
|
607
630
|
const record = ctx.boxes[index];
|
|
608
631
|
ctx.boxes[index] = { ...record, x: record.x + dx, y: record.y + dy };
|
|
609
632
|
}
|
|
610
|
-
for (let index = fragmentStart; index <
|
|
633
|
+
for (let index = fragmentStart; index < fragmentEnd; index++) {
|
|
611
634
|
ctx.fragments[index] = shiftedFragment(ctx.fragments[index], dx, dy);
|
|
612
635
|
}
|
|
613
636
|
}
|
|
@@ -674,14 +697,24 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
674
697
|
const containingHeight = ctx.containing?.height;
|
|
675
698
|
const resolveHeight = (value) => numberOrNull(resolveLength(value, containingHeight ?? NaN));
|
|
676
699
|
const specifiedHeight = resolveHeight(style?.height);
|
|
700
|
+
const ratio = preferredAspectRatio(style);
|
|
701
|
+
// The transferred height uses the box selected by box-sizing. It is
|
|
702
|
+
// definite for descendants (including percentage heights and grid tracks).
|
|
703
|
+
let ratioHeight = specifiedHeight == null && forcedHeight == null && ratio != null
|
|
704
|
+
? style.boxSizing === "border-box" ? toContentHeight(width / ratio) : contentWidth / ratio
|
|
705
|
+
: null;
|
|
677
706
|
const minHeight = resolveHeight(style?.minHeight);
|
|
678
707
|
const maxHeight = style?.maxHeight === "none" ? null : resolveHeight(style?.maxHeight);
|
|
708
|
+
if (ratioHeight != null) {
|
|
709
|
+
if (maxHeight != null) ratioHeight = Math.min(ratioHeight, toContentHeight(maxHeight));
|
|
710
|
+
if (minHeight != null) ratioHeight = Math.max(ratioHeight, toContentHeight(minHeight));
|
|
711
|
+
}
|
|
679
712
|
const previousContaining = ctx.containing;
|
|
680
713
|
ctx.containing = {
|
|
681
714
|
width: contentWidth,
|
|
682
715
|
height: forcedHeight != null
|
|
683
716
|
? Math.max(0, forcedHeight - border[0] - border[2] - padding[0] - padding[2])
|
|
684
|
-
: specifiedHeight != null ? toContentHeight(specifiedHeight) :
|
|
717
|
+
: specifiedHeight != null ? toContentHeight(specifiedHeight) : ratioHeight,
|
|
685
718
|
};
|
|
686
719
|
|
|
687
720
|
if (box.table) {
|
|
@@ -743,10 +776,25 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
743
776
|
ctx.containing = previousContaining;
|
|
744
777
|
let contentHeight = Math.max(0, contentBottom - contentTop);
|
|
745
778
|
if (specifiedHeight != null) contentHeight = toContentHeight(specifiedHeight);
|
|
779
|
+
else if (ratioHeight != null) contentHeight = style.minHeight === "auto" && style.overflowY === "visible"
|
|
780
|
+
? Math.max(contentHeight, ratioHeight) : ratioHeight;
|
|
746
781
|
if (maxHeight != null) contentHeight = Math.min(contentHeight, toContentHeight(maxHeight));
|
|
747
782
|
if (minHeight != null) contentHeight = Math.max(contentHeight, toContentHeight(minHeight));
|
|
748
783
|
if (forcedHeight != null) contentHeight = Math.max(0, forcedHeight - padding[0] - padding[2] - border[0] - border[2]);
|
|
749
|
-
if (columnItems?.length)
|
|
784
|
+
if (columnItems?.length) {
|
|
785
|
+
justifyColumn(ctx, columnItems, style?.justifyContent, contentHeight - (contentBottom - contentTop));
|
|
786
|
+
if(style?.flexDirection === "column-reverse") {
|
|
787
|
+
for(const [index,item] of columnItems.entries()){
|
|
788
|
+
const record=ctx.boxes[item.boxStart];
|
|
789
|
+
const position=item.mainY+record.y-item.initialY;
|
|
790
|
+
const delta=2*contentTop+contentHeight-2*position-item.mainHeight;
|
|
791
|
+
if(index===0&&baseline!=null)baseline+=delta;
|
|
792
|
+
const next=columnItems[index+1];
|
|
793
|
+
shiftOutput(ctx,item.boxStart,item.fragmentStart,0,delta,
|
|
794
|
+
next?.boxStart??ctx.boxes.length,next?.fragmentStart??ctx.fragments.length);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
750
798
|
if (gridResult) alignGridContent(ctx, gridResult, style?.alignContent, contentHeight - (contentBottom - contentTop));
|
|
751
799
|
record.height = forcedHeight ?? contentHeight + padding[0] + padding[2] + border[0] + border[2];
|
|
752
800
|
// Summary's disclosure marker participates in its inline content.
|
|
@@ -840,6 +888,8 @@ function roman(value) {
|
|
|
840
888
|
|
|
841
889
|
function layoutFlexColumn(ctx, box, x, width, y, minHeight, maxHeight) {
|
|
842
890
|
const style = box.style;
|
|
891
|
+
const reverse=style?.flexDirection === "column-reverse";
|
|
892
|
+
const margins=child=>{const m=child.style?.margin??ZERO_BOX;return reverse?[m[2],m[1],m[0],m[3]]:m;};
|
|
843
893
|
const gap = style?.rowGap ?? style?.gap ?? 0;
|
|
844
894
|
const definiteHeight = ctx.containing?.height;
|
|
845
895
|
// Most column containers have auto height. Without a main-axis constraint
|
|
@@ -854,10 +904,11 @@ function layoutFlexColumn(ctx, box, x, width, y, minHeight, maxHeight) {
|
|
|
854
904
|
for (const [index, child] of box.children.entries()) {
|
|
855
905
|
if (index) cursor += gap;
|
|
856
906
|
const horizontal = columnHorizontal(ctx, child, style, x, width);
|
|
857
|
-
const margin = child
|
|
907
|
+
const margin = margins(child);
|
|
858
908
|
items.push({boxStart:ctx.boxes.length,fragmentStart:ctx.fragments.length,
|
|
859
909
|
autoTop:margin[0] === "auto",autoBottom:margin[2] === "auto"});
|
|
860
910
|
const result = layoutBlock(ctx, child, horizontal.x, horizontal.width, cursor + numberOrZero(margin[0]));
|
|
911
|
+
Object.assign(items.at(-1),{mainY:cursor+numberOrZero(margin[0]),mainHeight:result.height,initialY:ctx.boxes[items.at(-1).boxStart].y});
|
|
861
912
|
cursor += numberOrZero(margin[0]) + result.height + numberOrZero(margin[2]);
|
|
862
913
|
baseline ??= result.baseline;
|
|
863
914
|
}
|
|
@@ -866,7 +917,7 @@ function layoutFlexColumn(ctx, box, x, width, y, minHeight, maxHeight) {
|
|
|
866
917
|
const items = box.children.map(child => {
|
|
867
918
|
const childStyle = child.style;
|
|
868
919
|
const horizontal = columnHorizontal(ctx, child, style, x, width);
|
|
869
|
-
const margin =
|
|
920
|
+
const margin = margins(child);
|
|
870
921
|
const padding = paddingBox(childStyle, width);
|
|
871
922
|
const border = borderWidths(childStyle);
|
|
872
923
|
const extra = padding[0] + padding[2] + border[0] + border[2];
|
|
@@ -908,6 +959,7 @@ function layoutFlexColumn(ctx, box, x, width, y, minHeight, maxHeight) {
|
|
|
908
959
|
const forcedHeight = item.definite || Math.abs(targetHeight - item.naturalHeight) > 1e-9 ? targetHeight : null;
|
|
909
960
|
const result = layoutBlock(ctx, item.box, item.horizontal.x, item.horizontal.width,
|
|
910
961
|
cursor + item.marginLeft, false, forcedHeight);
|
|
962
|
+
Object.assign(item,{mainY:cursor+item.marginLeft,mainHeight:result.height,initialY:ctx.boxes[item.boxStart].y});
|
|
911
963
|
cursor += item.marginLeft + result.height + item.marginRight;
|
|
912
964
|
baseline ??= result.baseline;
|
|
913
965
|
}
|
|
@@ -932,7 +984,14 @@ function layoutFlexRow(ctx, box, x, width, y) {
|
|
|
932
984
|
const style = box.style;
|
|
933
985
|
const columnGap = style?.columnGap ?? style?.gap ?? 0;
|
|
934
986
|
const rowGap = style?.rowGap ?? style?.gap ?? 0;
|
|
935
|
-
const items = box.children.map((child) =>
|
|
987
|
+
const items = box.children.map((child) => {
|
|
988
|
+
const item=flexItem(ctx,child,width);
|
|
989
|
+
if(style?.flexDirection === "row-reverse") {
|
|
990
|
+
[item.marginLeft,item.marginRight]=[item.marginRight,item.marginLeft];
|
|
991
|
+
[item.autoLeft,item.autoRight]=[item.autoRight,item.autoLeft];
|
|
992
|
+
}
|
|
993
|
+
return item;
|
|
994
|
+
});
|
|
936
995
|
|
|
937
996
|
const lines = [];
|
|
938
997
|
let line = [];
|
|
@@ -1007,7 +1066,8 @@ function layoutFlexRow(ctx, box, x, width, y) {
|
|
|
1007
1066
|
} else if (alignment === "end") {
|
|
1008
1067
|
itemY += crossSize - outerHeight;
|
|
1009
1068
|
}
|
|
1010
|
-
const
|
|
1069
|
+
const itemX=style?.flexDirection === "row-reverse"?x+width-(mainX-x)-item.target:mainX;
|
|
1070
|
+
const result = layoutBlock(ctx, item.box, itemX, item.target, itemY, false, forcedHeight);
|
|
1011
1071
|
baseline ??= result.baseline;
|
|
1012
1072
|
mainX += item.target + item.marginRight + (item.autoRight ? autoShare : 0) + columnGap + between;
|
|
1013
1073
|
}
|
|
@@ -1147,7 +1207,7 @@ function layoutGrid(ctx, box, x, width, y) {
|
|
|
1147
1207
|
const measured = dryLayout(ctx, item.box, horizontal.width);
|
|
1148
1208
|
item.height = measured.height + numberOrZero(margin[0]) + numberOrZero(margin[2]);
|
|
1149
1209
|
}
|
|
1150
|
-
const rows = sizeGridRows(grid, rowGap);
|
|
1210
|
+
const rows = sizeGridRows(grid, rowGap, ctx.containing?.height, style?.alignContent);
|
|
1151
1211
|
const rowStarts = trackStarts(rows, [], rowGap, y);
|
|
1152
1212
|
|
|
1153
1213
|
let baseline = null;
|
|
@@ -1164,7 +1224,8 @@ function layoutGrid(ctx, box, x, width, y) {
|
|
|
1164
1224
|
const contentHeight = item.height - margin.top - margin.bottom;
|
|
1165
1225
|
let itemY = areaY + margin.top;
|
|
1166
1226
|
let forcedHeight = null;
|
|
1167
|
-
if (["normal", "stretch"].includes(alignment) && numberOrNull(itemStyle?.height) == null
|
|
1227
|
+
if (["normal", "stretch"].includes(alignment) && numberOrNull(itemStyle?.height) == null
|
|
1228
|
+
&& (alignment === "stretch" || preferredAspectRatio(itemStyle) == null)) {
|
|
1168
1229
|
forcedHeight = Math.max(contentHeight, areaHeight - margin.top - margin.bottom);
|
|
1169
1230
|
} else if (alignment === "center") {
|
|
1170
1231
|
itemY += (areaHeight - item.height) / 2;
|
|
@@ -1756,10 +1817,10 @@ function updateGrowth(track, value) {
|
|
|
1756
1817
|
}
|
|
1757
1818
|
|
|
1758
1819
|
/** Row sizing: explicit lengths are fixed; other rows fit their tallest item. */
|
|
1759
|
-
function sizeGridRows(grid, gap) {
|
|
1820
|
+
function sizeGridRows(grid, gap, available = null, alignment = "normal") {
|
|
1760
1821
|
const rows = grid.rowTracks.map((track) => {
|
|
1761
|
-
const fixed = numberOrNull(resolveBreadth(track.min,
|
|
1762
|
-
const fixedMax = numberOrNull(resolveBreadth(track.max,
|
|
1822
|
+
const fixed = numberOrNull(resolveBreadth(track.min, available));
|
|
1823
|
+
const fixedMax = numberOrNull(resolveBreadth(track.max, available));
|
|
1763
1824
|
return fixed != null && fixedMax != null ? { size: fixedMax, fixed: true } : { size: fixed ?? 0, fixed: false };
|
|
1764
1825
|
});
|
|
1765
1826
|
for (const item of grid.items.filter((candidate) => candidate.rowSpan === 1)) {
|
|
@@ -1772,6 +1833,34 @@ function sizeGridRows(grid, gap) {
|
|
|
1772
1833
|
const flexible = spanned.filter((row) => !row.fixed);
|
|
1773
1834
|
if (item.height > current && flexible.length) flexible.at(-1).size += item.height - current;
|
|
1774
1835
|
}
|
|
1836
|
+
// Fractional row tracks consume the definite block-axis space, just as
|
|
1837
|
+
// column fr tracks consume inline space. Empty grid items still stretch
|
|
1838
|
+
// into those tracks (e.g. a 2x2 game board sized by aspect-ratio).
|
|
1839
|
+
const flexible=grid.rowTracks.flatMap((track,index)=>track.max?.fr>0?[index]:[]);
|
|
1840
|
+
if(flexible.length){
|
|
1841
|
+
let fraction=0;
|
|
1842
|
+
if(available!=null){
|
|
1843
|
+
const leftover=available-gap*Math.max(0,rows.length-1)-rows.reduce((sum,row,index)=>sum+(flexible.includes(index)?0:row.size),0);
|
|
1844
|
+
const frozen=new Set();
|
|
1845
|
+
for(let guard=0;guard<=flexible.length;guard++){
|
|
1846
|
+
const active=flexible.filter(index=>!frozen.has(index));
|
|
1847
|
+
const factors=Math.max(1,active.reduce((sum,index)=>sum+grid.rowTracks[index].max.fr,0));
|
|
1848
|
+
const space=leftover-[...frozen].reduce((sum,index)=>sum+rows[index].size,0);
|
|
1849
|
+
fraction=Math.max(0,space/factors);
|
|
1850
|
+
const violators=active.filter(index=>rows[index].size>fraction*grid.rowTracks[index].max.fr);
|
|
1851
|
+
if(!violators.length)break;
|
|
1852
|
+
for(const index of violators)frozen.add(index);
|
|
1853
|
+
}
|
|
1854
|
+
}else fraction=Math.max(...flexible.map(index=>rows[index].size/Math.max(1,grid.rowTracks[index].max.fr)));
|
|
1855
|
+
for(const index of flexible)rows[index].size=Math.max(rows[index].size,fraction*grid.rowTracks[index].max.fr);
|
|
1856
|
+
}
|
|
1857
|
+
// Normal grid content alignment stretches auto tracks to fill a definite
|
|
1858
|
+
// height, so place-items:center centers inside the entire grid area.
|
|
1859
|
+
if (available != null && ["normal", "stretch", undefined].includes(alignment)) {
|
|
1860
|
+
const free = available - rows.reduce((sum, row) => sum + row.size, 0) - gap * Math.max(0, rows.length - 1);
|
|
1861
|
+
const stretchy = rows.filter((row, index) => grid.rowTracks[index].max === "auto");
|
|
1862
|
+
if (free > 0 && stretchy.length) for (const row of stretchy) row.size += free / stretchy.length;
|
|
1863
|
+
}
|
|
1775
1864
|
return rows.map((row) => row.size);
|
|
1776
1865
|
}
|
|
1777
1866
|
|
|
@@ -1838,6 +1927,10 @@ function resolveHorizontal(ctx, box, containingX, containingWidth, shrink, perce
|
|
|
1838
1927
|
let width;
|
|
1839
1928
|
if (box.replaced) width = imageDimensions(box.node, containingWidth).width + extra;
|
|
1840
1929
|
else if (specified != null) width = borderBox(specified);
|
|
1930
|
+
else if (preferredAspectRatio(style) != null && numberOrNull(resolveLength(style?.height, ctx.containing?.height ?? NaN)) != null) {
|
|
1931
|
+
const height = resolveLength(style.height, ctx.containing?.height ?? NaN);
|
|
1932
|
+
width = borderBox(height * preferredAspectRatio(style));
|
|
1933
|
+
}
|
|
1841
1934
|
else if (shrink) width = Math.min(maxContentWidth(ctx, box), available);
|
|
1842
1935
|
else width = available;
|
|
1843
1936
|
const maxWidth = numberOrNull(resolveLength(style?.maxWidth, percentageBasis));
|
|
@@ -2735,7 +2828,7 @@ function imageDimensions(node, referenceWidth, referenceHeight = null) {
|
|
|
2735
2828
|
const intrinsicWidth = (canvas ? node.widthAttribute : node.resource?.width) ?? node.widthAttribute ?? 300;
|
|
2736
2829
|
const intrinsicHeight = (canvas ? node.heightAttribute : node.resource?.height) ?? node.heightAttribute ?? 150;
|
|
2737
2830
|
const emptyCanvas = node.tagName === "CANVAS" && !(intrinsicWidth > 0 && intrinsicHeight > 0);
|
|
2738
|
-
const ratio = emptyCanvas ? null : intrinsicWidth / intrinsicHeight;
|
|
2831
|
+
const ratio = preferredAspectRatio(node.style, emptyCanvas ? null : intrinsicWidth / intrinsicHeight);
|
|
2739
2832
|
const cssWidth = node.style?.width;
|
|
2740
2833
|
const cssHeight = node.style?.height;
|
|
2741
2834
|
let width = cssWidth !== "auto" && cssWidth != null
|
|
@@ -2746,7 +2839,7 @@ function imageDimensions(node, referenceWidth, referenceHeight = null) {
|
|
|
2746
2839
|
: null;
|
|
2747
2840
|
if (!Number.isFinite(width)) width = null;
|
|
2748
2841
|
if (!Number.isFinite(height)) height = null;
|
|
2749
|
-
if (width == null && height == null) [width, height] = [intrinsicWidth, intrinsicHeight];
|
|
2842
|
+
if (width == null && height == null) [width, height] = [intrinsicWidth, ratio == null ? intrinsicHeight : intrinsicWidth / ratio];
|
|
2750
2843
|
else if (width == null) width = ratio == null ? intrinsicWidth : height * ratio;
|
|
2751
2844
|
else if (height == null) height = ratio == null ? intrinsicHeight : width / ratio;
|
|
2752
2845
|
if (node.style?.maxWidth !== "none" && node.style?.maxWidth != null) {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Presentation timestamps for ordinary (nonfragmented) MP4 video. FFmpeg
|
|
2
|
+
// returns decoded pictures in presentation order; average FPS cannot time VFR.
|
|
3
|
+
// Unsupported tables/edits retain the existing decoder clock.
|
|
4
|
+
export function mp4FrameTimes(bytes) {
|
|
5
|
+
if (!(bytes instanceof Uint8Array)) return null;
|
|
6
|
+
try {
|
|
7
|
+
const v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);
|
|
8
|
+
let seen=0;
|
|
9
|
+
const u=(p,end)=>{if(p<0||p+4>end)throw Error('short field');return v.getUint32(p)};
|
|
10
|
+
const boxes=(start,end)=>{
|
|
11
|
+
const out=[];
|
|
12
|
+
for(let p=start;p<end;){
|
|
13
|
+
if(++seen>32768)throw Error('box limit');
|
|
14
|
+
let size=u(p,end),header=8;const type=u(p+4,end);
|
|
15
|
+
if(size===1){size=u(p+8,end)*4294967296+u(p+12,end);header=16}
|
|
16
|
+
if(size===0)size=end-p;
|
|
17
|
+
// Progressive files may end inside mdat; timing tables before it
|
|
18
|
+
// are already complete and do not depend on the arriving payload.
|
|
19
|
+
if(size>end-p && type===0x6d646174 && start===0)break;
|
|
20
|
+
if(!Number.isSafeInteger(size)||size<header||size>end-p)throw Error('bad box');
|
|
21
|
+
out.push({type,p:p+header,end:p+size});p+=size;
|
|
22
|
+
}return out;
|
|
23
|
+
};
|
|
24
|
+
const tag=s=>[...s].reduce((n,c)=>n*256+c.charCodeAt(0),0);
|
|
25
|
+
const find=(list,name)=>list.find(b=>b.type===tag(name));
|
|
26
|
+
const children=b=>boxes(b.p,b.end);
|
|
27
|
+
const top=boxes(0,bytes.length),moov=find(top,'moov');if(!moov)return null;
|
|
28
|
+
const movie=children(moov);if(find(movie,'mvex'))return null;
|
|
29
|
+
const tracks=movie.filter(b=>b.type===tag('trak'));
|
|
30
|
+
let result=null;
|
|
31
|
+
for(const trak of tracks){
|
|
32
|
+
const nodes=children(trak),mdia=find(nodes,'mdia');if(!mdia)continue;
|
|
33
|
+
const media=children(mdia),hdlr=find(media,'hdlr');
|
|
34
|
+
if(!hdlr||u(hdlr.p+8,hdlr.end)!==tag('vide'))continue;
|
|
35
|
+
if(result)return null;
|
|
36
|
+
const mdhd=find(media,'mdhd');if(!mdhd||bytes[mdhd.p]>1)return null;
|
|
37
|
+
const scale=u(mdhd.p+(bytes[mdhd.p]===1?20:12),mdhd.end);if(!scale)return null;
|
|
38
|
+
let offset=0;const edts=find(nodes,'edts');
|
|
39
|
+
if(edts){
|
|
40
|
+
const edit=find(children(edts),'elst');
|
|
41
|
+
if(!edit||bytes[edit.p]!==0||u(edit.p+4,edit.end)!==1||u(edit.p+16,edit.end)!==65536)return null;
|
|
42
|
+
offset=u(edit.p+12,edit.end);if(offset>=2147483648)return null;
|
|
43
|
+
}
|
|
44
|
+
const minf=find(media,'minf');if(!minf)return null;
|
|
45
|
+
const stbl=find(children(minf),'stbl');if(!stbl)return null;
|
|
46
|
+
const tables=children(stbl),stts=find(tables,'stts'),ctts=find(tables,'ctts');if(!stts||bytes[stts.p]!==0)return null;
|
|
47
|
+
const times=[],durations=[];let time=0;
|
|
48
|
+
const runs=u(stts.p+4,stts.end);if(runs>250000)return null;
|
|
49
|
+
for(let i=0;i<runs;i++){
|
|
50
|
+
const count=u(stts.p+8+i*8,stts.end),delta=u(stts.p+12+i*8,stts.end);
|
|
51
|
+
if(!delta||times.length+count>250000)return null;
|
|
52
|
+
for(let j=0;j<count;j++){times.push(time);durations.push(delta);time+=delta;if(!Number.isSafeInteger(time))return null;}
|
|
53
|
+
}
|
|
54
|
+
if(ctts){
|
|
55
|
+
if(bytes[ctts.p]>1)return null;let index=0;
|
|
56
|
+
const runs=u(ctts.p+4,ctts.end);if(runs>250000)return null;
|
|
57
|
+
for(let i=0;i<runs;i++){
|
|
58
|
+
const count=u(ctts.p+8+i*8,ctts.end);let delta=u(ctts.p+12+i*8,ctts.end);
|
|
59
|
+
if(bytes[ctts.p]===1)delta|=0;
|
|
60
|
+
if(index+count>times.length)return null;
|
|
61
|
+
for(let j=0;j<count;j++)times[index++]+=delta;
|
|
62
|
+
}if(index!==times.length)return null;
|
|
63
|
+
}
|
|
64
|
+
const keys=[];const stss=find(tables,'stss');
|
|
65
|
+
if(stss){const count=u(stss.p+4,stss.end);if(count>times.length)return null;
|
|
66
|
+
for(let i=0;i<count;i++){const n=u(stss.p+8+i*4,stss.end);if(!n||n>times.length)return null;keys.push((times[n-1]-offset)/scale)}
|
|
67
|
+
}else for(const t of times)keys.push((t-offset)/scale);
|
|
68
|
+
const frames=Float64Array.from(times,t=>(t-offset)/scale);frames.sort();keys.sort((a,b)=>a-b);
|
|
69
|
+
if(!frames.length||frames[0]<-1e-6)return null;
|
|
70
|
+
let end=0;for(let i=0;i<times.length;i++)end=Math.max(end,(times[i]+durations[i]-offset)/scale);
|
|
71
|
+
result={frames,keys,end};
|
|
72
|
+
}return result;
|
|
73
|
+
}catch{return null}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function timeVideoDecoder(decoder,bytes) {
|
|
77
|
+
const timeline=mp4FrameTimes(bytes);if(!timeline)return decoder;
|
|
78
|
+
let index=0;
|
|
79
|
+
return {
|
|
80
|
+
width:decoder.width,height:decoder.height,fps:decoder.fps,
|
|
81
|
+
get frameTime(){return timeline.frames[Math.max(0,index-1)]},
|
|
82
|
+
get nextFrameTime(){return timeline.frames[index]??timeline.end},
|
|
83
|
+
nextFrame(...args){const pixels=decoder.nextFrame(...args);if(pixels)index++;return pixels},
|
|
84
|
+
seek(ms){
|
|
85
|
+
decoder.seek(ms);const target=ms/1000;let key=timeline.keys[0]??0;
|
|
86
|
+
for(const t of timeline.keys){if(t>target+1e-6)break;key=t}
|
|
87
|
+
let low=0,high=timeline.frames.length;
|
|
88
|
+
while(low<high){const mid=(low+high)>>>1;if(timeline.frames[mid]<key-1e-6)low=mid+1;else high=mid}
|
|
89
|
+
index=low;
|
|
90
|
+
},
|
|
91
|
+
close(){decoder.close()},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Browser adapter only. All pixel composition and sampling use Land unchanged.
|
|
2
|
+
import {createDirectComposer, type DirectSurface} from '../../vendor/land-renderer/direct-compositor.ts';
|
|
3
|
+
export interface CanvasLayer {
|
|
4
|
+
pixels: Uint8Array; width: number; height: number;
|
|
5
|
+
x: number; y: number; drawWidth: number; drawHeight: number;
|
|
6
|
+
}
|
|
7
|
+
export class CanvasComposer {
|
|
8
|
+
private surfaces: DirectSurface[] = [];
|
|
9
|
+
private positions = new WeakMap<DirectSurface,{x:number;y:number}>();
|
|
10
|
+
private buffers = new WeakMap<Uint8Array,NonNullable<DirectSurface["pixels"]>>();
|
|
11
|
+
private base: {key:string;surface:DirectSurface} | null = null;
|
|
12
|
+
private readonly direct = createDirectComposer({input:{x:0,y:0,pointer:false},wm:{
|
|
13
|
+
renderSurfaces:()=>this.surfaces,origin:surface=>this.positions.get(surface)!,
|
|
14
|
+
}});
|
|
15
|
+
private surface(layer: CanvasLayer): DirectSurface {
|
|
16
|
+
let pixels=this.buffers.get(layer.pixels);
|
|
17
|
+
if(!pixels||pixels.width!==layer.width||pixels.height!==layer.height){pixels={bytes:layer.pixels,width:layer.width,height:layer.height,stride:layer.width*4,format:0};this.buffers.set(layer.pixels,pixels);}
|
|
18
|
+
const surface:DirectSurface={pixels,
|
|
19
|
+
scale:1,transform:0,viewportDestination:[layer.drawWidth,layer.drawHeight]};
|
|
20
|
+
this.positions.set(surface,{x:layer.x,y:layer.y});
|
|
21
|
+
return surface;
|
|
22
|
+
}
|
|
23
|
+
compose(width:number,height:number,background:readonly number[],layers:CanvasLayer[]):Uint8Array {
|
|
24
|
+
const key=[width,height,...background].join('/');
|
|
25
|
+
if(this.base?.key!==key){
|
|
26
|
+
const pixels=new Uint8Array(width*height*4);
|
|
27
|
+
// Browser RGBA and Land BGRA use identical alpha and blend operations.
|
|
28
|
+
// Supplying an opaque RGBA base also hides Land's default desktop color.
|
|
29
|
+
const words=new Uint32Array(pixels.buffer);
|
|
30
|
+
const color=new Uint8Array([background[0]!,background[1]!,background[2]!,255]);
|
|
31
|
+
words.fill(new Uint32Array(color.buffer)[0]!);
|
|
32
|
+
this.base={key,surface:this.surface({pixels,width,height,x:0,y:0,drawWidth:width,drawHeight:height})};
|
|
33
|
+
}
|
|
34
|
+
this.surfaces=[this.base.surface,...layers.map(layer=>this.surface(layer))];
|
|
35
|
+
const out=this.direct.compose(width,height);
|
|
36
|
+
return new Uint8Array(out.buffer,out.byteOffset,out.byteLength);
|
|
37
|
+
}
|
|
38
|
+
close():void {this.direct.close();this.surfaces=[];this.base=null;this.positions=new WeakMap();this.buffers=new WeakMap();}
|
|
39
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {rotationMatrix3D,multiplyRotation3D} from '../style/transform.js';
|
|
2
|
+
import {resolveAffine,multiplyTransform as composeTransforms} from '../style/transform.js';
|
|
1
3
|
/**
|
|
2
4
|
* Converts layout output into an immutable, serializable display list.
|
|
3
5
|
*
|
|
@@ -94,8 +96,13 @@ export function buildDisplayList(layout, tree = null, resources = null, backgrou
|
|
|
94
96
|
for (const box of boxesByLayer.get(layer.order) ?? []) paintBox(box);
|
|
95
97
|
for (const fragment of fragmentsByLayer.get(layer.order) ?? []) paintFragment(fragment);
|
|
96
98
|
};
|
|
99
|
+
// Absolute layout is deferred until its containing block's dimensions are
|
|
100
|
+
// known. Its allocation order is therefore not CSS tree/paint order.
|
|
101
|
+
const treeOrder = new Map();
|
|
102
|
+
const indexTree = node => { treeOrder.set(node.id, treeOrder.size); for (const child of node.children ?? []) indexTree(child); };
|
|
103
|
+
if (tree?.root) indexTree(tree.root);
|
|
97
104
|
const paintContext = (context) => {
|
|
98
|
-
const members = (children.get(context.order) ?? []).sort((left, right) => left.z - right.z || left.order - right.order);
|
|
105
|
+
const members = (children.get(context.order) ?? []).sort((left, right) => left.z - right.z || (treeOrder.get(left.nodeId) ?? left.order) - (treeOrder.get(right.nodeId) ?? right.order));
|
|
99
106
|
for (const layer of members) if (layer.z < 0) paintMember(layer);
|
|
100
107
|
paintLayerContent(context);
|
|
101
108
|
for (const layer of members) if (layer.z >= 0) paintMember(layer);
|
|
@@ -111,7 +118,7 @@ export function buildDisplayList(layout, tree = null, resources = null, backgrou
|
|
|
111
118
|
const start = items.length;
|
|
112
119
|
const rect = { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
113
120
|
hits.push({
|
|
114
|
-
nodeId: box.nodeId, rect: effect.transform ? transformRect(effect.transform, rect) : rect, clipRect: effect.clipRect, fixed: effect.fixed,
|
|
121
|
+
nodeId: box.nodeId, localRect:rect, transform:effect.transform, rect: effect.transform ? transformRect(effect.transform, rect) : rect, clipRect: effect.clipRect, fixed: effect.fixed,
|
|
115
122
|
});
|
|
116
123
|
reach(effect, rect);
|
|
117
124
|
const radii = cornerRadii(style, rect);
|
|
@@ -189,7 +196,7 @@ export function buildDisplayList(layout, tree = null, resources = null, backgrou
|
|
|
189
196
|
height: run.type === "image" ? run.height : fragment.height,
|
|
190
197
|
};
|
|
191
198
|
hits.push({
|
|
192
|
-
nodeId: run.nodeId,
|
|
199
|
+
nodeId: run.nodeId, localRect:hitRect, transform:effect.transform,
|
|
193
200
|
rect: effect.transform ? transformRect(effect.transform, hitRect) : hitRect,
|
|
194
201
|
clipRect: effect.clipRect,
|
|
195
202
|
fixed: effect.fixed,
|
|
@@ -296,6 +303,8 @@ export function hitTarget(displayList, x, y, scroll, skip = () => false) {
|
|
|
296
303
|
if (pointX < rect.x || pointX >= rect.x + rect.width || pointY < rect.y || pointY >= rect.y + rect.height) continue;
|
|
297
304
|
if (clipRect && (pointX < clipRect.x || pointX >= clipRect.x + clipRect.width
|
|
298
305
|
|| pointY < clipRect.y || pointY >= clipRect.y + clipRect.height)) continue;
|
|
306
|
+
const {transform,localRect}=hits[index];
|
|
307
|
+
if(transform&&localRect){const p=inverseTransformPoint(transform,pointX,pointY);if(p.x<localRect.x||p.x>=localRect.x+localRect.width||p.y<localRect.y||p.y>=localRect.y+localRect.height)continue;}
|
|
299
308
|
if (skip(nodeId)) continue;
|
|
300
309
|
return nodeId;
|
|
301
310
|
}
|
|
@@ -366,7 +375,11 @@ function paintEffects(layout, tree, scrollOffsetOf = null) {
|
|
|
366
375
|
const ownTransform = memo((node) => {
|
|
367
376
|
const transform = node.style?.transform;
|
|
368
377
|
const box = boxes.get(node.id);
|
|
369
|
-
if (!box || !transform
|
|
378
|
+
if (!box || !transform || (!transform.affine && (transform.scaleX == null || (transform.scaleX === 1 && transform.scaleY === 1)))) return null;
|
|
379
|
+
if (transform.affine) {
|
|
380
|
+
const {a,b,c,d,e,f}=resolveAffine(transform.affine,box.width,box.height), origin=transformOrigin(node.style.transformOrigin,box);
|
|
381
|
+
return {a,b,c,d,e:origin.x*(1-a)-c*origin.y+e,f:origin.y*(1-d)-b*origin.x+f};
|
|
382
|
+
}
|
|
370
383
|
const origin = transformOrigin(node.style.transformOrigin, box);
|
|
371
384
|
return {
|
|
372
385
|
a: transform.scaleX,
|
|
@@ -443,6 +456,17 @@ function paintEffects(layout, tree, scrollOffsetOf = null) {
|
|
|
443
456
|
transform = transform ? composeTransforms(transform, scrolled) : scrolled;
|
|
444
457
|
}
|
|
445
458
|
}
|
|
459
|
+
// Backface-hidden descendants share their ancestor's rotation only
|
|
460
|
+
// inside preserve-3d. Flattened groups begin a new local 3D context.
|
|
461
|
+
if(chain.some(node=>node.style?.backfaceVisibility==='hidden')){
|
|
462
|
+
const identity=[1,0,0,0,1,0,0,0,1];let rotation=identity;
|
|
463
|
+
for(let index=chain.length-1;index>=0;index--){
|
|
464
|
+
const style=chain[index].style;
|
|
465
|
+
rotation=multiplyRotation3D(rotation,rotationMatrix3D(style?.transform));
|
|
466
|
+
if(style?.backfaceVisibility==='hidden'&&rotation[8]<-1e-10){cache.set(key,null);return null;}
|
|
467
|
+
if(style?.transformStyle!=='preserve-3d')rotation=identity;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
446
470
|
const roundedClips = [];
|
|
447
471
|
for (const [node, clip, radii] of clips) {
|
|
448
472
|
const matrix = transforms.get(node);
|
|
@@ -495,16 +519,17 @@ function transformOrigin(value, box) {
|
|
|
495
519
|
}
|
|
496
520
|
|
|
497
521
|
/** `outer` after `inner`: x -> outer(inner(x)), for scales with translation. */
|
|
498
|
-
function
|
|
499
|
-
|
|
522
|
+
export function inverseTransformPoint(matrix,x,y) {
|
|
523
|
+
const {a,b=0,c=0,d,e,f}=matrix,det=a*d-b*c;
|
|
524
|
+
if(!det)return {x:Infinity,y:Infinity};
|
|
525
|
+
x-=e;y-=f;return {x:(d*x-c*y)/det,y:(a*y-b*x)/det};
|
|
500
526
|
}
|
|
501
527
|
|
|
502
|
-
function transformRect(matrix, rect) {
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
return { x: Math.min(x0, x1), y: Math.min(y0, y1), width: Math.abs(x1 - x0), height: Math.abs(y1 - y0) };
|
|
528
|
+
export function transformRect(matrix, rect) {
|
|
529
|
+
const {a,b=0,c=0,d,e,f}=matrix;
|
|
530
|
+
const points=[[rect.x,rect.y],[rect.x+rect.width,rect.y],[rect.x,rect.y+rect.height],[rect.x+rect.width,rect.y+rect.height]].map(([x,y])=>({x:a*x+c*y+e,y:b*x+d*y+f}));
|
|
531
|
+
const x=Math.min(...points.map(p=>p.x)),y=Math.min(...points.map(p=>p.y));
|
|
532
|
+
return {x,y,width:Math.max(...points.map(p=>p.x))-x,height:Math.max(...points.map(p=>p.y))-y};
|
|
508
533
|
}
|
|
509
534
|
|
|
510
535
|
function applyEffect(items, start, effect) {
|
|
@@ -16,21 +16,30 @@ export class RasterReadback {
|
|
|
16
16
|
this.#allocate = allocate;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
read(image, width, height) {
|
|
19
|
+
read(image, width, height, { premultiplied = false, previous = null, region = null, allocate = this.#allocate } = {}) {
|
|
20
20
|
const kit = this.#kit;
|
|
21
21
|
const bytes = width * height * 4;
|
|
22
22
|
if (!this.#scratch || this.#scratch.length < bytes) {
|
|
23
23
|
this.dispose();
|
|
24
24
|
this.#scratch = kit.Malloc(Uint8Array, bytes);
|
|
25
25
|
}
|
|
26
|
-
const
|
|
27
|
-
width,
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const partial = previous?.length === bytes && region
|
|
27
|
+
&& [region.x,region.y,region.width,region.height].every(Number.isInteger)
|
|
28
|
+
&& region.x >= 0 && region.y >= 0 && region.width > 0 && region.height > 0
|
|
29
|
+
&& region.x + region.width <= width && region.y + region.height <= height;
|
|
30
|
+
const x=partial?region.x:0,y=partial?region.y:0;
|
|
31
|
+
const rw=partial?region.width:width,rh=partial?region.height:height;
|
|
32
|
+
const native = image.readPixels(x, y, {
|
|
33
|
+
width:rw, height:rh, colorType: kit.ColorType.RGBA_8888,
|
|
34
|
+
alphaType: premultiplied ? kit.AlphaType.Premul : kit.AlphaType.Unpremul, colorSpace: kit.ColorSpace.SRGB,
|
|
35
|
+
}, this.#scratch, rw * 4);
|
|
30
36
|
if (!native) throw new Error("Unable to read raster pixels");
|
|
31
|
-
const pixels =
|
|
37
|
+
const pixels = allocate ? allocate(bytes)
|
|
32
38
|
: new Uint8Array(this.#shared ? new SharedArrayBuffer(bytes) : new ArrayBuffer(bytes));
|
|
33
|
-
|
|
39
|
+
if(partial){
|
|
40
|
+
pixels.set(previous);
|
|
41
|
+
for(let row=0;row<rh;row++)pixels.set(native.subarray(row*rw*4,(row+1)*rw*4),((y+row)*width+x)*4);
|
|
42
|
+
}else pixels.set(native.subarray(0, bytes));
|
|
34
43
|
return pixels;
|
|
35
44
|
}
|
|
36
45
|
|
|
@@ -12,7 +12,7 @@ export function supportsFrameCheckpoint(version = Bun.version) {
|
|
|
12
12
|
|
|
13
13
|
/** One callback collection per document, with a checkpoint between callbacks. */
|
|
14
14
|
export function createAnimationFrames({ invoke, now, checkpoint = drainMicrotasks,
|
|
15
|
-
frameTimers = null }) {
|
|
15
|
+
frameTimers = null, afterFrame = null }) {
|
|
16
16
|
let nextWakeId = 1;
|
|
17
17
|
// A private timer collection keeps frame handles separate from Window timers.
|
|
18
18
|
// timerTask=false also prevents a frame inheriting a timer's nesting clamp.
|
|
@@ -42,10 +42,15 @@ export function createAnimationFrames({ invoke, now, checkpoint = drainMicrotask
|
|
|
42
42
|
finally { checkpoint(); }
|
|
43
43
|
}
|
|
44
44
|
} finally {
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
// Paint belongs after this complete callback batch and its microtasks,
|
|
46
|
+
// before the next animation opportunity can overtake presentation.
|
|
47
|
+
try { if(!disposed)afterFrame?.(); }
|
|
48
|
+
finally {
|
|
49
|
+
running = false;
|
|
50
|
+
// Callbacks registered during delivery, including its microtasks, belong
|
|
51
|
+
// to the next frame. Long callbacks do not add another fixed 16 ms delay.
|
|
52
|
+
arm(Math.max(0, Math.ceil(16 - (now() - timestamp))));
|
|
53
|
+
}
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
56
|
return {
|