/** * Shape engine * * A shape is a path-in-rect function (`Shape` in `@tachui/types`) drawn as one * `` inside an `` that fills the shape's frame. The frame is * measured with a ResizeObserver and the path is recomputed from the measured * rect, so every shape is exact in any box. Percentage geometry was rejected: * SVG resolves a percentage radius against the normalized diagonal rather than * the short side, so a circle in a non-square box overshoots, and a capsule * has no percentage form at all. Measurement is also what a custom shape * (a user-supplied path-in-rect function) needs, so it is the foundation. * * The `` cannot be expressed as ordinary `DOMNode` children — the renderer * has no namespace support — so it is built with `createElementNS` and handed * over as an owned node through `reactiveElement`. The accessor always returns * the *same* element and updates its attributes in place: a signal-driven * stroke or a size change never replaces the element, so a CSS transition * running on it survives the update. The renderer's swap path (which `Symbol` * uses) would restart it. * * Modifiers land on a wrapper `div`, never on the owned node (see * `DOMNode.owned`), so `.frame()`, `.opacity()` and the rest behave as on any * element. Server-side the wrapper serializes with an `` shell inside it * — everything about the element that does not depend on a measurement, with * an empty `` — so the shape has its layout box in the first paint. The * geometry arrives when the client renders and measures. */ import type { ComponentProps, DOMNode, ModifiableComponentWithModifiers, Signal } from '@tachui/core'; import type { ColorAssetProxy } from '@tachui/core/assets'; import type { CloneableComponent, CloneOptions } from '@tachui/core/runtime/types'; import type { Shape, ShapeRect } from '@tachui/types/shapes'; /** * A fill or stroke style: a CSS color, a signal of one, or a color asset. */ export type ShapeStyle = string | Signal | ColorAssetProxy; export type ShapeLength = number | Signal; export type ShapeLineCap = 'butt' | 'round' | 'square'; export type ShapeLineJoin = 'miter' | 'round' | 'bevel'; /** * SwiftUI's `StrokeStyle`, as far as SVG expresses it. * * `lineWidth` here replaces the one `stroke()` takes, so the two forms can be * combined in either order: `.strokeStyle({ lineWidth: 4 }).stroke(tint)` * keeps the 4, because `stroke()` only writes a width it was actually given. */ export interface StrokeStyleOptions { lineWidth?: ShapeLength; lineCap?: ShapeLineCap | Signal; lineJoin?: ShapeLineJoin | Signal; /** Dash and gap lengths, in pixels. */ dash?: ShapeLength[]; dashPhase?: ShapeLength; } export interface ShapeProps extends ComponentProps { } /** * What a shape factory returns: the shape's own methods (`fill`, `stroke`, * `strokeBorder`, `inset`) plus the modifier chain, in either order. */ export type ShapeInstance = ShapeComponent & ModifiableComponentWithModifiers; /** * A length, with anything that is not a finite number treated as zero. * * A `NaN` inset would otherwise reach `Math.max(0, NaN)` and put `NaN` in the * path data, which renders as nothing with no error anywhere — the silent * failure `resolveStyle` avoids for colors. */ export declare function resolveLength(value: ShapeLength): number; export declare class ShapeComponent implements CloneableComponent, Shape { readonly shape: Shape; readonly kind: string; readonly type: "component"; readonly id: string; readonly props: ShapeProps; mounted: boolean; cleanup: (() => void)[]; private styling; private readonly measuredFrame; private readonly setMeasuredFrame; private svg; private pathElement; private observer; private measurePending; /** * Said once per shape, not once per repaint — and not once per clone. * * `ModifierBuilder.build()` renders a clone, so the instance that paints is * never the one the caller configured. A flag copied by value would let * each clone warn afresh; one cell shared by reference across the lineage * says it once for the shape the caller wrote. */ private warnings; /** * Whether a real measurement has landed for the element as currently * mounted. Cleared on teardown, because the frame kept from the last mount * is no evidence about the new host's size. */ private hasMeasured; constructor(shape: Shape, kind: string, props?: ShapeProps); /** Fill the shape. Without `fill` or `stroke`, a shape fills with `currentColor`. */ fill(style: ShapeStyle): this; /** * Stroke the shape's edge with a line `lineWidth` wide, centered on the * path, so half of it lies outside the frame as in SwiftUI. Without `fill` * the interior is left empty. */ stroke(style: ShapeStyle, lineWidth?: ShapeLength): this; /** * Stroke entirely inside the frame: the shape is inset by half the line * width first. SwiftUI's `strokeBorder`. */ strokeBorder(style: ShapeStyle, lineWidth?: ShapeLength): this; /** Shrink the shape by `by` on every side. Repeated calls accumulate. */ inset(by: ShapeLength): this; /** * Draw only the part of the path between `from` and `to`, as fractions of * its length. SwiftUI's `trim(from:to:)`. * * ```ts * Circle().trim(0, 0.75).stroke(tint, 4) // a 270° arc * Circle().trim(0, progress).stroke(tint, 4) * ``` * * The path starts where SwiftUI's does — a circle at the trailing edge, * three o'clock, running clockwise — so a ring that fills from the top * wants a quarter turn on top, as it does in SwiftUI: * `.rotationEffect(-90)`, or `.transform('rotate(-90deg)')`. * * **Stroke only.** This is dash geometry, not a trimmed path: SVG ignores * a dash pattern when filling, so `.trim(0, 0.5).fill(color)` fills the * whole shape, and `path()` — what `clipShape` reads — is the untrimmed * one. SwiftUI's `trim` returns a shape whose path really is trimmed and * so applies to fill and clipping too. Trimming the path itself is the * work that would close the gap; progress rings, which is what this is * for, are stroked. * * Both fractions are clamped to 0...1. `to` at or below `from` draws * nothing rather than wrapping, so a progress value arriving out of order * shows an empty ring instead of a full one — though with * `lineCap: 'round'` a zero-length dash still renders as a dot, which is * SVG's behaviour for any dashed path. */ trim(from?: ShapeLength, to?: ShapeLength): this; /** * Cap, join and dash for the stroke. SwiftUI's `StrokeStyle`. * * ```ts * Circle().strokeStyle({ lineWidth: 4, lineCap: 'round' }).stroke(tint) * Circle().strokeStyle({ dash: [6, 3] }).stroke(tint, 2) * ``` * * Only the keys present are changed, so repeated calls accumulate the way * `.inset()` does rather than resetting what an earlier one set. */ strokeStyle(options: StrokeStyleOptions): this; /** * Path data for this shape drawn in `rect`. * * This is the path the shape actually draws, so a consumer such as * `clipShape` sees the same geometry as the rendered ``. `paint()` * routes through here rather than computing its own rect. * * `trim()` is the one exception, and deliberately: it is drawn with stroke * dash attributes rather than by shortening the path, so it does not appear * here. A trimmed shape therefore clips, and fills, as the whole shape. */ path(rect: ShapeRect): string; /** * The rect the shape draws into: every `.inset()`, plus half the line width * when `strokeBorder` asked for the stroke to stay inside the frame. */ private drawRect; /** * The line width as drawn, floored at zero. * * SVG ignores a negative `stroke-width`, so it is floored rather than * written out and silently dropped by the renderer. The same floored width * is what `strokeBorder`'s inset is taken from: a raw negative one would * *outset* the path, pushing the fill outside the frame that * `strokeBorder` promises to stay inside, while no stroke is drawn at all. */ private strokeWidth; /** * The CSS `clip-path` for this shape filling its box. * * Insets do not apply, unlike `path`, because CSS basic shapes cannot * express an inset circle. See `Shape.clipPath` for why, and for the way * to add it if a consumer ever needs it. */ clipPath(): string; render(): DOMNode; private contentChildren; /** * The `` shell, described rather than built, for the server. * * There is no DOM to build into and no layout to measure, so the path's `d` * is unknowable here — but everything else about the element is known, and * emitting it gives the shape its layout box in the very first paint * instead of leaving a hole until scripts run. An ordinary node, not an * owned one: an owned node's element *is* its markup, so it needs a DOM to * serialize, while this shell is fully describable without one. * * The `` is left bare. Its fill and stroke would resolve server-side, * but with no `d` there is nothing for them to paint, so emitting them * would run a caller's signals and assets during serialization to no * visible end. */ private shellChildren; /** * The renderer's subscription. Its reads — the measured frame, insets, line * width and colors — are exactly the dependencies a repaint should have, * and it always returns the one element it built. */ private readonly contentElement; private readonly teardown; /** * Measure once as soon as the element is in the document. * * This accessor runs during the render pass, before the renderer inserts * the element, so measuring here would read a detached box of zeros. A * microtask lands after the synchronous render that mounts it. * * ResizeObserver's first delivery is also asynchronous, so without this the * shape would paint empty once before its real path arrived. It is the only * measurement at all where `ResizeObserver` is missing. * * It runs again for a shape that is unmounted and remounted — through a * toggled `Show`, say. The previous frame is kept so the shape repaints * with its old path rather than flashing empty, but it does not count as * having measured the new host; without `ResizeObserver` there is nothing * else to correct it. */ private scheduleFirstMeasure; private measureFromBox; private buildSvg; private observe; private measured; private totalInset; private paint; /** * Cap, join, and whichever of trim or a dash pattern is in play. * * The two cannot both be: they are the same two SVG attributes, and * `pathLength` rescales the units a dash length is measured in. Composing * them would mean computing the dash sequence for the trimmed segment, * which SwiftUI does and this does not yet. Trim wins, since it is the more * specific request, and says so once rather than silently. */ private paintStroke; dispose(): void; clone(options?: CloneOptions): this; shallowClone(): this; deepClone(): this; /** The clone carries the shape's styling; the element and observer are not shared. */ private cloneWith; } /** * Wrap a shape in the modifier chain. * * Under the default component proxy the instance's own methods resolve * through it, so nothing more is needed. With `proxyModifiers: false` * `withModifiers` returns a plain wrapper that carries neither the modifier * chain nor the prototype, which for an ordinary component only costs the * chainable modifier form — `.modifier.frame()` still works. A shape has no * such fallback, because `fill` and `stroke` are not modifiers and exist * nowhere else, so the methods are forwarded onto the wrapper explicitly. */ export declare function createShape(shape: Shape, kind: string): ShapeInstance; //# sourceMappingURL=ShapeComponent.d.ts.map