@solidrt/core 0.0.51 → 0.0.53
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 +105 -21
- package/README.md +1 -1
- 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 +207 -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 +5 -3
- package/examples/gpu-pipeline.tsx +2 -2
- 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/package.json +6 -5
- package/src/core.ts +19 -1
- package/src/gpu.ts +86 -34
- package/src/index.ts +8 -2
- package/src/logo.tsx +92 -0
- package/src/renderer.ts +189 -34
- package/src/runtime-modules.d.ts +4 -0
- package/src/scroll.ts +50 -14
- package/src/svg.ts +1 -1
- package/src/text-input.ts +0 -1
- package/src/types.d.ts +121 -29
- package/src/window.ts +52 -7
package/src/gpu.ts
CHANGED
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
// `flux:gpu` module.
|
|
12
12
|
//
|
|
13
13
|
// Sampling is a per-texture property declared at creation: `filter`
|
|
14
|
-
// ("linear" default | "nearest")
|
|
15
|
-
// every create* helper. One state for every
|
|
16
|
-
// and shader sampling both follow it - so a
|
|
17
|
-
// hard pixels everywhere (the retro/pixel-art
|
|
18
|
-
// big).
|
|
14
|
+
// ("linear" default | "nearest"), `wrap` ("clamp" default | "repeat") and
|
|
15
|
+
// `mipmap` (default false) on every create* helper. One state for every
|
|
16
|
+
// consumer - `<texture>` display and shader sampling both follow it - so a
|
|
17
|
+
// nearest texture upscales with hard pixels everywhere (the retro/pixel-art
|
|
18
|
+
// path: render small, display big). `mipmap: true` keeps a mip chain the
|
|
19
|
+
// runtime regenerates after every upload or render, so shader sampling of a
|
|
20
|
+
// minified texture (3d surfaces at distance, a supersampled target) does
|
|
21
|
+
// not alias; the display draw samples the full-size level only.
|
|
19
22
|
//
|
|
20
23
|
// Combining several passes is a render-tree job, not a shader one: stack
|
|
21
24
|
// `<texture>` elements and set their `blendMode` (e.g. `blendMode="plus"` for
|
|
@@ -39,7 +42,10 @@
|
|
|
39
42
|
// multiplied by its A - `vec4(rgb * a, a)`, not `vec4(rgb, a)`, which
|
|
40
43
|
// composites as opaque. That is what Impeller composites and what
|
|
41
44
|
// `<texture blendMode>` blends; `clearColor` is premultiplied too, so the
|
|
42
|
-
// default transparent black needs no thought.
|
|
45
|
+
// default transparent black needs no thought. Uploaded pixels follow the
|
|
46
|
+
// same rule: `decodeImage` premultiplies at the codec boundary (image
|
|
47
|
+
// files store straight alpha) and `encodeImage` undoes it, so pixels
|
|
48
|
+
// inside the app are premultiplied everywhere.
|
|
43
49
|
// - Values are non-linear RGBA8, with no color-space concept. Every texture
|
|
44
50
|
// and target holds 8-bit RGBA UNORM exactly as written; nothing converts to
|
|
45
51
|
// or from linear light. `filter: "linear"` averages and the `blend` modes
|
|
@@ -62,8 +68,8 @@ export type CreateOptions = { autoFree?: boolean; label?: string }
|
|
|
62
68
|
|
|
63
69
|
// Sampling options every texture-producing create* helper accepts, applied at
|
|
64
70
|
// creation as a property of the texture id (there is no set-sampler-later).
|
|
65
|
-
export type SamplerOptions = { filter?: gpu.FilterMode; wrap?: gpu.WrapMode }
|
|
66
|
-
export type { FilterMode, WrapMode } from "flux:gpu"
|
|
71
|
+
export type SamplerOptions = { filter?: gpu.FilterMode; wrap?: gpu.WrapMode; mipmap?: boolean }
|
|
72
|
+
export type { FilterMode, WrapMode, TextureBinding, TextureBindings } from "flux:gpu"
|
|
67
73
|
|
|
68
74
|
// Pixel format option for the pixel-upload creates (createTexture,
|
|
69
75
|
// createMutableTexture), fixed for the id's lifetime like the sampler state.
|
|
@@ -98,11 +104,16 @@ export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, Text
|
|
|
98
104
|
// resize in place at a stable id (so `<texture src>` and sampler bindings
|
|
99
105
|
// stay valid); because the id survives, the owner-scoped auto-free
|
|
100
106
|
// registered at creation keeps working and no re-registration is needed.
|
|
107
|
+
// depthTexture(target) names a draw target's sampleable depth (created with
|
|
108
|
+
// `depth: "texture"`): a sampler-only id bound like any texture, owned by
|
|
109
|
+
// the target - no auto-free of its own, it dies with the target's.
|
|
101
110
|
export {
|
|
111
|
+
depthTexture,
|
|
102
112
|
destroyTexture,
|
|
103
113
|
endBufferWrite,
|
|
104
114
|
resizeTexture,
|
|
105
115
|
setTargetParams,
|
|
116
|
+
setTargetRect,
|
|
106
117
|
setTargetSize,
|
|
107
118
|
setTargetTextures,
|
|
108
119
|
uploadTexture,
|
|
@@ -112,7 +123,9 @@ export {
|
|
|
112
123
|
// updated draw range (vertexCount after its buffer gained or lost dynamic
|
|
113
124
|
// geometry, firstVertex for a different window of a shared buffer,
|
|
114
125
|
// instanceCount for an instanced population; absent keys keep their current
|
|
115
|
-
// value, like params)
|
|
126
|
+
// value, like params) and/or swapped buffers (instanceBuffer pointed at a
|
|
127
|
+
// larger buffer once a population outgrows the old one - the growth
|
|
128
|
+
// primitive; replace-only, the range is rechecked); destroyBuffer is the manual
|
|
116
129
|
// cleanup path for buffers created outside a reactive scope. renderTarget is
|
|
117
130
|
// the explicit render verb for `render: "manual"` targets - targets whose
|
|
118
131
|
// pass is state (accumulation, feedback) rather than a pure function of its
|
|
@@ -122,22 +135,22 @@ export {
|
|
|
122
135
|
// GPU-side (exact, same size): seed a loadOp "load" accumulator, snapshot a
|
|
123
136
|
// ping-pong buffer, reset state to a known image.
|
|
124
137
|
export { copyTexture, destroyBuffer, renderTarget, setDraw } from "flux:gpu"
|
|
125
|
-
export type { BlendMode, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
138
|
+
export type { BlendMode, BufferUpdate, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, InstanceAttribute, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
126
139
|
|
|
127
140
|
// The draw-list verbs, re-exported raw: entries live and die with their draw
|
|
128
141
|
// target (see createDrawTarget below), so there is no per-entry lifetime to
|
|
129
142
|
// wrap. addDraw adds an entry (appended, or inserted via opts.before) and
|
|
130
143
|
// returns its stable DrawId; removeDraw drops one; setDrawParams /
|
|
131
|
-
// setDrawTextures / setDrawRange are the per-entry forms of
|
|
132
|
-
// setTargetTextures / setDraw
|
|
133
|
-
// merge and validation semantics. The per-object hot path is setDrawParams (a
|
|
144
|
+
// setDrawTextures / setDrawRange / setDrawBuffers are the per-entry forms of
|
|
145
|
+
// setTargetParams / setTargetTextures / setDraw (its range and buffer halves),
|
|
146
|
+
// taking (target, draw, value) with identical merge and validation semantics. The per-object hot path is setDrawParams (a
|
|
134
147
|
// moved mesh = one call with its new matrix); the per-target one is
|
|
135
148
|
// setTargetParams (exported above), which on a draw target writes the SHARED
|
|
136
149
|
// params every entry reads. setDrawOrder replaces the whole
|
|
137
150
|
// list order with a full permutation of the live ids - the sorting verb
|
|
138
151
|
// (opaque front-to-back, transparent back-to-front, re-issued when the
|
|
139
152
|
// camera moves).
|
|
140
|
-
export { addDraw, removeDraw, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
|
|
153
|
+
export { addDraw, removeDraw, setDrawBuffers, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
|
|
141
154
|
|
|
142
155
|
// The device ceilings (max texture/target size, sampler inputs per pass,
|
|
143
156
|
// vertex attributes per pipeline), queried once at startup. Creates and binds
|
|
@@ -162,6 +175,7 @@ export {
|
|
|
162
175
|
destroyRenderPipeline,
|
|
163
176
|
destroyShader,
|
|
164
177
|
linkProgram,
|
|
178
|
+
programAttributes,
|
|
165
179
|
} from "flux:gpu"
|
|
166
180
|
|
|
167
181
|
/**
|
|
@@ -203,7 +217,9 @@ export { captureSnapshot, readTexture } from "flux:gpu"
|
|
|
203
217
|
* Uploads raw pixels to an immutable GPU texture and returns its id (use it
|
|
204
218
|
* as `<texture src={id} />`). `data` must be exactly `width * height` pixels
|
|
205
219
|
* at the declared format's size (`* 4` bytes for the default "rgba8", `* 1`
|
|
206
|
-
* for "r8"); a mismatch throws.
|
|
220
|
+
* for "r8"); a mismatch throws. RGBA data is uploaded verbatim and composited
|
|
221
|
+
* as premultiplied alpha, like every texture: `decodeImage` and the readback
|
|
222
|
+
* calls already deliver that, and hand-built pixels must too (`rgb * a`). For pixels you intend to mutate and
|
|
207
223
|
* re-upload, use `createMutableTexture` instead. When called inside a
|
|
208
224
|
* reactive scope the texture is freed automatically once that owner is
|
|
209
225
|
* disposed; when called outside one (e.g. after an `await`, where the owner
|
|
@@ -283,7 +299,7 @@ export function createShaderTexture(
|
|
|
283
299
|
width: number,
|
|
284
300
|
height: number,
|
|
285
301
|
params?: gpu.ShaderParams | null,
|
|
286
|
-
opts?: CreateOptions & SamplerOptions & { textures?:
|
|
302
|
+
opts?: CreateOptions & SamplerOptions & { textures?: gpu.TextureBindings },
|
|
287
303
|
): gpu.TextureId {
|
|
288
304
|
let id = gpu.createShaderTexture(fragmentSrc, width, height, params, opts)
|
|
289
305
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
@@ -320,6 +336,12 @@ export function createShaderTexture(
|
|
|
320
336
|
* the default `"clear"` clears to `clearColor` per render; state that must
|
|
321
337
|
* read its own pixels (decay, blur, simulation) still ping-pongs across two
|
|
322
338
|
* manual targets, and `copyTexture` seeds either shape.
|
|
339
|
+
*
|
|
340
|
+
* `samples` (2, 4 or 8) multisamples the target's storage so filled
|
|
341
|
+
* geometry gets anti-aliased edges; the texture id still names a
|
|
342
|
+
* single-sample image, so nothing downstream changes. Clamped to the device
|
|
343
|
+
* maximum, falls back to single-sample (with a warning) where the driver
|
|
344
|
+
* refuses, and throws with `loadOp: "load"`.
|
|
323
345
|
*/
|
|
324
346
|
export function createShaderTarget(
|
|
325
347
|
pipeline: gpu.RenderPipelineId,
|
|
@@ -327,12 +349,16 @@ export function createShaderTarget(
|
|
|
327
349
|
height: number,
|
|
328
350
|
params?: gpu.ShaderParams | null,
|
|
329
351
|
opts?: {
|
|
330
|
-
textures?:
|
|
352
|
+
textures?: gpu.TextureBindings
|
|
331
353
|
buffer?: gpu.BufferId
|
|
332
354
|
instanceBuffer?: gpu.BufferId
|
|
355
|
+
/** One buffer per instance slot of the pipeline (index = the
|
|
356
|
+
* attributes' `slot`); pass this OR `instanceBuffer`, not both. */
|
|
357
|
+
instanceBuffers?: gpu.BufferId[]
|
|
333
358
|
clearColor?: [number, number, number, number]
|
|
334
359
|
render?: "auto" | "manual"
|
|
335
360
|
loadOp?: "clear" | "load"
|
|
361
|
+
samples?: 1 | 2 | 4 | 8
|
|
336
362
|
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
|
|
337
363
|
CreateOptions &
|
|
338
364
|
SamplerOptions,
|
|
@@ -352,7 +378,10 @@ export function createShaderTarget(
|
|
|
352
378
|
* `setDrawParams` / `setDrawTextures` / `setDrawRange`. `depth: true` gives
|
|
353
379
|
* the target the depth storage all entries share (cross-entry occlusion);
|
|
354
380
|
* whether an entry tests/writes it stays pipeline state, and a depth-testing
|
|
355
|
-
* pipeline into a depthless target throws at `addDraw`.
|
|
381
|
+
* pipeline into a depthless target throws at `addDraw`. `depth: "texture"`
|
|
382
|
+
* makes that storage a sampleable depth texture with its own id,
|
|
383
|
+
* `depthTexture(target)` - the shadow-map / depth-effect input; not with
|
|
384
|
+
* `samples`.
|
|
356
385
|
*
|
|
357
386
|
* `params` seeds the target's SHARED params - values every entry reads,
|
|
358
387
|
* written once per target instead of once per entry (a camera's
|
|
@@ -366,6 +395,13 @@ export function createShaderTarget(
|
|
|
366
395
|
* driven later with `setTargetTextures`, same precedence and coverage
|
|
367
396
|
* rules.
|
|
368
397
|
*
|
|
398
|
+
* `into` makes a sub-target: a draw target rendering into the rectangle at
|
|
399
|
+
* `x`/`y` (top-left origin) of draw target `into`'s storage, so N views or
|
|
400
|
+
* N shadow maps share one texture and ONE pass; the id is a draw target to
|
|
401
|
+
* every verb but not a texture (display and sample the parent, with
|
|
402
|
+
* `srcX`/`srcY` on the leaf), and `setTargetRect` moves it. Auto-free works
|
|
403
|
+
* the same way; destroying the parent takes its tiles.
|
|
404
|
+
*
|
|
369
405
|
* The render contract is unchanged: the list is input data, so an ordinary
|
|
370
406
|
* (`render: "auto"`) draw target re-renders exactly when its entries or
|
|
371
407
|
* their inputs change - a static scene costs zero passes, and one render is
|
|
@@ -379,11 +415,15 @@ export function createDrawTarget(
|
|
|
379
415
|
height: number,
|
|
380
416
|
params?: gpu.ShaderParams | null,
|
|
381
417
|
opts?: {
|
|
382
|
-
depth?: boolean
|
|
383
|
-
textures?:
|
|
418
|
+
depth?: boolean | "texture"
|
|
419
|
+
textures?: gpu.TextureBindings
|
|
384
420
|
clearColor?: [number, number, number, number]
|
|
385
421
|
render?: "auto" | "manual"
|
|
386
422
|
loadOp?: "clear" | "load"
|
|
423
|
+
samples?: 1 | 2 | 4 | 8
|
|
424
|
+
into?: gpu.TextureId
|
|
425
|
+
x?: number
|
|
426
|
+
y?: number
|
|
387
427
|
} & CreateOptions &
|
|
388
428
|
SamplerOptions,
|
|
389
429
|
): gpu.TextureId {
|
|
@@ -393,29 +433,31 @@ export function createDrawTarget(
|
|
|
393
433
|
}
|
|
394
434
|
|
|
395
435
|
/** The reactive shader description `createShaderTextureMemo` builds from.
|
|
396
|
-
* Sampling (`filter`/`wrap`) is creation-time state, so changing it rebuilds
|
|
436
|
+
* Sampling (`filter`/`wrap`/`mipmap`) is creation-time state, so changing it rebuilds
|
|
397
437
|
* at a fresh id, like a fragment-source or sampler-binding change. */
|
|
398
438
|
export type ShaderSpec = {
|
|
399
439
|
fragmentSrc: string
|
|
400
440
|
width: number
|
|
401
441
|
height: number
|
|
402
442
|
params?: gpu.ShaderParams
|
|
403
|
-
textures?:
|
|
443
|
+
textures?: gpu.TextureBindings
|
|
404
444
|
} & SamplerOptions
|
|
405
445
|
|
|
406
446
|
// Shallow name->value equality for params/textures records; treats undefined
|
|
407
447
|
// as the empty record. A param value may be a number or a flat number array
|
|
408
|
-
// (typed uniforms), so arrays compare elementwise
|
|
409
|
-
|
|
448
|
+
// (typed uniforms), so arrays compare elementwise; a texture binding may be
|
|
449
|
+
// an `{ id, filter?, wrap? }` override, compared field by field.
|
|
450
|
+
type RecordValue = number | number[] | gpu.TextureBinding
|
|
451
|
+
function sameValue(a: RecordValue | undefined, b: RecordValue | undefined): boolean {
|
|
410
452
|
if (a === b) return true
|
|
411
|
-
if (
|
|
412
|
-
|
|
453
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => v === b[i])
|
|
454
|
+
if (typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
455
|
+
return a.id === b.id && a.filter === b.filter && a.wrap === b.wrap
|
|
456
|
+
}
|
|
457
|
+
return false
|
|
413
458
|
}
|
|
414
459
|
|
|
415
|
-
function sameRecord(
|
|
416
|
-
a: Record<string, number | number[]> | undefined,
|
|
417
|
-
b: Record<string, number | number[]> | undefined,
|
|
418
|
-
): boolean {
|
|
460
|
+
function sameRecord(a: Record<string, RecordValue> | undefined, b: Record<string, RecordValue> | undefined): boolean {
|
|
419
461
|
if (a === b) return true
|
|
420
462
|
let ka = a ? Object.keys(a) : []
|
|
421
463
|
let kb = b ? Object.keys(b) : []
|
|
@@ -450,7 +492,12 @@ export function createShaderTextureMemo(
|
|
|
450
492
|
opts?: { onError?: (error: unknown) => void },
|
|
451
493
|
): () => gpu.TextureId {
|
|
452
494
|
let make = (s: ShaderSpec) =>
|
|
453
|
-
gpu.createShaderTexture(s.fragmentSrc, s.width, s.height, s.params, {
|
|
495
|
+
gpu.createShaderTexture(s.fragmentSrc, s.width, s.height, s.params, {
|
|
496
|
+
textures: s.textures,
|
|
497
|
+
filter: s.filter,
|
|
498
|
+
wrap: s.wrap,
|
|
499
|
+
mipmap: s.mipmap,
|
|
500
|
+
})
|
|
454
501
|
let current = untrack(spec)
|
|
455
502
|
let currentId = make(current)
|
|
456
503
|
let [id, setId] = createSignal(currentId)
|
|
@@ -460,7 +507,8 @@ export function createShaderTextureMemo(
|
|
|
460
507
|
next.fragmentSrc === current.fragmentSrc &&
|
|
461
508
|
sameRecord(next.textures, current.textures) &&
|
|
462
509
|
next.filter === current.filter &&
|
|
463
|
-
next.wrap === current.wrap
|
|
510
|
+
next.wrap === current.wrap &&
|
|
511
|
+
next.mipmap === current.mipmap
|
|
464
512
|
) {
|
|
465
513
|
// Program and inputs unchanged: mutate in place, the id stays stable.
|
|
466
514
|
if (next.width !== current.width || next.height !== current.height) {
|
|
@@ -545,11 +593,14 @@ export function createPipelineTexture(
|
|
|
545
593
|
height: number,
|
|
546
594
|
params?: gpu.ShaderParams | null,
|
|
547
595
|
opts?: {
|
|
548
|
-
textures?:
|
|
596
|
+
textures?: gpu.TextureBindings
|
|
549
597
|
attributes?: gpu.VertexAttribute[]
|
|
550
598
|
buffer?: gpu.BufferId
|
|
551
|
-
instanceAttributes?: gpu.
|
|
599
|
+
instanceAttributes?: gpu.InstanceAttribute[]
|
|
552
600
|
instanceBuffer?: gpu.BufferId
|
|
601
|
+
/** One buffer per instance slot (index = the attributes' `slot`);
|
|
602
|
+
* pass this OR `instanceBuffer`, not both. */
|
|
603
|
+
instanceBuffers?: gpu.BufferId[]
|
|
553
604
|
topology?: gpu.Topology
|
|
554
605
|
depth?: boolean
|
|
555
606
|
depthWrite?: boolean
|
|
@@ -558,6 +609,7 @@ export function createPipelineTexture(
|
|
|
558
609
|
clearColor?: [number, number, number, number]
|
|
559
610
|
render?: "auto" | "manual"
|
|
560
611
|
loadOp?: "clear" | "load"
|
|
612
|
+
samples?: 1 | 2 | 4 | 8
|
|
561
613
|
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
|
|
562
614
|
CreateOptions &
|
|
563
615
|
SamplerOptions,
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./renderer"
|
|
2
|
-
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, onPointerMove } from "./core"
|
|
2
|
+
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, snapshotTexture, onPointerMove } from "./core"
|
|
3
3
|
export type { BoundingBox, GlobalPointerEvent, TextLine } from "./core"
|
|
4
4
|
export { parseColor, mixColors, brightness, createLinearGradient, createRadialGradient } from "./color"
|
|
5
5
|
export type { Gradient, GradientStop } from "./color"
|
|
@@ -17,9 +17,11 @@ export type { TextureId } from "./gpu"
|
|
|
17
17
|
export { createImage, decodeImage, encodeImage } from "./image"
|
|
18
18
|
export type { DecodedImage, ImageSource } from "./image"
|
|
19
19
|
export { parseSvg, svg } from "./svg"
|
|
20
|
+
export { Logo } from "./logo"
|
|
21
|
+
export type { LogoProps } from "./logo"
|
|
20
22
|
export type { SvgDocument, SvgDraw } from "./svg"
|
|
21
23
|
export { createScroll } from "./scroll"
|
|
22
|
-
export type { Scroll, ScrollAxis, ScrollOffset, ScrollOptions } from "./scroll"
|
|
24
|
+
export type { Scroll, ScrollAxis, ScrollBehavior, ScrollOffset, ScrollOptions, ScrollToOptions } from "./scroll"
|
|
23
25
|
export { arena } from "./arena"
|
|
24
26
|
export type { ArenaOwner } from "./arena"
|
|
25
27
|
export { createPan } from "./pan"
|
|
@@ -31,6 +33,10 @@ export type {
|
|
|
31
33
|
LayoutProps,
|
|
32
34
|
TransformProps,
|
|
33
35
|
PointerProps,
|
|
36
|
+
TransitionProps,
|
|
37
|
+
Transition,
|
|
38
|
+
TransitionPropName,
|
|
39
|
+
TransitionEndEvent,
|
|
34
40
|
PointerEvent,
|
|
35
41
|
WheelEvent,
|
|
36
42
|
KeyEvent,
|
package/src/logo.tsx
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The SolidRT brand mark: seven puzzle segments authored in a 100x100 space,
|
|
2
|
+
// each filled with its own light-to-dark gradient. Static by default; the
|
|
3
|
+
// animated forms stagger a fade per segment, either once (fade in and hold)
|
|
4
|
+
// or in a loop (in, then out, repeat) like the scaffold's welcome screen.
|
|
5
|
+
import { For, Show, createSignal } from "solid-js"
|
|
6
|
+
import { createLinearGradient } from "./color"
|
|
7
|
+
import { onFrame } from "./window"
|
|
8
|
+
|
|
9
|
+
export interface LogoProps {
|
|
10
|
+
// Rendered width and height in pixels; the mark is square. Default 100.
|
|
11
|
+
size?: number
|
|
12
|
+
// "none" draws the mark at full opacity and requests no frames (default).
|
|
13
|
+
// "once" fades the segments in one after the other and then holds; "loop"
|
|
14
|
+
// fades them in, then out, and repeats.
|
|
15
|
+
animation?: "none" | "once" | "loop"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Per segment: the delay of its fade in ms (its fade out follows at the same
|
|
19
|
+
// offset after the last fade in has completed), its gradient, its path.
|
|
20
|
+
const SEGMENTS = [
|
|
21
|
+
{ base: 0, light: "#3f5494", dark: "#162b6c", d: "M50.000 50.000 L28.330 50.000 C28.330 48.810 27.695 47.711 26.665 47.116 C25.635 46.521 24.365 46.521 23.335 47.116 C22.305 47.711 21.670 48.810 21.670 50.000 L0.000 50.000 L50.000 0.000 L50.000 9.170 C48.810 9.170 47.711 9.805 47.116 10.835 C46.521 11.865 46.521 13.135 47.116 14.165 C47.711 15.195 48.810 15.830 50.000 15.830 L50.000 25.000 L50.000 34.170 C48.810 34.170 47.711 34.805 47.116 35.835 C46.521 36.865 46.521 38.135 47.116 39.165 C47.711 40.195 48.810 40.830 50.000 40.830 L50.000 50.000 Z" },
|
|
22
|
+
{ base: 90, light: "#547ebf", dark: "#2b5696", d: "M50.000 50.000 L50.000 59.170 C48.810 59.170 47.711 59.805 47.116 60.835 C46.521 61.865 46.521 63.135 47.116 64.165 C47.711 65.195 48.810 65.830 50.000 65.830 L50.000 75.000 L50.000 84.170 C48.810 84.170 47.711 84.805 47.116 85.835 C46.521 86.865 46.521 88.135 47.116 89.165 C47.711 90.195 48.810 90.830 50.000 90.830 L50.000 100.000 L0.000 50.000 L21.670 50.000 C21.670 48.810 22.305 47.711 23.335 47.116 C24.365 46.521 25.635 46.521 26.665 47.116 C27.695 47.711 28.330 48.810 28.330 50.000 L50.000 50.000 Z" },
|
|
23
|
+
{ base: 180, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 25.000 L50.000 15.830 C48.810 15.830 47.711 15.195 47.116 14.165 C46.521 13.135 46.521 11.865 47.116 10.835 C47.711 9.805 48.810 9.170 50.000 9.170 L50.000 0.000 L75.000 25.000 L65.830 25.000 C65.830 26.190 65.195 27.289 64.165 27.884 C63.135 28.479 61.865 28.479 60.835 27.884 C59.805 27.289 59.170 26.190 59.170 25.000 L50.000 25.000 Z" },
|
|
24
|
+
{ base: 270, light: "#547ebf", dark: "#2b5696", d: "M50.000 25.000 L59.170 25.000 C59.170 26.190 59.805 27.289 60.835 27.884 C61.865 28.479 63.135 28.479 64.165 27.884 C65.195 27.289 65.830 26.190 65.830 25.000 L75.000 25.000 L75.000 34.170 C73.810 34.170 72.711 34.805 72.116 35.835 C71.521 36.865 71.521 38.135 72.116 39.165 C72.711 40.195 73.810 40.830 75.000 40.830 L75.000 50.000 L65.830 50.000 C65.830 48.810 65.195 47.711 64.165 47.116 C63.135 46.521 61.865 46.521 60.835 47.116 C59.805 47.711 59.170 48.810 59.170 50.000 L50.000 50.000 L50.000 40.830 C48.810 40.830 47.711 40.195 47.116 39.165 C46.521 38.135 46.521 36.865 47.116 35.835 C47.711 34.805 48.810 34.170 50.000 34.170 L50.000 25.000 Z" },
|
|
25
|
+
{ base: 360, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 50.000 L59.170 50.000 C59.170 48.810 59.805 47.711 60.835 47.116 C61.865 46.521 63.135 46.521 64.165 47.116 C65.195 47.711 65.830 48.810 65.830 50.000 L75.000 50.000 L64.855 60.145 C64.013 59.304 62.787 58.976 61.638 59.283 C60.489 59.591 59.591 60.489 59.283 61.638 C58.976 62.787 59.304 64.013 60.145 64.855 L50.000 75.000 L50.000 65.830 C48.810 65.830 47.711 65.195 47.116 64.165 C46.521 63.135 46.521 61.865 47.116 60.835 C47.711 59.805 48.810 59.170 50.000 59.170 L50.000 50.000 Z" },
|
|
26
|
+
{ base: 450, light: "#3f5494", dark: "#162b6c", d: "M75.000 50.000 L75.000 59.170 C73.810 59.170 72.711 59.805 72.116 60.835 C71.521 61.865 71.521 63.135 72.116 64.165 C72.711 65.195 73.810 65.830 75.000 65.830 L75.000 75.000 L50.000 100.000 L50.000 90.830 C48.810 90.830 47.711 90.195 47.116 89.165 C46.521 88.135 46.521 86.865 47.116 85.835 C47.711 84.805 48.810 84.170 50.000 84.170 L50.000 75.000 L60.145 64.855 C59.304 64.013 58.976 62.787 59.283 61.638 C59.591 60.489 60.489 59.591 61.638 59.283 C62.787 58.976 64.013 59.304 64.855 60.145 L75.000 50.000 Z" },
|
|
27
|
+
{ base: 540, light: "#7ea9ea", dark: "#5681c1", d: "M100.000 50.000 L75.000 75.000 L75.000 65.830 C73.810 65.830 72.711 65.195 72.116 64.165 C71.521 63.135 71.521 61.865 72.116 60.835 C72.711 59.805 73.810 59.170 75.000 59.170 L75.000 50.000 L75.000 40.830 C73.810 40.830 72.711 40.195 72.116 39.165 C71.521 38.135 71.521 36.865 72.116 35.835 C72.711 34.805 73.810 34.170 75.000 34.170 L75.000 25.000 L100.000 50.000 Z" },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const FADE = 360
|
|
31
|
+
const LAST = SEGMENTS[SEGMENTS.length - 1]!.base
|
|
32
|
+
// The whole mark is visible from IN_DONE; a loop cycle fades out after that.
|
|
33
|
+
const IN_DONE = LAST + FADE
|
|
34
|
+
const CYCLE = IN_DONE + LAST + FADE
|
|
35
|
+
|
|
36
|
+
let clamp = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x)
|
|
37
|
+
let ease = (t: number) => 1 - (1 - t) * (1 - t)
|
|
38
|
+
let byte = (x: number) => Math.round(clamp(x) * 255).toString(16).padStart(2, "0")
|
|
39
|
+
|
|
40
|
+
export function Logo(props: LogoProps) {
|
|
41
|
+
let size = () => props.size ?? 100
|
|
42
|
+
let mode = () => props.animation ?? "none"
|
|
43
|
+
|
|
44
|
+
// Elapsed animation time; only advanced while an animated mode is mounted.
|
|
45
|
+
let [clock, setClock] = createSignal(0)
|
|
46
|
+
|
|
47
|
+
// The frame loop is mounted through the <Show> below so a mode change
|
|
48
|
+
// remounts it: onFrame holds a standing frame request while registered.
|
|
49
|
+
// "once" releases its request as soon as the last segment is in.
|
|
50
|
+
let Animate = () => {
|
|
51
|
+
let start = -1
|
|
52
|
+
let stop = onFrame((tick) => {
|
|
53
|
+
if (start < 0) start = tick
|
|
54
|
+
let t = tick - start
|
|
55
|
+
if (mode() === "loop") setClock(t % CYCLE)
|
|
56
|
+
else if (t < IN_DONE) setClock(t)
|
|
57
|
+
else {
|
|
58
|
+
setClock(IN_DONE)
|
|
59
|
+
stop()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let alpha = (seg: (typeof SEGMENTS)[number]) => {
|
|
66
|
+
if (mode() === "none") return 1
|
|
67
|
+
let t = clock()
|
|
68
|
+
if (t < seg.base) return 0
|
|
69
|
+
let fadeIn = clamp((t - seg.base) / FADE)
|
|
70
|
+
if (mode() === "once") return ease(fadeIn)
|
|
71
|
+
let end = IN_DONE + seg.base + FADE
|
|
72
|
+
if (t >= end) return 0
|
|
73
|
+
return ease(Math.min(fadeIn, clamp((end - t) / FADE)))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let fill = (seg: (typeof SEGMENTS)[number]) => {
|
|
77
|
+
let a = byte(alpha(seg))
|
|
78
|
+
return createLinearGradient(0, 0, 1, 1, [
|
|
79
|
+
{ offset: 0, color: seg.light + a },
|
|
80
|
+
{ offset: 1, color: seg.dark + a },
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<view width={size()} height={size()} designSize={[100, 100]}>
|
|
86
|
+
<Show when={mode() !== "none"}>
|
|
87
|
+
<Animate />
|
|
88
|
+
</Show>
|
|
89
|
+
<For each={SEGMENTS}>{(seg) => <d-path d={seg.d} color={fill(seg)} />}</For>
|
|
90
|
+
</view>
|
|
91
|
+
)
|
|
92
|
+
}
|