gesso-core 0.1.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,154 @@
1
1
  # gesso-core
2
2
 
3
+ ## 0.2.1
4
+
5
+ ## 0.2.0
6
+
7
+ ### Minor Changes
8
+
9
+ - be07839: **A clip can be driven now.** `videoSource` sealed its playback inside a
10
+ closure, so a video was two booleans: `loop` and `autoplay`, and no
11
+ pause, no seek, no rate, and no way to read where it was. Anything that
12
+ wanted a transport had to supply a `VideoResolver` of its own and drive
13
+ the position itself.
14
+
15
+ `VideoTransport` arrives through `onReady`, because a playback does not
16
+ exist until a file has been fetched and a decoder configured, and a
17
+ modifier attaches long before either. It carries `play`, `pause`,
18
+ `seek`, `setRate`, `setVolume`, `setMuted` and `retry`, and the
19
+ position, duration and state to read back.
20
+
21
+ Nothing on it is reactive, deliberately. `gesso-core` has no cells, so
22
+ it reports through `onChange` and the tier above wraps it in whatever
23
+ its own state primitive is. `onChange` does not fire as the position
24
+ moves: that happens on every frame of every clip, and a listener woken
25
+ sixty times a second to move a scrubber by less than a pixel is how a
26
+ video costs an application its frame budget. Read the position on the
27
+ frames you are already drawing, or extrapolate it as `VideoControls`
28
+ does, at ten a second.
29
+
30
+ **A clip scrolled out of view stops decoding.** A hidden tab already
31
+ stopped one, at the animation driver, but within a visible page nothing
32
+ did, so a page of clips cost the sum of all of them however few were on
33
+ screen. It never seeks, so coming back into view resumes rather than
34
+ reloads, and a box laid out to nothing counts as not measured yet
35
+ rather than as hidden.
36
+
37
+ **This is the minor, and it is one line of it.** `UiModifierHost` gains
38
+ a required `viewportBox()`, which is what the visibility rule is built
39
+ on. A modifier receives a host rather than implementing one, so an
40
+ application is unaffected; anything that implemented the interface
41
+ itself has one method to add. The budget that interface documents is
42
+ deliberately narrow, and the case for widening it is that a decoder, a
43
+ poller, or anything else that costs whether or not it is looked at has
44
+ no business running for a node a thousand pixels off the bottom.
45
+
46
+ `VideoClock` is the seam for a clip whose time belongs to something
47
+ else. Video drops frames and nobody can tell; audio can neither drop
48
+ nor resample without being heard, so where the two must agree the sound
49
+ leads and the picture follows. It is a seam rather than an
50
+ implementation because `AudioContext` does not exist on a worker.
51
+
52
+ ### Patch Changes
53
+
54
+ - be07839: **A 24fps clip played at 20 on a 60Hz display**, dropping one frame in
55
+ six, for ever, on the commonest clip rate there is.
56
+
57
+ An animation that declares `stepMs` is declaring a rate, and the driver
58
+ was treating it as a gap between samples. Frames arrive on the
59
+ display's refreshes, so a sample is nearly always served a little after
60
+ it was due; measuring the next step from when it was _served_ rounds
61
+ the period up to a whole refresh and then keeps the rounding. 41.67ms
62
+ wanted, the refresh at 33.3ms too early, the one at 50ms serves it, and
63
+ the next step measured from 50. The period is not 41.67ms but 50ms.
64
+
65
+ The ideal time now advances by exactly `stepMs`, so the served times
66
+ alternate between 33.3 and 50 and average out to the rate that was
67
+ asked for. A rate that divides the refresh exactly was never in
68
+ trouble, which is part of why this went unnoticed: 30fps on 60Hz was
69
+ always right.
70
+
71
+ Falling a whole step behind resynchronises rather than catching up. A
72
+ stall or a hidden tab leaves a backlog whose samples are, for a video,
73
+ pictures already past.
74
+
75
+ None of this touches a value. A tween's output has always been a pure
76
+ function of elapsed time, so nothing was shown at the wrong moment;
77
+ what was wrong is how often anything was shown at all, which no
78
+ assertion about values could have caught.
79
+
80
+ - be07839: **A clip you can seek, and a file you need not hold all of.** The media
81
+ tier could play a looping clip from the beginning and not much else.
82
+
83
+ A seek was a rewind: going backwards reset the decoder to sample zero,
84
+ so reaching three quarters of the way through a clip decoded the other
85
+ three quarters first. It now starts at the sync sample covering the
86
+ target and throws away the frames between, so the cost of a seek is one
87
+ group of pictures. Measured in a browser at 13 to 22ms for a 1280x992
88
+ H.264 clip on a software decoder.
89
+
90
+ **Presentation times are rebased to zero**, which sounds like
91
+ housekeeping and was a clip that would not show its first frame.
92
+ Anything encoded with B-frames has its first picture a reorder depth
93
+ into the file: a six second clip at 24fps reports 6.083s and has
94
+ nothing at all due at position zero. `present(0)` therefore found no
95
+ frame, and since a paused clip never asks again, a still stayed blank
96
+ for ever. Playing clips hid it by advancing past 83ms within two
97
+ frames.
98
+
99
+ Converting a decoded frame to a bitmap is asynchronous, so `present`
100
+ returns having chosen a frame that is not drawable yet. The surface now
101
+ says when one actually lands, through `VideoPlayback.onFrame`. Without
102
+ it a scrub on a paused clip moved the scrubber and left the picture
103
+ where it was.
104
+
105
+ **Fragmented MP4 is read**, out of `moof`/`traf`/`trun` with defaults
106
+ from `trex`, which is what a DASH or HLS segment is. Audio tracks are
107
+ read too, through `demuxMp4Audio`, so a player can know whether to
108
+ offer a mute button, though nothing here plays one. AV1 and VP9 codec
109
+ strings are assembled from their configuration records rather than
110
+ guessed: `av01` on its own is not a codec string, and a file reported
111
+ that way never played at all.
112
+
113
+ **A file need not be held whole.** `DefaultVideoResolver` takes a
114
+ `fetchRange` as well as a `fetch`, and they are two products rather
115
+ than a fast path and a slow one: a fifteen second loop is simplest held
116
+ in one buffer, and an hour of video is a gigabyte nobody will watch all
117
+ of. Given ranges, the resolver walks the top-level boxes to find the
118
+ `moov` whether it is at the front of the file or the back, and media
119
+ follows the decoder through `ByteSource`, whose `read` is synchronous
120
+ and may answer null because the decoder is fed from inside a frame.
121
+
122
+ `parseWebVtt` and `cueAt` read a caption track. They never throw: a
123
+ caption file is content, usually someone else's, and one malformed cue
124
+ in a hundred is not a reason to show none of them.
125
+
126
+ - be07839: **The render worker asks the compositor itself.** `UiHostFrameClock`
127
+ was built on a premise that expired: that `requestAnimationFrame` is
128
+ tied to the compositor and exists only on the main thread, which is why
129
+ the shell ran a loop and forwarded a tick per refresh.
130
+
131
+ `DedicatedWorkerGlobalScope.requestAnimationFrame` has been in Chrome
132
+ since 69, Firefox since 99 and Safari since 16.4, which made it
133
+ baseline in March 2023. Measured in a render worker: a median interval
134
+ of 16.70ms with a range of 16.6 to 16.8, which is vsync and not a
135
+ timer. Its timestamps are on the worker's own `performance.now()`
136
+ timeline, so nothing needs translating.
137
+
138
+ Counting `requestAnimationFrame` calls on the main thread over three
139
+ seconds: **180 before, 0 after.** The shell is out of the frame path
140
+ entirely, and with it goes the part of the design that was least
141
+ defensible, that a worker's heartbeat depended on the thread the worker
142
+ exists to be independent of. A blocked page used to stop the render
143
+ worker's frames until a stall watch noticed.
144
+
145
+ The forwarded path stays, feature-detected, for an older browser and
146
+ for a nested worker on Chromium, which has no frame callback of its
147
+ own. The contract is the same either way. Both stop for a hidden tab,
148
+ which is not luck: a worker's animation frames are serviced by its
149
+ owner window's rendering, and a window that is not rendering services
150
+ none.
151
+
3
152
  ## 0.1.0
