@solidrt/core 0.0.51 → 0.0.52

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.
@@ -0,0 +1,145 @@
1
+ // `points` turns a line into a polyline: a flat [x0, y0, x1, y1, ...] array in
2
+ // the element's local space, so geometry that changes every frame is one
3
+ // array write - no `d` string to format and re-parse. Three uses below:
4
+ // 1. A live trace: a Float32Array rebuilt each frame in onFrame and set as
5
+ // `points`. Typed arrays marshal like number[]; nothing is parsed.
6
+ // 2. A closed outline: `closed` strokes the segment back to the first point
7
+ // and joins there instead of capping both ends; the join style shows at
8
+ // the apex. A line's paint defaults to stroke; drawStyle="fill" fills the
9
+ // polygon instead (implicitly closed), "stroke-and-fill" does both.
10
+ // Dashing runs along the whole stroke, through the vertices.
11
+ // 3. Marching ants: `dashOffset` slides the dash pattern, so writing it every
12
+ // frame animates the dashes. The ring is dense (6 px segments under
13
+ // 12 px dashes), which only works because the phase carries across
14
+ // vertices; the two-point d-line and the d-path go through the same
15
+ // walker (a path's dashes are pieces of its curves, stroked as curves).
16
+ // 4. Partial draw: `pathLength={1}` makes the dash units fractions of the
17
+ // geometry's length, so `onLength={0.77} offLength={1}` draws the first
18
+ // 77% and an `onLength` written from 0 to 1 draws the geometry on, without
19
+ // knowing its length.
20
+ // 5. A laid-out <line points>: the points are content (like a path's `d`), so
21
+ // the box measures from their extent and takes part in the row.
22
+ // The two-endpoint form (x1..y2, on d-line only) is unchanged; while `points`
23
+ // is set it takes precedence over the endpoints.
24
+ import { render, onFrame, createSignal } from "@solidrt/core"
25
+
26
+ const SAMPLES = 200
27
+ const TRACE_W = 560
28
+ const TRACE_H = 160
29
+ const TRIANGLE = [20, 100, 70, 20, 120, 100]
30
+ const ZIGZAG = [0, 0, 30, 24, 60, 0, 90, 24, 120, 0]
31
+ const RING = ring(70, 60, 45, 48)
32
+ const CURVE = "M20 60 C 60 0, 100 120, 140 60 S 200 20, 200 60"
33
+ const ANTS_SPEED = 40 // local units per second
34
+ const DRAW_PERIOD = 3 // seconds per draw-on cycle
35
+
36
+ function ring(cx: number, cy: number, r: number, n: number): number[] {
37
+ let pts: number[] = []
38
+ for (let i = 0; i < n; i++) {
39
+ let a = (i / n) * Math.PI * 2
40
+ pts.push(cx + Math.cos(a) * r, cy + Math.sin(a) * r)
41
+ }
42
+ return pts
43
+ }
44
+
45
+ // A travelling wave inside a sine envelope, sampled into x, y pairs.
46
+ function wave(t: number): Float32Array {
47
+ let pts = new Float32Array(SAMPLES * 2)
48
+ for (let i = 0; i < SAMPLES; i++) {
49
+ let u = i / (SAMPLES - 1)
50
+ pts[2 * i] = u * TRACE_W
51
+ pts[2 * i + 1] = TRACE_H / 2 + Math.sin(u * 14 - t * 4) * Math.sin(u * Math.PI) * (TRACE_H / 2 - 8)
52
+ }
53
+ return pts
54
+ }
55
+
56
+ function App() {
57
+ let [trace, setTrace] = createSignal<Float32Array>(wave(0))
58
+ let [ants, setAnts] = createSignal(0)
59
+ let [drawn, setDrawn] = createSignal(0)
60
+ onFrame((tick) => {
61
+ setTrace(wave(tick / 1000))
62
+ setAnts((tick / 1000) * ANTS_SPEED)
63
+ setDrawn(((tick / 1000) % DRAW_PERIOD) / DRAW_PERIOD)
64
+ })
65
+
66
+ return (
67
+ <window padding={24} gap={20}>
68
+ <d-rect color="#0b0f17" />
69
+
70
+ <text fontSize={16} color="#8b949e">
71
+ live trace: a Float32Array of {SAMPLES} points written every frame
72
+ </text>
73
+ <view width={TRACE_W} height={TRACE_H} flexShrink={0}>
74
+ <d-rect radius={8} color="#151b28" />
75
+ <d-line points={trace()} color="#3fb950" strokeWidth={3} strokeJoin="round" />
76
+ </view>
77
+
78
+ <text fontSize={16} color="#8b949e">
79
+ closed (round join), open (round caps), filled, stroke-and-fill dashed
80
+ </text>
81
+ <view flexDirection="row" flexWrap="wrap" gap={20}>
82
+ <view width={140} height={120}>
83
+ <d-rect radius={8} color="#151b28" />
84
+ <d-line points={TRIANGLE} closed color="#e3b341" strokeWidth={8} strokeJoin="round" />
85
+ </view>
86
+ <view width={140} height={120}>
87
+ <d-rect radius={8} color="#151b28" />
88
+ <d-line points={TRIANGLE} color="#e3b341" strokeWidth={8} strokeCap="round" />
89
+ </view>
90
+ <view width={140} height={120}>
91
+ <d-rect radius={8} color="#151b28" />
92
+ <d-line points={TRIANGLE} drawStyle="fill" color="#a371f7" />
93
+ </view>
94
+ <view width={140} height={120}>
95
+ <d-rect radius={8} color="#151b28" />
96
+ <d-line points={TRIANGLE} closed drawStyle="stroke-and-fill" onLength={12} offLength={8} color="#f85149" strokeWidth={3} />
97
+ </view>
98
+ </view>
99
+
100
+ <text fontSize={16} color="#8b949e">
101
+ marching ants: dashOffset written every frame, on a dense ring, a segment and a path
102
+ </text>
103
+ <view flexDirection="row" flexWrap="wrap" gap={20}>
104
+ <view width={140} height={120}>
105
+ <d-rect radius={8} color="#151b28" />
106
+ <d-line points={RING} closed onLength={12} offLength={8} dashOffset={ants()} color="#e3b341" strokeWidth={3} />
107
+ </view>
108
+ <view width={300} height={120}>
109
+ <d-rect radius={8} color="#151b28" />
110
+ <d-line x1={20} y1={60} x2={280} y2={60} onLength={0} offLength={14} dashOffset={-ants()} color="#79c0ff" strokeWidth={6} strokeCap="round" />
111
+ </view>
112
+ <view width={220} height={120}>
113
+ <d-rect radius={8} color="#151b28" />
114
+ <d-path d={CURVE} drawStyle="stroke" onLength={10} offLength={6} dashOffset={ants()} color="#f778ba" strokeWidth={3} strokeCap="round" />
115
+ </view>
116
+ </view>
117
+
118
+ <text fontSize={16} color="#8b949e">
119
+ partial draw: pathLength=1 makes the pattern fractional, 77% of the curve and a triangle drawing on
120
+ </text>
121
+ <view flexDirection="row" flexWrap="wrap" gap={20}>
122
+ <view width={220} height={120}>
123
+ <d-rect radius={8} color="#151b28" />
124
+ <d-path d={CURVE} drawStyle="stroke" pathLength={1} onLength={0.77} offLength={1} color="#3fb950" strokeWidth={4} strokeCap="round" />
125
+ </view>
126
+ <view width={140} height={120}>
127
+ <d-rect radius={8} color="#151b28" />
128
+ <d-line points={TRIANGLE} closed pathLength={1} onLength={drawn()} offLength={1} color="#79c0ff" strokeWidth={4} strokeCap="round" strokeJoin="round" />
129
+ </view>
130
+ </view>
131
+
132
+ <view flexDirection="row" alignItems="center" gap={12}>
133
+ <text fontSize={16} color="#8b949e">
134
+ laid out:
135
+ </text>
136
+ <line points={ZIGZAG} color="#1f6feb" strokeWidth={3} strokeJoin="round" />
137
+ <text fontSize={16} color="#8b949e">
138
+ the box measures from the points
139
+ </text>
140
+ </view>
141
+ </window>
142
+ )
143
+ }
144
+
145
+ render(() => <App />)
@@ -3,7 +3,7 @@
3
3
  // it the source text (a string you import, fetch, or inline) and get back the
