@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/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") and `wrap` ("clamp" default | "repeat") on
15
- // every create* helper. One state for every consumer - `<texture>` display
16
- // and shader sampling both follow it - so a nearest texture upscales with
17
- // hard pixels everywhere (the retro/pixel-art path: render small, display
18
- // big). No mipmaps exist.
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); destroyBuffer is the manual
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 setTargetParams /
132
- // setTargetTextures / setDraw, taking (target, draw, value) with identical
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. For pixels you intend to mutate and
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?: Record<string, gpu.TextureId> },
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?: Record<string, gpu.TextureId>
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?: Record<string, gpu.TextureId>
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?: Record<string, gpu.TextureId>
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
- function sameValue(a: number | number[] | undefined, b: number | number[] | undefined): boolean {
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 (!Array.isArray(a) || !Array.isArray(b)) return false
412
- return a.length === b.length && a.every((v, i) => v === b[i])
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, { textures: s.textures, filter: s.filter, wrap: s.wrap })
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?: Record<string, gpu.TextureId>
596
+ textures?: gpu.TextureBindings
549
597
  attributes?: gpu.VertexAttribute[]
550
598
  buffer?: gpu.BufferId
551
- instanceAttributes?: gpu.VertexAttribute[]
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
+ }