4
153
 
5
154
  First public release.
package/README.md CHANGED
@@ -38,6 +38,6 @@ engine.explain(node);
38
38
 
39
39
  ## Documentation
40
40
 
41
- [Layout](https://github.com/kevinpbaker/gesso/blob/main/apps/docs/layout/flex.md) | [Text](https://github.com/kevinpbaker/gesso/blob/main/apps/docs/guide/text.md) | [Canvas2D and WebGPU](https://github.com/kevinpbaker/gesso/blob/main/apps/docs/rendering/canvas2d-and-webgpu.md) | [Asking the engine why](https://github.com/kevinpbaker/gesso/blob/main/apps/docs/layout/explain.md)
41
+ [Layout](https://gesso-docs.vercel.app/layout/flex) | [Text](https://gesso-docs.vercel.app/guide/text) | [Canvas2D and WebGPU](https://gesso-docs.vercel.app/rendering/canvas2d-and-webgpu) | [Asking the engine why](https://gesso-docs.vercel.app/layout/explain)
42
42
 
43
43
  MIT (c) Kevin Baker
@@ -16715,6 +16715,26 @@ var Canvas2DRenderer = class {
16715
16715
  height: 0
16716
16716
  };
16717
16717
  paint = createPaintState();
16718
+ /**
16719
+ * Whose style the shared paint scratch currently holds.
16720
+ *
16721
+ * `resolvePaintState` folds about thirty properties — several of
16722
+ * them inherited, so each is an ancestor walk — into one reused
16723
+ * object. A node that paints text resolved twice: once for its own
16724
+ * box, and again after the walk into its children, because a child
16725
+ * overwrites the scratch before the text is drawn.
16726
+ *
16727
+ * Only the second one is conditional. A leaf never had a child to
16728
+ * clobber it, and neither did a node whose children turned out to be
16729
+ * every one of them invisible, culled or lifted — and leaves are
16730
+ * most of the text in an application. Recording the owner answers
16731
+ * "is the scratch still mine" exactly, for one reference compare,
16732
+ * where asking `node.firstChild === null` would only approximate it
16733
+ * and would go quietly wrong as the early-outs in `renderNode`
16734
+ * change. Every write goes through `resolvePaint`, so the answer
16735
+ * cannot drift from the truth.
16736
+ */
16737
+ paintOwner = null;
16718
16738
  cullX = 0;
16719
16739
  cullY = 0;
16720
16740
  cullWidth = 0;
@@ -16764,10 +16784,19 @@ var Canvas2DRenderer = class {
16764
16784
  this.cullWidth = this.surface.logicalWidth;
16765
16785
  this.cullHeight = this.surface.logicalHeight;
16766
16786
  this.liftedPass.length = 0;
16787
+ this.paintOwner = null;
16767
16788
  this.renderNode(root, context, ctx, true, false);
16768
16789
  this.renderLifted(context, ctx);
16769
16790
  if (context.overlay !== void 0 && context.overlay.length > 0) drawOverlayShapes(ctx, context.overlay);
16770
16791
  }
16792
+ /**
16793
+ * Folds a node's style into the shared scratch and records that it
16794
+ * is now that node's. The one writer; see `paintOwner`.
16795
+ */
16796
+ resolvePaint(node) {
16797
+ this.paintOwner = node;
16798
+ return resolvePaintState(node, this.paint);
16799
+ }
16771
16800
  beginFrame(ctx) {
16772
16801
  ctx.setTransform(1, 0, 0, 1, 0, 0);
16773
16802
  ctx.clearRect(0, 0, this.surface.physicalWidth, this.surface.physicalHeight);
@@ -16783,7 +16812,7 @@ var Canvas2DRenderer = class {
16783
16812
  }
16784
16813
  const sticky = rec.stickyOffsetX !== 0 || rec.stickyOffsetY !== 0;
16785
16814
  if (cull && !this.intersectsCull(rec.x + rec.stickyOffsetX, rec.y + rec.stickyOffsetY, rec.width, rec.height)) return;
16786
- const paint = resolvePaintState(node, this.paint);
16815
+ const paint = this.resolvePaint(node);
16787
16816
  const hasText = paint.text !== void 0;
16788
16817
  let saves = 0;
16789
16818
  if (paint.opacity < 1 || paint.hasTransform || sticky) {
@@ -16818,12 +16847,12 @@ var Canvas2DRenderer = class {
16818
16847
  if (rec.scrollable) ctx.restore();
16819
16848
  if (rec.clips) this.popCull();
16820
16849
  if (hasText) {
16821
- resolvePaintState(node, this.paint);
16850
+ if (this.paintOwner !== node) this.resolvePaint(node);
16822
16851
  this.paintContent(ctx, rec, this.paint, context);
16823
16852
  }
16824
16853
  if (rec.liftBoundary && this.liftedPass.length > liftedBefore) this.renderLifted(context, ctx, liftedBefore, node);
16825
16854
  if (rec.clips) ctx.restore();
16826
- if (decorations !== null && hasDecorationPhase(decorations, true)) this.paintDecorations(ctx, node, rec, resolvePaintState(node, this.paint), decorations, true);
16855
+ if (decorations !== null && hasDecorationPhase(decorations, true)) this.paintDecorations(ctx, node, rec, this.paintOwner === node ? this.paint : this.resolvePaint(node), decorations, true);
16827
16856
  if (rec.scrollable) this.paintScrollbars(ctx, rec, context.now ?? performance.now());
16828
16857
  while (saves > 0) {
16829
16858
  ctx.restore();
@@ -16878,7 +16907,7 @@ var Canvas2DRenderer = class {
16878
16907
  const ancestor = chain[i];
16879
16908
  const rec = context.layout.recordFor(ancestor);
16880
16909
  if (rec === void 0) continue;
16881
- const paint = resolvePaintState(ancestor, this.paint);
16910
+ const paint = this.resolvePaint(ancestor);
16882
16911
  if (rec.stickyOffsetX !== 0 || rec.stickyOffsetY !== 0) ctx.translate(rec.stickyOffsetX, rec.stickyOffsetY);
16883
16912
  if (paint.opacity < 1) ctx.globalAlpha *= paint.opacity;
16884
16913
  if (paint.hasTransform) this.applyTransform(ctx, rec, paint);
@@ -17270,4 +17299,4 @@ function createCanvasSurface(host) {
17270
17299
  //#endregion
17271
17300
  export { prepareInputSurface as $, textContentOf as $i, nextWordEnd as $n, borderRadius as $r, createPaintState as $t, accumulatedOffsetTo as A, UiBeforeInputEvent as Aa, raiseContrast as Ai, editorFor as An, pathValuesEqual as Ar, EditableLayout as At, parseFlexDirection as B, UiWheelEvent as Ba, boxShadowArraysEqual as Bi, caretRectFor as Bn, radialGradient as Br, textMeasureRequest as Bt, describeLength as C, writeOverrideProperty as Ca, hasThemeExtension as Ci, linkOf as Cn, statesEqual as Cr, wordRangeIn as Ct, formatNumber as D, visualState as Da, withThemeExtension as Di, CARET_WIDTH as Dn, validateSubgrid as Dr, ELLIPSIS as Dt, formatExplanation as E, hasVisualState as Ea, themeExtensionsEqual as Ei, CARET_BLINK_MS as En, UI_SUBGRID_AXES as Er, spannedRunsFor as Et, constraintsEqual as F, UiKeyboardEvent as Fa, spacingEqual as Fi, nextCaretToggle as Fn, transformsEqual as Fr, fillTextRects as Ft, MainAxisAlignment as G, UiBinding as Ga, defaultTypography as Gi, offsetAtX as Gn, isFrLength as Gr, fontStackFor as Gt, sizeGridTracks as H, wheelDeltaYOf as Ha, defaultShapes as Hi, lineIndexForOffset as Hn, auto as Hr, textRunDecorations as Ht, tightenConstraints as I, UiPinchEvent as Ia, spacingSteps as Ii, syncEditorValue as In, MAX_GRADIENT_STOPS as Ir, layoutTextLines as It, parseMainAxisAlignment as J, defaultTextStyle as Ji, EditableTextModel as Jn, minmax as Jr, DEFAULT_FONT_SIZE as Jt, parseAlignContent as K, isTypographyRole as Ki, offsetForVerticalMove as Kn, isMinMaxTrack as Kr, registerFontStack as Kt, MAX_MEASURES_PER_CHILD as L, UiPointerEvent as La, defaultShadows as Li, resolveColorValue as Ln, gradientPaint as Lr, runCanvasStyleInto as Lt, LayoutRecord as M, UiFocusEvent as Ma, densityFactors as Mi, isEditableNode as Mn, parseTransform as Mr, buildFontString as Mt, Constraints as N, UiGestureEvent as Na, defaultSpacing as Ni, isMultiline as Nn, transform as Nr, createCanvasTextStyle as Nt, labelNode as O, visualStatesEqual as Oa, contrastRatio as Oi, EDITOR_PROP as On, inputsEqual as Or, layoutParagraph as Ot, clampSize as P, UiInputEvent as Pa, scaleSpacing as Pi, isReadOnly as Pn, transformIsIdentity as Pr, drawText as Pt, pointerDeviceOf as Q, spannedTextOf as Qi, nextGraphemeEnd as Qn, UiBorderRadiuses as Qr, computeObjectFitRect as Qt, isLayoutProtocol as R, UiTextChangeEvent as Ra, shadowsEqual as Ri, isVideoSurface as Rn, gradientsEqual as Rr, runFontString as Rt, SCROLLBAR_LINGER_MS as S, writeDeclaredProperty as Sa, defineThemeExtension as Si, linkHoverOf as Sn, normalizeStates as Sr, selectionRectsIn as St, formatConstraints as T, defaultVisualState as Ta, themeExtension as Ti, setLinkHover as Tn, validateStates as Tr, ParagraphTextMeasurer as Tt, AlignContent as U, UiNodeType as Ua, isShapeName as Ui, lineLimit as Un, fr as Ur, bumpFontStack as Ut, placeGridItems as V, noKeyModifiers as Va, boxShadowsEqual as Vi, lineIndexAtY as Vn, validateGradient as Vr, textRunBackgrounds as Vt, CrossAxisAlignment as W, DirtyFlags as Wa, shapesEqual as Wi, offsetAtPoint as Wn, isAutoLength as Wr, clearFontStacks as Wt, UiPlatformAdapter as X, resolvedSpansOf as Xi, lineEndAt as Xn, repeat as Xr, DEFAULT_TEXT_COLOR as Xt, CanvasPlatformSurface as Y, flattenTextSpans as Yi, graphemeBoundaries as Yn, percent as Yr, DEFAULT_FONT_WEIGHT as Yt, capturePointer as Z, spanAtOffset as Zi, lineStartAt as Zn, resolveLength as Zr, colorToCss as Zt, LABEL_PADDING_X as _, UiNode as _a, darkTheme as _i, isNodeInert as _n, UiProperties as _r, UiInputDispatcher as _t, PaintPictureCache as a, UiBasicColors as aa, uniformBorderRadius as ai, TEXT_MATCHES_PROP as an, resolveProperty as ar, isScrollContainer as at, LayoutEngine as b, overrideSources as ba, withContrast as bi, clearLinkHover as bn, isUiRole as br, paragraphGeometry as bt, EMPTY_RECORDING as c, colorValuesEqual as ca, UiEnvironmentKeys as ci, setMatchRanges as cn, UiGraph as cr, UiFocusManager as ct, tracePathData as d, parseColor as da, noInsets as di, selectableTextNodes as dn, getPropertyNames as dr, SCROLLBAR_INSET as dt, textSpansEqual as ea, borderRadiusCorners as ei, isPaintVisible as en, previousGraphemeStart as er, touchActionFor as et, ScaledImageCache as f, rgb8 as fa, observeViewportInsets as fi, selectableTextOf as fn, inheritedPropertyFlags as fr, SCROLLBAR_MIN_THUMB as ft, paintsAfterChildren as g, UiEnvironment as ga, unknownContainerSize as gi, isNodeHitTestable as gn, propertyValuesEqualByName as gr, scrollbarZoneAt as gt, hasDecorationPhase as h, findEnvironmentKey as ha, containerBands as hi, isNodeFocusable as hn, propertyIsInherited as hr, scrollbarThumbs as ht, traceRoundedRect as i, lightColors as ia, normalizeBorderRadius as ii, DEFAULT_LINE_HEIGHT_FACTOR as in, resolveNumber as ir, UiWheelController as it, contentOffset as j, UiEventType as ja, relativeLuminance as ji, editorOf as jn, UiTransforms as jr, applyCanvasTextStyle as jt, withOverrideSource as k, MOUSE_POINTER as ka, highContrastColors as ki, caretVisibleAt as kn, paintValuesEqual as kr, proportionalFontMetrics as kt, PaintRecorder as l, colorsEqual as la, UiInsetRegistry as li, TEXT_SELECTION_PROP as ln, closestPropertyName as lr, UiHitTester as lt, decorationRect as m, createEnvironmentKey as ma, bandOf as mi, setSelectionRange as mn, propertyEffects as mr, scrollbarThumb as mt, createCanvasSurface as n, colorsEqualPalette as na, borderRadiusIsZero as ni, normalizeVerticalAlign as nn, wordRangeAt as nr, UiGestureRecognizer as nt, paintPictures as o, colorToHex as oa, defineProperty as oi, clearMatchRanges as on, resolvePropertyByName as or, UiKeyboardController as ot, decorationColor as p, rgba as pa, UiContainerSizeSource as pi, selectionRangeOf as pn, inheritedPropertyNames as pr, SCROLLBAR_THICKNESS as pt, parseCrossAxisAlignment as q, typographyEqual as qi, selectionRects as qn, isPercentLength as qr, DEFAULT_FONT_FAMILY as qt, Canvas2DRenderer as r, darkColors as ra, borderRadiusValuesEqual as ri, resolvePaintState as rn, resolveBoolean as rr, UiPinchRecognizer as rt, PaintTarget as s, colorToRgba as sa, propertyValuesEqual as si, matchRangesOf as sn, resolveString as sr, UiPointerController as st, CanvasSurface as t, textStylesEqual as ta, borderRadiusEqual as ti, normalizeTextAlign as tn, previousWordStart as tr, UiTouchScroller as tt, replayPaint as u, normalizeColor as ua, insetsEqual as ui, clearSelectionRange as un, findPropertyDefinition as ur, SCROLLBAR_HOVER_ZONE as ut, drawOverlayShapes as v, DirtyNodeSet as va, lightTheme as vi, isNodeSelectable as vn, UI_ROLES as vr, hasDrawnText as vt, describeOverrides as w, UiVisualState as wa, noThemeExtensions as wi, linkRunAtPointIn as wn, validateRole as wr, CharacterCountTextMeasurer as wt, SCROLLBAR_FADE_MS as x, resetOverrideWarnings as xa, withDensity as xi, hasTextLinks as xn, isUiSemanticState as xr, paragraphGeometryFrom as xt, labelOrigin as y, clearOverrideProperty as ya, themesEqual as yi, resolveCursor as yn, UI_SEMANTIC_STATES as yr, offsetAtPointIn as yt, FlexDirection as z, UiWheelDeltaMode as za, boxShadow as zi, videoFrameSize as zn, linearGradient as zr, spanOf as zt };
17272
17301
 
17273
- //# sourceMappingURL=CanvasSurface-CrG-uJ5o.js.map
17302
+ //# sourceMappingURL=CanvasSurface-5dU-U3qQ.js.map