@solidrt/core 0.0.50 → 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.
- package/AGENTS.md +102 -21
- package/README.md +1 -1
- package/agents/painting.md +61 -0
- package/agents/performance.md +216 -0
- package/docs/index.md +154 -0
- package/docs/reference/detached.md +85 -0
- package/docs/reference/drawing.md +95 -0
- package/docs/reference/elements.md +56 -0
- package/docs/reference/gpu.md +204 -0
- package/docs/reference/index.md +50 -0
- package/docs/reference/input.md +58 -0
- package/docs/reference/layout.md +44 -0
- package/docs/reference/shaders.md +46 -0
- package/docs/reference/text.md +46 -0
- package/docs/reference/transforms.md +35 -0
- package/docs/reference/types.md +34 -0
- package/examples/README.md +7 -5
- package/examples/{sound.tsx → audio.tsx} +1 -1
- package/examples/gpu-pipeline.tsx +2 -2
- package/examples/gpu-sprites.tsx +102 -0
- package/examples/line-points.tsx +145 -0
- package/examples/parse-svg.tsx +6 -6
- package/examples/responsive-grid.tsx +1 -1
- package/examples/scroll.tsx +2 -2
- package/examples/snapshot-texture.tsx +72 -0
- package/examples/{view-viewbox.tsx → view-design-size.tsx} +33 -15
- package/jsx-runtime.d.ts +15 -14
- package/package.json +11 -9
- package/src/{sound.ts → audio.ts} +68 -15
- package/src/color.ts +17 -18
- package/src/core.ts +40 -3
- package/src/data.ts +99 -0
- package/src/gpu.ts +88 -34
- package/src/index.ts +9 -3
- package/src/logo.tsx +92 -0
- package/src/renderer.ts +219 -53
- package/src/runtime-modules.d.ts +7 -2
- package/src/scroll.ts +51 -15
- package/src/svg.ts +1 -1
- package/src/text-input.ts +297 -61
- package/src/types.d.ts +291 -31
- package/src/window.ts +110 -14
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Text
|
|
2
|
+
|
|
3
|
+
`<text>` is a shaped paragraph, and `<span>` is a styled run inside it. Text is
|
|
4
|
+
laid out by the engine's own layout and shaping, not by a browser: a paragraph
|
|
5
|
+
is one element that wraps, aligns and truncates as a unit.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<text fontSize={16} maxLines={2} textOverflow="ellipsis">
|
|
9
|
+
Weather for <span fontWeight={700}>Tuesday</span>
|
|
10
|
+
</text>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Run style
|
|
14
|
+
|
|
15
|
+
The style props of a run: paragraph defaults on `<text>`, overrides on
|
|
16
|
+
`<span>`.
|
|
17
|
+
|
|
18
|
+
{{ decl packages/core/src/types.d.ts TextRunProps }}
|
|
19
|
+
|
|
20
|
+
The cascade is intra-paragraph only. A span inherits from its enclosing span
|
|
21
|
+
and then from the `<text>`; nothing inherits across the element tree, so there
|
|
22
|
+
is no ambient font size to chase.
|
|
23
|
+
|
|
24
|
+
`lineHeight` is the one prop with a CSS reflex worth unlearning: it is a
|
|
25
|
+
multiplier of `fontSize`, not a pixel value.
|
|
26
|
+
|
|
27
|
+
## text
|
|
28
|
+
|
|
29
|
+
{{ decl packages/core/src/types.d.ts TextProps }}
|
|
30
|
+
|
|
31
|
+
Paragraph-level behavior lives here: `textAlign`, `maxLines` with
|
|
32
|
+
`textOverflow`, `textIndent`, and `textWrap` for line-breaking quality
|
|
33
|
+
(`"balance"` for headings, `"pretty"` to avoid a lone last word).
|
|
34
|
+
|
|
35
|
+
An element child of a `<text>` is an inline atom, which is where the
|
|
36
|
+
[layout](/core/reference/layout/) props `float` and `clear` apply: a floated
|
|
37
|
+
atom leaves the flow and the lines it overlaps wrap around it.
|
|
38
|
+
|
|
39
|
+
## span
|
|
40
|
+
|
|
41
|
+
{{ decl packages/core/src/types.d.ts SpanProps }}
|
|
42
|
+
|
|
43
|
+
Inline only: its children are text and other spans, and it has no layout box,
|
|
44
|
+
which is why it is the one element with no detached form. Pointer handlers on
|
|
45
|
+
a span fire for the boxes its text occupies on each line it spans, and bubble
|
|
46
|
+
to the enclosing spans and the text.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Transforms
|
|
2
|
+
|
|
3
|
+
Transform props apply after layout, at composite time. Nothing re-records and
|
|
4
|
+
nothing reflows, on a laid-out `view` as much as on a `d-view`, so these are
|
|
5
|
+
the props to animate.
|
|
6
|
+
|
|
7
|
+
They live on `ViewOwnProps`, which means both `view` and `d-view` have them;
|
|
8
|
+
see [elements](/core/reference/elements/).
|
|
9
|
+
|
|
10
|
+
{{ decl packages/core/src/types.d.ts TransformProps }}
|
|
11
|
+
|
|
12
|
+
## Origin
|
|
13
|
+
|
|
14
|
+
`originX` and `originY` are the point rotation and scale pivot around, split
|
|
15
|
+
per axis to match the engine's `x`/`y` prop convention.
|
|
16
|
+
|
|
17
|
+
{{ decl packages/core/src/types.d.ts OriginX }}
|
|
18
|
+
|
|
19
|
+
{{ decl packages/core/src/types.d.ts OriginY }}
|
|
20
|
+
|
|
21
|
+
A percentage origin tracks the layout size with no reactive wiring of your
|
|
22
|
+
own. On a `d-view` there is no box, so the origin defaults to the view's local
|
|
23
|
+
`(0,0)` - the origin its children's coordinates are authored against - and
|
|
24
|
+
`pct()` or keyword origins resolve against the inherited box, which is rarely
|
|
25
|
+
what you want. Pivot a `d-view` around its content by setting the origin in
|
|
26
|
+
pixels.
|
|
27
|
+
|
|
28
|
+
## Opacity
|
|
29
|
+
|
|
30
|
+
`opacity` is group opacity: the children composite together and then fade as a
|
|
31
|
+
whole, like CSS. It costs a compositing layer while below 1, unless the view
|
|
32
|
+
is a `repaintBoundary`, where it is hoisted to composite time for free.
|
|
33
|
+
|
|
34
|
+
To fade a single primitive, put the alpha in its `color` instead. Paint alpha
|
|
35
|
+
costs nothing.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Types
|
|
2
|
+
|
|
3
|
+
The shared aliases the element props refer to.
|
|
4
|
+
|
|
5
|
+
## Color
|
|
6
|
+
|
|
7
|
+
{{ decl packages/core/src/types.d.ts Color }}
|
|
8
|
+
|
|
9
|
+
Anywhere a `color` prop is accepted, a `Gradient` from
|
|
10
|
+
`createLinearGradient` or `createRadialGradient` is accepted too. Gradient
|
|
11
|
+
stops are positions in 0..1, not percentages.
|
|
12
|
+
|
|
13
|
+
## Percentages
|
|
14
|
+
|
|
15
|
+
{{ decl packages/core/src/types.d.ts Pct }}
|
|
16
|
+
|
|
17
|
+
`pct(50)` is the only way to write a percentage as a value. It is branded, so
|
|
18
|
+
a percentage cannot be confused with a pixel count by accident, and it
|
|
19
|
+
resolves against the element box wherever it is used - layout dimensions,
|
|
20
|
+
gaps, and transform origins alike.
|
|
21
|
+
|
|
22
|
+
## Children
|
|
23
|
+
|
|
24
|
+
{{ decl packages/core/src/types.d.ts Children }}
|
|
25
|
+
|
|
26
|
+
The element type is SolidJS's own, so the control-flow components (`For`,
|
|
27
|
+
`Show`, `Switch`) return something the JSX types accept.
|
|
28
|
+
|
|
29
|
+
## JSX plumbing
|
|
30
|
+
|
|
31
|
+
One declaration exists purely to tell TypeScript which prop receives JSX
|
|
32
|
+
children. It is not something an app refers to:
|
|
33
|
+
|
|
34
|
+
{{ decl packages/core/src/types.d.ts ElementChildrenAttribute }}
|
package/examples/README.md
CHANGED
|
@@ -8,7 +8,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
8
8
|
## Host elements and layout
|
|
9
9
|
- `window-root.tsx` - the minimal app; the root must be `<window>`.
|
|
10
10
|
- `view-layout.tsx` - `<view>` as a flex container; containers do not paint.
|
|
11
|
-
- `view-
|
|
11
|
+
- `view-design-size.tsx` - `designSize` on a `<view>`: author a scene once in fixed design units and let the view uniformly scale-and-center (letterbox) that space into its box. Children live in design space for layout as well as paint (the box they inherit IS the design size, so a bare `d-rect` fills it and a flex row lays out against the design width, never reflowing on resize); the view itself sizes like a replaced element whose intrinsic size is the design size; pointer `localX`/`localY` arrive in design units. The fixed-aspect alternative to `windowSizeClass` reflow for diagrams, slides, dashboards, game boards, scaled panels.
|
|
12
12
|
- `background-rect.tsx` - a `d-rect` filling its parent as a background.
|
|
13
13
|
- `detached-positioning.tsx` - the `d-` prefix: x/y placement, no reflow, detached-only children.
|
|
14
14
|
- `text-paint-styling.tsx` - the uniform `color` prop; `drawStyle="stroke"` vs fill.
|
|
@@ -21,6 +21,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
21
21
|
- `pointer-local-coords.tsx` - the three pointer coordinate frames (`clientX` window, `localX` the handling node's own frame, `parentX` its path-parent's frame - where the node's x/y live) and the transform-proof drag idiom: grab offset from `localX` at down, place with `parentX - offset` on moves. Exact inside rotated/scaled ancestors and when the pointer leaves the node mid-drag.
|
|
22
22
|
|
|
23
23
|
## Performance
|
|
24
|
+
- `snapshot-texture.tsx` - `snapshotTexture(ref)`: a `repaintBoundary="snapshot"` view's rasterized pixels as a live texture id; a shader texture samples the panel and a sibling `<texture>` shows the warped copy. The boundary re-rasterizes only when its content changes.
|
|
24
25
|
- `repaint-boundary.tsx` - `repaintBoundary` on a `<view>` to keep static content from rebuilding while a neighbor animates: `{true}` retains the recorded draw list, `"snapshot"` also retains the rasterized pixels as a GPU texture (for raster-expensive, screen-aligned, static subtrees). `"snapshot-no-aa"` rasterizes without anti-aliasing: cheaper, fine for text and axis-aligned rects, hard-edged on vector content.
|
|
25
26
|
|
|
26
27
|
## Scrolling
|
|
@@ -28,7 +29,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
28
29
|
|
|
29
30
|
## Window state
|
|
30
31
|
- `window-signals.tsx` - reactive `windowSize()` / `safeArea()` accessors (prefer over `onResize`).
|
|
31
|
-
- `responsive-grid.tsx` - one app across phone/tablet/desktop: `capabilities.windowSizeClass` (Material 3 breakpoints, a reactive getter) drives the column count and `windowSize()` sizes each card; reflows on resize. The reflow answer; for fixed-aspect content use `view-
|
|
32
|
+
- `responsive-grid.tsx` - one app across phone/tablet/desktop: `capabilities.windowSizeClass` (Material 3 breakpoints, a reactive getter) drives the column count and `windowSize()` sizes each card; reflows on resize. The reflow answer; for fixed-aspect content use `view-design-size.tsx` instead.
|
|
32
33
|
|
|
33
34
|
## Overlays
|
|
34
35
|
- `portal.tsx` - `createPortal` relocating content to the window root to escape clipping.
|
|
@@ -47,11 +48,12 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
47
48
|
- `window-shader.tsx` - the `shader` prop on `<window>`: the finished frame drawn through a raw-linked warp program before present, click to toggle between warp and identity.
|
|
48
49
|
- `window-shader-history.tsx` - the window shader's frame history: `previous` binds last frame as uPrevious, drawn as a one-frame motion echo behind an orbiting square; click toggles the echo term.
|
|
49
50
|
|
|
50
|
-
##
|
|
51
|
-
- `
|
|
51
|
+
## Audio
|
|
52
|
+
- `audio.tsx` - `createSound`: decode a clip once from bytes (here a binary import), replay cheaply; `overlap` stacking vs single-voice, `playing()` signal, release on unmount. `createPcmSound` for a synthesised clip from raw samples (a generated sine sweep). Points to `createSoundStream` for long tracks streamed from a path.
|
|
52
53
|
|
|
53
54
|
## Vector graphics
|
|
54
|
-
- `
|
|
55
|
+
- `line-points.tsx` - `points` on `line`/`d-line`: a flat `[x0, y0, x1, y1, ...]` array (or `Float32Array`) makes the polyline whose geometry is numbers - a live trace rewritten every frame with no `d` string to parse, `closed` outlines, `drawStyle="fill"` polygons (a line's paint defaults to stroke), per-segment dashing, and a laid-out `<line points>` measuring its box from the points.
|
|
56
|
+
- `parse-svg.tsx` - `parseSvg` turns a whole SVG *document string* (not HTML/JSX children) into plain draw data mapped to `<d-path>` inside a `designSize`-fitted view; per-shape hover highlighting shows the payoff (exact-outline hit testing, recolor without re-parse), plus a `currentColor` icon recolored via the `color` option. This is how to use existing icon libraries (Lucide, Heroicons, etc.) - hand their SVG source to `parseSvg`.
|
|
55
57
|
|
|
56
58
|
## Bundling assets
|
|
57
59
|
- `binary-import.tsx` - `import bytes from "./file" with { type: "binary" }` inlines a file's bytes into the bundle as a `Uint8Array` (the bytes are in memory, so `inline-image.tsx` displays them with the synchronous `decodeImage` + `createTexture` path).
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// demand and stays off the heap. Same play()/stop()/playing() surface,
|
|
24
24
|
// always single-voice.
|
|
25
25
|
import { render } from "@solidrt/core"
|
|
26
|
-
import { createPcmSound, createSound } from "@solidrt/core/
|
|
26
|
+
import { createPcmSound, createSound } from "@solidrt/core/audio"
|
|
27
27
|
import blipBytes from "./blip.wav" with { type: "binary" }
|
|
28
28
|
|
|
29
29
|
const RATE = 44100
|
|
@@ -101,11 +101,11 @@ function App() {
|
|
|
101
101
|
let [time, setTime] = createSignal(0)
|
|
102
102
|
onFrame((tick) => setTime(tick / 1000))
|
|
103
103
|
|
|
104
|
-
// Fill the window: the
|
|
104
|
+
// Fill the window: the design-size fits and centers the square content into
|
|
105
105
|
// the full-window view, so the projection is never stretched.
|
|
106
106
|
return (
|
|
107
107
|
<window>
|
|
108
|
-
<view width={pct(100)} height={pct(100)}
|
|
108
|
+
<view width={pct(100)} height={pct(100)} designSize={[1024, 1024]}>
|
|
109
109
|
<texture src={id} params={{ uTime: time() }} width={1024} height={1024} />
|
|
110
110
|
</view>
|
|
111
111
|
</window>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// The zero-copy per-frame geometry path: instanced quads whose records are
|
|
2
|
+
// rewritten every frame through a buffer write lease. beginBufferWrite hands
|
|
3
|
+
// back a Float32Array over runtime-owned memory (contents unspecified - fill
|
|
4
|
+
// everything you publish), the frame callback writes every live record into
|
|
5
|
+
// it, and endBufferWrite publishes by MOVING the block to the raster thread:
|
|
6
|
+
// no data copy anywhere on the CPU path, and no per-sprite property writes.
|
|
7
|
+
// Compare gpu-instancing.tsx, where the records are static and only the draw
|
|
8
|
+
// range changes; here the records themselves are the animation.
|
|
9
|
+
//
|
|
10
|
+
// The buffer is created from a byte LENGTH (zeroed storage) - the natural
|
|
11
|
+
// create when every byte arrives through the lease. Publishing a prefix and
|
|
12
|
+
// setDraw({ instanceCount }) keep buffer capacity and live population
|
|
13
|
+
// independent: reserve the max up front, publish what exists this frame.
|
|
14
|
+
import { render, onFrame } from "@solidrt/core"
|
|
15
|
+
import { beginBufferWrite, createBuffer, createPipelineTexture, endBufferWrite, glsl, setDraw } from "@solidrt/core/gpu"
|
|
16
|
+
|
|
17
|
+
const MAX_SPRITES = 2000
|
|
18
|
+
// floats per record: center vec2, half-size f32, tint vec3.
|
|
19
|
+
const RECORD = 6
|
|
20
|
+
|
|
21
|
+
let VERTEX = glsl`
|
|
22
|
+
in vec2 aPos;
|
|
23
|
+
in vec2 iCenter;
|
|
24
|
+
in float iSize;
|
|
25
|
+
in vec3 iTint;
|
|
26
|
+
out vec3 vTint;
|
|
27
|
+
|
|
28
|
+
void main() {
|
|
29
|
+
gl_Position = vec4(iCenter + aPos * iSize, 0.0, 1.0);
|
|
30
|
+
vTint = iTint;
|
|
31
|
+
}
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
let FRAGMENT = glsl`
|
|
35
|
+
in vec3 vTint;
|
|
36
|
+
|
|
37
|
+
void main() {
|
|
38
|
+
fragColor = vec4(vTint, 1.0);
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
// Simulation state lives in plain arrays; the instance buffer holds only
|
|
43
|
+
// this frame's published snapshot of it.
|
|
44
|
+
let x = new Float32Array(MAX_SPRITES)
|
|
45
|
+
let y = new Float32Array(MAX_SPRITES)
|
|
46
|
+
let vx = new Float32Array(MAX_SPRITES)
|
|
47
|
+
let vy = new Float32Array(MAX_SPRITES)
|
|
48
|
+
for (let i = 0; i < MAX_SPRITES; i++) {
|
|
49
|
+
x[i] = Math.random() * 1.9 - 0.95
|
|
50
|
+
y[i] = Math.random() * 1.9 - 0.95
|
|
51
|
+
vx[i] = (Math.random() * 2 - 1) * 0.01
|
|
52
|
+
vy[i] = (Math.random() * 2 - 1) * 0.01
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function App() {
|
|
56
|
+
// One unit quad (triangle strip), reused by every instance.
|
|
57
|
+
let quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), { label: "sprite-quad" })
|
|
58
|
+
let records = createBuffer(MAX_SPRITES * RECORD * 4, { label: "sprite-records" })
|
|
59
|
+
let id = createPipelineTexture(VERTEX, FRAGMENT, 720, 720, null, {
|
|
60
|
+
label: "sprites",
|
|
61
|
+
topology: "triangle-strip",
|
|
62
|
+
vertexCount: 4,
|
|
63
|
+
attributes: [{ name: "aPos", format: "vec2" }],
|
|
64
|
+
buffer: quad,
|
|
65
|
+
instanceAttributes: [
|
|
66
|
+
{ name: "iCenter", format: "vec2" },
|
|
67
|
+
{ name: "iSize", format: "f32" },
|
|
68
|
+
{ name: "iTint", format: "vec3" },
|
|
69
|
+
],
|
|
70
|
+
instanceBuffer: records,
|
|
71
|
+
instanceCount: MAX_SPRITES,
|
|
72
|
+
clearColor: [0.03, 0.03, 0.06, 1],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
onFrame(() => {
|
|
76
|
+
let out = beginBufferWrite(records)
|
|
77
|
+
for (let i = 0; i < MAX_SPRITES; i++) {
|
|
78
|
+
let nx = x[i]! + vx[i]!
|
|
79
|
+
let ny = y[i]! + vy[i]!
|
|
80
|
+
if (nx < -0.95 || nx > 0.95) vx[i] = -vx[i]!
|
|
81
|
+
else x[i] = nx
|
|
82
|
+
if (ny < -0.95 || ny > 0.95) vy[i] = -vy[i]!
|
|
83
|
+
else y[i] = ny
|
|
84
|
+
let at = i * RECORD
|
|
85
|
+
out[at] = x[i]!
|
|
86
|
+
out[at + 1] = y[i]!
|
|
87
|
+
out[at + 2] = 0.01 + 0.008 * (i % 5)
|
|
88
|
+
out[at + 3] = 0.5 + 0.5 * Math.cos(i * 0.11)
|
|
89
|
+
out[at + 4] = 0.5 + 0.5 * Math.cos(i * 0.13 + 2.1)
|
|
90
|
+
out[at + 5] = 0.5 + 0.5 * Math.cos(i * 0.17 + 4.2)
|
|
91
|
+
}
|
|
92
|
+
endBufferWrite(records)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<window alignItems="center" justifyContent="center">
|
|
97
|
+
<texture src={id} width={480} height={480} />
|
|
98
|
+
</window>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
render(() => <App />)
|
|
@@ -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 />)
|
package/examples/parse-svg.tsx
CHANGED
|
@@ -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 `
|
|
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
|
|
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
|
|
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}
|
|
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,
|
|
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}
|
|
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 `
|
|
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
|
package/examples/scroll.tsx
CHANGED
|
@@ -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 />)
|