@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
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
|
|
@@ -62,8 +65,8 @@ export type CreateOptions = { autoFree?: boolean; label?: string }
|
|
|
62
65
|
|
|
63
66
|
// Sampling options every texture-producing create* helper accepts, applied at
|
|
64
67
|
// 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"
|
|
68
|
+
export type SamplerOptions = { filter?: gpu.FilterMode; wrap?: gpu.WrapMode; mipmap?: boolean }
|
|
69
|
+
export type { FilterMode, WrapMode, TextureBinding, TextureBindings } from "flux:gpu"
|
|
67
70
|
|
|
68
71
|
// Pixel format option for the pixel-upload creates (createTexture,
|
|
69
72
|
// createMutableTexture), fixed for the id's lifetime like the sampler state.
|
|
@@ -98,8 +101,13 @@ export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, Text
|
|
|
98
101
|
// resize in place at a stable id (so `<texture src>` and sampler bindings
|
|
99
102
|
// stay valid); because the id survives, the owner-scoped auto-free
|
|
100
103
|
// registered at creation keeps working and no re-registration is needed.
|
|
104
|
+
// depthTexture(target) names a draw target's sampleable depth (created with
|
|
105
|
+
// `depth: "texture"`): a sampler-only id bound like any texture, owned by
|
|
106
|
+
// the target - no auto-free of its own, it dies with the target's.
|
|
101
107
|
export {
|
|
108
|
+
depthTexture,
|
|
102
109
|
destroyTexture,
|
|
110
|
+
endBufferWrite,
|
|
103
111
|
resizeTexture,
|
|
104
112
|
setTargetParams,
|
|
105
113
|
setTargetSize,
|
|
@@ -111,7 +119,9 @@ export {
|
|
|
111
119
|
// updated draw range (vertexCount after its buffer gained or lost dynamic
|
|
112
120
|
// geometry, firstVertex for a different window of a shared buffer,
|
|
113
121
|
// instanceCount for an instanced population; absent keys keep their current
|
|
114
|
-
// value, like params)
|
|
122
|
+
// value, like params) and/or swapped buffers (instanceBuffer pointed at a
|
|
123
|
+
// larger buffer once a population outgrows the old one - the growth
|
|
124
|
+
// primitive; replace-only, the range is rechecked); destroyBuffer is the manual
|
|
115
125
|
// cleanup path for buffers created outside a reactive scope. renderTarget is
|
|
116
126
|
// the explicit render verb for `render: "manual"` targets - targets whose
|
|
117
127
|
// pass is state (accumulation, feedback) rather than a pure function of its
|
|
@@ -121,22 +131,22 @@ export {
|
|
|
121
131
|
// GPU-side (exact, same size): seed a loadOp "load" accumulator, snapshot a
|
|
122
132
|
// ping-pong buffer, reset state to a known image.
|
|
123
133
|
export { copyTexture, destroyBuffer, renderTarget, setDraw } from "flux:gpu"
|
|
124
|
-
export type { BlendMode, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
134
|
+
export type { BlendMode, BufferUpdate, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, InstanceAttribute, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
125
135
|
|
|
126
136
|
// The draw-list verbs, re-exported raw: entries live and die with their draw
|
|
127
137
|
// target (see createDrawTarget below), so there is no per-entry lifetime to
|
|
128
138
|
// wrap. addDraw adds an entry (appended, or inserted via opts.before) and
|
|
129
139
|
// returns its stable DrawId; removeDraw drops one; setDrawParams /
|
|
130
|
-
// setDrawTextures / setDrawRange are the per-entry forms of
|
|
131
|
-
// setTargetTextures / setDraw
|
|
132
|
-
// merge and validation semantics. The per-object hot path is setDrawParams (a
|
|
140
|
+
// setDrawTextures / setDrawRange / setDrawBuffers are the per-entry forms of
|
|
141
|
+
// setTargetParams / setTargetTextures / setDraw (its range and buffer halves),
|
|
142
|
+
// taking (target, draw, value) with identical merge and validation semantics. The per-object hot path is setDrawParams (a
|
|
133
143
|
// moved mesh = one call with its new matrix); the per-target one is
|
|
134
144
|
// setTargetParams (exported above), which on a draw target writes the SHARED
|
|
135
145
|
// params every entry reads. setDrawOrder replaces the whole
|
|
136
146
|
// list order with a full permutation of the live ids - the sorting verb
|
|
137
147
|
// (opaque front-to-back, transparent back-to-front, re-issued when the
|
|
138
148
|
// camera moves).
|
|
139
|
-
export { addDraw, removeDraw, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
|
|
149
|
+
export { addDraw, removeDraw, setDrawBuffers, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
|
|
140
150
|
|
|
141
151
|
// The device ceilings (max texture/target size, sampler inputs per pass,
|
|
142
152
|
// vertex attributes per pipeline), queried once at startup. Creates and binds
|
|
@@ -161,6 +171,7 @@ export {
|
|
|
161
171
|
destroyRenderPipeline,
|
|
162
172
|
destroyShader,
|
|
163
173
|
linkProgram,
|
|
174
|
+
programAttributes,
|
|
164
175
|
} from "flux:gpu"
|
|
165
176
|
|
|
166
177
|
/**
|
|
@@ -282,7 +293,7 @@ export function createShaderTexture(
|
|
|
282
293
|
width: number,
|
|
283
294
|
height: number,
|
|
284
295
|
params?: gpu.ShaderParams | null,
|
|
285
|
-
opts?: CreateOptions & SamplerOptions & { textures?:
|
|
296
|
+
opts?: CreateOptions & SamplerOptions & { textures?: gpu.TextureBindings },
|
|
286
297
|
): gpu.TextureId {
|
|
287
298
|
let id = gpu.createShaderTexture(fragmentSrc, width, height, params, opts)
|
|
288
299
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
@@ -319,6 +330,12 @@ export function createShaderTexture(
|
|
|
319
330
|
* the default `"clear"` clears to `clearColor` per render; state that must
|
|
320
331
|
* read its own pixels (decay, blur, simulation) still ping-pongs across two
|
|
321
332
|
* manual targets, and `copyTexture` seeds either shape.
|
|
333
|
+
*
|
|
334
|
+
* `samples` (2, 4 or 8) multisamples the target's storage so filled
|
|
335
|
+
* geometry gets anti-aliased edges; the texture id still names a
|
|
336
|
+
* single-sample image, so nothing downstream changes. Clamped to the device
|
|
337
|
+
* maximum, falls back to single-sample (with a warning) where the driver
|
|
338
|
+
* refuses, and throws with `loadOp: "load"`.
|
|
322
339
|
*/
|
|
323
340
|
export function createShaderTarget(
|
|
324
341
|
pipeline: gpu.RenderPipelineId,
|
|
@@ -326,12 +343,16 @@ export function createShaderTarget(
|
|
|
326
343
|
height: number,
|
|
327
344
|
params?: gpu.ShaderParams | null,
|
|
328
345
|
opts?: {
|
|
329
|
-
textures?:
|
|
346
|
+
textures?: gpu.TextureBindings
|
|
330
347
|
buffer?: gpu.BufferId
|
|
331
348
|
instanceBuffer?: gpu.BufferId
|
|
349
|
+
/** One buffer per instance slot of the pipeline (index = the
|
|
350
|
+
* attributes' `slot`); pass this OR `instanceBuffer`, not both. */
|
|
351
|
+
instanceBuffers?: gpu.BufferId[]
|
|
332
352
|
clearColor?: [number, number, number, number]
|
|
333
353
|
render?: "auto" | "manual"
|
|
334
354
|
loadOp?: "clear" | "load"
|
|
355
|
+
samples?: 1 | 2 | 4 | 8
|
|
335
356
|
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
|
|
336
357
|
CreateOptions &
|
|
337
358
|
SamplerOptions,
|
|
@@ -351,7 +372,10 @@ export function createShaderTarget(
|
|
|
351
372
|
* `setDrawParams` / `setDrawTextures` / `setDrawRange`. `depth: true` gives
|
|
352
373
|
* the target the depth storage all entries share (cross-entry occlusion);
|
|
353
374
|
* whether an entry tests/writes it stays pipeline state, and a depth-testing
|
|
354
|
-
* pipeline into a depthless target throws at `addDraw`.
|
|
375
|
+
* pipeline into a depthless target throws at `addDraw`. `depth: "texture"`
|
|
376
|
+
* makes that storage a sampleable depth texture with its own id,
|
|
377
|
+
* `depthTexture(target)` - the shadow-map / depth-effect input; not with
|
|
378
|
+
* `samples`.
|
|
355
379
|
*
|
|
356
380
|
* `params` seeds the target's SHARED params - values every entry reads,
|
|
357
381
|
* written once per target instead of once per entry (a camera's
|
|
@@ -378,11 +402,12 @@ export function createDrawTarget(
|
|
|
378
402
|
height: number,
|
|
379
403
|
params?: gpu.ShaderParams | null,
|
|
380
404
|
opts?: {
|
|
381
|
-
depth?: boolean
|
|
382
|
-
textures?:
|
|
405
|
+
depth?: boolean | "texture"
|
|
406
|
+
textures?: gpu.TextureBindings
|
|
383
407
|
clearColor?: [number, number, number, number]
|
|
384
408
|
render?: "auto" | "manual"
|
|
385
409
|
loadOp?: "clear" | "load"
|
|
410
|
+
samples?: 1 | 2 | 4 | 8
|
|
386
411
|
} & CreateOptions &
|
|
387
412
|
SamplerOptions,
|
|
388
413
|
): gpu.TextureId {
|
|
@@ -392,29 +417,31 @@ export function createDrawTarget(
|
|
|
392
417
|
}
|
|
393
418
|
|
|
394
419
|
/** The reactive shader description `createShaderTextureMemo` builds from.
|
|
395
|
-
* Sampling (`filter`/`wrap`) is creation-time state, so changing it rebuilds
|
|
420
|
+
* Sampling (`filter`/`wrap`/`mipmap`) is creation-time state, so changing it rebuilds
|
|
396
421
|
* at a fresh id, like a fragment-source or sampler-binding change. */
|
|
397
422
|
export type ShaderSpec = {
|
|
398
423
|
fragmentSrc: string
|
|
399
424
|
width: number
|
|
400
425
|
height: number
|
|
401
426
|
params?: gpu.ShaderParams
|
|
402
|
-
textures?:
|
|
427
|
+
textures?: gpu.TextureBindings
|
|
403
428
|
} & SamplerOptions
|
|
404
429
|
|
|
405
430
|
// Shallow name->value equality for params/textures records; treats undefined
|
|
406
431
|
// as the empty record. A param value may be a number or a flat number array
|
|
407
|
-
// (typed uniforms), so arrays compare elementwise
|
|
408
|
-
|
|
432
|
+
// (typed uniforms), so arrays compare elementwise; a texture binding may be
|
|
433
|
+
// an `{ id, filter?, wrap? }` override, compared field by field.
|
|
434
|
+
type RecordValue = number | number[] | gpu.TextureBinding
|
|
435
|
+
function sameValue(a: RecordValue | undefined, b: RecordValue | undefined): boolean {
|
|
409
436
|
if (a === b) return true
|
|
410
|
-
if (
|
|
411
|
-
|
|
437
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => v === b[i])
|
|
438
|
+
if (typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
439
|
+
return a.id === b.id && a.filter === b.filter && a.wrap === b.wrap
|
|
440
|
+
}
|
|
441
|
+
return false
|
|
412
442
|
}
|
|
413
443
|
|
|
414
|
-
function sameRecord(
|
|
415
|
-
a: Record<string, number | number[]> | undefined,
|
|
416
|
-
b: Record<string, number | number[]> | undefined,
|
|
417
|
-
): boolean {
|
|
444
|
+
function sameRecord(a: Record<string, RecordValue> | undefined, b: Record<string, RecordValue> | undefined): boolean {
|
|
418
445
|
if (a === b) return true
|
|
419
446
|
let ka = a ? Object.keys(a) : []
|
|
420
447
|
let kb = b ? Object.keys(b) : []
|
|
@@ -449,7 +476,12 @@ export function createShaderTextureMemo(
|
|
|
449
476
|
opts?: { onError?: (error: unknown) => void },
|
|
450
477
|
): () => gpu.TextureId {
|
|
451
478
|
let make = (s: ShaderSpec) =>
|
|
452
|
-
gpu.createShaderTexture(s.fragmentSrc, s.width, s.height, s.params, {
|
|
479
|
+
gpu.createShaderTexture(s.fragmentSrc, s.width, s.height, s.params, {
|
|
480
|
+
textures: s.textures,
|
|
481
|
+
filter: s.filter,
|
|
482
|
+
wrap: s.wrap,
|
|
483
|
+
mipmap: s.mipmap,
|
|
484
|
+
})
|
|
453
485
|
let current = untrack(spec)
|
|
454
486
|
let currentId = make(current)
|
|
455
487
|
let [id, setId] = createSignal(currentId)
|
|
@@ -459,7 +491,8 @@ export function createShaderTextureMemo(
|
|
|
459
491
|
next.fragmentSrc === current.fragmentSrc &&
|
|
460
492
|
sameRecord(next.textures, current.textures) &&
|
|
461
493
|
next.filter === current.filter &&
|
|
462
|
-
next.wrap === current.wrap
|
|
494
|
+
next.wrap === current.wrap &&
|
|
495
|
+
next.mipmap === current.mipmap
|
|
463
496
|
) {
|
|
464
497
|
// Program and inputs unchanged: mutate in place, the id stays stable.
|
|
465
498
|
if (next.width !== current.width || next.height !== current.height) {
|
|
@@ -544,11 +577,14 @@ export function createPipelineTexture(
|
|
|
544
577
|
height: number,
|
|
545
578
|
params?: gpu.ShaderParams | null,
|
|
546
579
|
opts?: {
|
|
547
|
-
textures?:
|
|
580
|
+
textures?: gpu.TextureBindings
|
|
548
581
|
attributes?: gpu.VertexAttribute[]
|
|
549
582
|
buffer?: gpu.BufferId
|
|
550
|
-
instanceAttributes?: gpu.
|
|
583
|
+
instanceAttributes?: gpu.InstanceAttribute[]
|
|
551
584
|
instanceBuffer?: gpu.BufferId
|
|
585
|
+
/** One buffer per instance slot (index = the attributes' `slot`);
|
|
586
|
+
* pass this OR `instanceBuffer`, not both. */
|
|
587
|
+
instanceBuffers?: gpu.BufferId[]
|
|
552
588
|
topology?: gpu.Topology
|
|
553
589
|
depth?: boolean
|
|
554
590
|
depthWrite?: boolean
|
|
@@ -557,6 +593,7 @@ export function createPipelineTexture(
|
|
|
557
593
|
clearColor?: [number, number, number, number]
|
|
558
594
|
render?: "auto" | "manual"
|
|
559
595
|
loadOp?: "clear" | "load"
|
|
596
|
+
samples?: 1 | 2 | 4 | 8
|
|
560
597
|
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
|
|
561
598
|
CreateOptions &
|
|
562
599
|
SamplerOptions,
|
|
@@ -575,12 +612,29 @@ export function createPipelineTexture(
|
|
|
575
612
|
* created outside a reactive scope you must call `destroyBuffer` yourself.
|
|
576
613
|
* (Destruction order relative to pipelines does not matter.)
|
|
577
614
|
*/
|
|
578
|
-
export function createBuffer(data: ArrayBuffer | ArrayBufferView, opts?: CreateOptions): gpu.BufferId {
|
|
579
|
-
let id = gpu.createBuffer(toUint8(data), opts)
|
|
615
|
+
export function createBuffer(data: ArrayBuffer | ArrayBufferView | number, opts?: CreateOptions): gpu.BufferId {
|
|
616
|
+
let id = gpu.createBuffer(typeof data === "number" ? data : toUint8(data), opts)
|
|
580
617
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyBuffer(id))
|
|
581
618
|
return id
|
|
582
619
|
}
|
|
583
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Opens a zero-copy write into a vertex buffer: returns a Float32Array over
|
|
623
|
+
* runtime-owned memory spanning the whole buffer. Write records in place,
|
|
624
|
+
* then publish with {@link endBufferWrite} - the bytes move to the GPU with
|
|
625
|
+
* no copy on the CPU path, which is the per-frame streaming path (instanced
|
|
626
|
+
* sprites, dynamic geometry). Reach other element types through `.buffer`.
|
|
627
|
+
*
|
|
628
|
+
* Contents are UNSPECIFIED at begin (a recycled block holds what was
|
|
629
|
+
* published the time before last): fill everything you publish. One open
|
|
630
|
+
* write per buffer at a time. The view is detached at end/destroy - retained
|
|
631
|
+
* references become zero-length, so a stale write is inert, never a race.
|
|
632
|
+
*/
|
|
633
|
+
export function beginBufferWrite(id: gpu.BufferId): Float32Array {
|
|
634
|
+
let ab = gpu.beginBufferWrite(id)
|
|
635
|
+
return new Float32Array(ab, 0, (ab.byteLength / 4) | 0)
|
|
636
|
+
}
|
|
637
|
+
|
|
584
638
|
/**
|
|
585
639
|
* Overwrites part of a vertex buffer at `byteOffset` (default 0). Every
|
|
586
640
|
* pipeline drawing from the buffer re-renders with its last-applied params,
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export * from "./renderer"
|
|
2
|
-
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, layoutNextLine, 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"
|
|
6
6
|
export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, exit } from "./window"
|
|
7
7
|
export type { BackEvent } from "./window"
|
|
8
|
-
export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
8
|
+
export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight, lockPointer, pointerLocked } from "./window"
|
|
9
9
|
export { env } from "./environment"
|
|
10
10
|
export type { InputDevices, SystemTheme, Orientation, Visibility } from "./environment"
|
|
11
11
|
export { gamepads } from "./gamepad"
|
|
@@ -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
|
+
}
|