4
4
  // document's intrinsic size plus a flat list of draws whose keys match the
5
5
  // path element's props - so rendering is a map to <d-path>, wrapped in a view
6
- // whose `viewBox` fits the document's coordinate space into the box.
6
+ // whose `designSize` fits the document's coordinate space into the box.
7
7
  //
8
8
  // The point of draws-as-data over an opaque document element: every shape is
9
9
  // a real node you own. Below, the house highlights the shape under the
@@ -28,7 +28,7 @@ import { render, parseSvg, svg, createMemo, createSignal, For } from "@solidrt/c
28
28
  // the string unchanged; it exists so editors highlight the markup (like `glsl`
29
29
  // for shader sources).
30
30
  const HOUSE = svg`
31
- <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
31
+ <svg designSize="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
32
32
  <rect x="20" y="45" width="60" height="45" fill="#457b9d"/>
33
33
  <path d="M10 50 L50 15 L90 50 Z" fill="#e63946"/>
34
34
  <rect x="42" y="62" width="16" height="28" fill="#f1faee"/>
@@ -37,7 +37,7 @@ const HOUSE = svg`
37
37
 
38
38
  // Monochrome icon (Lucide arrow-right) drawn with currentColor, recolored below.
39
39
  const ARROW = svg`
40
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
40
+ <svg designSize="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
41
41
  stroke-linecap="round" stroke-linejoin="round">
42
42
  <path d="M5 12h14"/>
43
43
  <path d="M12 5l7 7-7 7"/>
@@ -54,7 +54,7 @@ function InteractiveHouse() {
54
54
  // re-records only when a draw inside it changes (hover), never because a
55
55
  // sibling elsewhere on the screen did.
56
56
  return (
57
- <view repaintBoundary width={240} height={240} viewBox={[doc().width, doc().height]}>
57
+ <view repaintBoundary width={240} height={240} designSize={[doc().width, doc().height]}>
58
58
  <For each={doc().draws}>
59
59
  {(draw, i) => (
60
60
  <d-path
@@ -69,14 +69,14 @@ function InteractiveHouse() {
69
69
  )
70
70
  }
71
71
 
72
- // The plain pattern: memoized parse, viewBox-fitted box, draws mapped to
72
+ // The plain pattern: memoized parse, designSize-fitted box, draws mapped to
73
73
  // <d-path>, and a plain repaintBoundary (the DL-reuse tier, not "snapshot")
74
74
  // so the static subtree never re-records alongside animating siblings. The
75
75
  // components-package Icon is this plus theming.
76
76
  function Svg(props: { src: string; size: number; color?: string }) {
77
77
  let doc = createMemo(() => parseSvg(props.src, { color: props.color }))
78
78
  return (
79
- <view repaintBoundary width={props.size} height={props.size} viewBox={[doc().width, doc().height]}>
79
+ <view repaintBoundary width={props.size} height={props.size} designSize={[doc().width, doc().height]}>
80
80
  <For each={doc().draws}>{(draw) => <d-path {...draw} />}</For>
81
81
  </view>
82
82
  )
@@ -12,7 +12,7 @@
12
12
  // This is the REFLOW answer, for layouts that genuinely rearrange across form
13
13
  // factors. For content with fixed internal geometry (diagrams, slides,
14
14
  // dashboards, game boards) do not branch on window size at all: author one
15
- // design space and let `viewBox` scale it to fit - see view-viewbox.tsx.
15
+ // design space and let `designSize` scale it to fit - see view-design-size.tsx.
16
16
  import { render, capabilities, windowSize, createMemo, For } from "@solidrt/core"
17
17
 
18
18
  const GAP = 16
@@ -11,7 +11,7 @@
11
11
  // Capture both with refs, pass their accessors to createScroll, then apply the
12
12
  // returned offset to the viewport's scrollX/scrollY. createScroll does no input,
13
13
  // so wire an event (here onWheel) to scroll.scrollBy - positive dy moves content
14
- // up. scrollTo(x, y) jumps to an absolute, clamped offset.
14
+ // up. scrollTo({ x, y }) jumps to an absolute, clamped offset.
15
15
  import { render, For, createScroll } from "@solidrt/core"
16
16
  import type { WheelEvent } from "@solidrt/core"
17
17
 
@@ -22,7 +22,7 @@ function App() {
22
22
  // Default axis is "vertical"; pass { axis: "horizontal" } or "both" for others.
23
23
  let scroll = createScroll(() => viewport, () => content)
24
24
 
25
- let onWheel = (e: WheelEvent) => scroll.scrollBy(e.deltaX, e.deltaY)
25
+ let onWheel = (e: WheelEvent) => scroll.scrollBy({ x: e.deltaX, y: e.deltaY })
26
26
 
27
27
  let rows = Array.from({ length: 30 }, (_, i) => i)
28
28
 
@@ -0,0 +1,72 @@
1
+ // A UI subtree as a live texture. snapshotTexture(ref) returns the texture id
2
+ // behind a repaintBoundary="snapshot" view: its rasterized pixels, as an
3
+ // ordinary texture id any GPU consumer samples. Here a shader texture binds
4
+ // it as uPanel and a sibling <texture> shows the result, so the panel on the
5
+ // left and its warped twin on the right are the same pixels. The id is
6
+ // stable; the runtime re-points it after every re-rasterization, and the
7
+ // boundary only re-rasterizes when its subtree changes - so the warp
8
+ // animates every frame while the panel's text is painted once per edit.
9
+ //
10
+ // Tap the panel to count. Both copies update, the mirror through the GPU.
11
+ import { render, onFrame, createSignal, snapshotTexture, Show } from "@solidrt/core"
12
+ import { createShaderTexture, glsl } from "@solidrt/core/gpu"
13
+
14
+ let MIRROR = glsl`
15
+ uniform sampler2D uPanel;
16
+ uniform float uTime;
17
+ void main() {
18
+ vec2 uv = vUV;
19
+ uv.x += sin(uv.y * 20.0 + uTime * 3.0) * 0.02;
20
+ vec4 panel = texture(uPanel, uv);
21
+ // Premultiplied source: tint the color, keep the alpha.
22
+ float glow = 0.5 + 0.5 * sin(uTime * 2.0 + uv.y * 6.0);
23
+ fragColor = vec4(panel.rgb * vec3(1.0, 0.7 + 0.3 * glow, 0.6), panel.a);
24
+ }
25
+ `
26
+
27
+ function App() {
28
+ let [count, setCount] = createSignal(0)
29
+ let [time, setTime] = createSignal(0)
30
+ onFrame(tick => setTime(tick / 1000))
31
+
32
+ let [panel, setPanel] = createSignal<{ id: number }>()
33
+
34
+ return (
35
+ <window alignItems="center" justifyContent="center" gap={40} flexDirection="row">
36
+ <view
37
+ ref={(n: { id: number }) => setPanel(n)}
38
+ repaintBoundary="snapshot"
39
+ onPointerDown={() => setCount(c => c + 1)}
40
+ width={240}
41
+ height={160}
42
+ padding={20}
43
+ gap={12}
44
+ flexDirection="column"
45
+ >
46
+ <rect position="absolute" width="100%" height="100%" radius={12} color="#1e2a44" />
47
+ <text fontSize={20} color="#ffffff">
48
+ Live panel
49
+ </text>
50
+ <text fontSize={40} color="#ffd166">
51
+ {String(count())}
52
+ </text>
53
+ <text fontSize={14} color="#9fb3d9">
54
+ tap to count
55
+ </text>
56
+ </view>
57
+ <Show when={panel()} keyed>
58
+ {p => {
59
+ // The id is valid as soon as the boundary exists; the texture is
60
+ // empty until its first paint, then live.
61
+ let mirror = createShaderTexture(MIRROR, 480, 320, { uTime: 0 }, {
62
+ textures: { uPanel: snapshotTexture(p) },
63
+ label: "panel-mirror",
64
+ })
65
+ return <texture src={mirror} width={240} height={160} params={{ uTime: time() }} />
66
+ }}
67
+ </Show>
68
+ </window>
69
+ )
70
+ }
71
+
72
+ render(() => <App />)
@@ -1,21 +1,26 @@
1
- // `viewBox` on a <view> is the fixed-aspect answer to "many screen sizes": you
2
- // author the whole scene once, in your own made-up design units, and the view
3
- // scales that space to fit its box. It is SVG's viewBox generalized to any
4
- // subtree, not an SVG-only thing.
1
+ // `designSize` on a <view> is the fixed-aspect answer to "many screen sizes":
2
+ // you author the whole scene once, in your own made-up design units, and the
3
+ // view scales that space to fit its box. It is SVG's viewBox generalized off
4
+ // the graphics format and onto a layout element - the design size is the only
5
+ // irreducible half of it, since a transform already does what min-x/min-y did.
5
6
  //
6
7
  // Four facts, each demonstrated below:
7
- // 1. It NEVER sizes the element. `viewBox` is a pure fit transform; the box
8
- // still takes its size from layout (here `flex={1}`). A view with only a
9
- // viewBox and no layout size is zero-sized and paints nothing.
8
+ // 1. The view sizes like a REPLACED element (think <img>): its intrinsic size
9
+ // is the design size, one sized axis derives the other from the design
10
+ // aspect, and layout props override it as usual - here `flex={1}` takes
11
+ // the whole window. It never refuses to shrink: a design has no size it
12
+ // cannot scale below.
10
13
  // 2. It fits UNIFORMLY and centers - one scale for both axes, SVG's default
11
14
  // preserveAspectRatio. Content never stretches; the leftover on the loose
12
15
  // axis is letterbox, showing the window background through it.
13
- // 3. Children live in DESIGN space. Their x/y, w/h, fontSize, stroke widths -
14
- // everything resolves against the design size, not the box. The box a child
15
- // inherits IS the design size, so a bare `d-rect` fills the design space
16
- // and detached text wraps at its width.
16
+ // 3. Children live in DESIGN space, laid-out ones included. x/y, w/h,
17
+ // fontSize, stroke widths, flex, percentages, text wrapping - everything
18
+ // resolves against the design size, not the box. The box a child inherits
19
+ // IS the design size, so a bare `d-rect` fills the design space, detached
20
+ // text wraps at its width, and a flex row is laid out at the design width
21
+ // whatever the window - nothing reflows on resize, the fit does the work.
17
22
  // 4. Pointer coordinates arrive in design space too. localX/localY on the
18
- // viewBox view (and on anything under it) read in design units, so no
23
+ // design-size view (and on anything under it) read in design units, so no
19
24
  // scale factor is threaded through the app's hit math.
20
25
  //
21
26
  // The payoff: no `windowSizeClass` branching, no per-breakpoint sizes, no
@@ -44,14 +49,15 @@ function App() {
44
49
  <window>
45
50
  <d-rect color="#0b0f17" />
46
51
 
47
- {/* flex={1} sizes the box; viewBox only maps content into it (fact 1). */}
52
+ {/* flex={1} overrides the intrinsic design size: the box is the whole
53
+ window, and the fit maps the design into it (fact 1). */}
48
54
  <view
49
55
  flex={1}
50
- viewBox={[DESIGN_W, DESIGN_H]}
56
+ designSize={[DESIGN_W, DESIGN_H]}
51
57
  onPointerMove={(e) => setAt({ x: e.localX, y: e.localY })}
52
58
  onPointerLeave={() => setAt(null)}
53
59
  >
54
- {/* No w/h, so it fills the box it inherits - which under a viewBox is
60
+ {/* No w/h, so it fills the box it inherits - which under a design size is
55
61
  the design space (fact 3). Its edges are the letterbox edges. */}
56
62
  <d-rect color="#151b28" />
57
63
 
@@ -78,6 +84,18 @@ function App() {
78
84
  <Show when={at()}>
79
85
  {(a) => <d-oval x={a().x - 8} y={a().y - 8} w={16} h={16} color="#f85149" />}
80
86
  </Show>
87
+
88
+ {/* A laid-out row under the design size (fact 3): positioned and sized in
89
+ design units, the bar flexing against the design width. Resize the
90
+ window: it scales with the scene instead of reflowing. */}
91
+ <view position="absolute" left={40} right={40} top={364} flexDirection="row" alignItems="center" gap={12}>
92
+ <text fontSize={16} color="#8b949e">
93
+ flex row, laid out in design units
94
+ </text>
95
+ <view flex={1} height={10}>
96
+ <d-rect radius={5} color="#1f6feb" />
97
+ </view>
98
+ </view>
81
99
  </view>
82
100
  </window>
83
101
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.51",
3
+ "version": "0.0.52",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -25,14 +25,15 @@
25
25
  "examples/",
26
26
  "jsx-runtime.d.ts",
27
27
  "agents/",
28
+ "docs/",
28
29
  "AGENTS.md"
29
30
  ],
30
31
  "devDependencies": {
31
- "@solidrt/flux-types": "0.0.51"
32
+ "@solidrt/flux-types": "0.0.52"
32
33
  },
33
34
  "peerDependencies": {
34
- "@solidjs/signals": "2.0.0-rc.0",
35
- "@solidjs/universal": "2.0.0-rc.0",
36
- "solid-js": "2.0.0-rc.0"
35
+ "@solidjs/signals": "2.0.0-rc.1",
36
+ "@solidjs/universal": "2.0.0-rc.1",
37
+ "solid-js": "2.0.0-rc.1"
37
38
  }
38
39
  }
package/src/core.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as tree from "flux:rendertree"
2
+ import type { TextureId } from "flux:gpu"
2
3
  import { createSignal, onCleanup } from "@solidjs/signals"
3
4
  import { on } from "srt:events"
4
5
 
@@ -310,7 +311,8 @@ export interface BoundingBox {
310
311
  * reactive: call it inside `onLayout` (or an event handler) to get values for
311
312
  * the current frame. Transforms anywhere in the chain (including the node's
312
313
  * own) compose fully; the box is the axis-aligned bounds of the transformed
313
- * quad.
314
+ * quad. A detached node reports its painted box: its own w/h (or the
315
+ * inherited box), or for `d-line` and `d-path` the geometry plus its stroke.
314
316
  */
315
317
  export function getBoundingBox(node: { id: number }): BoundingBox | null {
316
318
  return tree.getBoundingBox(node.id)
@@ -324,6 +326,22 @@ export function getBoundingBoxViewport(node: { id: number }): BoundingBox | null
324
326
  return tree.getBoundingBoxViewport(node.id)
325
327
  }
326
328
 
329
+ /**
330
+ * The texture id of a snapshot boundary's retained rasterization
331
+ * (`repaintBoundary="snapshot"`): the subtree's pixels as a live texture any
332
+ * GPU consumer - a `<texture>`, a shader or draw target binding, a 3d
333
+ * material - samples by id. Allocated on the first call and stable for the
334
+ * node's lifetime; the runtime re-points it at the current pixels after every
335
+ * rasterization, and only when the subtree changes (a static panel on an
336
+ * animated consumer costs no repaint). Empty until the first paint. Owned by
337
+ * the boundary: `destroyTexture` throws on it, and unmounting the boundary
338
+ * releases it. Throws if the node is not a snapshot boundary. Not a readback:
339
+ * for pixels on the CPU use `captureSnapshot`.
340
+ */
341
+ export function snapshotTexture(node: { id: number }): TextureId {
342
+ return tree.snapshotTexture(node.id)
343
+ }
344
+
327
345
  /**
328
346
  * Measures the rendered size of `text` in layout pixels under the given font
329
347
  * options (family, size, weight, style, maxLines), without adding it to the