diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 13304f0791..f921d5f023 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,4 +1,4 @@
-name: Test
+name: test packages
on:
push:
@@ -22,5 +22,5 @@ jobs:
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Run Tests
- run: pnpm turbo test
+ run: pnpm turbo test --filter='!./crates/*'
diff --git a/.gitignore b/.gitignore
index 51da199c84..b89c5c2492 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,3 @@
-
-# Created by https://www.toptal.com/developers/gitignore/api/node,react,macos,windows,visualstudiocode
-# Edit at https://www.toptal.com/developers/gitignore?templates=node,react,macos,windows,visualstudiocode
-
### macOS ###
# General
.DS_Store
@@ -197,11 +193,13 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
-# End of https://www.toptal.com/developers/gitignore/api/node,react,macos,windows,visualstudiocode
.vercel
# Turborepo
.turbo
# node-compile-cache
-node-compile-cache/
\ No newline at end of file
+node-compile-cache/
+
+# rust
+/target
\ No newline at end of file
diff --git a/.ref/canvaskit/canvaskit.d.ts b/.ref/canvaskit/canvaskit.d.ts
new file mode 100644
index 0000000000..192b842c9f
--- /dev/null
+++ b/.ref/canvaskit/canvaskit.d.ts
@@ -0,0 +1,5366 @@
+// Minimum TypeScript Version: 4.4
+///
+
+export default function CanvasKitInit(
+ opts?: CanvasKitInitOptions
+): Promise;
+
+export interface CanvasKitInitOptions {
+ /**
+ * This callback will be invoked when the CanvasKit loader needs to fetch a file (e.g.
+ * the blob of WASM code). The correct url prefix should be applied.
+ * @param file - the name of the file that is about to be loaded.
+ */
+ locateFile(file: string): string;
+}
+
+export interface CanvasKit {
+ // Helpers
+ /**
+ * Constructs a Color with the same API as CSS's rgba(), that is
+ * Internally, Colors are four unpremultiplied 32-bit floats: r, g, b, a.
+ * In order to construct one with more precision or in a wider gamut,
+ * use CanvasKit.Color4f().
+ *
+ * @param r - red value, clamped to [0, 255].
+ * @param g - green value, clamped to [0, 255].
+ * @param b - blue value, clamped to [0, 255].
+ * @param a - alpha value, from 0 to 1.0. By default is 1.0 (opaque).
+ */
+ Color(r: number, g: number, b: number, a?: number): Color;
+
+ /**
+ * Construct a 4-float color. Float values are typically between 0.0 and 1.0.
+ * @param r - red value.
+ * @param g - green value.
+ * @param b - blue value.
+ * @param a - alpha value. By default is 1.0 (opaque).
+ */
+ Color4f(r: number, g: number, b: number, a?: number): Color;
+
+ /**
+ * Constructs a Color as a 32 bit unsigned integer, with 8 bits assigned to each channel.
+ * Channels are expected to be between 0 and 255 and will be clamped as such.
+ * If a is omitted, it will be 255 (opaque).
+ *
+ * This is not the preferred way to use colors in Skia APIs, use Color or Color4f.
+ * @param r - red value, clamped to [0, 255].
+ * @param g - green value, clamped to [0, 255].
+ * @param b - blue value, clamped to [0, 255].
+ * @param a - alpha value, from 0 to 1.0. By default is 1.0 (opaque).
+ */
+ ColorAsInt(r: number, g: number, b: number, a?: number): ColorInt;
+
+ /**
+ * Returns a css style [r, g, b, a] where r, g, b are returned as
+ * ints in the range [0, 255] and where a is scaled between 0 and 1.0.
+ * [Deprecated] - this is trivial now that Color is 4 floats.
+ */
+ getColorComponents(c: Color): number[];
+
+ /**
+ * Takes in a CSS color value and returns a CanvasKit.Color
+ * (which is an array of 4 floats in RGBA order). An optional colorMap
+ * may be provided which maps custom strings to values.
+ * In the CanvasKit canvas2d shim layer, we provide this map for processing
+ * canvas2d calls, but not here for code size reasons.
+ */
+ parseColorString(color: string, colorMap?: Record): Color;
+
+ /**
+ * Returns a copy of the passed in color with a new alpha value applied.
+ * [Deprecated] - this is trivial now that Color is 4 floats.
+ */
+ multiplyByAlpha(c: Color, alpha: number): Color;
+
+ /**
+ * Computes color values for one-pass tonal alpha.
+ * Note, if malloced colors are passed in, the memory pointed at by the MallocObj
+ * will be overwritten with the computed tonal colors (and thus the return val can be
+ * ignored).
+ * @param colors
+ */
+ computeTonalColors(colors: TonalColorsInput): TonalColorsOutput;
+
+ /**
+ * Returns a rectangle with the given paramaters. See Rect.h for more.
+ * @param left - The x coordinate of the upper-left corner.
+ * @param top - The y coordinate of the upper-left corner.
+ * @param right - The x coordinate of the lower-right corner.
+ * @param bottom - The y coordinate of the lower-right corner.
+ */
+ LTRBRect(left: number, top: number, right: number, bottom: number): Rect;
+
+ /**
+ * Returns a rectangle with the given paramaters. See Rect.h for more.
+ * @param x - The x coordinate of the upper-left corner.
+ * @param y - The y coordinate of the upper-left corner.
+ * @param width - The width of the rectangle.
+ * @param height - The height of the rectangle.
+ */
+ XYWHRect(x: number, y: number, width: number, height: number): Rect;
+
+ /**
+ * Returns a rectangle with the given integer paramaters. See Rect.h for more.
+ * @param left - The x coordinate of the upper-left corner.
+ * @param top - The y coordinate of the upper-left corner.
+ * @param right - The x coordinate of the lower-right corner.
+ * @param bottom - The y coordinate of the lower-right corner.
+ */
+ LTRBiRect(left: number, top: number, right: number, bottom: number): IRect;
+
+ /**
+ * Returns a rectangle with the given paramaters. See Rect.h for more.
+ * @param x - The x coordinate of the upper-left corner.
+ * @param y - The y coordinate of the upper-left corner.
+ * @param width - The width of the rectangle.
+ * @param height - The height of the rectangle.
+ */
+ XYWHiRect(x: number, y: number, width: number, height: number): IRect;
+
+ /**
+ * Returns a rectangle with rounded corners consisting of the given rectangle and
+ * the same radiusX and radiusY for all four corners.
+ * @param rect - The base rectangle.
+ * @param rx - The radius of the corners in the x direction.
+ * @param ry - The radius of the corners in the y direction.
+ */
+ RRectXY(rect: InputRect, rx: number, ry: number): RRect;
+
+ /**
+ * Generate bounding box for shadows relative to path. Includes both the ambient and spot
+ * shadow bounds. This pairs with Canvas.drawShadow().
+ * See SkShadowUtils.h for more details.
+ * @param ctm - Current transformation matrix to device space.
+ * @param path - The occluder used to generate the shadows.
+ * @param zPlaneParams - Values for the plane function which returns the Z offset of the
+ * occluder from the canvas based on local x and y values (the current
+ * matrix is not applied).
+ * @param lightPos - The 3D position of the light relative to the canvas plane. This is
+ * independent of the canvas's current matrix.
+ * @param lightRadius - The radius of the disc light.
+ * @param flags - See SkShadowUtils.h; 0 means use default options.
+ * @param dstRect - if provided, the bounds will be copied into this rect instead of allocating
+ * a new one.
+ * @returns The bounding rectangle or null if it could not be computed.
+ */
+ getShadowLocalBounds(
+ ctm: InputMatrix,
+ path: Path,
+ zPlaneParams: InputVector3,
+ lightPos: InputVector3,
+ lightRadius: number,
+ flags: number,
+ dstRect?: Rect
+ ): Rect | null;
+
+ /**
+ * Malloc returns a TypedArray backed by the C++ memory of the
+ * given length. It should only be used by advanced users who
+ * can manage memory and initialize values properly. When used
+ * correctly, it can save copying of data between JS and C++.
+ * When used incorrectly, it can lead to memory leaks.
+ * Any memory allocated by CanvasKit.Malloc needs to be released with CanvasKit.Free.
+ *
+ * const mObj = CanvasKit.Malloc(Float32Array, 20);
+ * Get a TypedArray view around the malloc'd memory (this does not copy anything).
+ * const ta = mObj.toTypedArray();
+ * // store data into ta
+ * const cf = CanvasKit.ColorFilter.MakeMatrix(ta); // mObj could also be used.
+ *
+ * // eventually...
+ * CanvasKit.Free(mObj);
+ *
+ * @param typedArray - constructor for the typedArray.
+ * @param len - number of *elements* to store.
+ */
+ Malloc(typedArray: TypedArrayConstructor, len: number): MallocObj;
+
+ /**
+ * As Malloc but for GlyphIDs. This helper exists to make sure the JS side and the C++ side
+ * stay in agreement with how wide GlyphIDs are.
+ * @param len - number of GlyphIDs to make space for.
+ */
+ MallocGlyphIDs(len: number): MallocObj;
+
+ /**
+ * Free frees the memory returned by Malloc.
+ * Any memory allocated by CanvasKit.Malloc needs to be released with CanvasKit.Free.
+ */
+ Free(m: MallocObj): void;
+
+ // Surface related functions
+ /**
+ * Creates a Surface on a given canvas. If both GPU and CPU modes have been compiled in, this
+ * will first try to create a GPU surface and then fallback to a CPU one if that fails. If just
+ * the CPU mode has been compiled in, a CPU surface will be created.
+ * @param canvas - either a canvas or a string with the DOM id of it.
+ * @deprecated - Use MakeSWCanvasSurface, MakeWebGLCanvasSurface, or MakeGPUCanvasSurface.
+ */
+ MakeCanvasSurface(
+ canvas: HTMLCanvasElement | OffscreenCanvas | string
+ ): Surface | null;
+
+ /**
+ * Creates a Raster (CPU) Surface that will draw into the provided Malloc'd buffer. This allows
+ * clients to efficiently be able to read the current pixels w/o having to copy.
+ * The length of pixels must be at least height * bytesPerRow bytes big.
+ * @param ii
+ * @param pixels
+ * @param bytesPerRow - How many bytes are per row. This is at least width * bytesPerColorType. For example,
+ * an 8888 ColorType has 4 bytes per pixel, so a 5 pixel wide 8888 surface needs at least
+ * 5 * 4 = 20 bytesPerRow. Some clients may have more than the usual to make the data line
+ * up with a particular multiple.
+ */
+ MakeRasterDirectSurface(
+ ii: ImageInfo,
+ pixels: MallocObj,
+ bytesPerRow: number
+ ): Surface | null;
+
+ /**
+ * Creates a CPU backed (aka raster) surface.
+ * @param canvas - either a canvas or a string with the DOM id of it.
+ */
+ MakeSWCanvasSurface(
+ canvas: HTMLCanvasElement | OffscreenCanvas | string
+ ): Surface | null;
+
+ /**
+ * A helper for creating a WebGL backed (aka GPU) surface and falling back to a CPU surface if
+ * the GPU one cannot be created. This works for both WebGL 1 and WebGL 2.
+ * @param canvas - Either a canvas or a string with the DOM id of it.
+ * @param colorSpace - One of the supported color spaces. Default is SRGB.
+ * @param opts - Options that will get passed to the creation of the WebGL context.
+ */
+ MakeWebGLCanvasSurface(
+ canvas: HTMLCanvasElement | OffscreenCanvas | string,
+ colorSpace?: ColorSpace,
+ opts?: WebGLOptions
+ ): Surface | null;
+
+ /**
+ * Returns a CPU backed surface with the given dimensions, an SRGB colorspace, Unpremul
+ * alphaType and 8888 color type. The pixels belonging to this surface will be in memory and
+ * not visible.
+ * @param width - number of pixels of the width of the drawable area.
+ * @param height - number of pixels of the height of the drawable area.
+ */
+ MakeSurface(width: number, height: number): Surface | null;
+
+ /**
+ * Creates a WebGL Context from the given canvas with the given options. If options are omitted,
+ * sensible defaults will be used.
+ * @param canvas
+ * @param opts
+ */
+ GetWebGLContext(
+ canvas: HTMLCanvasElement | OffscreenCanvas,
+ opts?: WebGLOptions
+ ): WebGLContextHandle;
+
+ /**
+ * Creates a GrDirectContext from the given WebGL Context.
+ * @param ctx
+ * @deprecated Use MakeWebGLContext instead.
+ */
+ MakeGrContext(ctx: WebGLContextHandle): GrDirectContext | null;
+
+ /**
+ * Creates a GrDirectContext from the given WebGL Context.
+ * @param ctx
+ */
+ MakeWebGLContext(ctx: WebGLContextHandle): GrDirectContext | null;
+
+ /**
+ * Creates a Surface that will be drawn to the given GrDirectContext (and show up on screen).
+ * @param ctx
+ * @param width - number of pixels of the width of the visible area.
+ * @param height - number of pixels of the height of the visible area.
+ * @param colorSpace
+ * @param sampleCount - sample count value from GL_SAMPLES. If not provided this will be looked up from
+ * the canvas.
+ * @param stencil - stencil count value from GL_STENCIL_BITS. If not provided this will be looked up
+ * from the WebGL Context.
+ */
+ MakeOnScreenGLSurface(
+ ctx: GrDirectContext,
+ width: number,
+ height: number,
+ colorSpace: ColorSpace,
+ sampleCount?: number,
+ stencil?: number
+ ): Surface | null;
+
+ /**
+ * Creates a context that operates over the given WebGPU Device.
+ * @param device
+ */
+ MakeGPUDeviceContext(device: GPUDevice): WebGPUDeviceContext | null;
+
+ /**
+ * Creates a Surface that draws to the given GPU texture.
+ * @param ctx
+ * @param texture - A texture that was created on the GPU device associated with `ctx`.
+ * @param width - Width of the visible region in pixels.
+ * @param height - Height of the visible region in pixels.
+ * @param colorSpace
+ */
+ MakeGPUTextureSurface(
+ ctx: WebGPUDeviceContext,
+ texture: GPUTexture,
+ width: number,
+ height: number,
+ colorSpace: ColorSpace
+ ): Surface | null;
+
+ /**
+ * Creates and configures a WebGPU context for the given canvas.
+ * @param ctx
+ * @param canvas
+ * @param opts
+ */
+ MakeGPUCanvasContext(
+ ctx: WebGPUDeviceContext,
+ canvas: HTMLCanvasElement,
+ opts?: WebGPUCanvasOptions
+ ): WebGPUCanvasContext | null;
+
+ /**
+ * Creates a Surface backed by the next available texture in the swapchain associated with the
+ * given WebGPU canvas context. The context must have been already successfully configured using
+ * the same GPUDevice associated with `ctx`.
+ * @param canvasContext - WebGPU context associated with the canvas. The canvas can either be an
+ * on-screen HTMLCanvasElement or an OffscreenCanvas.
+ * @param colorSpace
+ * @param width - width of the visible region. If not present, the canvas width from `canvasContext`
+ * is used.
+ * @param height - height of the visible region. If not present, the canvas width from `canvasContext`
+ * is used.
+ */
+ MakeGPUCanvasSurface(
+ canvasContext: WebGPUCanvasContext,
+ colorSpace: ColorSpace,
+ width?: number,
+ height?: number
+ ): Surface | null;
+
+ /**
+ * Returns a (non-visible) Surface on the GPU. It has the given dimensions and uses 8888
+ * color depth and premultiplied alpha. See Surface.h for more details.
+ * @param ctx
+ * @param width
+ * @param height
+ */
+ MakeRenderTarget(
+ ctx: GrDirectContext,
+ width: number,
+ height: number
+ ): Surface | null;
+
+ /**
+ * Returns a (non-visible) Surface on the GPU. It has the settings provided by image info.
+ * See Surface.h for more details.
+ * @param ctx
+ * @param info
+ */
+ MakeRenderTarget(ctx: GrDirectContext, info: ImageInfo): Surface | null;
+
+ /**
+ * Returns a texture-backed image based on the content in src. It assumes the image is
+ * RGBA_8888, unpremul and SRGB. This image can be re-used across multiple surfaces.
+ *
+ * Not available for software-backed surfaces.
+ * @param src - CanvasKit will take ownership of the TextureSource and clean it up when
+ * the image is destroyed.
+ * @param info - If provided, will be used to determine the width/height/format of the
+ * source image. If not, sensible defaults will be used.
+ * @param srcIsPremul - set to true if the src data has premultiplied alpha. Otherwise, it will
+ * be assumed to be Unpremultiplied. Note: if this is true and info specifies
+ * Unpremul, Skia will not convert the src pixels first.
+ */
+ MakeLazyImageFromTextureSource(
+ src: TextureSource,
+ info?: ImageInfo | PartialImageInfo,
+ srcIsPremul?: boolean
+ ): Image;
+
+ /**
+ * Deletes the associated WebGLContext. Function not available on the CPU version.
+ * @param ctx
+ */
+ deleteContext(ctx: WebGLContextHandle): void;
+
+ /**
+ * Returns the max size of the global cache for bitmaps used by CanvasKit.
+ */
+ getDecodeCacheLimitBytes(): number;
+ /**
+ * Returns the current size of the global cache for bitmaps used by CanvasKit.
+ */
+ getDecodeCacheUsedBytes(): number;
+
+ /**
+ * Sets the max size of the global cache for bitmaps used by CanvasKit.
+ * @param size - number of bytes that can be used to cache bitmaps.
+ */
+ setDecodeCacheLimitBytes(size: number): void;
+
+ /**
+ * Decodes the given bytes into an animated image. Returns null if the bytes were invalid.
+ * The passed in bytes will be copied into the WASM heap, so the caller can dispose of them.
+ *
+ * The returned AnimatedImage will be "pointing to" the first frame, i.e. currentFrameDuration
+ * and makeImageAtCurrentFrame will be referring to the first frame.
+ * @param bytes
+ */
+ MakeAnimatedImageFromEncoded(
+ bytes: Uint8Array | ArrayBuffer
+ ): AnimatedImage | null;
+
+ /**
+ * Returns an emulated Canvas2D of the given size.
+ * @param width
+ * @param height
+ */
+ MakeCanvas(width: number, height: number): EmulatedCanvas2D;
+
+ /**
+ * Returns an image with the given pixel data and format.
+ * Note that we will always make a copy of the pixel data, because of inconsistencies in
+ * behavior between GPU and CPU (i.e. the pixel data will be turned into a GPU texture and
+ * not modifiable after creation).
+ *
+ * @param info
+ * @param bytes - bytes representing the pixel data.
+ * @param bytesPerRow
+ */
+ MakeImage(
+ info: ImageInfo,
+ bytes: number[] | Uint8Array | Uint8ClampedArray,
+ bytesPerRow: number
+ ): Image | null;
+
+ /**
+ * Return an Image backed by the encoded data, but attempt to defer decoding until the image
+ * is actually used/drawn. This deferral allows the system to cache the result, either on the
+ * CPU or on the GPU, depending on where the image is drawn.
+ * This decoding uses the codecs that have been compiled into CanvasKit. If the bytes are
+ * invalid (or an unrecognized codec), null will be returned. See Image.h for more details.
+ * @param bytes
+ */
+ MakeImageFromEncoded(bytes: Uint8Array | ArrayBuffer): Image | null;
+
+ /**
+ * Returns an Image with the data from the provided CanvasImageSource (e.g. ). This will
+ * use the browser's built in codecs, in that src will be drawn to a canvas and then readback
+ * and placed into an Image.
+ * @param src
+ */
+ MakeImageFromCanvasImageSource(src: CanvasImageSource): Image;
+
+ /**
+ * Returns an SkPicture which has been serialized previously to the given bytes.
+ * @param bytes
+ */
+ MakePicture(bytes: Uint8Array | ArrayBuffer): SkPicture | null;
+
+ /**
+ * Returns an Vertices based on the given positions and optional parameters.
+ * See SkVertices.h (especially the Builder) for more details.
+ * @param mode
+ * @param positions
+ * @param textureCoordinates
+ * @param colors - either a list of int colors or a flattened color array.
+ * @param indices
+ * @param isVolatile
+ */
+ MakeVertices(
+ mode: VertexMode,
+ positions: InputFlattenedPointArray,
+ textureCoordinates?: InputFlattenedPointArray | null,
+ colors?: Float32Array | ColorIntArray | null,
+ indices?: number[] | null,
+ isVolatile?: boolean
+ ): Vertices;
+
+ /**
+ * Returns a Skottie animation built from the provided json string.
+ * Requires that Skottie be compiled into CanvasKit.
+ * Note: this animation will not be able to display text or images.
+ * @param json
+ */
+ MakeAnimation(json: string): SkottieAnimation;
+
+ /**
+ * Returns a managed Skottie animation built from the provided json string and assets.
+ * Requires that Skottie be compiled into CanvasKit.
+ * @param json
+ * @param assets - a dictionary of named blobs: { key: ArrayBuffer, ... }
+ * @param filterPrefix - an optional string acting as a name filter for selecting "interesting"
+ * Lottie properties (surfaced in the embedded player controls)
+ * @param soundMap - an optional mapping of sound identifiers (strings) to AudioPlayers.
+ * Only needed if the animation supports sound.
+ */
+ MakeManagedAnimation(
+ json: string,
+ assets?: Record,
+ filterPrefix?: string,
+ soundMap?: SoundMap
+ ): ManagedSkottieAnimation;
+
+ // Constructors, i.e. things made with `new CanvasKit.Foo()`;
+ readonly ImageData: ImageDataConstructor;
+ readonly ParagraphStyle: ParagraphStyleConstructor;
+ readonly ContourMeasureIter: ContourMeasureIterConstructor;
+ readonly Font: FontConstructor;
+ readonly Paint: DefaultConstructor;
+ readonly Path: PathConstructorAndFactory;
+ readonly PictureRecorder: DefaultConstructor;
+ readonly TextStyle: TextStyleConstructor;
+ readonly SlottableTextProperty: SlottableTextPropertyConstructor;
+
+ // Factories, i.e. things made with CanvasKit.Foo.MakeTurboEncabulator()
+ readonly ParagraphBuilder: ParagraphBuilderFactory;
+ readonly Blender: BlenderFactory;
+ readonly ColorFilter: ColorFilterFactory;
+ readonly FontCollection: FontCollectionFactory;
+ readonly FontMgr: FontMgrFactory;
+ readonly ImageFilter: ImageFilterFactory;
+ readonly MaskFilter: MaskFilterFactory;
+ readonly PathEffect: PathEffectFactory;
+ readonly RuntimeEffect: RuntimeEffectFactory;
+ readonly Shader: ShaderFactory;
+ readonly TextBlob: TextBlobFactory;
+ readonly Typeface: TypefaceFactory;
+ readonly TypefaceFontProvider: TypefaceFontProviderFactory;
+
+ // Misc
+ readonly ColorMatrix: ColorMatrixHelpers;
+ readonly Matrix: Matrix3x3Helpers;
+ readonly M44: Matrix4x4Helpers;
+ readonly Vector: VectorHelpers;
+
+ // Core Enums
+ readonly AlphaType: AlphaTypeEnumValues;
+ readonly BlendMode: BlendModeEnumValues;
+ readonly BlurStyle: BlurStyleEnumValues;
+ readonly ClipOp: ClipOpEnumValues;
+ readonly ColorChannel: ColorChannelEnumValues;
+ readonly ColorType: ColorTypeEnumValues;
+ readonly FillType: FillTypeEnumValues;
+ readonly FilterMode: FilterModeEnumValues;
+ readonly FontEdging: FontEdgingEnumValues;
+ readonly FontHinting: FontHintingEnumValues;
+ readonly GlyphRunFlags: GlyphRunFlagValues;
+ readonly ImageFormat: ImageFormatEnumValues;
+ readonly MipmapMode: MipmapModeEnumValues;
+ readonly PaintStyle: PaintStyleEnumValues;
+ readonly Path1DEffect: Path1DEffectStyleEnumValues;
+ readonly PathOp: PathOpEnumValues;
+ readonly PointMode: PointModeEnumValues;
+ readonly ColorSpace: ColorSpaceEnumValues;
+ readonly StrokeCap: StrokeCapEnumValues;
+ readonly StrokeJoin: StrokeJoinEnumValues;
+ readonly TileMode: TileModeEnumValues;
+ readonly VertexMode: VertexModeEnumValues;
+ readonly InputState: InputStateEnumValues;
+ readonly ModifierKey: ModifierKeyEnumValues;
+
+ // Core Constants
+ readonly TRANSPARENT: Color;
+ readonly BLACK: Color;
+ readonly WHITE: Color;
+ readonly RED: Color;
+ readonly GREEN: Color;
+ readonly BLUE: Color;
+ readonly YELLOW: Color;
+ readonly CYAN: Color;
+ readonly MAGENTA: Color;
+
+ readonly MOVE_VERB: number;
+ readonly LINE_VERB: number;
+ readonly QUAD_VERB: number;
+ readonly CONIC_VERB: number;
+ readonly CUBIC_VERB: number;
+ readonly CLOSE_VERB: number;
+
+ readonly SaveLayerInitWithPrevious: SaveLayerFlag;
+ readonly SaveLayerF16ColorType: SaveLayerFlag;
+
+ /**
+ * Use this shadow flag to indicate the occluding object is not opaque. Knowing that the
+ * occluder is opaque allows us to cull shadow geometry behind it and improve performance.
+ */
+ readonly ShadowTransparentOccluder: number;
+ /**
+ * Use this shadow flag to not use analytic shadows.
+ */
+ readonly ShadowGeometricOnly: number;
+ /**
+ * Use this shadow flag to indicate the light position represents a direction and light radius
+ * is blur radius at elevation 1.
+ */
+ readonly ShadowDirectionalLight: number;
+
+ readonly gpu?: boolean; // true if GPU code was compiled in
+ readonly managed_skottie?: boolean; // true if advanced (managed) Skottie code was compiled in
+ readonly rt_effect?: boolean; // true if RuntimeEffect was compiled in
+ readonly skottie?: boolean; // true if base Skottie code was compiled in
+
+ // Paragraph Enums
+ readonly Affinity: AffinityEnumValues;
+ readonly DecorationStyle: DecorationStyleEnumValues;
+ readonly FontSlant: FontSlantEnumValues;
+ readonly FontWeight: FontWeightEnumValues;
+ readonly FontWidth: FontWidthEnumValues;
+ readonly PlaceholderAlignment: PlaceholderAlignmentEnumValues;
+ readonly RectHeightStyle: RectHeightStyleEnumValues;
+ readonly RectWidthStyle: RectWidthStyleEnumValues;
+ readonly TextAlign: TextAlignEnumValues;
+ readonly TextBaseline: TextBaselineEnumValues;
+ readonly TextDirection: TextDirectionEnumValues;
+ readonly TextHeightBehavior: TextHeightBehaviorEnumValues;
+
+ // other enums
+ readonly VerticalTextAlign: VerticalTextAlignEnumValues;
+ readonly ResizePolicy: ResizePolicyEnumValues;
+
+ // Paragraph Constants
+ readonly NoDecoration: number;
+ readonly UnderlineDecoration: number;
+ readonly OverlineDecoration: number;
+ readonly LineThroughDecoration: number;
+
+ // Unicode enums
+ readonly CodeUnitFlags: CodeUnitFlagsEnumValues;
+}
+
+export interface Camera {
+ /** a 3d point locating the camera. */
+ eye: Vector3;
+ /** center of attention - the 3d point the camera is looking at. */
+ coa: Vector3;
+ /**
+ * A unit vector pointing the cameras up direction. Note that using only eye and coa
+ * would leave the roll of the camera unspecified.
+ */
+ up: Vector3;
+ /** near clipping plane distance */
+ near: number;
+ /** far clipping plane distance */
+ far: number;
+ /** field of view in radians */
+ angle: AngleInRadians;
+}
+
+/**
+ * CanvasKit is built with Emscripten and Embind. Embind adds the following methods to all objects
+ * that are exposed with it.
+ * This _type field is necessary for the TypeScript compiler to differentiate
+ * between opaque types such as Shader and ColorFilter. It doesn't exist at runtime.
+ */
+export interface EmbindObject {
+ _type: T;
+ delete(): void;
+ deleteLater(): void;
+ isAliasOf(other: any): boolean;
+ isDeleted(): boolean;
+}
+
+/**
+ * Represents the set of enum values.
+ */
+export interface EmbindEnum {
+ readonly values: number[];
+}
+
+/**
+ * Represents a single member of an enum.
+ */
+export interface EmbindEnumEntity {
+ readonly value: number;
+}
+
+export interface EmulatedCanvas2D {
+ /**
+ * Cleans up all resources associated with this emulated canvas.
+ */
+ dispose(): void;
+ /**
+ * Decodes an image with the given bytes.
+ * @param bytes
+ */
+ decodeImage(bytes: ArrayBuffer | Uint8Array): Image;
+
+ /**
+ * Returns an emulated canvas2d context if type == '2d', null otherwise.
+ * @param type
+ */
+ getContext(type: string): EmulatedCanvas2DContext | null;
+
+ /**
+ * Loads the given font with the given descriptors. Emulates new FontFace().
+ * @param bytes
+ * @param descriptors
+ */
+ loadFont(
+ bytes: ArrayBuffer | Uint8Array,
+ descriptors: Record
+ ): void;
+
+ /**
+ * Returns an new emulated Path2D object.
+ * @param str - an SVG string representing a path.
+ */
+ makePath2D(str?: string): EmulatedPath2D;
+
+ /**
+ * Returns the current canvas as a base64 encoded image string.
+ * @param codec - image/png by default; image/jpeg also supported.
+ * @param quality
+ */
+ toDataURL(codec?: string, quality?: number): string;
+}
+
+/** Part of the Canvas2D emulation code */
+export type EmulatedCanvas2DContext = CanvasRenderingContext2D;
+export type EmulatedImageData = ImageData;
+export type EmulatedPath2D = Path2D;
+
+export interface FontStyle {
+ weight?: FontWeight;
+ width?: FontWidth;
+ slant?: FontSlant;
+}
+
+/**
+ * See GrDirectContext.h for more on this class.
+ */
+export interface GrDirectContext extends EmbindObject<"GrDirectContext"> {
+ getResourceCacheLimitBytes(): number;
+ getResourceCacheUsageBytes(): number;
+ releaseResourcesAndAbandonContext(): void;
+ setResourceCacheLimitBytes(bytes: number): void;
+}
+
+/**
+ * Represents the context backed by a WebGPU device instance.
+ */
+export type WebGPUDeviceContext = GrDirectContext;
+
+/**
+ * Represents the canvas context and swapchain backed by a WebGPU device.
+ */
+export interface WebGPUCanvasContext {
+ /**
+ * A convenient way to draw multiple frames over the swapchain texture sequence associated with
+ * a canvas element. Each call internally constructs a new Surface that targets the current
+ * GPUTexture in swapchain.
+ *
+ * This requires an environment where a global function called requestAnimationFrame is
+ * available (e.g. on the web, not on Node). The internally created surface is flushed and
+ * destroyed automatically by this wrapper once the `drawFrame` callback returns.
+ *
+ * Users can call canvasContext.requestAnimationFrame in the callback function to
+ * draw multiple frames, e.g. of an animation.
+ */
+ requestAnimationFrame(drawFrame: (_: Canvas) => void): void;
+}
+
+/**
+ * The glyph and grapheme cluster information associated with a code point within
+ * a paragraph.
+ */
+export interface GlyphInfo {
+ /**
+ * The layout bounds of the grapheme cluster the code point belongs to, in
+ * the paragraph's coordinates.
+ *
+ * This width of the rect is horizontal advance of the grapheme cluster,
+ * the height of the rect is the line height when the grapheme cluster
+ * occupies a full line.
+ */
+ graphemeLayoutBounds: Rect;
+ /**
+ * The left-closed-right-open UTF-16 range of the grapheme cluster the code
+ * point belongs to.
+ */
+ graphemeClusterTextRange: URange;
+ /** The writing direction of the grapheme cluster. */
+ dir: TextDirection;
+ /**
+ * Whether the associated glyph points to an ellipsis added by the text
+ * layout library.
+ *
+ * The text layout library truncates the lines that exceed the specified
+ * max line number, and may add an ellipsis to replace the last few code
+ * points near the logical end of the last visible line. If True, this object
+ * marks the logical end of the list of GlyphInfo objects that are
+ * retrievable from the text layout library.
+ */
+ isEllipsis: boolean;
+}
+
+/**
+ * See Metrics.h for more on this struct.
+ */
+export interface LineMetrics {
+ /** The index in the text buffer the line begins. */
+ startIndex: number;
+ /** The index in the text buffer the line ends. */
+ endIndex: number;
+ endExcludingWhitespaces: number;
+ endIncludingNewline: number;
+ /** True if the line ends in a hard break (e.g. newline) */
+ isHardBreak: boolean;
+ /**
+ * The final computed ascent for the line. This can be impacted by
+ * the strut, height, scaling, as well as outlying runs that are very tall.
+ */
+ ascent: number;
+ /**
+ * The final computed descent for the line. This can be impacted by
+ * the strut, height, scaling, as well as outlying runs that are very tall.
+ */
+ descent: number;
+ /** round(ascent + descent) */
+ height: number;
+ /** width of the line */
+ width: number;
+ /** The left edge of the line. The right edge can be obtained with `left + width` */
+ left: number;
+ /** The y position of the baseline for this line from the top of the paragraph. */
+ baseline: number;
+ /** Zero indexed line number. */
+ lineNumber: number;
+}
+
+export interface Range {
+ first: number;
+ last: number;
+}
+
+/**
+ * Information for a run of shaped text. See Paragraph.getShapedLines()
+ *
+ * Notes:
+ * positions is documented as Float32, but it holds twice as many as you expect, and they
+ * are treated logically as pairs of floats: {x0, y0}, {x1, y1}, ... for each glyph.
+ *
+ * positions and offsets arrays have 1 extra slot (actually 2 for positions)
+ * to describe the location "after" the last glyph in the glyphs array.
+ */
+export interface GlyphRun {
+ typeface: Typeface; // currently set to null (temporary)
+ size: number;
+ fakeBold: boolean;
+ fakeItalic: boolean;
+
+ glyphs: Uint16Array;
+ positions: Float32Array; // alternating x0, y0, x1, y1, ...
+ offsets: Uint32Array;
+ flags: number; // see GlyphRunFlags
+}
+
+/**
+ * Information for a paragraph of text. See Paragraph.getShapedLines()
+ */
+export interface ShapedLine {
+ textRange: Range; // first and last character offsets for the line (derived from runs[])
+ top: number; // top y-coordinate for the line
+ bottom: number; // bottom y-coordinate for the line
+ baseline: number; // baseline y-coordinate for the line
+ runs: GlyphRun[]; // array of GlyphRun objects for the line
+}
+
+/**
+ * Input to ShapeText(..., FontBlock[], ...);
+ */
+export interface FontBlock {
+ length: number; // number of text codepoints this block is applied to
+
+ typeface: Typeface;
+ size: number;
+ fakeBold: boolean;
+ fakeItalic: boolean;
+}
+
+/**
+ * This object is a wrapper around a pointer to some memory on the WASM heap. The type of the
+ * pointer was determined at creation time.
+ */
+export interface MallocObj {
+ /**
+ * The number of objects this pointer refers to.
+ */
+ readonly length: number;
+ /**
+ * The "pointer" into the WASM memory. Should be fixed over the lifetime of the object.
+ */
+ readonly byteOffset: number;
+ /**
+ * Return a read/write view into a subset of the memory. Do not cache the TypedArray this
+ * returns, it may be invalidated if the WASM heap is resized. This is the same as calling
+ * .toTypedArray().subarray() except the returned TypedArray can also be passed into an API
+ * and not cause an additional copy.
+ */
+ subarray(start: number, end: number): TypedArray;
+ /**
+ * Return a read/write view of the memory. Do not cache the TypedArray this returns, it may be
+ * invalidated if the WASM heap is resized. If this TypedArray is passed into a CanvasKit API,
+ * it will not be copied again, only the pointer will be re-used.
+ */
+ toTypedArray(): TypedArray;
+}
+
+/**
+ * This represents a subset of an animation's duration.
+ */
+export interface AnimationMarker {
+ name: string;
+ t0: number; // 0.0 to 1.0
+ t1: number; // 0.0 to 1.0
+}
+
+/**
+ * This object maintains a single audio layer during skottie playback
+ */
+export interface AudioPlayer {
+ /**
+ * Playback control callback, emitted for each corresponding Animation::seek().
+ *
+ * Will seek to time t (seconds) relative to the layer's timeline origin.
+ * Negative t values are used to signal off state (stop playback outside layer span).
+ */
+ seek(t: number): void;
+}
+
+/**
+ * Mapping of sound names (strings) to AudioPlayers
+ */
+export interface SoundMap {
+ /**
+ * Returns AudioPlayer for a certain audio layer
+ * @param key string identifier, name of audio file the desired AudioPlayer manages
+ */
+ getPlayer(key: string): AudioPlayer;
+}
+
+/**
+ * Named color property.
+ */
+export interface ColorProperty {
+ /**
+ * Property identifier, usually the node name.
+ */
+ key: string;
+ /**
+ * Property value (RGBA, 255-based).
+ */
+ value: ColorInt;
+}
+
+/**
+ * Named opacity property.
+ */
+export interface OpacityProperty {
+ /**
+ * Property identifier, usually the node name.
+ */
+ key: string;
+ /**
+ * Property value (0..100).
+ */
+ value: number;
+}
+
+/**
+ * Text property value.
+ */
+export interface TextValue {
+ /**
+ * The text string payload.
+ */
+ text: string;
+ /**
+ * Font size.
+ */
+ size: number;
+}
+
+/**
+ * Named text property.
+ */
+export interface TextProperty {
+ /**
+ * Property identifier, usually the node name.
+ */
+ key: string;
+ /**
+ * Property value.
+ */
+ value: TextValue;
+}
+
+/**
+ * Transform property value. Maps to AE styled transform.
+ */
+export interface TransformValue {
+ /**
+ * Anchor point for transform. x and y value.
+ */
+ anchor: Point;
+ /**
+ * Position of transform. x and y value.
+ */
+ position: Point;
+ /**
+ * Scale of transform. x and y value.
+ */
+ scale: Vector2;
+ /**
+ * Rotation of transform in degrees.
+ */
+ rotation: number;
+ /**
+ * Skew to apply during transform.
+ */
+ skew: number;
+ /**
+ * Direction of skew in degrees.
+ */
+ skew_axis: number;
+}
+
+/**
+ * Named transform property for Skottie property observer.
+ */
+export interface TransformProperty {
+ /**
+ * Property identifier, usually the node name.
+ */
+ key: string;
+ /**
+ * Property value.
+ */
+ value: TransformValue;
+}
+
+/**
+ * Collection of slot IDs sorted by value type
+ */
+export interface SlotInfo {
+ colorSlotIDs: string[];
+ scalarSlotIDs: string[];
+ vec2SlotIDs: string[];
+ imageSlotIDs: string[];
+ textSlotIDs: string[];
+}
+
+/**
+ * Text property for ManagedAnimation's slot support
+ */
+export interface SlottableTextProperty {
+ typeface?: Typeface;
+ text?: string;
+
+ textSize?: number;
+ minTextSize?: number;
+ maxTextSize?: number;
+ strokeWidth?: number;
+ lineHeight?: number;
+ lineShift?: number;
+ ascent?: number;
+ maxLines?: number;
+
+ horizAlign?: TextAlignEnumValues;
+ vertAlign?: VerticalTextAlignEnumValues;
+ strokeJoin?: StrokeJoinEnumValues;
+ direction?: TextDirectionEnumValues;
+ linebreak?: LineBreakTypeEnumValues;
+ resize?: ResizePolicyEnumValues;
+
+ boundingBox?: InputRect;
+ fillColor?: InputColor;
+ strokeColor?: InputColor;
+}
+
+export interface ManagedSkottieAnimation extends SkottieAnimation {
+ setColor(key: string, color: InputColor): boolean;
+ setOpacity(key: string, opacity: number): boolean;
+ setText(key: string, text: string, size: number): boolean;
+ setTransform(
+ key: string,
+ anchor: InputPoint,
+ position: InputPoint,
+ scale: InputVector2,
+ rotation: number,
+ skew: number,
+ skew_axis: number
+ ): boolean;
+ getMarkers(): AnimationMarker[];
+ getColorProps(): ColorProperty[];
+ getOpacityProps(): OpacityProperty[];
+ getTextProps(): TextProperty[];
+ getTransformProps(): TransformProperty[];
+
+ // Slots in Lottie were exposed with bodymovin version 5.11.0
+ // Properties tracked under the Essential Graphics window in AE will be "slotted". These slots
+ // can be observed and editted live like with the other get/set tools. The slot id passed in
+ // must match the name of the property in the Essential Graphics window. Property Groups support
+ // one-to-many relationships.
+ getSlotInfo(): SlotInfo;
+
+ setColorSlot(key: string, color: InputColor): boolean;
+ setScalarSlot(key: string, scalar: number): boolean;
+ setVec2Slot(key: string, vec2: InputVector2): boolean;
+ setTextSlot(key: string, text: SlottableTextProperty): boolean;
+ setImageSlot(key: string, assetName: string): boolean;
+
+ getColorSlot(key: string): Color | null;
+ getScalarSlot(key: string): number | null;
+ getVec2Slot(key: string): Vector2 | null;
+ getTextSlot(key: string): SlottableTextProperty | null;
+
+ // Attach a WYSIWYG editor to the text layer identified by 'id' and 'index' (multiple layers
+ // can be grouped with the same ID).
+ // Other layers with the same ID are attached as dependents, and updated on the fly as the
+ // edited layer changes.
+ attachEditor(id: string, index: number): boolean;
+
+ // Enable/disable the current editor.
+ enableEditor(enable: boolean): void;
+
+ // Send key events to the active editor.
+ dispatchEditorKey(key: string): boolean;
+
+ // Send pointer events to the active editor, in canvas coordinates.
+ dispatchEditorPointer(
+ x: number,
+ y: number,
+ state: InputState,
+ modifier: ModifierKey
+ ): boolean;
+
+ // Adjust the relative cursor weight (default: 1).
+ setEditorCursorWeight(w: number): void;
+}
+
+/**
+ * See Paragraph.h for more information on this class. This is only available if Paragraph has
+ * been compiled in.
+ */
+export interface Paragraph extends EmbindObject<"Paragraph"> {
+ didExceedMaxLines(): boolean;
+ getAlphabeticBaseline(): number;
+
+ /**
+ * Returns the index of the glyph that corresponds to the provided coordinate,
+ * with the top left corner as the origin, and +y direction as down.
+ */
+ getGlyphPositionAtCoordinate(dx: number, dy: number): PositionWithAffinity;
+ /**
+ * Returns the information associated with the closest glyph at the specified
+ * paragraph coordinate, or null if the paragraph is empty.
+ */
+ getClosestGlyphInfoAtCoordinate(dx: number, dy: number): GlyphInfo | null;
+ /**
+ * Returns the information associated with the glyph at the specified UTF-16
+ * offset within the paragraph's visible lines, or null if the index is out
+ * of bounds, or points to a codepoint that is logically after the last
+ * visible codepoint.
+ */
+ getGlyphInfoAt(index: number): GlyphInfo | null;
+
+ getHeight(): number;
+ getIdeographicBaseline(): number;
+ /**
+ * Returns the line number of the line that contains the specified UTF-16
+ * offset within the paragraph, or -1 if the index is out of bounds, or
+ * points to a codepoint that is logically after the last visible codepoint.
+ */
+ getLineNumberAt(index: number): number;
+ getLineMetrics(): LineMetrics[];
+ /**
+ * Returns the LineMetrics of the line at the specified line number, or null
+ * if the line number is out of bounds, or is larger than or equal to the
+ * specified max line number.
+ */
+ getLineMetricsAt(lineNumber: number): LineMetrics | null;
+ getLongestLine(): number;
+ getMaxIntrinsicWidth(): number;
+ getMaxWidth(): number;
+ getMinIntrinsicWidth(): number;
+ /**
+ * Returns the total number of visible lines in the paragraph.
+ */
+ getNumberOfLines(): number;
+ getRectsForPlaceholders(): RectWithDirection[];
+
+ /**
+ * Returns bounding boxes that enclose all text in the range of glpyh indexes [start, end).
+ * @param start
+ * @param end
+ * @param hStyle
+ * @param wStyle
+ */
+ getRectsForRange(
+ start: number,
+ end: number,
+ hStyle: RectHeightStyle,
+ wStyle: RectWidthStyle
+ ): RectWithDirection[];
+
+ /**
+ * Finds the first and last glyphs that define a word containing the glyph at index offset.
+ * @param offset
+ */
+ getWordBoundary(offset: number): URange;
+
+ /**
+ * Returns an array of ShapedLine objects, describing the paragraph.
+ */
+ getShapedLines(): ShapedLine[];
+
+ /**
+ * Lays out the text in the paragraph so it is wrapped to the given width.
+ * @param width
+ */
+ layout(width: number): void;
+
+ /**
+ * When called after shaping, returns the glyph IDs which were not matched
+ * by any of the provided fonts.
+ */
+ unresolvedCodepoints(): number[];
+}
+
+export interface ParagraphBuilder extends EmbindObject<"ParagraphBuilder"> {
+ /**
+ * Pushes the information required to leave an open space.
+ * @param width
+ * @param height
+ * @param alignment
+ * @param baseline
+ * @param offset
+ */
+ addPlaceholder(
+ width?: number,
+ height?: number,
+ alignment?: PlaceholderAlignment,
+ baseline?: TextBaseline,
+ offset?: number
+ ): void;
+
+ /**
+ * Adds text to the builder. Forms the proper runs to use the upper-most style
+ * on the style_stack.
+ * @param str
+ */
+ addText(str: string): void;
+
+ /**
+ * Returns a Paragraph object that can be used to be layout and paint the text to an
+ * Canvas.
+ */
+ build(): Paragraph;
+
+ /**
+ * @param words is an array of word edges (starting or ending). You can
+ * pass 2 elements (0 as a start of the entire text and text.size as the
+ * end). This information is only needed for a specific API method getWords.
+ *
+ * The indices are expected to be relative to the UTF-8 representation of
+ * the text.
+ */
+ setWordsUtf8(words: InputWords): void;
+ /**
+ * @param words is an array of word edges (starting or ending). You can
+ * pass 2 elements (0 as a start of the entire text and text.size as the
+ * end). This information is only needed for a specific API method getWords.
+ *
+ * The indices are expected to be relative to the UTF-16 representation of
+ * the text.
+ *
+ * The `Intl.Segmenter` API can be used as a source for this data.
+ */
+ setWordsUtf16(words: InputWords): void;
+
+ /**
+ * @param graphemes is an array of indexes in the input text that point
+ * to the start of each grapheme.
+ *
+ * The indices are expected to be relative to the UTF-8 representation of
+ * the text.
+ */
+ setGraphemeBreaksUtf8(graphemes: InputGraphemes): void;
+ /**
+ * @param graphemes is an array of indexes in the input text that point
+ * to the start of each grapheme.
+ *
+ * The indices are expected to be relative to the UTF-16 representation of
+ * the text.
+ *
+ * The `Intl.Segmenter` API can be used as a source for this data.
+ */
+ setGraphemeBreaksUtf16(graphemes: InputGraphemes): void;
+
+ /**
+ * @param lineBreaks is an array of unsigned integers that should be
+ * treated as pairs (index, break type) that point to the places of possible
+ * line breaking if needed. It should include 0 as the first element.
+ * Break type == 0 means soft break, break type == 1 is a hard break.
+ *
+ * The indices are expected to be relative to the UTF-8 representation of
+ * the text.
+ */
+ setLineBreaksUtf8(lineBreaks: InputLineBreaks): void;
+ /**
+ * @param lineBreaks is an array of unsigned integers that should be
+ * treated as pairs (index, break type) that point to the places of possible
+ * line breaking if needed. It should include 0 as the first element.
+ * Break type == 0 means soft break, break type == 1 is a hard break.
+ *
+ * The indices are expected to be relative to the UTF-16 representation of
+ * the text.
+ *
+ * Chrome's `v8BreakIterator` API can be used as a source for this data.
+ */
+ setLineBreaksUtf16(lineBreaks: InputLineBreaks): void;
+
+ /**
+ * Returns the entire Paragraph text (which is useful in case that text
+ * was produced as a set of addText calls).
+ */
+ getText(): string;
+
+ /**
+ * Remove a style from the stack. Useful to apply different styles to chunks
+ * of text such as bolding.
+ */
+ pop(): void;
+
+ /**
+ * Push a style to the stack. The corresponding text added with addText will
+ * use the top-most style.
+ * @param text
+ */
+ pushStyle(text: TextStyle): void;
+
+ /**
+ * Pushes a TextStyle using paints instead of colors for foreground and background.
+ * @param textStyle
+ * @param fg
+ * @param bg
+ */
+ pushPaintStyle(textStyle: TextStyle, fg: Paint, bg: Paint): void;
+
+ /**
+ * Resets this builder to its initial state, discarding any text, styles, placeholders that have
+ * been added, but keeping the initial ParagraphStyle.
+ */
+ reset(): void;
+}
+
+export interface ParagraphStyle {
+ disableHinting?: boolean;
+ ellipsis?: string;
+ heightMultiplier?: number;
+ maxLines?: number;
+ replaceTabCharacters?: boolean;
+ strutStyle?: StrutStyle;
+ textAlign?: TextAlign;
+ textDirection?: TextDirection;
+ textHeightBehavior?: TextHeightBehavior;
+ textStyle?: TextStyle;
+ applyRoundingHack?: boolean;
+}
+
+export interface PositionWithAffinity {
+ pos: number;
+ affinity: Affinity;
+}
+
+export interface SkSLUniform {
+ columns: number;
+ rows: number;
+ /** The index into the uniforms array that this uniform begins. */
+ slot: number;
+ isInteger: boolean;
+}
+
+/**
+ * See SkAnimatedImage.h for more information on this class.
+ */
+export interface AnimatedImage extends EmbindObject<"AnimatedImage"> {
+ /**
+ * Returns the length of the current frame in ms.
+ */
+ currentFrameDuration(): number;
+ /**
+ * Decodes the next frame. Returns the length of that new frame in ms.
+ * Returns -1 when the animation is on the last frame.
+ */
+ decodeNextFrame(): number;
+
+ /**
+ * Return the total number of frames in the animation.
+ */
+ getFrameCount(): number;
+
+ /**
+ * Return the repetition count for this animation.
+ */
+ getRepetitionCount(): number;
+
+ /**
+ * Returns the possibly scaled height of the image.
+ */
+ height(): number;
+
+ /**
+ * Returns a still image of the current frame or null if there is no current frame.
+ */
+ makeImageAtCurrentFrame(): Image | null;
+
+ /**
+ * Reset the animation to the beginning.
+ */
+ reset(): void;
+
+ /**
+ * Returns the possibly scaled width of the image.
+ */
+ width(): number;
+}
+
+/**
+ * See SkBlender.h for more on this class. The objects are opaque.
+ */
+export type Blender = EmbindObject<"Blender">;
+
+/**
+ * See SkCanvas.h for more information on this class.
+ */
+export interface Canvas extends EmbindObject<"Canvas"> {
+ /**
+ * Fills the current clip with the given color using Src BlendMode.
+ * This has the effect of replacing all pixels contained by clip with color.
+ * @param color
+ */
+ clear(color: InputColor): void;
+
+ /**
+ * Replaces clip with the intersection or difference of the current clip and path,
+ * with an aliased or anti-aliased clip edge.
+ * @param path
+ * @param op
+ * @param doAntiAlias
+ */
+ clipPath(path: Path, op: ClipOp, doAntiAlias: boolean): void;
+
+ /**
+ * Replaces clip with the intersection or difference of the current clip and rect,
+ * with an aliased or anti-aliased clip edge.
+ * @param rect
+ * @param op
+ * @param doAntiAlias
+ */
+ clipRect(rect: InputRect, op: ClipOp, doAntiAlias: boolean): void;
+
+ /**
+ * Replaces clip with the intersection or difference of the current clip and rrect,
+ * with an aliased or anti-aliased clip edge.
+ * @param rrect
+ * @param op
+ * @param doAntiAlias
+ */
+ clipRRect(rrect: InputRRect, op: ClipOp, doAntiAlias: boolean): void;
+
+ /**
+ * Replaces current matrix with m premultiplied with the existing matrix.
+ * @param m
+ */
+ concat(m: InputMatrix): void;
+
+ /**
+ * Draws arc using clip, Matrix, and Paint paint.
+ *
+ * Arc is part of oval bounded by oval, sweeping from startAngle to startAngle plus
+ * sweepAngle. startAngle and sweepAngle are in degrees.
+ * @param oval - bounds of oval containing arc to draw
+ * @param startAngle - angle in degrees where arc begins
+ * @param sweepAngle - sweep angle in degrees; positive is clockwise
+ * @param useCenter - if true, include the center of the oval
+ * @param paint
+ */
+ drawArc(
+ oval: InputRect,
+ startAngle: AngleInDegrees,
+ sweepAngle: AngleInDegrees,
+ useCenter: boolean,
+ paint: Paint
+ ): void;
+
+ /**
+ * Draws a set of sprites from atlas, using clip, Matrix, and optional Paint paint.
+ * @param atlas - Image containing sprites
+ * @param srcRects - Rect locations of sprites in atlas
+ * @param dstXforms - RSXform mappings for sprites in atlas
+ * @param paint
+ * @param blendMode - BlendMode combining colors and sprites
+ * @param colors - If provided, will be blended with sprite using blendMode.
+ * @param sampling - Specifies sampling options. If null, bilinear is used.
+ */
+ drawAtlas(
+ atlas: Image,
+ srcRects: InputFlattenedRectangleArray,
+ dstXforms: InputFlattenedRSXFormArray,
+ paint: Paint,
+ blendMode?: BlendMode | null,
+ colors?: ColorIntArray | null,
+ sampling?: CubicResampler | FilterOptions
+ ): void;
+
+ /**
+ * Draws a circle at (cx, cy) with the given radius.
+ * @param cx
+ * @param cy
+ * @param radius
+ * @param paint
+ */
+ drawCircle(cx: number, cy: number, radius: number, paint: Paint): void;
+
+ /**
+ * Fills clip with the given color.
+ * @param color
+ * @param blendMode - defaults to SrcOver.
+ */
+ drawColor(color: InputColor, blendMode?: BlendMode): void;
+
+ /**
+ * Fills clip with the given color.
+ * @param r - red value (typically from 0 to 1.0).
+ * @param g - green value (typically from 0 to 1.0).
+ * @param b - blue value (typically from 0 to 1.0).
+ * @param a - alpha value, range 0 to 1.0 (1.0 is opaque).
+ * @param blendMode - defaults to SrcOver.
+ */
+ drawColorComponents(
+ r: number,
+ g: number,
+ b: number,
+ a: number,
+ blendMode?: BlendMode
+ ): void;
+
+ /**
+ * Fills clip with the given color.
+ * @param color
+ * @param blendMode - defaults to SrcOver.
+ */
+ drawColorInt(color: ColorInt, blendMode?: BlendMode): void;
+
+ /**
+ * Draws RRect outer and inner using clip, Matrix, and Paint paint.
+ * outer must contain inner or the drawing is undefined.
+ * @param outer
+ * @param inner
+ * @param paint
+ */
+ drawDRRect(outer: InputRRect, inner: InputRRect, paint: Paint): void;
+
+ /**
+ * Draws a run of glyphs, at corresponding positions, in a given font.
+ * @param glyphs the array of glyph IDs (Uint16TypedArray)
+ * @param positions the array of x,y floats to position each glyph
+ * @param x x-coordinate of the origin of the entire run
+ * @param y y-coordinate of the origin of the entire run
+ * @param font the font that contains the glyphs
+ * @param paint
+ */
+ drawGlyphs(
+ glyphs: InputGlyphIDArray,
+ positions: InputFlattenedPointArray,
+ x: number,
+ y: number,
+ font: Font,
+ paint: Paint
+ ): void;
+
+ /**
+ * Draws the given image with its top-left corner at (left, top) using the current clip,
+ * the current matrix, and optionally-provided paint.
+ * @param img
+ * @param left
+ * @param top
+ * @param paint
+ */
+ drawImage(img: Image, left: number, top: number, paint?: Paint | null): void;
+
+ /**
+ * Draws the given image with its top-left corner at (left, top) using the current clip,
+ * the current matrix. It will use the cubic sampling options B and C if necessary.
+ * @param img
+ * @param left
+ * @param top
+ * @param B - See CubicResampler in SkSamplingOptions.h for more information
+ * @param C - See CubicResampler in SkSamplingOptions.h for more information
+ * @param paint
+ */
+ drawImageCubic(
+ img: Image,
+ left: number,
+ top: number,
+ B: number,
+ C: number,
+ paint?: Paint | null
+ ): void;
+
+ /**
+ * Draws the given image with its top-left corner at (left, top) using the current clip,
+ * the current matrix. It will use the provided sampling options if necessary.
+ * @param img
+ * @param left
+ * @param top
+ * @param fm - The filter mode.
+ * @param mm - The mipmap mode. Note: for settings other than None, the image must have mipmaps
+ * calculated with makeCopyWithDefaultMipmaps;
+ * @param paint
+ */
+ drawImageOptions(
+ img: Image,
+ left: number,
+ top: number,
+ fm: FilterMode,
+ mm: MipmapMode,
+ paint?: Paint | null
+ ): void;
+
+ /**
+ * Draws the provided image stretched proportionally to fit into dst rectangle.
+ * The center rectangle divides the image into nine sections: four sides, four corners, and
+ * the center.
+ * @param img
+ * @param center
+ * @param dest
+ * @param filter - what technique to use when sampling the image
+ * @param paint
+ */
+ drawImageNine(
+ img: Image,
+ center: InputIRect,
+ dest: InputRect,
+ filter: FilterMode,
+ paint?: Paint | null
+ ): void;
+
+ /**
+ * Draws sub-rectangle src from provided image, scaled and translated to fill dst rectangle.
+ * @param img
+ * @param src
+ * @param dest
+ * @param paint
+ * @param fastSample - if false, will filter strictly within src.
+ */
+ drawImageRect(
+ img: Image,
+ src: InputRect,
+ dest: InputRect,
+ paint: Paint,
+ fastSample?: boolean
+ ): void;
+
+ /**
+ * Draws sub-rectangle src from provided image, scaled and translated to fill dst rectangle.
+ * It will use the cubic sampling options B and C if necessary.
+ * @param img
+ * @param src
+ * @param dest
+ * @param B - See CubicResampler in SkSamplingOptions.h for more information
+ * @param C - See CubicResampler in SkSamplingOptions.h for more information
+ * @param paint
+ */
+ drawImageRectCubic(
+ img: Image,
+ src: InputRect,
+ dest: InputRect,
+ B: number,
+ C: number,
+ paint?: Paint | null
+ ): void;
+
+ /**
+ * Draws sub-rectangle src from provided image, scaled and translated to fill dst rectangle.
+ * It will use the provided sampling options if necessary.
+ * @param img
+ * @param src
+ * @param dest
+ * @param fm - The filter mode.
+ * @param mm - The mipmap mode. Note: for settings other than None, the image must have mipmaps
+ * calculated with makeCopyWithDefaultMipmaps;
+ * @param paint
+ */
+ drawImageRectOptions(
+ img: Image,
+ src: InputRect,
+ dest: InputRect,
+ fm: FilterMode,
+ mm: MipmapMode,
+ paint?: Paint | null
+ ): void;
+
+ /**
+ * Draws line segment from (x0, y0) to (x1, y1) using the current clip, current matrix,
+ * and the provided paint.
+ * @param x0
+ * @param y0
+ * @param x1
+ * @param y1
+ * @param paint
+ */
+ drawLine(x0: number, y0: number, x1: number, y1: number, paint: Paint): void;
+
+ /**
+ * Draws an oval bounded by the given rectangle using the current clip, current matrix,
+ * and the provided paint.
+ * @param oval
+ * @param paint
+ */
+ drawOval(oval: InputRect, paint: Paint): void;
+
+ /**
+ * Fills clip with the given paint.
+ * @param paint
+ */
+ drawPaint(paint: Paint): void;
+
+ /**
+ * Draws the given Paragraph at the provided coordinates.
+ * Requires the Paragraph code to be compiled in.
+ * @param p
+ * @param x
+ * @param y
+ */
+ drawParagraph(p: Paragraph, x: number, y: number): void;
+
+ /**
+ * Draws the given path using the current clip, current matrix, and the provided paint.
+ * @param path
+ * @param paint
+ */
+ drawPath(path: Path, paint: Paint): void;
+
+ /**
+ * Draws a cubic patch defined by 12 control points [top, right, bottom, left] with optional
+ * colors and shader-coordinates [4] specifed for each corner [top-left, top-right, bottom-right, bottom-left]
+ * @param cubics 12 points : 4 connected cubics specifying the boundary of the patch
+ * @param colors optional colors interpolated across the patch
+ * @param texs optional shader coordinates interpolated across the patch
+ * @param mode Specifies how shader and colors blend (if both are specified)
+ * @param paint
+ */
+ drawPatch(
+ cubics: InputFlattenedPointArray,
+ colors?: ColorIntArray | Color[] | null,
+ texs?: InputFlattenedPointArray | null,
+ mode?: BlendMode | null,
+ paint?: Paint
+ ): void;
+
+ /**
+ * Draws the given picture using the current clip, current matrix, and the provided paint.
+ * @param skp
+ */
+ drawPicture(skp: SkPicture): void;
+
+ /**
+ * Draws the given points using the current clip, current matrix, and the provided paint.
+ *
+ * See Canvas.h for more on the mode and its interaction with paint.
+ * @param mode
+ * @param points
+ * @param paint
+ */
+ drawPoints(
+ mode: PointMode,
+ points: InputFlattenedPointArray,
+ paint: Paint
+ ): void;
+
+ /**
+ * Draws the given rectangle using the current clip, current matrix, and the provided paint.
+ * @param rect
+ * @param paint
+ */
+ drawRect(rect: InputRect, paint: Paint): void;
+
+ /**
+ * Draws the given rectangle using the current clip, current matrix, and the provided paint.
+ * @param left
+ * @param top
+ * @param right
+ * @param bottom
+ * @param paint
+ */
+ drawRect4f(
+ left: number,
+ top: number,
+ right: number,
+ bottom: number,
+ paint: Paint
+ ): void;
+
+ /**
+ * Draws the given rectangle with rounded corners using the current clip, current matrix,
+ * and the provided paint.
+ * @param rrect
+ * @param paint
+ */
+ drawRRect(rrect: InputRRect, paint: Paint): void;
+
+ /**
+ * Draw an offset spot shadow and outlining ambient shadow for the given path using a disc
+ * light. See SkShadowUtils.h for more details
+ * @param path - The occluder used to generate the shadows.
+ * @param zPlaneParams - Values for the plane function which returns the Z offset of the
+ * occluder from the canvas based on local x and y values (the current
+ * matrix is not applied).
+ * @param lightPos - The 3D position of the light relative to the canvas plane. This is
+ * independent of the canvas's current matrix.
+ * @param lightRadius - The radius of the disc light.
+ * @param ambientColor - The color of the ambient shadow.
+ * @param spotColor - The color of the spot shadow.
+ * @param flags - See SkShadowUtils.h; 0 means use default options.
+ */
+ drawShadow(
+ path: Path,
+ zPlaneParams: InputVector3,
+ lightPos: InputVector3,
+ lightRadius: number,
+ ambientColor: InputColor,
+ spotColor: InputColor,
+ flags: number
+ ): void;
+
+ /**
+ * Draw the given text at the location (x, y) using the provided paint and font. The text will
+ * be drawn as is; no shaping, left-to-right, etc.
+ * @param str
+ * @param x
+ * @param y
+ * @param paint
+ * @param font
+ */
+ drawText(str: string, x: number, y: number, paint: Paint, font: Font): void;
+
+ /**
+ * Draws the given TextBlob at (x, y) using the current clip, current matrix, and the
+ * provided paint. Reminder that the fonts used to draw TextBlob are part of the blob.
+ * @param blob
+ * @param x
+ * @param y
+ * @param paint
+ */
+ drawTextBlob(blob: TextBlob, x: number, y: number, paint: Paint): void;
+
+ /**
+ * Draws the given vertices (a triangle mesh) using the current clip, current matrix, and the
+ * provided paint.
+ * If paint contains an Shader and vertices does not contain texCoords, the shader
+ * is mapped using the vertices' positions.
+ * If vertices colors are defined in vertices, and Paint paint contains Shader,
+ * BlendMode mode combines vertices colors with Shader.
+ * @param verts
+ * @param mode
+ * @param paint
+ */
+ drawVertices(verts: Vertices, mode: BlendMode, paint: Paint): void;
+
+ /**
+ * Returns the bounds of clip, unaffected by the canvas's matrix.
+ * If the clip is empty, all four integers in the returned rectangle will equal zero.
+ *
+ * @param output - if provided, the results will be copied into the given array instead of
+ * allocating a new one.
+ */
+ getDeviceClipBounds(output?: IRect): IRect;
+
+ /**
+ * Returns true if the given rect, transformed by the current canvas
+ * transform, can be quickly determined to fall entirely outside the clip.
+ */
+ quickReject(rect: InputRect): boolean;
+
+ /**
+ * Returns the current transform from local coordinates to the 'device', which for most
+ * purposes means pixels.
+ */
+ getLocalToDevice(): Matrix4x4;
+
+ /**
+ * Returns the number of saved states, each containing: Matrix and clip.
+ * Equals the number of save() calls less the number of restore() calls plus one.
+ * The save count of a new canvas is one.
+ */
+ getSaveCount(): number;
+
+ /**
+ * Legacy version of getLocalToDevice(), which strips away any Z information, and
+ * just returns a 3x3 version.
+ */
+ getTotalMatrix(): number[];
+
+ /**
+ * Creates Surface matching info and props, and associates it with Canvas.
+ * Returns null if no match found.
+ * @param info
+ */
+ makeSurface(info: ImageInfo): Surface | null;
+
+ /**
+ * Returns a TypedArray containing the pixels reading starting at (srcX, srcY) and does not
+ * exceed the size indicated by imageInfo. See SkCanvas.h for more on the caveats.
+ *
+ * If dest is not provided, we allocate memory equal to the provided height * the provided
+ * bytesPerRow to fill the data with.
+ *
+ * This is generally a very expensive call for the GPU backend.
+ *
+ * @param srcX
+ * @param srcY
+ * @param imageInfo - describes the destination format of the pixels.
+ * @param dest - If provided, the pixels will be copied into the allocated buffer allowing
+ * access to the pixels without allocating a new TypedArray.
+ * @param bytesPerRow - number of bytes per row. Must be provided if dest is set. This
+ * depends on destination ColorType. For example, it must be at least 4 * width for
+ * the 8888 color type.
+ * @returns a TypedArray appropriate for the specified ColorType. Note that 16 bit floats are
+ * not supported in JS, so that colorType corresponds to raw bytes Uint8Array.
+ */
+ readPixels(
+ srcX: number,
+ srcY: number,
+ imageInfo: ImageInfo,
+ dest?: MallocObj,
+ bytesPerRow?: number
+ ): Float32Array | Uint8Array | null;
+
+ /**
+ * Removes changes to the current matrix and clip since Canvas state was
+ * last saved. The state is removed from the stack.
+ * Does nothing if the stack is empty.
+ */
+ restore(): void;
+
+ /**
+ * Restores state to a previous stack value.
+ * @param saveCount
+ */
+ restoreToCount(saveCount: number): void;
+
+ /**
+ * Rotates the current matrix by the number of degrees.
+ * @param rot - angle of rotation in degrees.
+ * @param rx
+ * @param ry
+ */
+ rotate(rot: AngleInDegrees, rx: number, ry: number): void;
+
+ /**
+ * Saves the current matrix and clip and returns current height of the stack.
+ */
+ save(): number;
+
+ /**
+ * Saves Matrix and clip, and allocates a SkBitmap for subsequent drawing.
+ * Calling restore() discards changes to Matrix and clip, and draws the SkBitmap.
+ * It returns the height of the stack.
+ * See Canvas.h for more.
+ * @param paint
+ * @param bounds
+ * @param backdrop
+ * @param flags
+ * @param backdropFilterTileMode
+ */
+ saveLayer(
+ paint?: Paint,
+ bounds?: InputRect | null,
+ backdrop?: ImageFilter | null,
+ flags?: SaveLayerFlag,
+ backdropFilterTileMode?: TileMode
+ ): number;
+
+ /**
+ * Scales the current matrix by sx on the x-axis and sy on the y-axis.
+ * @param sx
+ * @param sy
+ */
+ scale(sx: number, sy: number): void;
+
+ /**
+ * Skews Matrix by sx on the x-axis and sy on the y-axis. A positive value of sx
+ * skews the drawing right as y-axis values increase; a positive value of sy skews
+ * the drawing down as x-axis values increase.
+ * @param sx
+ * @param sy
+ */
+ skew(sx: number, sy: number): void;
+
+ /**
+ * Translates Matrix by dx along the x-axis and dy along the y-axis.
+ * @param dx
+ * @param dy
+ */
+ translate(dx: number, dy: number): void;
+
+ /**
+ * Writes the given rectangle of pixels to the provided coordinates. The source pixels
+ * will be converted to the canvas's alphaType and colorType if they do not match.
+ * @param pixels
+ * @param srcWidth
+ * @param srcHeight
+ * @param destX
+ * @param destY
+ * @param alphaType - defaults to Unpremul
+ * @param colorType - defaults to RGBA_8888
+ * @param colorSpace - defaults to SRGB
+ */
+ writePixels(
+ pixels: Uint8Array | number[],
+ srcWidth: number,
+ srcHeight: number,
+ destX: number,
+ destY: number,
+ alphaType?: AlphaType,
+ colorType?: ColorType,
+ colorSpace?: ColorSpace
+ ): boolean;
+}
+
+/**
+ * See SkColorFilter.h for more on this class. The objects are opaque.
+ */
+export type ColorFilter = EmbindObject<"ColorFilter">;
+
+export interface ContourMeasureIter extends EmbindObject<"ContourMeasureIter"> {
+ /**
+ * Iterates through contours in path, returning a contour-measure object for each contour
+ * in the path. Returns null when it is done.
+ *
+ * See SkContourMeasure.h for more details.
+ */
+ next(): ContourMeasure | null;
+}
+
+export interface ContourMeasure extends EmbindObject<"ContourMeasure"> {
+ /**
+ * Returns the given position and tangent line for the distance on the given contour.
+ * The return value is 4 floats in this order: posX, posY, vecX, vecY.
+ * @param distance - will be pinned between 0 and length().
+ * @param output - if provided, the four floats of the PosTan will be copied into this array
+ * instead of allocating a new one.
+ */
+ getPosTan(distance: number, output?: PosTan): PosTan;
+
+ /**
+ * Returns an Path representing the segement of this contour.
+ * @param startD - will be pinned between 0 and length()
+ * @param stopD - will be pinned between 0 and length()
+ * @param startWithMoveTo
+ */
+ getSegment(startD: number, stopD: number, startWithMoveTo: boolean): Path;
+
+ /**
+ * Returns true if the contour is closed.
+ */
+ isClosed(): boolean;
+
+ /**
+ * Returns the length of this contour.
+ */
+ length(): number;
+}
+
+export interface FontMetrics {
+ ascent: number; // suggested space above the baseline. < 0
+ descent: number; // suggested space below the baseline. > 0
+ leading: number; // suggested spacing between descent of previous line and ascent of next line.
+ bounds?: Rect; // smallest rect containing all glyphs (relative to 0,0)
+}
+
+/**
+ * See SkFont.h for more on this class.
+ */
+export interface Font extends EmbindObject<"Font"> {
+ /**
+ * Returns the FontMetrics for this font.
+ */
+ getMetrics(): FontMetrics;
+
+ /**
+ * Retrieves the bounds for each glyph in glyphs.
+ * If paint is not null, its stroking, PathEffect, and MaskFilter fields are respected.
+ * These are returned as flattened rectangles. For each glyph, there will be 4 floats for
+ * left, top, right, bottom (relative to 0, 0) for that glyph.
+ * @param glyphs
+ * @param paint
+ * @param output - if provided, the results will be copied into this array.
+ */
+ getGlyphBounds(
+ glyphs: InputGlyphIDArray,
+ paint?: Paint | null,
+ output?: Float32Array
+ ): Float32Array;
+
+ /**
+ * Retrieves the glyph ids for each code point in the provided string. This call is passed to
+ * the typeface of this font. Note that glyph IDs are typeface-dependent; different faces
+ * may have different ids for the same code point.
+ * @param str
+ * @param numCodePoints - the number of code points in the string. Defaults to str.length.
+ * @param output - if provided, the results will be copied into this array.
+ */
+ getGlyphIDs(
+ str: string,
+ numCodePoints?: number,
+ output?: GlyphIDArray
+ ): GlyphIDArray;
+
+ /**
+ * Retrieves the advanceX measurements for each glyph.
+ * If paint is not null, its stroking, PathEffect, and MaskFilter fields are respected.
+ * One width per glyph is returned in the returned array.
+ * @param glyphs
+ * @param paint
+ * @param output - if provided, the results will be copied into this array.
+ */
+ getGlyphWidths(
+ glyphs: InputGlyphIDArray,
+ paint?: Paint | null,
+ output?: Float32Array
+ ): Float32Array;
+
+ /**
+ * Computes any intersections of a thick "line" and a run of positionsed glyphs.
+ * The thick line is represented as a top and bottom coordinate (positive for
+ * below the baseline, negative for above). If there are no intersections
+ * (e.g. if this is intended as an underline, and there are no "collisions")
+ * then the returned array will be empty. If there are intersections, the array
+ * will contain pairs of X coordinates [start, end] for each segment that
+ * intersected with a glyph.
+ *
+ * @param glyphs the glyphs to intersect with
+ * @param positions x,y coordinates (2 per glyph) for each glyph
+ * @param top top of the thick "line" to use for intersection testing
+ * @param bottom bottom of the thick "line" to use for intersection testing
+ * @return array of [start, end] x-coordinate pairs. Maybe be empty.
+ */
+ getGlyphIntercepts(
+ glyphs: InputGlyphIDArray,
+ positions: Float32Array | number[],
+ top: number,
+ bottom: number
+ ): Float32Array;
+
+ /**
+ * Returns text scale on x-axis. Default value is 1.
+ */
+ getScaleX(): number;
+
+ /**
+ * Returns text size in points.
+ */
+ getSize(): number;
+
+ /**
+ * Returns text skew on x-axis. Default value is zero.
+ */
+ getSkewX(): number;
+
+ /**
+ * Returns embolden effect for this font. Default value is false.
+ */
+ isEmbolden(): boolean;
+
+ /**
+ * Returns the Typeface set for this font.
+ */
+ getTypeface(): Typeface | null;
+
+ /**
+ * Requests, but does not require, that edge pixels draw opaque or with partial transparency.
+ * @param edging
+ */
+ setEdging(edging: FontEdging): void;
+
+ /**
+ * Requests, but does not require, to use bitmaps in fonts instead of outlines.
+ * @param embeddedBitmaps
+ */
+ setEmbeddedBitmaps(embeddedBitmaps: boolean): void;
+
+ /**
+ * Sets level of glyph outline adjustment.
+ * @param hinting
+ */
+ setHinting(hinting: FontHinting): void;
+
+ /**
+ * Requests, but does not require, linearly scalable font and glyph metrics.
+ *
+ * For outline fonts 'true' means font and glyph metrics should ignore hinting and rounding.
+ * Note that some bitmap formats may not be able to scale linearly and will ignore this flag.
+ * @param linearMetrics
+ */
+ setLinearMetrics(linearMetrics: boolean): void;
+
+ /**
+ * Sets the text scale on the x-axis.
+ * @param sx
+ */
+ setScaleX(sx: number): void;
+
+ /**
+ * Sets the text size in points on this font.
+ * @param points
+ */
+ setSize(points: number): void;
+
+ /**
+ * Sets the text-skew on the x axis for this font.
+ * @param sx
+ */
+ setSkewX(sx: number): void;
+
+ /**
+ * Set embolden effect for this font.
+ * @param embolden
+ */
+ setEmbolden(embolden: boolean): void;
+
+ /**
+ * Requests, but does not require, that glyphs respect sub-pixel positioning.
+ * @param subpixel
+ */
+ setSubpixel(subpixel: boolean): void;
+
+ /**
+ * Sets the typeface to use with this font. null means to clear the typeface and use the
+ * default one.
+ * @param face
+ */
+ setTypeface(face: Typeface | null): void;
+}
+
+/**
+ * See SkFontMgr.h for more details
+ */
+export interface FontMgr extends EmbindObject<"FontMgr"> {
+ /**
+ * Return the number of font families loaded in this manager. Useful for debugging.
+ */
+ countFamilies(): number;
+
+ /**
+ * Return the nth family name. Useful for debugging.
+ * @param index
+ */
+ getFamilyName(index: number): string;
+
+ /**
+ * Find the closest matching typeface to the specified familyName and style.
+ */
+ matchFamilyStyle(name: string, style: FontStyle): Typeface;
+}
+
+/**
+ * See SkImage.h for more information on this class.
+ */
+export interface Image extends EmbindObject<"Image"> {
+ /**
+ * Encodes this image's pixels to the specified format and returns them. Must be built with
+ * the specified codec. If the options are unspecified, sensible defaults will be
+ * chosen.
+ * @param fmt - PNG is the default value.
+ * @param quality - a value from 0 to 100; 100 is the least lossy. May be ignored.
+ */
+ encodeToBytes(fmt?: EncodedImageFormat, quality?: number): Uint8Array | null;
+
+ /**
+ * Returns the color space associated with this object.
+ * It is the user's responsibility to call delete() on this after it has been used.
+ */
+ getColorSpace(): ColorSpace;
+
+ /**
+ * Returns the width, height, colorType and alphaType associated with this image.
+ * Colorspace is separate so as to not accidentally leak that memory.
+ */
+ getImageInfo(): PartialImageInfo;
+
+ /**
+ * Return the height in pixels of the image.
+ */
+ height(): number;
+
+ /**
+ * Returns an Image with the same "base" pixels as the this image, but with mipmap levels
+ * automatically generated and attached.
+ */
+ makeCopyWithDefaultMipmaps(): Image;
+
+ /**
+ * Returns this image as a shader with the specified tiling. It will use cubic sampling.
+ * @param tx - tile mode in the x direction.
+ * @param ty - tile mode in the y direction.
+ * @param B - See CubicResampler in SkSamplingOptions.h for more information
+ * @param C - See CubicResampler in SkSamplingOptions.h for more information
+ * @param localMatrix
+ */
+ makeShaderCubic(
+ tx: TileMode,
+ ty: TileMode,
+ B: number,
+ C: number,
+ localMatrix?: InputMatrix
+ ): Shader;
+
+ /**
+ * Returns this image as a shader with the specified tiling. It will use cubic sampling.
+ * @param tx - tile mode in the x direction.
+ * @param ty - tile mode in the y direction.
+ * @param fm - The filter mode.
+ * @param mm - The mipmap mode. Note: for settings other than None, the image must have mipmaps
+ * calculated with makeCopyWithDefaultMipmaps;
+ * @param localMatrix
+ */
+ makeShaderOptions(
+ tx: TileMode,
+ ty: TileMode,
+ fm: FilterMode,
+ mm: MipmapMode,
+ localMatrix?: InputMatrix
+ ): Shader;
+
+ /**
+ * Returns a TypedArray containing the pixels reading starting at (srcX, srcY) and does not
+ * exceed the size indicated by imageInfo. See SkImage.h for more on the caveats.
+ *
+ * If dest is not provided, we allocate memory equal to the provided height * the provided
+ * bytesPerRow to fill the data with.
+ *
+ * @param srcX
+ * @param srcY
+ * @param imageInfo - describes the destination format of the pixels.
+ * @param dest - If provided, the pixels will be copied into the allocated buffer allowing
+ * access to the pixels without allocating a new TypedArray.
+ * @param bytesPerRow - number of bytes per row. Must be provided if dest is set. This
+ * depends on destination ColorType. For example, it must be at least 4 * width for
+ * the 8888 color type.
+ * @returns a TypedArray appropriate for the specified ColorType. Note that 16 bit floats are
+ * not supported in JS, so that colorType corresponds to raw bytes Uint8Array.
+ */
+ readPixels(
+ srcX: number,
+ srcY: number,
+ imageInfo: ImageInfo,
+ dest?: MallocObj,
+ bytesPerRow?: number
+ ): Float32Array | Uint8Array | null;
+
+ /**
+ * Return the width in pixels of the image.
+ */
+ width(): number;
+}
+
+/**
+ * See ImageFilter.h for more on this class. The objects are opaque.
+ */
+export interface ImageFilter extends EmbindObject<"ImageFilter"> {
+ /**
+ * Returns an IRect that is the updated bounds of inputRect after this
+ * filter has been applied.
+ *
+ * @param drawBounds - The local (pre-transformed) bounding box of the
+ * geometry being drawn _before_ the filter is applied.
+ * @param ctm - If provided, the current transform at the time the filter
+ * would be used.
+ * @param outputRect - If provided, the result will be output to this array
+ * rather than allocating a new one.
+ * @returns an IRect describing the updated bounds.
+ */
+ getOutputBounds(
+ drawBounds: Rect,
+ ctm?: InputMatrix,
+ outputRect?: IRect
+ ): IRect;
+}
+
+export interface ImageInfo {
+ alphaType: AlphaType;
+ colorSpace: ColorSpace;
+ colorType: ColorType;
+ height: number;
+ width: number;
+}
+
+export interface PartialImageInfo {
+ alphaType: AlphaType;
+ colorType: ColorType;
+ height: number;
+ width: number;
+}
+
+/*
+ * Specifies sampling with bicubic coefficients
+ */
+export interface CubicResampler {
+ B: number; // 0..1
+ C: number; // 0..1
+}
+
+/**
+ * Specifies sampling using filter and mipmap options
+ */
+export interface FilterOptions {
+ filter: FilterMode;
+ mipmap?: MipmapMode; // defaults to None if not specified
+}
+
+/**
+ * See SkMaskFilter.h for more on this class. The objects are opaque.
+ */
+export type MaskFilter = EmbindObject<"MaskFilter">;
+
+/**
+ * See SkPaint.h for more information on this class.
+ */
+export interface Paint extends EmbindObject<"Paint"> {
+ /**
+ * Returns a copy of this paint.
+ */
+ copy(): Paint;
+
+ /**
+ * Retrieves the alpha and RGB unpremultiplied. RGB are extended sRGB values
+ * (sRGB gamut, and encoded with the sRGB transfer function).
+ */
+ getColor(): Color;
+
+ /**
+ * Returns the geometry drawn at the beginning and end of strokes.
+ */
+ getStrokeCap(): StrokeCap;
+
+ /**
+ * Returns the geometry drawn at the corners of strokes.
+ */
+ getStrokeJoin(): StrokeJoin;
+
+ /**
+ * Returns the limit at which a sharp corner is drawn beveled.
+ */
+ getStrokeMiter(): number;
+
+ /**
+ * Returns the thickness of the pen used to outline the shape.
+ */
+ getStrokeWidth(): number;
+
+ /**
+ * Replaces alpha, leaving RGBA unchanged. 0 means fully transparent, 1.0 means opaque.
+ * @param alpha
+ */
+ setAlphaf(alpha: number): void;
+
+ /**
+ * Requests, but does not require, that edge pixels draw opaque or with
+ * partial transparency.
+ * @param aa
+ */
+ setAntiAlias(aa: boolean): void;
+
+ /**
+ * Sets the blend mode that is, the mode used to combine source color
+ * with destination color.
+ * @param mode
+ */
+ setBlendMode(mode: BlendMode): void;
+
+ /**
+ * Sets the current blender, increasing its refcnt, and if a blender is already
+ * present, decreasing that object's refcnt.
+ *
+ * * A nullptr blender signifies the default SrcOver behavior.
+ *
+ * * For convenience, you can call setBlendMode() if the blend effect can be expressed
+ * as one of those values.
+ * @param blender
+ */
+ setBlender(blender: Blender): void;
+
+ /**
+ * Sets alpha and RGB used when stroking and filling. The color is four floating
+ * point values, unpremultiplied. The color values are interpreted as being in
+ * the provided colorSpace.
+ * @param color
+ * @param colorSpace - defaults to sRGB
+ */
+ setColor(color: InputColor, colorSpace?: ColorSpace): void;
+
+ /**
+ * Sets alpha and RGB used when stroking and filling. The color is four floating
+ * point values, unpremultiplied. The color values are interpreted as being in
+ * the provided colorSpace.
+ * @param r
+ * @param g
+ * @param b
+ * @param a
+ * @param colorSpace - defaults to sRGB
+ */
+ setColorComponents(
+ r: number,
+ g: number,
+ b: number,
+ a: number,
+ colorSpace?: ColorSpace
+ ): void;
+
+ /**
+ * Sets the current color filter, replacing the existing one if there was one.
+ * @param filter
+ */
+ setColorFilter(filter: ColorFilter | null): void;
+
+ /**
+ * Sets the color used when stroking and filling. The color values are interpreted as being in
+ * the provided colorSpace.
+ * @param color
+ * @param colorSpace - defaults to sRGB.
+ */
+ setColorInt(color: ColorInt, colorSpace?: ColorSpace): void;
+
+ /**
+ * Requests, but does not require, to distribute color error.
+ * @param shouldDither
+ */
+ setDither(shouldDither: boolean): void;
+
+ /**
+ * Sets the current image filter, replacing the existing one if there was one.
+ * @param filter
+ */
+ setImageFilter(filter: ImageFilter | null): void;
+
+ /**
+ * Sets the current mask filter, replacing the existing one if there was one.
+ * @param filter
+ */
+ setMaskFilter(filter: MaskFilter | null): void;
+
+ /**
+ * Sets the current path effect, replacing the existing one if there was one.
+ * @param effect
+ */
+ setPathEffect(effect: PathEffect | null): void;
+
+ /**
+ * Sets the current shader, replacing the existing one if there was one.
+ * @param shader
+ */
+ setShader(shader: Shader | null): void;
+
+ /**
+ * Sets the geometry drawn at the beginning and end of strokes.
+ * @param cap
+ */
+ setStrokeCap(cap: StrokeCap): void;
+
+ /**
+ * Sets the geometry drawn at the corners of strokes.
+ * @param join
+ */
+ setStrokeJoin(join: StrokeJoin): void;
+
+ /**
+ * Sets the limit at which a sharp corner is drawn beveled.
+ * @param limit
+ */
+ setStrokeMiter(limit: number): void;
+
+ /**
+ * Sets the thickness of the pen used to outline the shape.
+ * @param width
+ */
+ setStrokeWidth(width: number): void;
+
+ /**
+ * Sets whether the geometry is filled or stroked.
+ * @param style
+ */
+ setStyle(style: PaintStyle): void;
+}
+
+/**
+ * See SkPath.h for more information on this class.
+ */
+export interface Path extends EmbindObject<"Path"> {
+ /**
+ * Appends arc to Path, as the start of new contour. Arc added is part of ellipse
+ * bounded by oval, from startAngle through sweepAngle. Both startAngle and
+ * sweepAngle are measured in degrees, where zero degrees is aligned with the
+ * positive x-axis, and positive sweeps extends arc clockwise.
+ * Returns the modified path for easier chaining.
+ * @param oval
+ * @param startAngle
+ * @param sweepAngle
+ */
+ addArc(
+ oval: InputRect,
+ startAngle: AngleInDegrees,
+ sweepAngle: AngleInDegrees
+ ): Path;
+
+ /**
+ * Adds circle centered at (x, y) of size radius to the path.
+ * Has no effect if radius is zero or negative.
+ *
+ * @param x center of circle
+ * @param y center of circle
+ * @param radius distance from center to edge
+ * @param isCCW - if the path should be drawn counter-clockwise or not
+ * @return reference to SkPath
+ */
+ addCircle(x: number, y: number, r: number, isCCW?: boolean): Path;
+
+ /**
+ * Adds oval to Path, appending kMove_Verb, four kConic_Verb, and kClose_Verb.
+ * Oval is upright ellipse bounded by Rect oval with radii equal to half oval width
+ * and half oval height. Oval begins at start and continues clockwise by default.
+ * Returns the modified path for easier chaining.
+ * @param oval
+ * @param isCCW - if the path should be drawn counter-clockwise or not
+ * @param startIndex - index of initial point of ellipse
+ */
+ addOval(oval: InputRect, isCCW?: boolean, startIndex?: number): Path;
+
+ /**
+ * Takes 1, 2, 7, or 10 required args, where the first arg is always the path.
+ * The last arg is an optional boolean and chooses between add or extend mode.
+ * The options for the remaining args are:
+ * - an array of 6 or 9 parameters (perspective is optional)
+ * - the 9 parameters of a full matrix or
+ * the 6 non-perspective params of a matrix.
+ * Returns the modified path for easier chaining (or null if params were incorrect).
+ * @param args
+ */
+ addPath(...args: any[]): Path | null;
+
+ /**
+ * Adds contour created from array of n points, adding (count - 1) line segments.
+ * Contour added starts at pts[0], then adds a line for every additional point
+ * in pts array. If close is true, appends kClose_Verb to Path, connecting
+ * pts[count - 1] and pts[0].
+ * Returns the modified path for easier chaining.
+ * @param points
+ * @param close - if true, will add a line connecting last point to the first point.
+ */
+ addPoly(points: InputFlattenedPointArray, close: boolean): Path;
+
+ /**
+ * Adds Rect to Path, appending kMove_Verb, three kLine_Verb, and kClose_Verb,
+ * starting with top-left corner of Rect; followed by top-right, bottom-right,
+ * and bottom-left if isCCW is false; or followed by bottom-left,
+ * bottom-right, and top-right if isCCW is true.
+ * Returns the modified path for easier chaining.
+ * @param rect
+ * @param isCCW
+ */
+ addRect(rect: InputRect, isCCW?: boolean): Path;
+
+ /**
+ * Adds rrect to Path, creating a new closed contour.
+ * Returns the modified path for easier chaining.
+ * @param rrect
+ * @param isCCW
+ */
+ addRRect(rrect: InputRRect, isCCW?: boolean): Path;
+
+ /**
+ * Adds the given verbs and associated points/weights to the path. The process
+ * reads the first verb from verbs and then the appropriate number of points from the
+ * FlattenedPointArray (e.g. 2 points for moveTo, 4 points for quadTo, etc). If the verb is
+ * a conic, a weight will be read from the WeightList.
+ * Returns the modified path for easier chaining
+ * @param verbs - the verbs that create this path, in the order of being drawn.
+ * @param points - represents n points with 2n floats.
+ * @param weights - used if any of the verbs are conics, can be omitted otherwise.
+ */
+ addVerbsPointsWeights(
+ verbs: VerbList,
+ points: InputFlattenedPointArray,
+ weights?: WeightList
+ ): Path;
+
+ /**
+ * Adds an arc to this path, emulating the Canvas2D behavior.
+ * Returns the modified path for easier chaining.
+ * @param x
+ * @param y
+ * @param radius
+ * @param startAngle
+ * @param endAngle
+ * @param isCCW
+ */
+ arc(
+ x: number,
+ y: number,
+ radius: number,
+ startAngle: AngleInRadians,
+ endAngle: AngleInRadians,
+ isCCW?: boolean
+ ): Path;
+
+ /**
+ * Appends arc to Path. Arc added is part of ellipse
+ * bounded by oval, from startAngle through sweepAngle. Both startAngle and
+ * sweepAngle are measured in degrees, where zero degrees is aligned with the
+ * positive x-axis, and positive sweeps extends arc clockwise.
+ * Returns the modified path for easier chaining.
+ * @param oval
+ * @param startAngle
+ * @param endAngle
+ * @param forceMoveTo
+ */
+ arcToOval(
+ oval: InputRect,
+ startAngle: AngleInDegrees,
+ endAngle: AngleInDegrees,
+ forceMoveTo: boolean
+ ): Path;
+
+ /**
+ * Appends arc to Path. Arc is implemented by one or more conics weighted to
+ * describe part of oval with radii (rx, ry) rotated by xAxisRotate degrees. Arc
+ * curves from last Path Point to (x, y), choosing one of four possible routes:
+ * clockwise or counterclockwise, and smaller or larger. See SkPath.h for more details.
+ * Returns the modified path for easier chaining.
+ * @param rx
+ * @param ry
+ * @param xAxisRotate
+ * @param useSmallArc
+ * @param isCCW
+ * @param x
+ * @param y
+ */
+ arcToRotated(
+ rx: number,
+ ry: number,
+ xAxisRotate: AngleInDegrees,
+ useSmallArc: boolean,
+ isCCW: boolean,
+ x: number,
+ y: number
+ ): Path;
+
+ /**
+ * Appends arc to Path, after appending line if needed. Arc is implemented by conic
+ * weighted to describe part of circle. Arc is contained by tangent from
+ * last Path point to (x1, y1), and tangent from (x1, y1) to (x2, y2). Arc
+ * is part of circle sized to radius, positioned so it touches both tangent lines.
+ * Returns the modified path for easier chaining.
+ * @param x1
+ * @param y1
+ * @param x2
+ * @param y2
+ * @param radius
+ */
+ arcToTangent(
+ x1: number,
+ y1: number,
+ x2: number,
+ y2: number,
+ radius: number
+ ): Path;
+
+ /**
+ * Appends CLOSE_VERB to Path. A closed contour connects the first and last point
+ * with a line, forming a continuous loop.
+ * Returns the modified path for easier chaining.
+ */
+ close(): Path;
+
+ /**
+ * Returns minimum and maximum axes values of the lines and curves in Path.
+ * Returns (0, 0, 0, 0) if Path contains no points.
+ * Returned bounds width and height may be larger or smaller than area affected
+ * when Path is drawn.
+ *
+ * Behaves identically to getBounds() when Path contains
+ * only lines. If Path contains curves, computed bounds includes
+ * the maximum extent of the quad, conic, or cubic; is slower than getBounds();
+ * and unlike getBounds(), does not cache the result.
+ * @param outputArray - if provided, the bounding box will be copied into this array instead of
+ * allocating a new one.
+ */
+ computeTightBounds(outputArray?: Rect): Rect;
+
+ /**
+ * Adds conic from last point towards (x1, y1), to (x2, y2), weighted by w.
+ * If Path is empty, or path is closed, the last point is set to (0, 0)
+ * before adding conic.
+ * Returns the modified path for easier chaining.
+ * @param x1
+ * @param y1
+ * @param x2
+ * @param y2
+ * @param w
+ */
+ conicTo(x1: number, y1: number, x2: number, y2: number, w: number): Path;
+
+ /**
+ * Returns true if the point (x, y) is contained by Path, taking into
+ * account FillType.
+ * @param x
+ * @param y
+ */
+ contains(x: number, y: number): boolean;
+
+ /**
+ * Returns a copy of this Path.
+ */
+ copy(): Path;
+
+ /**
+ * Returns the number of points in this path. Initially zero.
+ */
+ countPoints(): number;
+
+ /**
+ * Adds cubic from last point towards (x1, y1), then towards (x2, y2), ending at
+ * (x3, y3). If Path is empty, or path is closed, the last point is set to
+ * (0, 0) before adding cubic.
+ * @param cpx1
+ * @param cpy1
+ * @param cpx2
+ * @param cpy2
+ * @param x
+ * @param y
+ */
+ cubicTo(
+ cpx1: number,
+ cpy1: number,
+ cpx2: number,
+ cpy2: number,
+ x: number,
+ y: number
+ ): Path;
+
+ /**
+ * Changes this path to be the dashed version of itself. This is the same effect as creating
+ * a DashPathEffect and calling filterPath on this path.
+ * @param on
+ * @param off
+ * @param phase
+ */
+ dash(on: number, off: number, phase: number): boolean;
+
+ /**
+ * Returns true if other path is equal to this path.
+ * @param other
+ */
+ equals(other: Path): boolean;
+
+ /**
+ * Returns minimum and maximum axes values of Point array.
+ * Returns (0, 0, 0, 0) if Path contains no points. Returned bounds width and height may
+ * be larger or smaller than area affected when Path is drawn.
+ * @param outputArray - if provided, the bounding box will be copied into this array instead of
+ * allocating a new one.
+ */
+ getBounds(outputArray?: Rect): Rect;
+
+ /**
+ * Return the FillType for this path.
+ */
+ getFillType(): FillType;
+
+ /**
+ * Returns the Point at index in Point array. Valid range for index is
+ * 0 to countPoints() - 1.
+ * @param index
+ * @param outputArray - if provided, the point will be copied into this array instead of
+ * allocating a new one.
+ */
+ getPoint(index: number, outputArray?: Point): Point;
+
+ /**
+ * Returns true if there are no verbs in the path.
+ */
+ isEmpty(): boolean;
+
+ /**
+ * Returns true if the path is volatile; it will not be altered or discarded
+ * by the caller after it is drawn. Path by default have volatile set false, allowing
+ * Surface to attach a cache of data which speeds repeated drawing. If true, Surface
+ * may not speed repeated drawing.
+ */
+ isVolatile(): boolean;
+
+ /**
+ * Adds line from last point to (x, y). If Path is empty, or last path is closed,
+ * last point is set to (0, 0) before adding line.
+ * Returns the modified path for easier chaining.
+ * @param x
+ * @param y
+ */
+ lineTo(x: number, y: number): Path;
+
+ /**
+ * Returns a new path that covers the same area as the original path, but with the
+ * Winding FillType. This may re-draw some contours in the path as counter-clockwise
+ * instead of clockwise to achieve that effect. If such a transformation cannot
+ * be done, null is returned.
+ */
+ makeAsWinding(): Path | null;
+
+ /**
+ * Adds beginning of contour at the given point.
+ * Returns the modified path for easier chaining.
+ * @param x
+ * @param y
+ */
+ moveTo(x: number, y: number): Path;
+
+ /**
+ * Translates all the points in the path by dx, dy.
+ * Returns the modified path for easier chaining.
+ * @param dx
+ * @param dy
+ */
+ offset(dx: number, dy: number): Path;
+
+ /**
+ * Combines this path with the other path using the given PathOp. Returns false if the operation
+ * fails.
+ * @param other
+ * @param op
+ */
+ op(other: Path, op: PathOp): boolean;
+
+ /**
+ * Adds quad from last point towards (x1, y1), to (x2, y2).
+ * If Path is empty, or path is closed, last point is set to (0, 0) before adding quad.
+ * Returns the modified path for easier chaining.
+ * @param x1
+ * @param y1
+ * @param x2
+ * @param y2
+ */
+ quadTo(x1: number, y1: number, x2: number, y2: number): Path;
+
+ /**
+ * Relative version of arcToRotated.
+ * @param rx
+ * @param ry
+ * @param xAxisRotate
+ * @param useSmallArc
+ * @param isCCW
+ * @param dx
+ * @param dy
+ */
+ rArcTo(
+ rx: number,
+ ry: number,
+ xAxisRotate: AngleInDegrees,
+ useSmallArc: boolean,
+ isCCW: boolean,
+ dx: number,
+ dy: number
+ ): Path;
+
+ /**
+ * Relative version of conicTo.
+ * @param dx1
+ * @param dy1
+ * @param dx2
+ * @param dy2
+ * @param w
+ */
+ rConicTo(dx1: number, dy1: number, dx2: number, dy2: number, w: number): Path;
+
+ /**
+ * Relative version of cubicTo.
+ * @param cpx1
+ * @param cpy1
+ * @param cpx2
+ * @param cpy2
+ * @param x
+ * @param y
+ */
+ rCubicTo(
+ cpx1: number,
+ cpy1: number,
+ cpx2: number,
+ cpy2: number,
+ x: number,
+ y: number
+ ): Path;
+
+ /**
+ * Sets Path to its initial state.
+ * Removes verb array, point array, and weights, and sets FillType to Winding.
+ * Internal storage associated with Path is released
+ */
+ reset(): void;
+
+ /**
+ * Sets Path to its initial state.
+ * Removes verb array, point array, and weights, and sets FillType to Winding.
+ * Internal storage associated with Path is *not* released.
+ * Use rewind() instead of reset() if Path storage will be reused and performance
+ * is critical.
+ */
+ rewind(): void;
+
+ /**
+ * Relative version of lineTo.
+ * @param x
+ * @param y
+ */
+ rLineTo(x: number, y: number): Path;
+
+ /**
+ * Relative version of moveTo.
+ * @param x
+ * @param y
+ */
+ rMoveTo(x: number, y: number): Path;
+
+ /**
+ * Relative version of quadTo.
+ * @param x1
+ * @param y1
+ * @param x2
+ * @param y2
+ */
+ rQuadTo(x1: number, y1: number, x2: number, y2: number): Path;
+
+ /**
+ * Sets FillType, the rule used to fill Path.
+ * @param fill
+ */
+ setFillType(fill: FillType): void;
+
+ /**
+ * Specifies whether Path is volatile; whether it will be altered or discarded
+ * by the caller after it is drawn. Path by default have volatile set false.
+ *
+ * Mark animating or temporary paths as volatile to improve performance.
+ * Mark unchanging Path non-volatile to improve repeated rendering.
+ * @param volatile
+ */
+ setIsVolatile(volatile: boolean): void;
+
+ /**
+ * Set this path to a set of non-overlapping contours that describe the
+ * same area as the original path.
+ * The curve order is reduced where possible so that cubics may
+ * be turned into quadratics, and quadratics maybe turned into lines.
+ *
+ * Returns true if operation was able to produce a result.
+ */
+ simplify(): boolean;
+
+ /**
+ * Turns this path into the filled equivalent of the stroked path. Returns null if the operation
+ * fails (e.g. the path is a hairline).
+ * @param opts - describe how stroked path should look.
+ */
+ stroke(opts?: StrokeOpts): Path | null;
+
+ /**
+ * Serializes the contents of this path as a series of commands.
+ * The first item will be a verb, followed by any number of arguments needed. Then it will
+ * be followed by another verb, more arguments and so on.
+ */
+ toCmds(): Float32Array;
+
+ /**
+ * Returns this path as an SVG string.
+ */
+ toSVGString(): string;
+
+ /**
+ * Takes a 3x3 matrix as either an array or as 9 individual params.
+ * @param args
+ */
+ transform(...args: any[]): Path;
+
+ /**
+ * Take start and stop "t" values (values between 0...1), and modify this path such that
+ * it is a subset of the original path.
+ * The trim values apply to the entire path, so if it contains several contours, all of them
+ * are including in the calculation.
+ * Null is returned if either input value is NaN.
+ * @param startT - a value in the range [0.0, 1.0]. 0.0 is the beginning of the path.
+ * @param stopT - a value in the range [0.0, 1.0]. 1.0 is the end of the path.
+ * @param isComplement
+ */
+ trim(startT: number, stopT: number, isComplement: boolean): Path | null;
+}
+
+/**
+ * See SkPathEffect.h for more on this class. The objects are opaque.
+ */
+export type PathEffect = EmbindObject<"PathEffect">;
+
+/**
+ * See SkPicture.h for more information on this class.
+ *
+ * Of note, SkPicture is *not* what is colloquially thought of as a "picture" (what we
+ * call a bitmap). An SkPicture is a series of draw commands.
+ */
+export interface SkPicture extends EmbindObject<"SkPicture"> {
+ /**
+ * Returns a new shader that will draw with this picture.
+ *
+ * @param tmx The tiling mode to use when sampling in the x-direction.
+ * @param tmy The tiling mode to use when sampling in the y-direction.
+ * @param mode How to filter the tiles
+ * @param localMatrix Optional matrix used when sampling
+ * @param tileRect The tile rectangle in picture coordinates: this represents the subset
+ * (or superset) of the picture used when building a tile. It is not
+ * affected by localMatrix and does not imply scaling (only translation
+ * and cropping). If null, the tile rect is considered equal to the picture
+ * bounds.
+ */
+ makeShader(
+ tmx: TileMode,
+ tmy: TileMode,
+ mode: FilterMode,
+ localMatrix?: InputMatrix,
+ tileRect?: InputRect
+ ): Shader;
+
+ /**
+ * Return the bounding area for the Picture.
+ * @param outputArray - if provided, the bounding box will be copied into this array instead of
+ * allocating a new one.
+ */
+ cullRect(outputArray?: Rect): Rect;
+
+ /**
+ * Returns the approximate byte size. Does not include large objects.
+ */
+ approximateBytesUsed(): number;
+
+ /**
+ * Returns the serialized format of this SkPicture. The format may change at anytime and
+ * no promises are made for backwards or forward compatibility.
+ */
+ serialize(): Uint8Array | null;
+}
+
+export interface PictureRecorder extends EmbindObject<"PictureRecorder"> {
+ /**
+ * Returns a canvas on which to draw. When done drawing, call finishRecordingAsPicture()
+ *
+ * @param bounds - a rect to cull the results.
+ * @param computeBounds - Optional boolean (default false) which tells the
+ * recorder to compute a more accurate bounds for the
+ * cullRect of the picture.
+ */
+ beginRecording(bounds: InputRect, computeBounds?: boolean): Canvas;
+
+ /**
+ * Returns the captured draw commands as a picture and invalidates the canvas returned earlier.
+ */
+ finishRecordingAsPicture(): SkPicture;
+}
+
+/**
+ * See SkRuntimeEffect.h for more details.
+ */
+export interface RuntimeEffect extends EmbindObject<"RuntimeEffect"> {
+ /**
+ * Returns a shader executed using the given uniform data.
+ * @param uniforms
+ */
+ makeBlender(uniforms: Float32Array | number[] | MallocObj): Blender;
+
+ /**
+ * Returns a shader executed using the given uniform data.
+ * @param uniforms
+ * @param localMatrix
+ */
+ makeShader(
+ uniforms: Float32Array | number[] | MallocObj,
+ localMatrix?: InputMatrix
+ ): Shader;
+
+ /**
+ * Returns a shader executed using the given uniform data and the children as inputs.
+ * @param uniforms
+ * @param children
+ * @param localMatrix
+ */
+ makeShaderWithChildren(
+ uniforms: Float32Array | number[] | MallocObj,
+ children?: Shader[],
+ localMatrix?: InputMatrix
+ ): Shader;
+
+ /**
+ * Returns the nth uniform from the effect.
+ * @param index
+ */
+ getUniform(index: number): SkSLUniform;
+
+ /**
+ * Returns the number of uniforms on the effect.
+ */
+ getUniformCount(): number;
+
+ /**
+ * Returns the total number of floats across all uniforms on the effect. This is the length
+ * of the uniforms array expected by makeShader. For example, an effect with a single float3
+ * uniform, would return 1 from `getUniformCount()`, but 3 from `getUniformFloatCount()`.
+ */
+ getUniformFloatCount(): number;
+
+ /**
+ * Returns the name of the nth effect uniform.
+ * @param index
+ */
+ getUniformName(index: number): string;
+}
+
+/**
+ * See SkShader.h for more on this class. The objects are opaque.
+ */
+export type Shader = EmbindObject<"Shader">;
+
+export interface Surface extends EmbindObject<"Surface"> {
+ /**
+ * A convenient way to draw exactly once on the canvas associated with this surface.
+ * This requires an environment where a global function called requestAnimationFrame is
+ * available (e.g. on the web, not on Node). Users do not need to flush the surface,
+ * or delete/dispose of it as that is taken care of automatically with this wrapper.
+ *
+ * Node users should call getCanvas() and work with that canvas directly.
+ */
+ drawOnce(drawFrame: (_: Canvas) => void): void;
+
+ /**
+ * Clean up the surface and any extra memory.
+ * [Deprecated]: In the future, calls to delete() will be sufficient to clean up the memory.
+ */
+ dispose(): void;
+
+ /**
+ * Make sure any queued draws are sent to the screen or the GPU.
+ */
+ flush(): void;
+
+ /**
+ * Return a canvas that is backed by this surface. Any draws to the canvas will (eventually)
+ * show up on the surface. The returned canvas is owned by the surface and does NOT need to
+ * be cleaned up by the client.
+ */
+ getCanvas(): Canvas;
+
+ /**
+ * Returns the height of this surface in pixels.
+ */
+ height(): number;
+
+ /**
+ * Returns the ImageInfo associated with this surface.
+ */
+ imageInfo(): ImageInfo;
+
+ /**
+ * Creates an Image from the provided texture and info. The Image will own the texture;
+ * when the image is deleted, the texture will be cleaned up.
+ * @param tex
+ * @param info - describes the content of the texture.
+ */
+ makeImageFromTexture(tex: WebGLTexture, info: ImageInfo): Image | null;
+
+ /**
+ * Returns a texture-backed image based on the content in src. It uses RGBA_8888, unpremul
+ * and SRGB - for more control, use makeImageFromTexture.
+ *
+ * The underlying texture for this image will be created immediately from src, so
+ * it can be disposed of after this call. This image will *only* be usable for this
+ * surface (because WebGL textures are not transferable to other WebGL contexts).
+ * For an image that can be used across multiple surfaces, at the cost of being lazily
+ * loaded, see MakeLazyImageFromTextureSource.
+ *
+ * Not available for software-backed surfaces.
+ * @param src
+ * @param info - If provided, will be used to determine the width/height/format of the
+ * source image. If not, sensible defaults will be used.
+ * @param srcIsPremul - set to true if the src data has premultiplied alpha. Otherwise, it will
+ * be assumed to be Unpremultiplied. Note: if this is true and info specifies
+ * Unpremul, Skia will not convert the src pixels first.
+ */
+ makeImageFromTextureSource(
+ src: TextureSource,
+ info?: ImageInfo | PartialImageInfo,
+ srcIsPremul?: boolean
+ ): Image | null;
+
+ /**
+ * Returns current contents of the surface as an Image. This image will be optimized to be
+ * drawn to another surface of the same type. For example, if this surface is backed by the
+ * GPU, the returned Image will be backed by a GPU texture.
+ */
+ makeImageSnapshot(bounds?: InputIRect): Image;
+
+ /**
+ * Returns a compatible Surface, haring the same raster or GPU properties of the original.
+ * The pixels are not shared.
+ * @param info - width, height, etc of the Surface.
+ */
+ makeSurface(info: ImageInfo): Surface;
+
+ /**
+ * Returns if this Surface is a GPU-backed surface or not.
+ */
+ reportBackendTypeIsGPU(): boolean;
+
+ /**
+ * A convenient way to draw multiple frames on the canvas associated with this surface.
+ * This requires an environment where a global function called requestAnimationFrame is
+ * available (e.g. on the web, not on Node). Users do not need to flush the surface,
+ * as that is taken care of automatically with this wrapper.
+ *
+ * Users should probably call surface.requestAnimationFrame in the callback function to
+ * draw multiple frames, e.g. of an animation.
+ *
+ * Node users should call getCanvas() and work with that canvas directly.
+ *
+ * Returns the animation id.
+ */
+ requestAnimationFrame(drawFrame: (_: Canvas) => void): number;
+
+ /**
+ * If this surface is GPU-backed, return the sample count of the surface.
+ */
+ sampleCnt(): number;
+
+ /**
+ * Updates the underlying GPU texture of the image to be the contents of the provided
+ * TextureSource. Has no effect on CPU backend or if img was not created with either
+ * makeImageFromTextureSource or makeImageFromTexture.
+ * If the provided TextureSource is of different dimensions than the Image, the contents
+ * will be deformed (e.g. squished). The ColorType, AlphaType, and ColorSpace of src should
+ * match the original settings used to create the Image or it may draw strange.
+ *
+ * @param img - A texture-backed Image.
+ * @param src - A valid texture source of any dimensions.
+ * @param srcIsPremul - set to true if the src data has premultiplied alpha. Otherwise, it will
+ * be assumed to be Unpremultiplied. Note: if this is true and the image was
+ * created with Unpremul, Skia will not convert.
+ */
+ updateTextureFromSource(
+ img: Image,
+ src: TextureSource,
+ srcIsPremul?: boolean
+ ): void;
+
+ /**
+ * Returns the width of this surface in pixels.
+ */
+ width(): number;
+}
+
+/**
+ * See SkTextBlob.h for more on this class. The objects are opaque.
+ */
+export type TextBlob = EmbindObject<"TextBlob">;
+
+/**
+ * See SkTypeface.h for more on this class. The objects are opaque.
+ */
+export interface Typeface extends EmbindObject<"Typeface"> {
+ /**
+ * Retrieves the glyph ids for each code point in the provided string. Note that glyph IDs
+ * are typeface-dependent; different faces may have different ids for the same code point.
+ * @param str
+ * @param numCodePoints - the number of code points in the string. Defaults to str.length.
+ * @param output - if provided, the results will be copied into this array.
+ */
+ getGlyphIDs(
+ str: string,
+ numCodePoints?: number,
+ output?: GlyphIDArray
+ ): GlyphIDArray;
+
+ /**
+ * Return the typeface family name.
+ */
+ getFamilyName(): string;
+}
+
+/**
+ * See SkVertices.h for more on this class.
+ */
+export interface Vertices extends EmbindObject<"Vertices"> {
+ /**
+ * Return the bounding area for the vertices.
+ * @param outputArray - if provided, the bounding box will be copied into this array instead of
+ * allocating a new one.
+ */
+ bounds(outputArray?: Rect): Rect;
+
+ /**
+ * Return a unique ID for this vertices object.
+ */
+ uniqueID(): number;
+}
+
+export interface SkottieAnimation extends EmbindObject<"SkottieAnimation"> {
+ /**
+ * Returns the animation duration in seconds.
+ */
+ duration(): number;
+ /**
+ * Returns the animation frame rate (frames / second).
+ */
+ fps(): number;
+
+ /**
+ * Draws current animation frame. Must call seek or seekFrame first.
+ * @param canvas
+ * @param dstRect
+ */
+ render(canvas: Canvas, dstRect?: InputRect): void;
+
+ /**
+ * [deprecated] - use seekFrame
+ * @param t - value from [0.0, 1.0]; 0 is first frame, 1 is final frame.
+ * @param damageRect - will copy damage frame into this if provided.
+ */
+ seek(t: number, damageRect?: Rect): Rect;
+
+ /**
+ * Update the animation state to match |t|, specified as a frame index
+ * i.e. relative to duration() * fps().
+ *
+ * Returns the rectangle that was affected by this animation.
+ *
+ * @param frame - Fractional values are allowed and meaningful - e.g.
+ * 0.0 -> first frame
+ * 1.0 -> second frame
+ * 0.5 -> halfway between first and second frame
+ * @param damageRect - will copy damage frame into this if provided.
+ */
+ seekFrame(frame: number, damageRect?: Rect): Rect;
+
+ /**
+ * Return the size of this animation.
+ * @param outputSize - If provided, the size will be copied into here as width, height.
+ */
+ size(outputSize?: Point): Point;
+ version(): string;
+}
+
+/**
+ * Options used for Path.stroke(). If an option is omitted, a sensible default will be used.
+ */
+export interface StrokeOpts {
+ /** The width of the stroked lines. */
+ width?: number;
+ miter_limit?: number;
+ /**
+ * if > 1, increase precision, else if (0 < resScale < 1) reduce precision to
+ * favor speed and size
+ */
+ precision?: number;
+ join?: StrokeJoin;
+ cap?: StrokeCap;
+}
+
+export interface StrutStyle {
+ strutEnabled?: boolean;
+ fontFamilies?: string[];
+ fontStyle?: FontStyle;
+ fontSize?: number;
+ heightMultiplier?: number;
+ halfLeading?: boolean;
+ leading?: number;
+ forceStrutHeight?: boolean;
+}
+
+export interface TextFontFeatures {
+ name: string;
+ value: number;
+}
+
+export interface TextFontVariations {
+ axis: string;
+ value: number;
+}
+
+export interface TextShadow {
+ color?: InputColor;
+ /**
+ * 2d array for x and y offset. Defaults to [0, 0]
+ */
+ offset?: number[];
+ blurRadius?: number;
+}
+
+export interface TextStyle {
+ backgroundColor?: InputColor;
+ color?: InputColor;
+ decoration?: number;
+ decorationColor?: InputColor;
+ decorationThickness?: number;
+ decorationStyle?: DecorationStyle;
+ fontFamilies?: string[];
+ fontFeatures?: TextFontFeatures[];
+ fontSize?: number;
+ fontStyle?: FontStyle;
+ fontVariations?: TextFontVariations[];
+ foregroundColor?: InputColor;
+ heightMultiplier?: number;
+ halfLeading?: boolean;
+ letterSpacing?: number;
+ locale?: string;
+ shadows?: TextShadow[];
+ textBaseline?: TextBaseline;
+ wordSpacing?: number;
+}
+
+export interface TonalColorsInput {
+ ambient: InputColor;
+ spot: InputColor;
+}
+
+export interface TonalColorsOutput {
+ ambient: Color;
+ spot: Color;
+}
+
+export interface TypefaceFontProvider extends FontMgr {
+ /**
+ * Registers a given typeface with the given family name (ignoring whatever name the
+ * typface has for itself).
+ * @param bytes - the raw bytes for a typeface.
+ * @param family
+ */
+ registerFont(bytes: ArrayBuffer | Uint8Array, family: string): void;
+}
+
+/**
+ * See FontCollection.h in SkParagraph for more details
+ */
+export interface FontCollection extends EmbindObject<"FontCollection"> {
+ /**
+ * Enable fallback to dynamically discovered fonts for characters that are not handled
+ * by the text style's fonts.
+ */
+ enableFontFallback(): void;
+
+ /**
+ * Set the default provider used to locate fonts.
+ */
+ setDefaultFontManager(fontManager: TypefaceFontProvider | null): void;
+}
+
+export interface URange {
+ start: number;
+ end: number;
+}
+
+/**
+ * Options for configuring a WebGL context. If an option is omitted, a sensible default will
+ * be used. These are defined by the WebGL standards.
+ */
+export interface WebGLOptions {
+ alpha?: number;
+ antialias?: number;
+ depth?: number;
+ enableExtensionsByDefault?: number;
+ explicitSwapControl?: number;
+ failIfMajorPerformanceCaveat?: number;
+ majorVersion?: number;
+ minorVersion?: number;
+ preferLowPowerToHighPerformance?: number;
+ premultipliedAlpha?: number;
+ preserveDrawingBuffer?: number;
+ renderViaOffscreenBackBuffer?: number;
+ stencil?: number;
+}
+
+/**
+ * Options for configuring a canvas WebGPU context. If an option is omitted, a default specified by
+ * the WebGPU standard will be used.
+ */
+export interface WebGPUCanvasOptions {
+ format?: GPUTextureFormat;
+ alphaMode?: GPUCanvasAlphaMode;
+}
+
+export interface DefaultConstructor {
+ new (): T;
+}
+
+export interface ColorMatrixHelpers {
+ /**
+ * Returns a new ColorMatrix that is the result of multiplying outer*inner
+ * @param outer
+ * @param inner
+ */
+ concat(outer: ColorMatrix, inner: ColorMatrix): ColorMatrix;
+
+ /**
+ * Returns an identity ColorMatrix.
+ */
+ identity(): ColorMatrix;
+
+ /**
+ * Sets the 4 "special" params that will translate the colors after they are multiplied
+ * by the 4x4 matrix.
+ * @param m
+ * @param dr - delta red
+ * @param dg - delta green
+ * @param db - delta blue
+ * @param da - delta alpha
+ */
+ postTranslate(
+ m: ColorMatrix,
+ dr: number,
+ dg: number,
+ db: number,
+ da: number
+ ): ColorMatrix;
+
+ /**
+ * Returns a new ColorMatrix that is rotated around a given axis.
+ * @param axis - 0 for red, 1 for green, 2 for blue
+ * @param sine - sin(angle)
+ * @param cosine - cos(angle)
+ */
+ rotated(axis: number, sine: number, cosine: number): ColorMatrix;
+
+ /**
+ * Returns a new ColorMatrix that scales the colors as specified.
+ * @param redScale
+ * @param greenScale
+ * @param blueScale
+ * @param alphaScale
+ */
+ scaled(
+ redScale: number,
+ greenScale: number,
+ blueScale: number,
+ alphaScale: number
+ ): ColorMatrix;
+}
+
+/**
+ * A constructor for making an ImageData that is compatible with the Canvas2D emulation code.
+ */
+export interface ImageDataConstructor {
+ new (width: number, height: number): EmulatedImageData;
+ new (
+ pixels: Uint8ClampedArray,
+ width: number,
+ height: number
+ ): EmulatedImageData;
+}
+
+/**
+ * TODO(kjlubick) Make this API return Float32Arrays
+ */
+export interface Matrix3x3Helpers {
+ /**
+ * Returns a new identity 3x3 matrix.
+ */
+ identity(): number[];
+
+ /**
+ * Returns the inverse of the given 3x3 matrix or null if it is not invertible.
+ * @param m
+ */
+ invert(m: Matrix3x3 | number[]): number[] | null;
+
+ /**
+ * Maps the given 2d points according to the given 3x3 matrix.
+ * @param m
+ * @param points - the flattened points to map; the results are computed in place on this array.
+ */
+ mapPoints(m: Matrix3x3 | number[], points: number[]): number[];
+
+ /**
+ * Multiplies the provided 3x3 matrices together from left to right.
+ * @param matrices
+ */
+ multiply(...matrices: Array): number[];
+
+ /**
+ * Returns a new 3x3 matrix representing a rotation by n radians.
+ * @param radians
+ * @param px - the X value to rotate around, defaults to 0.
+ * @param py - the Y value to rotate around, defaults to 0.
+ */
+ rotated(radians: AngleInRadians, px?: number, py?: number): number[];
+
+ /**
+ * Returns a new 3x3 matrix representing a scale in the x and y directions.
+ * @param sx - the scale in the X direction.
+ * @param sy - the scale in the Y direction.
+ * @param px - the X value to scale from, defaults to 0.
+ * @param py - the Y value to scale from, defaults to 0.
+ */
+ scaled(sx: number, sy: number, px?: number, py?: number): number[];
+
+ /**
+ * Returns a new 3x3 matrix representing a scale in the x and y directions.
+ * @param kx - the kurtosis in the X direction.
+ * @param ky - the kurtosis in the Y direction.
+ * @param px - the X value to skew from, defaults to 0.
+ * @param py - the Y value to skew from, defaults to 0.
+ */
+ skewed(kx: number, ky: number, px?: number, py?: number): number[];
+
+ /**
+ * Returns a new 3x3 matrix representing a translation in the x and y directions.
+ * @param dx
+ * @param dy
+ */
+ translated(dx: number, dy: number): number[];
+}
+
+/**
+ * See SkM44.h for more details.
+ */
+export interface Matrix4x4Helpers {
+ /**
+ * Returns a new identity 4x4 matrix.
+ */
+ identity(): number[];
+
+ /**
+ * Returns the inverse of the given 4x4 matrix or null if it is not invertible.
+ * @param matrix
+ */
+ invert(matrix: Matrix4x4 | number[]): number[] | null;
+
+ /**
+ * Return a new 4x4 matrix representing a camera at eyeVec, pointed at centerVec.
+ * @param eyeVec
+ * @param centerVec
+ * @param upVec
+ */
+ lookat(eyeVec: Vector3, centerVec: Vector3, upVec: Vector3): number[];
+
+ /**
+ * Multiplies the provided 4x4 matrices together from left to right.
+ * @param matrices
+ */
+ multiply(...matrices: Array): number[];
+
+ /**
+ * Returns the inverse of the given 4x4 matrix or throws if it is not invertible.
+ * @param matrix
+ */
+ mustInvert(matrix: Matrix4x4 | number[]): number[];
+
+ /**
+ * Returns a new 4x4 matrix representing a perspective.
+ * @param near
+ * @param far
+ * @param radians
+ */
+ perspective(near: number, far: number, radians: AngleInRadians): number[];
+
+ /**
+ * Returns the value at the specified row and column of the given 4x4 matrix.
+ * @param matrix
+ * @param row
+ * @param col
+ */
+ rc(matrix: Matrix4x4 | number[], row: number, col: number): number;
+
+ /**
+ * Returns a new 4x4 matrix representing a rotation around the provided vector.
+ * @param axis
+ * @param radians
+ */
+ rotated(axis: Vector3, radians: AngleInRadians): number[];
+
+ /**
+ * Returns a new 4x4 matrix representing a rotation around the provided vector.
+ * Rotation is provided redundantly as both sin and cos values.
+ * This rotate can be used when you already have the cosAngle and sinAngle values
+ * so you don't have to atan(cos/sin) to call roatated() which expects an angle in radians.
+ * This does no checking! Behavior for invalid sin or cos values or non-normalized axis vectors
+ * is incorrect. Prefer rotated().
+ * @param axis
+ * @param sinAngle
+ * @param cosAngle
+ */
+ rotatedUnitSinCos(
+ axis: Vector3,
+ sinAngle: number,
+ cosAngle: number
+ ): number[];
+
+ /**
+ * Returns a new 4x4 matrix representing a scale by the provided vector.
+ * @param vec
+ */
+ scaled(vec: Vector3): number[];
+
+ /**
+ * Returns a new 4x4 matrix that sets up a 3D perspective view from a given camera.
+ * @param area - describes the viewport. (0, 0, canvas_width, canvas_height) suggested.
+ * @param zScale - describes the scale of the z axis. min(width, height)/2 suggested
+ * @param cam
+ */
+ setupCamera(area: InputRect, zScale: number, cam: Camera): number[];
+
+ /**
+ * Returns a new 4x4 matrix representing a translation by the provided vector.
+ * @param vec
+ */
+ translated(vec: Vector3): number[];
+
+ /**
+ * Returns a new 4x4 matrix that is the transpose of this 4x4 matrix.
+ * @param matrix
+ */
+ transpose(matrix: Matrix4x4 | number[]): number[];
+}
+
+/**
+ * For more information, see SkBlender.h.
+ */
+export interface BlenderFactory {
+ /**
+ * Create a blender that implements the specified BlendMode.
+ * @param mode
+ */
+ Mode(mode: BlendMode): Blender;
+}
+
+export interface ParagraphBuilderFactory {
+ /**
+ * Creates a ParagraphBuilder using the fonts available from the given font manager.
+ * @param style
+ * @param fontManager
+ */
+ Make(style: ParagraphStyle, fontManager: FontMgr): ParagraphBuilder;
+
+ /**
+ * Creates a ParagraphBuilder using the fonts available from the given font provider.
+ * @param style
+ * @param fontSrc
+ */
+ MakeFromFontProvider(
+ style: ParagraphStyle,
+ fontSrc: TypefaceFontProvider
+ ): ParagraphBuilder;
+
+ /**
+ * Creates a ParagraphBuilder using the given font collection.
+ * @param style
+ * @param fontCollection
+ */
+ MakeFromFontCollection(
+ style: ParagraphStyle,
+ fontCollection: FontCollection
+ ): ParagraphBuilder;
+
+ /**
+ * Return a shaped array of lines
+ */
+ ShapeText(text: string, runs: FontBlock[], width?: number): ShapedLine[];
+
+ /**
+ * Whether the paragraph builder requires ICU data to be provided by the
+ * client.
+ */
+ RequiresClientICU(): boolean;
+}
+
+export interface ParagraphStyleConstructor {
+ /**
+ * Fills out all optional fields with defaults. The emscripten bindings complain if there
+ * is a field undefined and it was expecting a float (for example).
+ * @param ps
+ */
+ new (ps: ParagraphStyle): ParagraphStyle;
+}
+
+/**
+ * See SkColorFilter.h for more.
+ */
+export interface ColorFilterFactory {
+ /**
+ * Makes a color filter with the given color, blend mode, and colorSpace.
+ * @param color
+ * @param mode
+ * @param colorSpace - If omitted, will use SRGB
+ */
+ MakeBlend(
+ color: InputColor,
+ mode: BlendMode,
+ colorSpace?: ColorSpace
+ ): ColorFilter;
+
+ /**
+ * Makes a color filter composing two color filters.
+ * @param outer
+ * @param inner
+ */
+ MakeCompose(outer: ColorFilter, inner: ColorFilter): ColorFilter;
+
+ /**
+ * Makes a color filter that is linearly interpolated between two other color filters.
+ * @param t - a float in the range of 0.0 to 1.0.
+ * @param dst
+ * @param src
+ */
+ MakeLerp(t: number, dst: ColorFilter, src: ColorFilter): ColorFilter;
+
+ /**
+ * Makes a color filter that converts between linear colors and sRGB colors.
+ */
+ MakeLinearToSRGBGamma(): ColorFilter;
+
+ /**
+ * Creates a color filter using the provided color matrix.
+ * @param cMatrix
+ */
+ MakeMatrix(cMatrix: InputColorMatrix): ColorFilter;
+
+ /**
+ * Makes a color filter that converts between sRGB colors and linear colors.
+ */
+ MakeSRGBToLinearGamma(): ColorFilter;
+
+ /**
+ * Makes a color filter that multiplies the luma of its input into the alpha channel,
+ * and sets the red, green, and blue channels to zero.
+ */
+ MakeLuma(): ColorFilter;
+}
+
+export interface ContourMeasureIterConstructor {
+ /**
+ * Creates an ContourMeasureIter with the given path.
+ * @param path
+ * @param forceClosed - if path should be forced close before measuring it.
+ * @param resScale - controls the precision of the measure. values > 1 increase the
+ * precision (and possibly slow down the computation).
+ */
+ new (path: Path, forceClosed: boolean, resScale: number): ContourMeasureIter;
+}
+
+/**
+ * See SkFont.h for more.
+ */
+export interface FontConstructor extends DefaultConstructor {
+ /**
+ * Constructs Font with default values with Typeface.
+ * @param face
+ * @param size - font size in points. If not specified, uses a default value.
+ */
+ new (face: Typeface | null, size?: number): Font;
+
+ /**
+ * Constructs Font with default values with Typeface and size in points,
+ * horizontal scale, and horizontal skew. Horizontal scale emulates condensed
+ * and expanded fonts. Horizontal skew emulates oblique fonts.
+ * @param face
+ * @param size
+ * @param scaleX
+ * @param skewX
+ */
+ new (
+ face: Typeface | null,
+ size: number,
+ scaleX: number,
+ skewX: number
+ ): Font;
+}
+
+export interface FontMgrFactory {
+ /**
+ * Create an FontMgr with the created font data. Returns null if buffers was empty.
+ * @param buffers
+ */
+ FromData(...buffers: ArrayBuffer[]): FontMgr | null;
+}
+
+/**
+ * See //include/effects/SkImageFilters.h for more.
+ */
+export interface ImageFilterFactory {
+ /**
+ * Create a filter that takes a BlendMode and uses it to composite the two filters together.
+ *
+ * At least one of background and foreground should be non-null in nearly all circumstances.
+ *
+ * @param blend The blend mode that defines the compositing operation
+ * @param background The Dst pixels used in blending; if null, use the dynamic source image
+ * (e.g. a saved layer).
+ * @param foreground The Src pixels used in blending; if null, use the dynamic source image.
+ */
+ MakeBlend(
+ blend: BlendMode,
+ background: ImageFilter | null,
+ foreground: ImageFilter | null
+ ): ImageFilter;
+
+ /**
+ * Create a filter that blurs its input by the separate X and Y sigmas. The provided tile mode
+ * is used when the blur kernel goes outside the input image.
+ *
+ * @param sigmaX - The Gaussian sigma value for blurring along the X axis.
+ * @param sigmaY - The Gaussian sigma value for blurring along the Y axis.
+ * @param mode
+ * @param input - if null, it will use the dynamic source image (e.g. a saved layer)
+ */
+ MakeBlur(
+ sigmaX: number,
+ sigmaY: number,
+ mode: TileMode,
+ input: ImageFilter | null
+ ): ImageFilter;
+
+ /**
+ * Create a filter that applies the color filter to the input filter results.
+ * @param cf
+ * @param input - if null, it will use the dynamic source image (e.g. a saved layer)
+ */
+ MakeColorFilter(cf: ColorFilter, input: ImageFilter | null): ImageFilter;
+
+ /**
+ * Create a filter that composes 'inner' with 'outer', such that the results of 'inner' are
+ * treated as the source bitmap passed to 'outer'.
+ * If either param is null, the other param will be returned.
+ * @param outer
+ * @param inner - if null, it will use the dynamic source image (e.g. a saved layer)
+ */
+ MakeCompose(
+ outer: ImageFilter | null,
+ inner: ImageFilter | null
+ ): ImageFilter;
+
+ /**
+ * Create a filter that dilates each input pixel's channel values to the max value within the
+ * given radii along the x and y axes.
+ * @param radiusX The distance to dilate along the x axis to either side of each pixel.
+ * @param radiusY The distance to dilate along the y axis to either side of each pixel.
+ * @param input if null, it will use the dynamic source image (e.g. a saved layer).
+ */
+ MakeDilate(
+ radiusX: number,
+ radiusY: number,
+ input: ImageFilter | null
+ ): ImageFilter;
+
+ /**
+ * Create a filter that moves each pixel in its color input based on an (x,y) vector encoded
+ * in its displacement input filter. Two color components of the displacement image are
+ * mapped into a vector as scale * (color[xChannel], color[yChannel]), where the channel
+ * selectors are one of R, G, B, or A.
+ * The mapping takes the 0-255 RGBA values of the image and scales them to be [-0.5 to 0.5],
+ * in a similar fashion to https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDisplacementMap
+ *
+ * At least one of displacement and color should be non-null in nearly all circumstances.
+ *
+ * @param xChannel RGBA channel that encodes the x displacement per pixel.
+ * @param yChannel RGBA channel that encodes the y displacement per pixel.
+ * @param scale Scale applied to displacement extracted from image.
+ * @param displacement The filter defining the displacement image, or null to use source.
+ * @param color The filter providing the color pixels to be displaced, or null to use source.
+ */
+ MakeDisplacementMap(
+ xChannel: ColorChannel,
+ yChannel: ColorChannel,
+ scale: number,
+ displacement: ImageFilter | null,
+ color: ImageFilter | null
+ ): ImageFilter;
+ /**
+ * Create a filter that draws a drop shadow under the input content. This filter produces an
+ * image that includes the inputs' content.
+ * @param dx The X offset of the shadow.
+ * @param dy The Y offset of the shadow.
+ * @param sigmaX The blur radius for the shadow, along the X axis.
+ * @param sigmaY The blur radius for the shadow, along the Y axis.
+ * @param color The color of the drop shadow.
+ * @param input The input filter; if null, it will use the dynamic source image.
+ */
+ MakeDropShadow(
+ dx: number,
+ dy: number,
+ sigmaX: number,
+ sigmaY: number,
+ color: Color,
+ input: ImageFilter | null
+ ): ImageFilter;
+
+ /**
+ * Just like MakeDropShadow, except the input content is not in the resulting image.
+ * @param dx The X offset of the shadow.
+ * @param dy The Y offset of the shadow.
+ * @param sigmaX The blur radius for the shadow, along the X axis.
+ * @param sigmaY The blur radius for the shadow, along the Y axis.
+ * @param color The color of the drop shadow.
+ * @param input The input filter; if null, it will use the dynamic source image.
+ */
+ MakeDropShadowOnly(
+ dx: number,
+ dy: number,
+ sigmaX: number,
+ sigmaY: number,
+ color: Color,
+ input: ImageFilter | null
+ ): ImageFilter;
+
+ /**
+ * Create a filter that erodes each input pixel's channel values to the minimum channel value
+ * within the given radii along the x and y axes.
+ * @param radiusX The distance to erode along the x axis to either side of each pixel.
+ * @param radiusY The distance to erode along the y axis to either side of each pixel.
+ * @param input if null, it will use the dynamic source image (e.g. a saved layer).
+ */
+ MakeErode(
+ radiusX: number,
+ radiusY: number,
+ input: ImageFilter | null
+ ): ImageFilter;
+
+ /**
+ * Create a filter using the given image as a source. Returns null if 'image' is null.
+ *
+ * @param img The image that is output by the filter, subset by 'srcRect'.
+ * @param sampling The sampling to use when drawing the image.
+ */
+ MakeImage(
+ img: Image,
+ sampling: FilterOptions | CubicResampler
+ ): ImageFilter | null;
+
+ /**
+ * Create a filter that draws the 'srcRect' portion of image into 'dstRect' using the given
+ * filter quality. Similar to Canvas.drawImageRect. Returns null if 'image' is null.
+ *
+ * @param img The image that is output by the filter, subset by 'srcRect'.
+ * @param sampling The sampling to use when drawing the image.
+ * @param srcRect The source pixels sampled into 'dstRect'.
+ * @param dstRect The local rectangle to draw the image into.
+ */
+ MakeImage(
+ img: Image,
+ sampling: FilterOptions | CubicResampler,
+ srcRect: InputRect,
+ dstRect: InputRect
+ ): ImageFilter | null;
+
+ /**
+ * Create a filter that transforms the input image by 'matrix'. This matrix transforms the
+ * local space, which means it effectively happens prior to any transformation coming from the
+ * Canvas initiating the filtering.
+ * @param matr
+ * @param sampling
+ * @param input - if null, it will use the dynamic source image (e.g. a saved layer)
+ */
+ MakeMatrixTransform(
+ matr: InputMatrix,
+ sampling: FilterOptions | CubicResampler,
+ input: ImageFilter | null
+ ): ImageFilter;
+
+ /**
+ * Create a filter that offsets the input filter by the given vector.
+ * @param dx The x offset in local space that the image is shifted.
+ * @param dy The y offset in local space that the image is shifted.
+ * @param input The input that will be moved, if null, will use the dynamic source image.
+ */
+ MakeOffset(dx: number, dy: number, input: ImageFilter | null): ImageFilter;
+
+ /**
+ * Transforms a shader into an image filter
+ *
+ * @param shader - The Shader to be transformed
+ */
+ MakeShader(shader: Shader): ImageFilter;
+}
+
+/**
+ * See SkMaskFilter.h for more details.
+ */
+export interface MaskFilterFactory {
+ /**
+ * Create a blur maskfilter
+ * @param style
+ * @param sigma - Standard deviation of the Gaussian blur to apply. Must be > 0.
+ * @param respectCTM - if true the blur's sigma is modified by the CTM.
+ */
+ MakeBlur(style: BlurStyle, sigma: number, respectCTM: boolean): MaskFilter;
+}
+
+/**
+ * Contains the ways to create a Path.
+ */
+export interface PathConstructorAndFactory extends DefaultConstructor {
+ /**
+ * Returns true if the two paths contain equal verbs and equal weights.
+ * @param path1 first path to compate
+ * @param path2 second path to compare
+ * @return true if Path can be interpolated equivalent
+ */
+ CanInterpolate(path1: Path, path2: Path): boolean;
+
+ /**
+ * Creates a new path from the given list of path commands. If this fails, null will be
+ * returned instead.
+ * @param cmds
+ */
+ MakeFromCmds(cmds: InputCommands): Path | null;
+
+ /**
+ * Creates a new path by combining the given paths according to op. If this fails, null will
+ * be returned instead.
+ * @param one
+ * @param two
+ * @param op
+ */
+ MakeFromOp(one: Path, two: Path, op: PathOp): Path | null;
+
+ /**
+ * Interpolates between Path with point array of equal size.
+ * Copy verb array and weights to result, and set result path to a weighted
+ * average of this path array and ending path.
+ *
+ * weight is most useful when between zero (ending path) and
+ * one (this path); will work with values outside of this
+ * range.
+ *
+ * interpolate() returns undefined if path is not
+ * the same size as ending path. Call isInterpolatable() to check Path
+ * compatibility prior to calling interpolate().
+ *
+ * @param start path to interpolate from
+ * @param end path to interpolate with
+ * @param weight contribution of this path, and
+ * one minus contribution of ending path
+ * @return Path replaced by interpolated averages or null if
+ * not interpolatable
+ */
+ MakeFromPathInterpolation(
+ start: Path,
+ end: Path,
+ weight: number
+ ): Path | null;
+
+ /**
+ * Creates a new path from the provided SVG string. If this fails, null will be
+ * returned instead.
+ * @param str
+ */
+ MakeFromSVGString(str: string): Path | null;
+
+ /**
+ * Creates a new path using the provided verbs and associated points and weights. The process
+ * reads the first verb from verbs and then the appropriate number of points from the
+ * FlattenedPointArray (e.g. 2 points for moveTo, 4 points for quadTo, etc). If the verb is
+ * a conic, a weight will be read from the WeightList.
+ * If the data is malformed (e.g. not enough points), the resulting path will be incomplete.
+ * @param verbs - the verbs that create this path, in the order of being drawn.
+ * @param points - represents n points with 2n floats.
+ * @param weights - used if any of the verbs are conics, can be omitted otherwise.
+ */
+ MakeFromVerbsPointsWeights(
+ verbs: VerbList,
+ points: InputFlattenedPointArray,
+ weights?: WeightList
+ ): Path;
+}
+
+/**
+ * See SkPathEffect.h for more details.
+ */
+export interface PathEffectFactory {
+ /**
+ * Returns a PathEffect that can turn sharp corners into rounded corners.
+ * @param radius - if <=0, returns null
+ */
+ MakeCorner(radius: number): PathEffect | null;
+
+ /**
+ * Returns a PathEffect that add dashes to the path.
+ *
+ * See SkDashPathEffect.h for more details.
+ *
+ * @param intervals - even number of entries with even indicies specifying the length of
+ * the "on" intervals, and the odd indices specifying the length of "off".
+ * @param phase - offset length into the intervals array. Defaults to 0.
+ */
+ MakeDash(intervals: number[], phase?: number): PathEffect;
+
+ /**
+ * Returns a PathEffect that breaks path into segments of segLength length, and randomly move
+ * the endpoints away from the original path by a maximum of deviation.
+ * @param segLength - length of the subsegments.
+ * @param dev - limit of the movement of the endpoints.
+ * @param seedAssist - modifies the randomness. See SkDiscretePathEffect.h for more.
+ */
+ MakeDiscrete(segLength: number, dev: number, seedAssist: number): PathEffect;
+
+ /**
+ * Returns a PathEffect that will fill the drawing path with a pattern made by applying
+ * the given matrix to a repeating set of infinitely long lines of the given width.
+ * For example, the scale of the provided matrix will determine how far apart the lines
+ * should be drawn its rotation affects the lines' orientation.
+ * @param width - must be >= 0
+ * @param matrix
+ */
+ MakeLine2D(width: number, matrix: InputMatrix): PathEffect | null;
+
+ /**
+ * Returns a PathEffect which implements dashing by replicating the specified path.
+ * @param path The path to replicate (dash)
+ * @param advance The space between instances of path
+ * @param phase distance (mod advance) along path for its initial position
+ * @param style how to transform path at each point (based on the current
+ * position and tangent)
+ */
+ MakePath1D(
+ path: Path,
+ advance: number,
+ phase: number,
+ style: Path1DEffectStyle
+ ): PathEffect | null;
+
+ /**
+ * Returns a PathEffect that will fill the drawing path with a pattern by repeating the
+ * given path according to the provided matrix. For example, the scale of the matrix
+ * determines how far apart the path instances should be drawn.
+ * @param matrix
+ * @param path
+ */
+ MakePath2D(matrix: InputMatrix, path: Path): PathEffect | null;
+}
+
+/**
+ * See RuntimeEffect.h for more details.
+ */
+export interface DebugTrace extends EmbindObject<"DebugTrace"> {
+ writeTrace(): string;
+}
+
+export interface TracedShader {
+ shader: Shader;
+ debugTrace: DebugTrace;
+}
+
+export interface RuntimeEffectFactory {
+ /**
+ * Compiles a RuntimeEffect from the given shader code.
+ * @param sksl - Source code for a shader written in SkSL
+ * @param callback - will be called with any compilation error. If not provided, errors will
+ * be printed to console.log().
+ */
+ Make(sksl: string, callback?: (err: string) => void): RuntimeEffect | null;
+
+ /**
+ * Compiles a RuntimeEffect from the given blender code.
+ * @param sksl - Source code for a blender written in SkSL
+ * @param callback - will be called with any compilation error. If not provided, errors will
+ * be printed to console.log().
+ */
+ MakeForBlender(
+ sksl: string,
+ callback?: (err: string) => void
+ ): RuntimeEffect | null;
+
+ /**
+ * Adds debug tracing to an existing RuntimeEffect.
+ * @param shader - An already-assembled shader, created with RuntimeEffect.makeShader.
+ * @param traceCoordX - the X coordinate of the device-space pixel to trace
+ * @param traceCoordY - the Y coordinate of the device-space pixel to trace
+ */
+ MakeTraced(
+ shader: Shader,
+ traceCoordX: number,
+ traceCoordY: number
+ ): TracedShader;
+}
+
+/**
+ * For more information, see SkShaders.h.
+ */
+export interface ShaderFactory {
+ /**
+ * Returns a shader that combines the given shaders with a BlendMode.
+ * @param mode
+ * @param one
+ * @param two
+ */
+ MakeBlend(mode: BlendMode, one: Shader, two: Shader): Shader;
+
+ /**
+ * Returns a shader with a given color and colorspace.
+ * @param color
+ * @param space
+ */
+ MakeColor(color: InputColor, space: ColorSpace): Shader;
+
+ /**
+ * Returns a shader with Perlin Fractal Noise.
+ * See SkPerlinNoiseShader.h for more details
+ * @param baseFreqX - base frequency in the X direction; range [0.0, 1.0]
+ * @param baseFreqY - base frequency in the Y direction; range [0.0, 1.0]
+ * @param octaves
+ * @param seed
+ * @param tileW - if this and tileH are non-zero, the frequencies will be modified so that the
+ * noise will be tileable for the given size.
+ * @param tileH - if this and tileW are non-zero, the frequencies will be modified so that the
+ * noise will be tileable for the given size.
+ */
+ MakeFractalNoise(
+ baseFreqX: number,
+ baseFreqY: number,
+ octaves: number,
+ seed: number,
+ tileW: number,
+ tileH: number
+ ): Shader;
+
+ /**
+ * Returns a shader that generates a linear gradient between the two specified points.
+ * See SkGradientShader.h for more.
+ * @param start
+ * @param end
+ * @param colors - colors to be distributed between start and end.
+ * @param pos - May be null. The relative positions of colors. If supplied must be same length
+ * as colors.
+ * @param mode
+ * @param localMatrix
+ * @param flags - By default gradients will interpolate their colors in unpremul space
+ * and then premultiply each of the results. By setting this to 1, the
+ * gradients will premultiply their colors first, and then interpolate
+ * between them.
+ * @param colorSpace
+ */
+ MakeLinearGradient(
+ start: InputPoint,
+ end: InputPoint,
+ colors: InputFlexibleColorArray,
+ pos: number[] | null,
+ mode: TileMode,
+ localMatrix?: InputMatrix,
+ flags?: number,
+ colorSpace?: ColorSpace
+ ): Shader;
+
+ /**
+ * Returns a shader that generates a radial gradient given the center and radius.
+ * See SkGradientShader.h for more.
+ * @param center
+ * @param radius
+ * @param colors - colors to be distributed between the center and edge.
+ * @param pos - May be null. The relative positions of colors. If supplied must be same length
+ * as colors. Range [0.0, 1.0]
+ * @param mode
+ * @param localMatrix
+ * @param flags - 0 to interpolate colors in unpremul, 1 to interpolate colors in premul.
+ * @param colorSpace
+ */
+ MakeRadialGradient(
+ center: InputPoint,
+ radius: number,
+ colors: InputFlexibleColorArray,
+ pos: number[] | null,
+ mode: TileMode,
+ localMatrix?: InputMatrix,
+ flags?: number,
+ colorSpace?: ColorSpace
+ ): Shader;
+
+ /**
+ * Returns a shader that generates a sweep gradient given a center.
+ * See SkGradientShader.h for more.
+ * @param cx
+ * @param cy
+ * @param colors - colors to be distributed around the center, within the provided angles.
+ * @param pos - May be null. The relative positions of colors. If supplied must be same length
+ * as colors. Range [0.0, 1.0]
+ * @param mode
+ * @param localMatrix
+ * @param flags - 0 to interpolate colors in unpremul, 1 to interpolate colors in premul.
+ * @param startAngle - angle corresponding to 0.0. Defaults to 0 degrees.
+ * @param endAngle - angle corresponding to 1.0. Defaults to 360 degrees.
+ * @param colorSpace
+ */
+ MakeSweepGradient(
+ cx: number,
+ cy: number,
+ colors: InputFlexibleColorArray,
+ pos: number[] | null,
+ mode: TileMode,
+ localMatrix?: InputMatrix | null,
+ flags?: number,
+ startAngle?: AngleInDegrees,
+ endAngle?: AngleInDegrees,
+ colorSpace?: ColorSpace
+ ): Shader;
+
+ /**
+ * Returns a shader with Perlin Turbulence.
+ * See SkPerlinNoiseShader.h for more details
+ * @param baseFreqX - base frequency in the X direction; range [0.0, 1.0]
+ * @param baseFreqY - base frequency in the Y direction; range [0.0, 1.0]
+ * @param octaves
+ * @param seed
+ * @param tileW - if this and tileH are non-zero, the frequencies will be modified so that the
+ * noise will be tileable for the given size.
+ * @param tileH - if this and tileW are non-zero, the frequencies will be modified so that the
+ * noise will be tileable for the given size.
+ */
+ MakeTurbulence(
+ baseFreqX: number,
+ baseFreqY: number,
+ octaves: number,
+ seed: number,
+ tileW: number,
+ tileH: number
+ ): Shader;
+
+ /**
+ * Returns a shader that generates a conical gradient given two circles.
+ * See SkGradientShader.h for more.
+ * @param start
+ * @param startRadius
+ * @param end
+ * @param endRadius
+ * @param colors
+ * @param pos
+ * @param mode
+ * @param localMatrix
+ * @param flags
+ * @param colorSpace
+ */
+ MakeTwoPointConicalGradient(
+ start: InputPoint,
+ startRadius: number,
+ end: InputPoint,
+ endRadius: number,
+ colors: InputFlexibleColorArray,
+ pos: number[] | null,
+ mode: TileMode,
+ localMatrix?: InputMatrix,
+ flags?: number,
+ colorSpace?: ColorSpace
+ ): Shader;
+}
+
+/**
+ * See SkTextBlob.h for more details.
+ */
+export interface TextBlobFactory {
+ /**
+ * Return a TextBlob with a single run of text.
+ *
+ * It does not perform typeface fallback for characters not found in the Typeface.
+ * It does not perform kerning or other complex shaping; glyphs are positioned based on their
+ * default advances.
+ * @param glyphs - if using Malloc'd array, be sure to use CanvasKit.MallocGlyphIDs().
+ * @param font
+ */
+ MakeFromGlyphs(glyphs: InputGlyphIDArray, font: Font): TextBlob;
+
+ /**
+ * Returns a TextBlob built from a single run of text with rotation, scale, and translations.
+ *
+ * It uses the default character-to-glyph mapping from the typeface in the font.
+ * @param str
+ * @param rsxforms
+ * @param font
+ */
+ MakeFromRSXform(
+ str: string,
+ rsxforms: InputFlattenedRSXFormArray,
+ font: Font
+ ): TextBlob;
+
+ /**
+ * Returns a TextBlob built from a single run of text with rotation, scale, and translations.
+ *
+ * @param glyphs - if using Malloc'd array, be sure to use CanvasKit.MallocGlyphIDs().
+ * @param rsxforms
+ * @param font
+ */
+ MakeFromRSXformGlyphs(
+ glyphs: InputGlyphIDArray,
+ rsxforms: InputFlattenedRSXFormArray,
+ font: Font
+ ): TextBlob;
+
+ /**
+ * Return a TextBlob with a single run of text.
+ *
+ * It uses the default character-to-glyph mapping from the typeface in the font.
+ * It does not perform typeface fallback for characters not found in the Typeface.
+ * It does not perform kerning or other complex shaping; glyphs are positioned based on their
+ * default advances.
+ * @param str
+ * @param font
+ */
+ MakeFromText(str: string, font: Font): TextBlob;
+
+ /**
+ * Returns a TextBlob that has the glyphs following the contours of the given path.
+ *
+ * It is a convenience wrapper around MakeFromRSXform and ContourMeasureIter.
+ * @param str
+ * @param path
+ * @param font
+ * @param initialOffset - the length in pixels to start along the path.
+ */
+ MakeOnPath(
+ str: string,
+ path: Path,
+ font: Font,
+ initialOffset?: number
+ ): TextBlob;
+}
+
+export interface TextStyleConstructor {
+ /**
+ * Fills out all optional fields with defaults. The emscripten bindings complain if there
+ * is a field undefined and it was expecting a float (for example).
+ * @param ts
+ */
+ new (ts: TextStyle): TextStyle;
+}
+
+export interface SlottableTextPropertyConstructor {
+ /**
+ * Fills out all optional fields with defaults. The emscripten bindings complain if there
+ * is a field undefined and it was expecting a float (for example).
+ * @param text
+ */
+ new (text: SlottableTextProperty): SlottableTextProperty;
+}
+
+export interface TypefaceFactory {
+ /**
+ * By default, CanvasKit has a default monospace typeface compiled in so that text works out
+ * of the box. This returns that typeface if it is available, null otherwise.
+ */
+ GetDefault(): Typeface | null;
+
+ /**
+ * Create a typeface using Freetype from the specified bytes and return it. CanvasKit supports
+ * .ttf, .woff and .woff2 fonts. It returns null if the bytes cannot be decoded.
+ * @param fontData
+ */
+ MakeTypefaceFromData(fontData: ArrayBuffer): Typeface | null;
+ // Legacy
+ MakeFreeTypeFaceFromData(fontData: ArrayBuffer): Typeface | null;
+}
+
+export interface TypefaceFontProviderFactory {
+ /**
+ * Return an empty TypefaceFontProvider
+ */
+ Make(): TypefaceFontProvider;
+}
+
+export interface FontCollectionFactory {
+ /**
+ * Return an empty FontCollection
+ */
+ Make(): FontCollection;
+}
+
+/**
+ * Functions for manipulating vectors. It is Loosely based off of SkV3 in SkM44.h but Skia
+ * also has SkVec2 and Skv4. This combines them and works on vectors of any length.
+ */
+export interface VectorHelpers {
+ /**
+ * Adds 2 vectors together, term by term, returning a new Vector.
+ * @param a
+ * @param b
+ */
+ add(a: VectorN, b: VectorN): VectorN;
+
+ /**
+ * Returns the cross product of the two vectors. Only works for length 3.
+ * @param a
+ * @param b
+ */
+ cross(a: Vector3, b: Vector3): Vector3;
+
+ /**
+ * Returns the length(sub(a, b))
+ * @param a
+ * @param b
+ */
+ dist(a: VectorN, b: VectorN): number;
+
+ /**
+ * Returns the dot product of the two vectors.
+ * @param a
+ * @param b
+ */
+ dot(a: VectorN, b: VectorN): number;
+
+ /**
+ * Returns the length of this vector, which is always positive.
+ * @param v
+ */
+ length(v: VectorN): number;
+
+ /**
+ * Returns the length squared of this vector.
+ * @param v
+ */
+ lengthSquared(v: VectorN): number;
+
+ /**
+ * Returns a new vector which is v multiplied by the scalar s.
+ * @param v
+ * @param s
+ */
+ mulScalar(v: VectorN, s: number): VectorN;
+
+ /**
+ * Returns a normalized vector.
+ * @param v
+ */
+ normalize(v: VectorN): VectorN;
+
+ /**
+ * Subtracts vector b from vector a (termwise).
+ * @param a
+ * @param b
+ */
+ sub(a: VectorN, b: VectorN): VectorN;
+}
+
+/**
+ * A PosTan is a Float32Array of length 4, representing a position and a tangent vector. In order,
+ * the values are [px, py, tx, ty].
+ */
+export type PosTan = Float32Array;
+/**
+ * An Color is represented by 4 floats, typically with values between 0 and 1.0. In order,
+ * the floats correspond to red, green, blue, alpha.
+ */
+export type Color = Float32Array;
+export type ColorInt = number; // deprecated, prefer Color
+/**
+ * An ColorMatrix is a 4x4 color matrix that transforms the 4 color channels
+ * with a 1x4 matrix that post-translates those 4 channels.
+ * For example, the following is the layout with the scale (S) and post-transform
+ * (PT) items indicated.
+ * RS, 0, 0, 0 | RPT
+ * 0, GS, 0, 0 | GPT
+ * 0, 0, BS, 0 | BPT
+ * 0, 0, 0, AS | APT
+ */
+export type ColorMatrix = Float32Array;
+/**
+ * An IRect is represented by 4 ints. In order, the ints correspond to left, top,
+ * right, bottom. See Rect.h for more
+ */
+export type IRect = Int32Array;
+/**
+ * A Point is represented by 2 floats: (x, y).
+ */
+export type Point = Float32Array;
+/**
+ * A Rect is represented by 4 floats. In order, the floats correspond to left, top,
+ * right, bottom. See Rect.h for more
+ */
+export type Rect = Float32Array;
+
+export interface RectWithDirection {
+ rect: Rect;
+ dir: TextDirection;
+}
+
+/**
+ * An RRect (rectangle with rounded corners) is represented by 12 floats. In order, the floats
+ * correspond to left, top, right, bottom and then in pairs, the radiusX, radiusY for upper-left,
+ * upper-right, lower-right, lower-left. See RRect.h for more.
+ */
+export type RRect = Float32Array;
+
+export type WebGLContextHandle = number;
+export type AngleInDegrees = number;
+export type AngleInRadians = number;
+export type SaveLayerFlag = number;
+
+export type TypedArrayConstructor =
+ | Float32ArrayConstructor
+ | Int32ArrayConstructor
+ | Int16ArrayConstructor
+ | Int8ArrayConstructor
+ | Uint32ArrayConstructor
+ | Uint16ArrayConstructor
+ | Uint8ArrayConstructor;
+export type TypedArray =
+ | Float32Array
+ | Int32Array
+ | Int16Array
+ | Int8Array
+ | Uint32Array
+ | Uint16Array
+ | Uint8Array;
+
+export type ColorIntArray = MallocObj | Uint32Array | number[];
+/**
+ * FlattenedPointArray represents n points by 2*n float values. In order, the values should
+ * be the x, y for each point.
+ */
+export type FlattenedPointArray = Float32Array;
+/**
+ * FlattenedRectangleArray represents n rectangles by 4*n float values. In order, the values should
+ * be the top, left, right, bottom point for each rectangle.
+ */
+export type FlattenedRectangleArray = Float32Array;
+
+export type GlyphIDArray = Uint16Array;
+/**
+ * A command is a verb and then any arguments needed to fulfill that path verb.
+ * InputCommands is a flattened structure of one or more of these.
+ * Examples:
+ * [CanvasKit.MOVE_VERB, 0, 10,
+ * CanvasKit.QUAD_VERB, 20, 50, 45, 60,
+ * CanvasKit.LINE_VERB, 30, 40]
+ */
+export type InputCommands = MallocObj | Float32Array | number[];
+/**
+ * VerbList holds verb constants like CanvasKit.MOVE_VERB, CanvasKit.CUBIC_VERB.
+ */
+export type VerbList = MallocObj | Uint8Array | number[];
+/**
+ * WeightList holds weights for conics when making paths.
+ */
+export type WeightList = MallocObj | Float32Array | number[];
+
+export type Matrix4x4 = Float32Array;
+export type Matrix3x3 = Float32Array;
+export type Matrix3x2 = Float32Array;
+
+/**
+ * Vector2 represents an x, y coordinate or vector. It has length 2.
+ */
+export type Vector2 = Point;
+
+/**
+ * Vector3 represents an x, y, z coordinate or vector. It has length 3.
+ */
+export type Vector3 = number[];
+
+/**
+ * VectorN represents a vector of length n.
+ */
+export type VectorN = number[];
+
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory as colors.
+ * Length 4.
+ */
+export type InputColor = MallocObj | Color | number[];
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory as color matrices.
+ * Length 20.
+ */
+export type InputColorMatrix = MallocObj | ColorMatrix | number[];
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory as glyph IDs.
+ * Length n for n glyph IDs.
+ */
+export type InputGlyphIDArray = MallocObj | GlyphIDArray | number[];
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory as flattened points.
+ * Length 2 * n for n points.
+ */
+export type InputFlattenedPointArray =
+ | MallocObj
+ | FlattenedPointArray
+ | number[];
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory as flattened rectangles.
+ * Length 4 * n for n rectangles.
+ */
+export type InputFlattenedRectangleArray =
+ | MallocObj
+ | FlattenedRectangleArray
+ | number[];
+/**
+ * Some APIs accept a flattened array of colors in one of two ways - groups of 4 float values for
+ * r, g, b, a or just integers that have 8 bits for each these. CanvasKit will detect which one
+ * it is and act accordingly. Additionally, this can be an array of Float32Arrays of length 4
+ * (e.g. Color). This is convenient for things like gradients when matching up colors to stops.
+ */
+export type InputFlexibleColorArray =
+ | Float32Array
+ | Uint32Array
+ | Float32Array[];
+/**
+ * CanvasKit APIs accept a Float32Array or a normal array (of length 2) as a Point.
+ */
+export type InputPoint = Point | number[];
+/**
+ * CanvasKit APIs accept all of these matrix types. Under the hood, we generally use 4x4 matrices.
+ */
+export type InputMatrix =
+ | MallocObj
+ | Matrix4x4
+ | Matrix3x3
+ | Matrix3x2
+ | DOMMatrix
+ | number[];
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory as rectangles.
+ * Length 4.
+ */
+export type InputRect = MallocObj | Rect | number[];
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory as (int) rectangles.
+ * Length 4.
+ */
+export type InputIRect = MallocObj | IRect | number[];
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory as rectangles with
+ * rounded corners. Length 12.
+ */
+export type InputRRect = MallocObj | RRect | number[];
+/**
+ * This represents n RSXforms by 4*n float values. In order, the values should
+ * be scos, ssin, tx, ty for each RSXForm. See RSXForm.h for more details.
+ */
+export type InputFlattenedRSXFormArray = MallocObj | Float32Array | number[];
+
+/**
+ * InputVector2 maps to InputPoint, the alias is to not use the word "Point" when not accurate, but
+ * they are in practice the same, a representation of x and y.
+ */
+export type InputVector2 = InputPoint;
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory as a vector of 3 floats.
+ * For example, this is the x, y, z coordinates.
+ */
+export type InputVector3 = MallocObj | Vector3 | Float32Array;
+
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory
+ * for bidi regions. Regions are triples of integers
+ * [startIdx, stopIdx, bidiLevel]
+ * where startIdx is inclusive and stopIdx is exclusive.
+ * Length 3 * n where n is the number of regions.
+ */
+export type InputBidiRegions = MallocObj | Uint32Array | number[];
+
+/**
+ * CanvasKit APIs accept normal arrays, typed arrays, or Malloc'd memory for
+ * words, graphemes or line breaks.
+ */
+export type InputWords = MallocObj | Uint32Array | number[];
+export type InputGraphemes = MallocObj | Uint32Array | number[];
+export type InputLineBreaks = MallocObj | Uint32Array | number[];
+
+/**
+ * These are the types that webGL's texImage2D supports as a way to get data from as a texture.
+ * Not listed, but also supported are https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame
+ */
+export type TextureSource =
+ | TypedArray
+ | HTMLImageElement
+ | HTMLVideoElement
+ | ImageData
+ | ImageBitmap;
+
+export type AlphaType = EmbindEnumEntity;
+export type BlendMode = EmbindEnumEntity;
+export type BlurStyle = EmbindEnumEntity;
+export type ClipOp = EmbindEnumEntity;
+export type ColorChannel = EmbindEnumEntity;
+export type ColorSpace = EmbindObject<"ColorSpace">;
+export type ColorType = EmbindEnumEntity;
+export type EncodedImageFormat = EmbindEnumEntity;
+export type FillType = EmbindEnumEntity;
+export type FilterMode = EmbindEnumEntity;
+export type FontEdging = EmbindEnumEntity;
+export type FontHinting = EmbindEnumEntity;
+export type MipmapMode = EmbindEnumEntity;
+export type PaintStyle = EmbindEnumEntity;
+export type Path1DEffectStyle = EmbindEnumEntity;
+export type PathOp = EmbindEnumEntity;
+export type PointMode = EmbindEnumEntity;
+export type StrokeCap = EmbindEnumEntity;
+export type StrokeJoin = EmbindEnumEntity;
+export type TileMode = EmbindEnumEntity;
+export type VertexMode = EmbindEnumEntity;
+export type InputState = EmbindEnumEntity;
+export type ModifierKey = EmbindEnumEntity;
+
+export type Affinity = EmbindEnumEntity;
+export type DecorationStyle = EmbindEnumEntity;
+export type FontSlant = EmbindEnumEntity;
+export type FontWeight = EmbindEnumEntity;
+export type FontWidth = EmbindEnumEntity;
+export type PlaceholderAlignment = EmbindEnumEntity;
+export type RectHeightStyle = EmbindEnumEntity;
+export type RectWidthStyle = EmbindEnumEntity;
+export type TextAlign = EmbindEnumEntity;
+export type TextBaseline = EmbindEnumEntity;
+export type TextDirection = EmbindEnumEntity;
+export type LineBreakType = EmbindEnumEntity;
+export type TextHeightBehavior = EmbindEnumEntity;
+export type CodeUnitFlags = EmbindEnumEntity;
+
+export interface AffinityEnumValues extends EmbindEnum {
+ Upstream: Affinity;
+ Downstream: Affinity;
+}
+
+export interface AlphaTypeEnumValues extends EmbindEnum {
+ Opaque: AlphaType;
+ Premul: AlphaType;
+ Unpremul: AlphaType;
+}
+
+export interface BlendModeEnumValues extends EmbindEnum {
+ Clear: BlendMode;
+ Src: BlendMode;
+ Dst: BlendMode;
+ SrcOver: BlendMode;
+ DstOver: BlendMode;
+ SrcIn: BlendMode;
+ DstIn: BlendMode;
+ SrcOut: BlendMode;
+ DstOut: BlendMode;
+ SrcATop: BlendMode;
+ DstATop: BlendMode;
+ Xor: BlendMode;
+ Plus: BlendMode;
+ Modulate: BlendMode;
+ Screen: BlendMode;
+ Overlay: BlendMode;
+ Darken: BlendMode;
+ Lighten: BlendMode;
+ ColorDodge: BlendMode;
+ ColorBurn: BlendMode;
+ HardLight: BlendMode;
+ SoftLight: BlendMode;
+ Difference: BlendMode;
+ Exclusion: BlendMode;
+ Multiply: BlendMode;
+ Hue: BlendMode;
+ Saturation: BlendMode;
+ Color: BlendMode;
+ Luminosity: BlendMode;
+}
+
+export interface BlurStyleEnumValues extends EmbindEnum {
+ Normal: BlurStyle;
+ Solid: BlurStyle;
+ Outer: BlurStyle;
+ Inner: BlurStyle;
+}
+
+export interface ClipOpEnumValues extends EmbindEnum {
+ Difference: ClipOp;
+ Intersect: ClipOp;
+}
+
+/**
+ * The currently supported color spaces. These are all singleton values.
+ */
+export interface ColorSpaceEnumValues {
+ // not a typical enum, but effectively like one.
+ // These are all singleton values - don't call delete on them.
+ readonly SRGB: ColorSpace;
+ readonly DISPLAY_P3: ColorSpace;
+ readonly ADOBE_RGB: ColorSpace;
+
+ /**
+ * Returns true if the two color spaces are equal.
+ * @param a
+ * @param b
+ */
+ Equals(a: ColorSpace, b: ColorSpace): boolean;
+}
+
+export interface ColorChannelEnumValues extends EmbindEnum {
+ Red: ColorChannel;
+ Green: ColorChannel;
+ Blue: ColorChannel;
+ Alpha: ColorChannel;
+}
+
+export interface ColorTypeEnumValues extends EmbindEnum {
+ Alpha_8: ColorType;
+ RGB_565: ColorType;
+ RGBA_8888: ColorType;
+ BGRA_8888: ColorType;
+ RGBA_1010102: ColorType;
+ RGB_101010x: ColorType;
+ Gray_8: ColorType;
+ RGBA_F16: ColorType;
+ RGBA_F32: ColorType;
+}
+
+export interface DecorationStyleEnumValues extends EmbindEnum {
+ Solid: DecorationStyle;
+ Double: DecorationStyle;
+ Dotted: DecorationStyle;
+ Dashed: DecorationStyle;
+ Wavy: DecorationStyle;
+}
+
+export interface FillTypeEnumValues extends EmbindEnum {
+ Winding: FillType;
+ EvenOdd: FillType;
+}
+
+export interface FilterModeEnumValues extends EmbindEnum {
+ Linear: FilterMode;
+ Nearest: FilterMode;
+}
+
+export interface FontEdgingEnumValues extends EmbindEnum {
+ Alias: FontEdging;
+ AntiAlias: FontEdging;
+ SubpixelAntiAlias: FontEdging;
+}
+
+export interface FontHintingEnumValues extends EmbindEnum {
+ None: FontHinting;
+ Slight: FontHinting;
+ Normal: FontHinting;
+ Full: FontHinting;
+}
+
+export interface FontSlantEnumValues extends EmbindEnum {
+ Upright: FontSlant;
+ Italic: FontSlant;
+ Oblique: FontSlant;
+}
+
+export interface FontWeightEnumValues extends EmbindEnum {
+ Invisible: FontWeight;
+ Thin: FontWeight;
+ ExtraLight: FontWeight;
+ Light: FontWeight;
+ Normal: FontWeight;
+ Medium: FontWeight;
+ SemiBold: FontWeight;
+ Bold: FontWeight;
+ ExtraBold: FontWeight;
+ Black: FontWeight;
+ ExtraBlack: FontWeight;
+}
+
+export interface FontWidthEnumValues extends EmbindEnum {
+ UltraCondensed: FontWidth;
+ ExtraCondensed: FontWidth;
+ Condensed: FontWidth;
+ SemiCondensed: FontWidth;
+ Normal: FontWidth;
+ SemiExpanded: FontWidth;
+ Expanded: FontWidth;
+ ExtraExpanded: FontWidth;
+ UltraExpanded: FontWidth;
+}
+
+/*
+ * These values can be OR'd together
+ */
+export interface GlyphRunFlagValues {
+ IsWhiteSpace: number;
+}
+
+export interface ImageFormatEnumValues extends EmbindEnum {
+ // TODO(kjlubick) When these are compiled in depending on the availability of the codecs,
+ // be sure to make these nullable.
+ PNG: EncodedImageFormat;
+ JPEG: EncodedImageFormat;
+ WEBP: EncodedImageFormat;
+}
+
+export interface MipmapModeEnumValues extends EmbindEnum {
+ None: MipmapMode;
+ Nearest: MipmapMode;
+ Linear: MipmapMode;
+}
+
+export interface PaintStyleEnumValues extends EmbindEnum {
+ Fill: PaintStyle;
+ Stroke: PaintStyle;
+}
+
+export interface Path1DEffectStyleEnumValues extends EmbindEnum {
+ // Translate the shape to each position
+ Translate: Path1DEffectStyle;
+ // Rotate the shape about its center
+ Rotate: Path1DEffectStyle;
+ // Transform each point and turn lines into curves
+ Morph: Path1DEffectStyle;
+}
+
+export interface PathOpEnumValues extends EmbindEnum {
+ Difference: PathOp;
+ Intersect: PathOp;
+ Union: PathOp;
+ XOR: PathOp;
+ ReverseDifference: PathOp;
+}
+
+export interface PlaceholderAlignmentEnumValues extends EmbindEnum {
+ Baseline: PlaceholderAlignment;
+ AboveBaseline: PlaceholderAlignment;
+ BelowBaseline: PlaceholderAlignment;
+ Top: PlaceholderAlignment;
+ Bottom: PlaceholderAlignment;
+ Middle: PlaceholderAlignment;
+}
+
+export interface CodeUnitFlagsEnumValues extends EmbindEnum {
+ NoCodeUnitFlag: CodeUnitFlags;
+ Whitespace: CodeUnitFlags;
+ Space: CodeUnitFlags;
+ Control: CodeUnitFlags;
+ Ideographic: CodeUnitFlags;
+}
+
+export interface PointModeEnumValues extends EmbindEnum {
+ Points: PointMode;
+ Lines: PointMode;
+ Polygon: PointMode;
+}
+
+export interface RectHeightStyleEnumValues extends EmbindEnum {
+ Tight: RectHeightStyle;
+ Max: RectHeightStyle;
+ IncludeLineSpacingMiddle: RectHeightStyle;
+ IncludeLineSpacingTop: RectHeightStyle;
+ IncludeLineSpacingBottom: RectHeightStyle;
+ Strut: RectHeightStyle;
+}
+
+export interface RectWidthStyleEnumValues extends EmbindEnum {
+ Tight: RectWidthStyle;
+ Max: RectWidthStyle;
+}
+
+export interface StrokeCapEnumValues extends EmbindEnum {
+ Butt: StrokeCap;
+ Round: StrokeCap;
+ Square: StrokeCap;
+}
+
+export interface StrokeJoinEnumValues extends EmbindEnum {
+ Bevel: StrokeJoin;
+ Miter: StrokeJoin;
+ Round: StrokeJoin;
+}
+
+export interface TextAlignEnumValues extends EmbindEnum {
+ Left: TextAlign;
+ Right: TextAlign;
+ Center: TextAlign;
+ Justify: TextAlign;
+ Start: TextAlign;
+ End: TextAlign;
+}
+
+export interface TextBaselineEnumValues extends EmbindEnum {
+ Alphabetic: TextBaseline;
+ Ideographic: TextBaseline;
+}
+
+export interface TextDirectionEnumValues extends EmbindEnum {
+ LTR: TextDirection;
+ RTL: TextDirection;
+}
+
+export interface LineBreakTypeEnumValues extends EmbindEnum {
+ SoftLineBreak: LineBreakType;
+ HardtLineBreak: LineBreakType;
+}
+
+export interface TextHeightBehaviorEnumValues extends EmbindEnum {
+ All: TextHeightBehavior;
+ DisableFirstAscent: TextHeightBehavior;
+ DisableLastDescent: TextHeightBehavior;
+ DisableAll: TextHeightBehavior;
+}
+
+export interface TileModeEnumValues extends EmbindEnum {
+ Clamp: TileMode;
+ Decal: TileMode;
+ Mirror: TileMode;
+ Repeat: TileMode;
+}
+
+export interface VertexModeEnumValues extends EmbindEnum {
+ Triangles: VertexMode;
+ TrianglesStrip: VertexMode;
+ TriangleFan: VertexMode;
+}
+
+export interface InputStateEnumValues extends EmbindEnum {
+ Down: InputState;
+ Up: InputState;
+ Move: InputState;
+ Right: InputState; // fling only
+ Left: InputState; // fling only
+}
+
+export interface ModifierKeyEnumValues extends EmbindEnum {
+ None: ModifierKey;
+ Shift: ModifierKey;
+ Control: ModifierKey;
+ Option: ModifierKey;
+ Command: ModifierKey;
+ FirstPress: ModifierKey;
+}
+
+export type VerticalAlign = EmbindEnumEntity;
+
+export interface VerticalTextAlignEnumValues extends EmbindEnum {
+ Top: VerticalAlign;
+ TopBaseline: VerticalAlign;
+
+ // Skottie vertical alignment extensions
+ // Visual alignement modes -- these are using tight visual bounds for the paragraph.
+ VisualTop: VerticalAlign; // visual top -> text box top
+ VisualCenter: VerticalAlign; // visual center -> text box center
+ VisualBottom: VerticalAlign; // visual bottom -> text box bottom
+}
+
+export type ResizePolicy = EmbindEnumEntity;
+
+export interface ResizePolicyEnumValues extends EmbindEnum {
+ // Use the specified text size.
+ None: ResizePolicy;
+ // Resize the text such that the extent box fits (snuggly) in the text box,
+ // both horizontally and vertically.
+ ScaleToFit: ResizePolicy;
+ // Same kScaleToFit if the text doesn't fit at the specified font size.
+ // Otherwise, same as kNone.
+ DownscaleToFit: ResizePolicy;
+}
diff --git a/.ref/figma/README.md b/.ref/figma/README.md
new file mode 100644
index 0000000000..c57c1d5844
--- /dev/null
+++ b/.ref/figma/README.md
@@ -0,0 +1,4 @@
+# Figma typings reference
+
+- https://github.com/figma/plugin-typings/blob/master/plugin-api.d.ts
+- https://github.com/figma/rest-api-spec/blob/main/dist/api_types.ts
diff --git a/packages/grida-canvas-schema/.ref/figma.d.ts b/.ref/figma/figma-plugin-api.d.ts
similarity index 100%
rename from packages/grida-canvas-schema/.ref/figma.d.ts
rename to .ref/figma/figma-plugin-api.d.ts
diff --git a/.ref/figma/figma-rest-api.d.ts b/.ref/figma/figma-rest-api.d.ts
new file mode 100644
index 0000000000..e8dab55cc7
--- /dev/null
+++ b/.ref/figma/figma-rest-api.d.ts
@@ -0,0 +1,7351 @@
+export type IsLayerTrait = {
+ /**
+ * A string uniquely identifying this node within the document.
+ */
+ id: string
+
+ /**
+ * The name given to the node by the user in the tool.
+ */
+ name: string
+
+ /**
+ * The type of the node
+ */
+ type: string
+
+ /**
+ * Whether or not the node is visible on the canvas.
+ */
+ visible?: boolean
+
+ /**
+ * If true, layer is locked and cannot be edited
+ */
+ locked?: boolean
+
+ /**
+ * Whether the layer is fixed while the parent is scrolling
+ *
+ * @deprecated
+ */
+ isFixed?: boolean
+
+ /**
+ * How layer should be treated when the frame is resized
+ */
+ scrollBehavior: 'SCROLLS' | 'FIXED' | 'STICKY_SCROLLS'
+
+ /**
+ * The rotation of the node, if not 0.
+ */
+ rotation?: number
+
+ /**
+ * A mapping of a layer's property to component property name of component properties attached to
+ * this node. The component property name can be used to look up more information on the
+ * corresponding component's or component set's componentPropertyDefinitions.
+ */
+ componentPropertyReferences?: { [key: string]: string }
+
+ /**
+ * Data written by plugins that is visible only to the plugin that wrote it. Requires the
+ * `pluginData` to include the ID of the plugin.
+ */
+ pluginData?: unknown
+
+ /**
+ * Data written by plugins that is visible to all plugins. Requires the `pluginData` parameter to
+ * include the string "shared".
+ */
+ sharedPluginData?: unknown
+
+ /**
+ * A mapping of field to the variables applied to this field. Most fields will only map to a single
+ * `VariableAlias`. However, for properties like `fills`, `strokes`, `size`, `componentProperties`,
+ * and `textRangeFills`, it is possible to have multiple variables bound to the field.
+ */
+ boundVariables?: {
+ size?: {
+ x?: VariableAlias
+
+ y?: VariableAlias
+ }
+
+ individualStrokeWeights?: {
+ top?: VariableAlias
+
+ bottom?: VariableAlias
+
+ left?: VariableAlias
+
+ right?: VariableAlias
+ }
+
+ characters?: VariableAlias
+
+ itemSpacing?: VariableAlias
+
+ paddingLeft?: VariableAlias
+
+ paddingRight?: VariableAlias
+
+ paddingTop?: VariableAlias
+
+ paddingBottom?: VariableAlias
+
+ visible?: VariableAlias
+
+ topLeftRadius?: VariableAlias
+
+ topRightRadius?: VariableAlias
+
+ bottomLeftRadius?: VariableAlias
+
+ bottomRightRadius?: VariableAlias
+
+ minWidth?: VariableAlias
+
+ maxWidth?: VariableAlias
+
+ minHeight?: VariableAlias
+
+ maxHeight?: VariableAlias
+
+ counterAxisSpacing?: VariableAlias
+
+ opacity?: VariableAlias
+
+ fontFamily?: VariableAlias[]
+
+ fontSize?: VariableAlias[]
+
+ fontStyle?: VariableAlias[]
+
+ fontWeight?: VariableAlias[]
+
+ letterSpacing?: VariableAlias[]
+
+ lineHeight?: VariableAlias[]
+
+ paragraphSpacing?: VariableAlias[]
+
+ paragraphIndent?: VariableAlias[]
+
+ fills?: VariableAlias[]
+
+ strokes?: VariableAlias[]
+
+ componentProperties?: { [key: string]: VariableAlias }
+
+ textRangeFills?: VariableAlias[]
+
+ effects?: VariableAlias[]
+
+ layoutGrids?: VariableAlias[]
+
+ rectangleCornerRadii?: {
+ RECTANGLE_TOP_LEFT_CORNER_RADIUS?: VariableAlias
+
+ RECTANGLE_TOP_RIGHT_CORNER_RADIUS?: VariableAlias
+
+ RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS?: VariableAlias
+
+ RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS?: VariableAlias
+ }
+ }
+
+ /**
+ * A mapping of variable collection ID to mode ID representing the explicitly set modes for this
+ * node.
+ */
+ explicitVariableModes?: { [key: string]: string }
+}
+
+export type HasChildrenTrait = {
+ /**
+ * An array of nodes that are direct children of this node
+ */
+ children: SubcanvasNode[]
+}
+
+export type HasLayoutTrait = {
+ /**
+ * Bounding box of the node in absolute space coordinates.
+ */
+ absoluteBoundingBox: Rectangle | null
+
+ /**
+ * The actual bounds of a node accounting for drop shadows, thick strokes, and anything else that
+ * may fall outside the node's regular bounding box defined in `x`, `y`, `width`, and `height`. The
+ * `x` and `y` inside this property represent the absolute position of the node on the page. This
+ * value will be `null` if the node is invisible.
+ */
+ absoluteRenderBounds: Rectangle | null
+
+ /**
+ * Keep height and width constrained to same ratio.
+ */
+ preserveRatio?: boolean
+
+ /**
+ * Horizontal and vertical layout constraints for node.
+ */
+ constraints?: LayoutConstraint
+
+ /**
+ * The top two rows of a matrix that represents the 2D transform of this node relative to its
+ * parent. The bottom row of the matrix is implicitly always (0, 0, 1). Use to transform coordinates
+ * in geometry. Only present if `geometry=paths` is passed.
+ */
+ relativeTransform?: Transform
+
+ /**
+ * Width and height of element. This is different from the width and height of the bounding box in
+ * that the absolute bounding box represents the element after scaling and rotation. Only present if
+ * `geometry=paths` is passed.
+ */
+ size?: Vector
+
+ /**
+ * Determines if the layer should stretch along the parent's counter axis. This property is only
+ * provided for direct children of auto-layout frames.
+ *
+ * - `INHERIT`
+ * - `STRETCH`
+ *
+ * In previous versions of auto layout, determined how the layer is aligned inside an auto-layout
+ * frame. This property is only provided for direct children of auto-layout frames.
+ *
+ * - `MIN`
+ * - `CENTER`
+ * - `MAX`
+ * - `STRETCH`
+ *
+ * In horizontal auto-layout frames, "MIN" and "MAX" correspond to "TOP" and "BOTTOM". In vertical
+ * auto-layout frames, "MIN" and "MAX" correspond to "LEFT" and "RIGHT".
+ */
+ layoutAlign?: 'INHERIT' | 'STRETCH' | 'MIN' | 'CENTER' | 'MAX'
+
+ /**
+ * This property is applicable only for direct children of auto-layout frames, ignored otherwise.
+ * Determines whether a layer should stretch along the parent's primary axis. A `0` corresponds to a
+ * fixed size and `1` corresponds to stretch.
+ */
+ layoutGrow?: 0 | 1
+
+ /**
+ * Determines whether a layer's size and position should be determined by auto-layout settings or
+ * manually adjustable.
+ */
+ layoutPositioning?: 'AUTO' | 'ABSOLUTE'
+
+ /**
+ * The minimum width of the frame. This property is only applicable for auto-layout frames or direct
+ * children of auto-layout frames.
+ */
+ minWidth?: number
+
+ /**
+ * The maximum width of the frame. This property is only applicable for auto-layout frames or direct
+ * children of auto-layout frames.
+ */
+ maxWidth?: number
+
+ /**
+ * The minimum height of the frame. This property is only applicable for auto-layout frames or
+ * direct children of auto-layout frames.
+ */
+ minHeight?: number
+
+ /**
+ * The maximum height of the frame. This property is only applicable for auto-layout frames or
+ * direct children of auto-layout frames.
+ */
+ maxHeight?: number
+
+ /**
+ * The horizontal sizing setting on this auto-layout frame or frame child.
+ *
+ * - `FIXED`
+ * - `HUG`: only valid on auto-layout frames and text nodes
+ * - `FILL`: only valid on auto-layout frame children
+ */
+ layoutSizingHorizontal?: 'FIXED' | 'HUG' | 'FILL'
+
+ /**
+ * The vertical sizing setting on this auto-layout frame or frame child.
+ *
+ * - `FIXED`
+ * - `HUG`: only valid on auto-layout frames and text nodes
+ * - `FILL`: only valid on auto-layout frame children
+ */
+ layoutSizingVertical?: 'FIXED' | 'HUG' | 'FILL'
+}
+
+export type HasFramePropertiesTrait = {
+ /**
+ * Whether or not this node clip content outside of its bounds
+ */
+ clipsContent: boolean
+
+ /**
+ * Background of the node. This is deprecated, as backgrounds for frames are now in the `fills`
+ * field.
+ *
+ * @deprecated
+ */
+ background?: Paint[]
+
+ /**
+ * Background color of the node. This is deprecated, as frames now support more than a solid color
+ * as a background. Please use the `fills` field instead.
+ *
+ * @deprecated
+ */
+ backgroundColor?: RGBA
+
+ /**
+ * An array of layout grids attached to this node (see layout grids section for more details). GROUP
+ * nodes do not have this attribute
+ */
+ layoutGrids?: LayoutGrid[]
+
+ /**
+ * Whether a node has primary axis scrolling, horizontal or vertical.
+ */
+ overflowDirection?:
+ | 'HORIZONTAL_SCROLLING'
+ | 'VERTICAL_SCROLLING'
+ | 'HORIZONTAL_AND_VERTICAL_SCROLLING'
+ | 'NONE'
+
+ /**
+ * Whether this layer uses auto-layout to position its children.
+ */
+ layoutMode?: 'NONE' | 'HORIZONTAL' | 'VERTICAL'
+
+ /**
+ * Whether the primary axis has a fixed length (determined by the user) or an automatic length
+ * (determined by the layout engine). This property is only applicable for auto-layout frames.
+ */
+ primaryAxisSizingMode?: 'FIXED' | 'AUTO'
+
+ /**
+ * Whether the counter axis has a fixed length (determined by the user) or an automatic length
+ * (determined by the layout engine). This property is only applicable for auto-layout frames.
+ */
+ counterAxisSizingMode?: 'FIXED' | 'AUTO'
+
+ /**
+ * Determines how the auto-layout frame's children should be aligned in the primary axis direction.
+ * This property is only applicable for auto-layout frames.
+ */
+ primaryAxisAlignItems?: 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN'
+
+ /**
+ * Determines how the auto-layout frame's children should be aligned in the counter axis direction.
+ * This property is only applicable for auto-layout frames.
+ */
+ counterAxisAlignItems?: 'MIN' | 'CENTER' | 'MAX' | 'BASELINE'
+
+ /**
+ * The padding between the left border of the frame and its children. This property is only
+ * applicable for auto-layout frames.
+ */
+ paddingLeft?: number
+
+ /**
+ * The padding between the right border of the frame and its children. This property is only
+ * applicable for auto-layout frames.
+ */
+ paddingRight?: number
+
+ /**
+ * The padding between the top border of the frame and its children. This property is only
+ * applicable for auto-layout frames.
+ */
+ paddingTop?: number
+
+ /**
+ * The padding between the bottom border of the frame and its children. This property is only
+ * applicable for auto-layout frames.
+ */
+ paddingBottom?: number
+
+ /**
+ * The distance between children of the frame. Can be negative. This property is only applicable for
+ * auto-layout frames.
+ */
+ itemSpacing?: number
+
+ /**
+ * Determines the canvas stacking order of layers in this frame. When true, the first layer will be
+ * draw on top. This property is only applicable for auto-layout frames.
+ */
+ itemReverseZIndex?: boolean
+
+ /**
+ * Determines whether strokes are included in layout calculations. When true, auto-layout frames
+ * behave like css "box-sizing: border-box". This property is only applicable for auto-layout
+ * frames.
+ */
+ strokesIncludedInLayout?: boolean
+
+ /**
+ * Whether this auto-layout frame has wrapping enabled.
+ */
+ layoutWrap?: 'NO_WRAP' | 'WRAP'
+
+ /**
+ * The distance between wrapped tracks of an auto-layout frame. This property is only applicable for
+ * auto-layout frames with `layoutWrap: "WRAP"`
+ */
+ counterAxisSpacing?: number
+
+ /**
+ * Determines how the auto-layout frame’s wrapped tracks should be aligned in the counter axis
+ * direction. This property is only applicable for auto-layout frames with `layoutWrap: "WRAP"`.
+ */
+ counterAxisAlignContent?: 'AUTO' | 'SPACE_BETWEEN'
+}
+
+export type HasBlendModeAndOpacityTrait = {
+ /**
+ * How this node blends with nodes behind it in the scene (see blend mode section for more details)
+ */
+ blendMode: BlendMode
+
+ /**
+ * Opacity of the node
+ */
+ opacity?: number
+}
+
+export type HasExportSettingsTrait = {
+ /**
+ * An array of export settings representing images to export from the node.
+ */
+ exportSettings?: ExportSetting[]
+}
+
+export type HasGeometryTrait = MinimalFillsTrait &
+ MinimalStrokesTrait & {
+ /**
+ * Map from ID to PaintOverride for looking up fill overrides. To see which regions are overriden,
+ * you must use the `geometry=paths` option. Each path returned may have an `overrideID` which maps
+ * to this table.
+ */
+ fillOverrideTable?: { [key: string]: PaintOverride | null }
+
+ /**
+ * Only specified if parameter `geometry=paths` is used. An array of paths representing the object
+ * fill.
+ */
+ fillGeometry?: Path[]
+
+ /**
+ * Only specified if parameter `geometry=paths` is used. An array of paths representing the object
+ * stroke.
+ */
+ strokeGeometry?: Path[]
+
+ /**
+ * A string enum describing the end caps of vector paths.
+ */
+ strokeCap?:
+ | 'NONE'
+ | 'ROUND'
+ | 'SQUARE'
+ | 'LINE_ARROW'
+ | 'TRIANGLE_ARROW'
+ | 'DIAMOND_FILLED'
+ | 'CIRCLE_FILLED'
+ | 'TRIANGLE_FILLED'
+ | 'WASHI_TAPE_1'
+ | 'WASHI_TAPE_2'
+ | 'WASHI_TAPE_3'
+ | 'WASHI_TAPE_4'
+ | 'WASHI_TAPE_5'
+ | 'WASHI_TAPE_6'
+
+ /**
+ * Only valid if `strokeJoin` is "MITER". The corner angle, in degrees, below which `strokeJoin`
+ * will be set to "BEVEL" to avoid super sharp corners. By default this is 28.96 degrees.
+ */
+ strokeMiterAngle?: number
+ }
+
+export type MinimalFillsTrait = {
+ /**
+ * An array of fill paints applied to the node.
+ */
+ fills: Paint[]
+
+ /**
+ * A mapping of a StyleType to style ID (see Style) of styles present on this node. The style ID can
+ * be used to look up more information about the style in the top-level styles field.
+ */
+ styles?: { [key: string]: string }
+}
+
+export type MinimalStrokesTrait = {
+ /**
+ * An array of stroke paints applied to the node.
+ */
+ strokes?: Paint[]
+
+ /**
+ * The weight of strokes on the node.
+ */
+ strokeWeight?: number
+
+ /**
+ * Position of stroke relative to vector outline, as a string enum
+ *
+ * - `INSIDE`: stroke drawn inside the shape boundary
+ * - `OUTSIDE`: stroke drawn outside the shape boundary
+ * - `CENTER`: stroke drawn centered along the shape boundary
+ */
+ strokeAlign?: 'INSIDE' | 'OUTSIDE' | 'CENTER'
+
+ /**
+ * A string enum with value of "MITER", "BEVEL", or "ROUND", describing how corners in vector paths
+ * are rendered.
+ */
+ strokeJoin?: 'MITER' | 'BEVEL' | 'ROUND'
+
+ /**
+ * An array of floating point numbers describing the pattern of dash length and gap lengths that the
+ * vector stroke will use when drawn.
+ *
+ * For example a value of [1, 2] indicates that the stroke will be drawn with a dash of length 1
+ * followed by a gap of length 2, repeated.
+ */
+ strokeDashes?: number[]
+}
+
+export type IndividualStrokesTrait = {
+ /**
+ * An object including the top, bottom, left, and right stroke weights. Only returned if individual
+ * stroke weights are used.
+ */
+ individualStrokeWeights?: StrokeWeights
+}
+
+export type CornerTrait = {
+ /**
+ * Radius of each corner if a single radius is set for all corners
+ */
+ cornerRadius?: number
+
+ /**
+ * A value that lets you control how "smooth" the corners are. Ranges from 0 to 1. 0 is the default
+ * and means that the corner is perfectly circular. A value of 0.6 means the corner matches the iOS
+ * 7 "squircle" icon shape. Other values produce various other curves.
+ */
+ cornerSmoothing?: number
+
+ /**
+ * Array of length 4 of the radius of each corner of the frame, starting in the top left and
+ * proceeding clockwise.
+ *
+ * Values are given in the order top-left, top-right, bottom-right, bottom-left.
+ */
+ rectangleCornerRadii?: number[]
+}
+
+export type HasEffectsTrait = {
+ /**
+ * An array of effects attached to this node (see effects section for more details)
+ */
+ effects: Effect[]
+}
+
+export type HasMaskTrait = {
+ /**
+ * Does this node mask sibling nodes in front of it?
+ */
+ isMask?: boolean
+
+ /**
+ * If this layer is a mask, this property describes the operation used to mask the layer's siblings.
+ * The value may be one of the following:
+ *
+ * - ALPHA: the mask node's alpha channel will be used to determine the opacity of each pixel in the
+ * masked result.
+ * - VECTOR: if the mask node has visible fill paints, every pixel inside the node's fill regions will
+ * be fully visible in the masked result. If the mask has visible stroke paints, every pixel
+ * inside the node's stroke regions will be fully visible in the masked result.
+ * - LUMINANCE: the luminance value of each pixel of the mask node will be used to determine the
+ * opacity of that pixel in the masked result.
+ */
+ maskType?: 'ALPHA' | 'VECTOR' | 'LUMINANCE'
+
+ /**
+ * True if maskType is VECTOR. This field is deprecated; use maskType instead.
+ *
+ * @deprecated
+ */
+ isMaskOutline?: boolean
+}
+
+export type ComponentPropertiesTrait = {
+ /**
+ * A mapping of name to `ComponentPropertyDefinition` for every component property on this
+ * component. Each property has a type, defaultValue, and other optional values.
+ */
+ componentPropertyDefinitions?: { [key: string]: ComponentPropertyDefinition }
+}
+
+export type TypePropertiesTrait = {
+ /**
+ * The raw characters in the text node.
+ */
+ characters: string
+
+ /**
+ * Style of text including font family and weight.
+ */
+ style: TypeStyle
+
+ /**
+ * The array corresponds to characters in the text box, where each element references the
+ * 'styleOverrideTable' to apply specific styles to each character. The array's length can be less
+ * than or equal to the number of characters due to the removal of trailing zeros. Elements with a
+ * value of 0 indicate characters that use the default type style. If the array is shorter than the
+ * total number of characters, the characters beyond the array's length also use the default style.
+ */
+ characterStyleOverrides: number[]
+
+ /**
+ * Internal property, preserved for backward compatibility. Avoid using this value.
+ */
+ layoutVersion?: number
+
+ /**
+ * Map from ID to TypeStyle for looking up style overrides.
+ */
+ styleOverrideTable: { [key: string]: TypeStyle }
+
+ /**
+ * An array with the same number of elements as lines in the text node, where lines are delimited by
+ * newline or paragraph separator characters. Each element in the array corresponds to the list type
+ * of a specific line. List types are represented as string enums with one of these possible
+ * values:
+ *
+ * - `NONE`: Not a list item.
+ * - `ORDERED`: Text is an ordered list (numbered).
+ * - `UNORDERED`: Text is an unordered list (bulleted).
+ */
+ lineTypes: ('NONE' | 'ORDERED' | 'UNORDERED')[]
+
+ /**
+ * An array with the same number of elements as lines in the text node, where lines are delimited by
+ * newline or paragraph separator characters. Each element in the array corresponds to the
+ * indentation level of a specific line.
+ */
+ lineIndentations: number[]
+}
+
+export type TextPathPropertiesTrait = {
+ /**
+ * The raw characters in the text path node.
+ */
+ characters: string
+
+ /**
+ * Style of text including font family and weight.
+ */
+ style: TextPathTypeStyle
+
+ /**
+ * The array corresponds to characters in the text box, where each element references the
+ * 'styleOverrideTable' to apply specific styles to each character. The array's length can be less
+ * than or equal to the number of characters due to the removal of trailing zeros. Elements with a
+ * value of 0 indicate characters that use the default type style. If the array is shorter than the
+ * total number of characters, the characters beyond the array's length also use the default style.
+ */
+ characterStyleOverrides: number[]
+
+ /**
+ * Internal property, preserved for backward compatibility. Avoid using this value.
+ */
+ layoutVersion?: number
+
+ /**
+ * Map from ID to TextPathTypeStyle for looking up style overrides.
+ */
+ styleOverrideTable: { [key: string]: TextPathTypeStyle }
+}
+
+export type HasTextSublayerTrait = {
+ /**
+ * Text contained within a text box.
+ */
+ characters: string
+}
+
+export type TransitionSourceTrait = {
+ /**
+ * Node ID of node to transition to in prototyping
+ */
+ transitionNodeID?: string
+
+ /**
+ * The duration of the prototyping transition on this node (in milliseconds). This will override the
+ * default transition duration on the prototype, for this node.
+ */
+ transitionDuration?: number
+
+ /**
+ * The easing curve used in the prototyping transition on this node.
+ */
+ transitionEasing?: EasingType
+
+ interactions?: Interaction[]
+}
+
+export type DevStatusTrait = {
+ /**
+ * Represents whether or not a node has a particular handoff (or dev) status applied to it.
+ */
+ devStatus?: {
+ type: 'NONE' | 'READY_FOR_DEV' | 'COMPLETED'
+
+ /**
+ * An optional field where the designer can add more information about the design and what has
+ * changed.
+ */
+ description?: string
+ }
+}
+
+export type AnnotationsTrait = object
+
+export type FrameTraits = IsLayerTrait &
+ HasBlendModeAndOpacityTrait &
+ HasChildrenTrait &
+ HasLayoutTrait &
+ HasFramePropertiesTrait &
+ CornerTrait &
+ HasGeometryTrait &
+ HasExportSettingsTrait &
+ HasEffectsTrait &
+ HasMaskTrait &
+ TransitionSourceTrait &
+ IndividualStrokesTrait &
+ DevStatusTrait &
+ AnnotationsTrait
+
+export type DefaultShapeTraits = IsLayerTrait &
+ HasBlendModeAndOpacityTrait &
+ HasLayoutTrait &
+ HasGeometryTrait &
+ HasExportSettingsTrait &
+ HasEffectsTrait &
+ HasMaskTrait &
+ TransitionSourceTrait
+
+export type CornerRadiusShapeTraits = DefaultShapeTraits & CornerTrait
+
+export type RectangularShapeTraits = DefaultShapeTraits &
+ CornerTrait &
+ IndividualStrokesTrait &
+ AnnotationsTrait
+
+export type Node =
+ | BooleanOperationNode
+ | ComponentNode
+ | ComponentSetNode
+ | ConnectorNode
+ | EllipseNode
+ | EmbedNode
+ | FrameNode
+ | GroupNode
+ | InstanceNode
+ | LineNode
+ | LinkUnfurlNode
+ | RectangleNode
+ | RegularPolygonNode
+ | SectionNode
+ | ShapeWithTextNode
+ | SliceNode
+ | StarNode
+ | StickyNode
+ | TableNode
+ | TableCellNode
+ | TextNode
+ | TextPathNode
+ | TransformGroupNode
+ | VectorNode
+ | WashiTapeNode
+ | WidgetNode
+ | DocumentNode
+ | CanvasNode
+
+export type DocumentNode = {
+ type: 'DOCUMENT'
+
+ children: CanvasNode[]
+} & IsLayerTrait
+
+export type CanvasNode = {
+ type: 'CANVAS'
+
+ children: SubcanvasNode[]
+
+ /**
+ * Background color of the canvas.
+ */
+ backgroundColor: RGBA
+
+ /**
+ * Node ID that corresponds to the start frame for prototypes. This is deprecated with the
+ * introduction of multiple flows. Please use the `flowStartingPoints` field.
+ *
+ * @deprecated
+ */
+ prototypeStartNodeID: string | null
+
+ /**
+ * An array of flow starting points sorted by its position in the prototype settings panel.
+ */
+ flowStartingPoints: FlowStartingPoint[]
+
+ /**
+ * The device used to view a prototype.
+ */
+ prototypeDevice: PrototypeDevice
+
+ /**
+ * The background color of the prototype (currently only supports a single solid color paint).
+ */
+ prototypeBackgrounds?: RGBA[]
+
+ measurements?: Measurement[]
+} & IsLayerTrait &
+ HasExportSettingsTrait
+
+export type SubcanvasNode =
+ | BooleanOperationNode
+ | ComponentNode
+ | ComponentSetNode
+ | ConnectorNode
+ | EllipseNode
+ | EmbedNode
+ | FrameNode
+ | GroupNode
+ | InstanceNode
+ | LineNode
+ | LinkUnfurlNode
+ | RectangleNode
+ | RegularPolygonNode
+ | SectionNode
+ | ShapeWithTextNode
+ | SliceNode
+ | StarNode
+ | StickyNode
+ | TableNode
+ | TableCellNode
+ | TextNode
+ | TextPathNode
+ | TransformGroupNode
+ | VectorNode
+ | WashiTapeNode
+ | WidgetNode
+
+export type BooleanOperationNode = {
+ /**
+ * The type of this node, represented by the string literal "BOOLEAN_OPERATION"
+ */
+ type: 'BOOLEAN_OPERATION'
+
+ /**
+ * A string enum indicating the type of boolean operation applied.
+ */
+ booleanOperation: 'UNION' | 'INTERSECT' | 'SUBTRACT' | 'EXCLUDE'
+} & IsLayerTrait &
+ HasBlendModeAndOpacityTrait &
+ HasChildrenTrait &
+ HasLayoutTrait &
+ HasGeometryTrait &
+ HasExportSettingsTrait &
+ HasEffectsTrait &
+ HasMaskTrait &
+ TransitionSourceTrait
+
+export type SectionNode = {
+ /**
+ * The type of this node, represented by the string literal "SECTION"
+ */
+ type: 'SECTION'
+
+ /**
+ * Whether the contents of the section are visible
+ */
+ sectionContentsHidden: boolean
+} & IsLayerTrait &
+ HasGeometryTrait &
+ HasChildrenTrait &
+ HasLayoutTrait &
+ DevStatusTrait
+
+export type FrameNode = {
+ /**
+ * The type of this node, represented by the string literal "FRAME"
+ */
+ type: 'FRAME'
+} & FrameTraits
+
+export type GroupNode = {
+ /**
+ * The type of this node, represented by the string literal "GROUP"
+ */
+ type: 'GROUP'
+} & FrameTraits
+
+export type ComponentNode = {
+ /**
+ * The type of this node, represented by the string literal "COMPONENT"
+ */
+ type: 'COMPONENT'
+} & FrameTraits &
+ ComponentPropertiesTrait
+
+export type ComponentSetNode = {
+ /**
+ * The type of this node, represented by the string literal "COMPONENT_SET"
+ */
+ type: 'COMPONENT_SET'
+} & FrameTraits &
+ ComponentPropertiesTrait
+
+export type VectorNode = {
+ /**
+ * The type of this node, represented by the string literal "VECTOR"
+ */
+ type: 'VECTOR'
+} & CornerRadiusShapeTraits &
+ AnnotationsTrait
+
+export type StarNode = {
+ /**
+ * The type of this node, represented by the string literal "STAR"
+ */
+ type: 'STAR'
+} & CornerRadiusShapeTraits &
+ AnnotationsTrait
+
+export type LineNode = {
+ /**
+ * The type of this node, represented by the string literal "LINE"
+ */
+ type: 'LINE'
+} & DefaultShapeTraits &
+ AnnotationsTrait
+
+export type EllipseNode = {
+ /**
+ * The type of this node, represented by the string literal "ELLIPSE"
+ */
+ type: 'ELLIPSE'
+
+ arcData: ArcData
+} & DefaultShapeTraits &
+ AnnotationsTrait
+
+export type RegularPolygonNode = {
+ /**
+ * The type of this node, represented by the string literal "REGULAR_POLYGON"
+ */
+ type: 'REGULAR_POLYGON'
+} & CornerRadiusShapeTraits &
+ AnnotationsTrait
+
+export type RectangleNode = {
+ /**
+ * The type of this node, represented by the string literal "RECTANGLE"
+ */
+ type: 'RECTANGLE'
+} & RectangularShapeTraits
+
+export type TextNode = {
+ /**
+ * The type of this node, represented by the string literal "TEXT"
+ */
+ type: 'TEXT'
+} & DefaultShapeTraits &
+ TypePropertiesTrait &
+ AnnotationsTrait
+
+export type TextPathNode = {
+ /**
+ * The type of this node, represented by the string literal "TEXT_PATH"
+ */
+ type: 'TEXT_PATH'
+} & DefaultShapeTraits &
+ TextPathPropertiesTrait
+
+export type TableNode = {
+ /**
+ * The type of this node, represented by the string literal "TABLE"
+ */
+ type: 'TABLE'
+} & IsLayerTrait &
+ HasChildrenTrait &
+ HasLayoutTrait &
+ MinimalStrokesTrait &
+ HasEffectsTrait &
+ HasBlendModeAndOpacityTrait &
+ HasExportSettingsTrait
+
+export type TableCellNode = {
+ /**
+ * The type of this node, represented by the string literal "TABLE_CELL"
+ */
+ type: 'TABLE_CELL'
+} & IsLayerTrait &
+ MinimalFillsTrait &
+ HasLayoutTrait &
+ HasTextSublayerTrait
+
+export type TransformGroupNode = {
+ /**
+ * The type of this node, represented by the string literal "TRANSFORM_GROUP"
+ */
+ type: 'TRANSFORM_GROUP'
+} & FrameTraits
+
+export type SliceNode = {
+ /**
+ * The type of this node, represented by the string literal "SLICE"
+ */
+ type: 'SLICE'
+} & IsLayerTrait
+
+export type InstanceNode = {
+ /**
+ * The type of this node, represented by the string literal "INSTANCE"
+ */
+ type: 'INSTANCE'
+
+ /**
+ * ID of component that this instance came from.
+ */
+ componentId: string
+
+ /**
+ * If true, this node has been marked as exposed to its containing component or component set.
+ */
+ isExposedInstance?: boolean
+
+ /**
+ * IDs of instances that have been exposed to this node's level.
+ */
+ exposedInstances?: string[]
+
+ /**
+ * A mapping of name to `ComponentProperty` for all component properties on this instance. Each
+ * property has a type, value, and other optional values.
+ */
+ componentProperties?: { [key: string]: ComponentProperty }
+
+ /**
+ * An array of all of the fields directly overridden on this instance. Inherited overrides are not
+ * included.
+ */
+ overrides: Overrides[]
+} & FrameTraits
+
+export type EmbedNode = {
+ /**
+ * The type of this node, represented by the string literal "EMBED"
+ */
+ type: 'EMBED'
+} & IsLayerTrait &
+ HasExportSettingsTrait
+
+export type LinkUnfurlNode = {
+ /**
+ * The type of this node, represented by the string literal "LINK_UNFURL"
+ */
+ type: 'LINK_UNFURL'
+} & IsLayerTrait &
+ HasExportSettingsTrait
+
+export type StickyNode = {
+ /**
+ * The type of this node, represented by the string literal "STICKY"
+ */
+ type: 'STICKY'
+
+ /**
+ * If true, author name is visible.
+ */
+ authorVisible?: boolean
+} & IsLayerTrait &
+ HasLayoutTrait &
+ HasBlendModeAndOpacityTrait &
+ MinimalFillsTrait &
+ HasMaskTrait &
+ HasEffectsTrait &
+ HasExportSettingsTrait &
+ HasTextSublayerTrait
+
+export type ShapeWithTextNode = {
+ /**
+ * The type of this node, represented by the string literal "SHAPE_WITH_TEXT"
+ */
+ type: 'SHAPE_WITH_TEXT'
+
+ /**
+ * Geometric shape type. Most shape types have the same name as their tooltip but there are a few
+ * exceptions. ENG_DATABASE: Cylinder, ENG_QUEUE: Horizontal cylinder, ENG_FILE: File, ENG_FOLDER:
+ * Folder.
+ */
+ shapeType: ShapeType
+} & IsLayerTrait &
+ HasLayoutTrait &
+ HasBlendModeAndOpacityTrait &
+ MinimalFillsTrait &
+ HasMaskTrait &
+ HasEffectsTrait &
+ HasExportSettingsTrait &
+ HasTextSublayerTrait &
+ CornerTrait &
+ MinimalStrokesTrait
+
+export type ConnectorNode = {
+ /**
+ * The type of this node, represented by the string literal "CONNECTOR"
+ */
+ type: 'CONNECTOR'
+
+ /**
+ * The starting point of the connector.
+ */
+ connectorStart: ConnectorEndpoint
+
+ /**
+ * The ending point of the connector.
+ */
+ connectorEnd: ConnectorEndpoint
+
+ /**
+ * A string enum describing the end cap of the start of the connector.
+ */
+ connectorStartStrokeCap:
+ | 'NONE'
+ | 'LINE_ARROW'
+ | 'TRIANGLE_ARROW'
+ | 'DIAMOND_FILLED'
+ | 'CIRCLE_FILLED'
+ | 'TRIANGLE_FILLED'
+
+ /**
+ * A string enum describing the end cap of the end of the connector.
+ */
+ connectorEndStrokeCap:
+ | 'NONE'
+ | 'LINE_ARROW'
+ | 'TRIANGLE_ARROW'
+ | 'DIAMOND_FILLED'
+ | 'CIRCLE_FILLED'
+ | 'TRIANGLE_FILLED'
+
+ /**
+ * Connector line type.
+ */
+ connectorLineType: ConnectorLineType
+
+ /**
+ * Connector text background.
+ */
+ textBackground?: ConnectorTextBackground
+} & IsLayerTrait &
+ HasLayoutTrait &
+ HasBlendModeAndOpacityTrait &
+ HasEffectsTrait &
+ HasExportSettingsTrait &
+ HasTextSublayerTrait &
+ MinimalStrokesTrait
+
+export type WashiTapeNode = {
+ /**
+ * The type of this node, represented by the string literal "WASHI_TAPE"
+ */
+ type: 'WASHI_TAPE'
+} & DefaultShapeTraits
+
+export type WidgetNode = {
+ /**
+ * The type of this node, represented by the string literal "WIDGET"
+ */
+ type: 'WIDGET'
+} & IsLayerTrait &
+ HasExportSettingsTrait &
+ HasChildrenTrait
+
+/**
+ * An RGB color
+ */
+export type RGB = {
+ /**
+ * Red channel value, between 0 and 1.
+ */
+ r: number
+
+ /**
+ * Green channel value, between 0 and 1.
+ */
+ g: number
+
+ /**
+ * Blue channel value, between 0 and 1.
+ */
+ b: number
+}
+
+/**
+ * An RGBA color
+ */
+export type RGBA = {
+ /**
+ * Red channel value, between 0 and 1.
+ */
+ r: number
+
+ /**
+ * Green channel value, between 0 and 1.
+ */
+ g: number
+
+ /**
+ * Blue channel value, between 0 and 1.
+ */
+ b: number
+
+ /**
+ * Alpha channel value, between 0 and 1.
+ */
+ a: number
+}
+
+/**
+ * A flow starting point used when launching a prototype to enter Presentation view.
+ */
+export type FlowStartingPoint = {
+ /**
+ * Unique identifier specifying the frame.
+ */
+ nodeId: string
+
+ /**
+ * Name of flow.
+ */
+ name: string
+}
+
+/**
+ * A width and a height.
+ */
+export type Size = {
+ /**
+ * The width of a size.
+ */
+ width: number
+
+ /**
+ * The height of a size.
+ */
+ height: number
+}
+
+/**
+ * The device used to view a prototype.
+ */
+export type PrototypeDevice = {
+ type: 'NONE' | 'PRESET' | 'CUSTOM' | 'PRESENTATION'
+
+ size?: Size
+
+ presetIdentifier?: string
+
+ rotation: 'NONE' | 'CCW_90'
+}
+
+/**
+ * Sizing constraint for exports.
+ */
+export type Constraint = {
+ /**
+ * Type of constraint to apply:
+ *
+ * - `SCALE`: Scale by `value`.
+ * - `WIDTH`: Scale proportionally and set width to `value`.
+ * - `HEIGHT`: Scale proportionally and set height to `value`.
+ */
+ type: 'SCALE' | 'WIDTH' | 'HEIGHT'
+
+ /**
+ * See type property for effect of this field.
+ */
+ value: number
+}
+
+/**
+ * An export setting.
+ */
+export type ExportSetting = {
+ suffix: string
+
+ format: 'JPG' | 'PNG' | 'SVG' | 'PDF'
+
+ constraint: Constraint
+}
+
+/**
+ * This type is a string enum with the following possible values
+ *
+ * Normal blends:
+ *
+ * - `PASS_THROUGH` (only applicable to objects with children)
+ * - `NORMAL`
+ *
+ * Darken:
+ *
+ * - `DARKEN`
+ * - `MULTIPLY`
+ * - `LINEAR_BURN`
+ * - `COLOR_BURN`
+ *
+ * Lighten:
+ *
+ * - `LIGHTEN`
+ * - `SCREEN`
+ * - `LINEAR_DODGE`
+ * - `COLOR_DODGE`
+ *
+ * Contrast:
+ *
+ * - `OVERLAY`
+ * - `SOFT_LIGHT`
+ * - `HARD_LIGHT`
+ *
+ * Inversion:
+ *
+ * - `DIFFERENCE`
+ * - `EXCLUSION`
+ *
+ * Component:
+ *
+ * - `HUE`
+ * - `SATURATION`
+ * - `COLOR`
+ * - `LUMINOSITY`
+ */
+export type BlendMode =
+ | 'PASS_THROUGH'
+ | 'NORMAL'
+ | 'DARKEN'
+ | 'MULTIPLY'
+ | 'LINEAR_BURN'
+ | 'COLOR_BURN'
+ | 'LIGHTEN'
+ | 'SCREEN'
+ | 'LINEAR_DODGE'
+ | 'COLOR_DODGE'
+ | 'OVERLAY'
+ | 'SOFT_LIGHT'
+ | 'HARD_LIGHT'
+ | 'DIFFERENCE'
+ | 'EXCLUSION'
+ | 'HUE'
+ | 'SATURATION'
+ | 'COLOR'
+ | 'LUMINOSITY'
+
+/**
+ * A 2d vector.
+ */
+export type Vector = {
+ /**
+ * X coordinate of the vector.
+ */
+ x: number
+
+ /**
+ * Y coordinate of the vector.
+ */
+ y: number
+}
+
+/**
+ * A single color stop with its position along the gradient axis, color, and bound variables if any
+ */
+export type ColorStop = {
+ /**
+ * Value between 0 and 1 representing position along gradient axis.
+ */
+ position: number
+
+ /**
+ * Color attached to corresponding position.
+ */
+ color: RGBA
+
+ /**
+ * The variables bound to a particular gradient stop
+ */
+ boundVariables?: { color?: VariableAlias }
+}
+
+/**
+ * A transformation matrix is standard way in computer graphics to represent translation and
+ * rotation. These are the top two rows of a 3x3 matrix. The bottom row of the matrix is assumed to
+ * be [0, 0, 1]. This is known as an affine transform and is enough to represent translation,
+ * rotation, and skew.
+ *
+ * The identity transform is [[1, 0, 0], [0, 1, 0]].
+ *
+ * A translation matrix will typically look like:
+ *
+ * ;[
+ * [1, 0, tx],
+ * [0, 1, ty],
+ * ]
+ *
+ * And a rotation matrix will typically look like:
+ *
+ * ;[
+ * [cos(angle), sin(angle), 0],
+ * [-sin(angle), cos(angle), 0],
+ * ]
+ *
+ * Another way to think about this transform is as three vectors:
+ *
+ * - The x axis (t[0][0], t[1][0])
+ * - The y axis (t[0][1], t[1][1])
+ * - The translation offset (t[0][2], t[1][2])
+ *
+ * The most common usage of the Transform matrix is the `relativeTransform property`. This
+ * particular usage of the matrix has a few additional restrictions. The translation offset can take
+ * on any value but we do enforce that the axis vectors are unit vectors (i.e. have length 1). The
+ * axes are not required to be at 90° angles to each other.
+ */
+export type Transform = number[][]
+
+/**
+ * Image filters to apply to the node.
+ */
+export type ImageFilters = {
+ exposure?: number
+
+ contrast?: number
+
+ saturation?: number
+
+ temperature?: number
+
+ tint?: number
+
+ highlights?: number
+
+ shadows?: number
+}
+
+export type BasePaint = {
+ /**
+ * Is the paint enabled?
+ */
+ visible?: boolean
+
+ /**
+ * Overall opacity of paint (colors within the paint can also have opacity values which would blend
+ * with this)
+ */
+ opacity?: number
+
+ /**
+ * How this node blends with nodes behind it in the scene
+ */
+ blendMode: BlendMode
+}
+
+export type SolidPaint = {
+ /**
+ * The string literal "SOLID" representing the paint's type. Always check the `type` before reading
+ * other properties.
+ */
+ type: 'SOLID'
+
+ /**
+ * Solid color of the paint
+ */
+ color: RGBA
+
+ /**
+ * The variables bound to a particular field on this paint
+ */
+ boundVariables?: { color?: VariableAlias }
+} & BasePaint
+
+export type GradientPaint = {
+ /**
+ * The string literal representing the paint's type. Always check the `type` before reading other
+ * properties.
+ */
+ type: 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND'
+
+ /**
+ * This field contains three vectors, each of which are a position in normalized object space
+ * (normalized object space is if the top left corner of the bounding box of the object is (0, 0)
+ * and the bottom right is (1,1)). The first position corresponds to the start of the gradient
+ * (value 0 for the purposes of calculating gradient stops), the second position is the end of the
+ * gradient (value 1), and the third handle position determines the width of the gradient.
+ */
+ gradientHandlePositions: Vector[]
+
+ /**
+ * Positions of key points along the gradient axis with the colors anchored there. Colors along the
+ * gradient are interpolated smoothly between neighboring gradient stops.
+ */
+ gradientStops: ColorStop[]
+} & BasePaint
+
+export type ImagePaint = {
+ /**
+ * The string literal "IMAGE" representing the paint's type. Always check the `type` before reading
+ * other properties.
+ */
+ type: 'IMAGE'
+
+ /**
+ * Image scaling mode.
+ */
+ scaleMode: 'FILL' | 'FIT' | 'TILE' | 'STRETCH'
+
+ /**
+ * A reference to an image embedded in this node. To download the image using this reference, use
+ * the `GET file images` endpoint to retrieve the mapping from image references to image URLs.
+ */
+ imageRef: string
+
+ /**
+ * Affine transform applied to the image, only present if `scaleMode` is `STRETCH`
+ */
+ imageTransform?: Transform
+
+ /**
+ * Amount image is scaled by in tiling, only present if scaleMode is `TILE`.
+ */
+ scalingFactor?: number
+
+ /**
+ * Defines what image filters have been applied to this paint, if any. If this property is not
+ * defined, no filters have been applied.
+ */
+ filters?: ImageFilters
+
+ /**
+ * Image rotation, in degrees.
+ */
+ rotation?: number
+
+ /**
+ * A reference to an animated GIF embedded in this node. To download the image using this reference,
+ * use the `GET file images` endpoint to retrieve the mapping from image references to image URLs.
+ */
+ gifRef?: string
+} & BasePaint
+
+export type PatternPaint = {
+ /**
+ * The string literal "PATTERN" representing the paint's type. Always check the `type` before
+ * reading other properties.
+ */
+ type: 'PATTERN'
+
+ /**
+ * The node id of the source node for the pattern
+ */
+ sourceNodeId: string
+
+ /**
+ * The tile type for the pattern
+ */
+ tileType: 'RECTANGULAR' | 'HORIZONTAL_HEXAGONAL' | 'VERTICAL_HEXAGONAL'
+
+ /**
+ * The scaling factor for the pattern
+ */
+ scalingFactor: number
+
+ /**
+ * The spacing for the pattern
+ */
+ spacing: Vector
+
+ /**
+ * The horizontal alignment for the pattern
+ */
+ horizontalAlignment: 'START' | 'CENTER' | 'END'
+
+ /**
+ * The vertical alignment for the pattern
+ */
+ verticalAlignment: 'START' | 'CENTER' | 'END'
+} & BasePaint
+
+export type Paint = SolidPaint | GradientPaint | ImagePaint | PatternPaint
+
+/**
+ * Layout constraint relative to containing Frame
+ */
+export type LayoutConstraint = {
+ /**
+ * Vertical constraint (relative to containing frame) as an enum:
+ *
+ * - `TOP`: Node is laid out relative to top of the containing frame
+ * - `BOTTOM`: Node is laid out relative to bottom of the containing frame
+ * - `CENTER`: Node is vertically centered relative to containing frame
+ * - `TOP_BOTTOM`: Both top and bottom of node are constrained relative to containing frame (node
+ * stretches with frame)
+ * - `SCALE`: Node scales vertically with containing frame
+ */
+ vertical: 'TOP' | 'BOTTOM' | 'CENTER' | 'TOP_BOTTOM' | 'SCALE'
+
+ /**
+ * Horizontal constraint (relative to containing frame) as an enum:
+ *
+ * - `LEFT`: Node is laid out relative to left of the containing frame
+ * - `RIGHT`: Node is laid out relative to right of the containing frame
+ * - `CENTER`: Node is horizontally centered relative to containing frame
+ * - `LEFT_RIGHT`: Both left and right of node are constrained relative to containing frame (node
+ * stretches with frame)
+ * - `SCALE`: Node scales horizontally with containing frame
+ */
+ horizontal: 'LEFT' | 'RIGHT' | 'CENTER' | 'LEFT_RIGHT' | 'SCALE'
+}
+
+/**
+ * A rectangle that expresses a bounding box in absolute coordinates.
+ */
+export type Rectangle = {
+ /**
+ * X coordinate of top left corner of the rectangle.
+ */
+ x: number
+
+ /**
+ * Y coordinate of top left corner of the rectangle.
+ */
+ y: number
+
+ /**
+ * Width of the rectangle.
+ */
+ width: number
+
+ /**
+ * Height of the rectangle.
+ */
+ height: number
+}
+
+/**
+ * Guides to align and place objects within a frames.
+ */
+export type LayoutGrid = {
+ /**
+ * Orientation of the grid as a string enum
+ *
+ * - `COLUMNS`: Vertical grid
+ * - `ROWS`: Horizontal grid
+ * - `GRID`: Square grid
+ */
+ pattern: 'COLUMNS' | 'ROWS' | 'GRID'
+
+ /**
+ * Width of column grid or height of row grid or square grid spacing.
+ */
+ sectionSize: number
+
+ /**
+ * Is the grid currently visible?
+ */
+ visible: boolean
+
+ /**
+ * Color of the grid
+ */
+ color: RGBA
+
+ /**
+ * Positioning of grid as a string enum
+ *
+ * - `MIN`: Grid starts at the left or top of the frame
+ * - `MAX`: Grid starts at the right or bottom of the frame
+ * - `STRETCH`: Grid is stretched to fit the frame
+ * - `CENTER`: Grid is center aligned
+ */
+ alignment: 'MIN' | 'MAX' | 'STRETCH' | 'CENTER'
+
+ /**
+ * Spacing in between columns and rows
+ */
+ gutterSize: number
+
+ /**
+ * Spacing before the first column or row
+ */
+ offset: number
+
+ /**
+ * Number of columns or rows
+ */
+ count: number
+
+ /**
+ * The variables bound to a particular field on this layout grid
+ */
+ boundVariables?: {
+ gutterSize?: VariableAlias
+
+ numSections?: VariableAlias
+
+ sectionSize?: VariableAlias
+
+ offset?: VariableAlias
+ }
+}
+
+/**
+ * Base properties shared by all shadow effects
+ */
+export type BaseShadowEffect = {
+ /**
+ * The color of the shadow
+ */
+ color: RGBA
+
+ /**
+ * Blend mode of the shadow
+ */
+ blendMode: BlendMode
+
+ /**
+ * How far the shadow is projected in the x and y directions
+ */
+ offset: Vector
+
+ /**
+ * Radius of the blur effect (applies to shadows as well)
+ */
+ radius: number
+
+ /**
+ * The distance by which to expand (or contract) the shadow.
+ *
+ * For drop shadows, a positive `spread` value creates a shadow larger than the node, whereas a
+ * negative value creates a shadow smaller than the node.
+ *
+ * For inner shadows, a positive `spread` value contracts the shadow. Spread values are only
+ * accepted on rectangles and ellipses, or on frames, components, and instances with visible fill
+ * paints and `clipsContent` enabled. When left unspecified, the default value is 0.
+ */
+ spread?: number
+
+ /**
+ * Whether this shadow is visible.
+ */
+ visible: boolean
+
+ /**
+ * The variables bound to a particular field on this shadow effect
+ */
+ boundVariables?: {
+ radius?: VariableAlias
+
+ spread?: VariableAlias
+
+ color?: VariableAlias
+
+ offsetX?: VariableAlias
+
+ offsetY?: VariableAlias
+ }
+}
+
+export type DropShadowEffect = {
+ /**
+ * A string literal representing the effect's type. Always check the type before reading other
+ * properties.
+ */
+ type: 'DROP_SHADOW'
+
+ /**
+ * Whether to show the shadow behind translucent or transparent pixels
+ */
+ showShadowBehindNode: boolean
+} & BaseShadowEffect
+
+export type InnerShadowEffect = {
+ /**
+ * A string literal representing the effect's type. Always check the type before reading other
+ * properties.
+ */
+ type?: 'INNER_SHADOW'
+} & BaseShadowEffect
+
+export type BlurEffect = NormalBlurEffect | ProgressiveBlurEffect
+
+/**
+ * Base properties shared by all blur effects
+ */
+export type BaseBlurEffect = {
+ /**
+ * A string literal representing the effect's type. Always check the type before reading other
+ * properties.
+ */
+ type: 'LAYER_BLUR' | 'BACKGROUND_BLUR'
+
+ /**
+ * Whether this blur is active.
+ */
+ visible: boolean
+
+ /**
+ * Radius of the blur effect
+ */
+ radius: number
+
+ /**
+ * The variables bound to a particular field on this blur effect
+ */
+ boundVariables?: { radius?: VariableAlias }
+}
+
+export type NormalBlurEffect = {
+ /**
+ * The string literal 'NORMAL' representing the blur type. Always check the blurType before reading
+ * other properties.
+ */
+ blurType?: 'NORMAL'
+} & BaseBlurEffect
+
+export type ProgressiveBlurEffect = {
+ /**
+ * The string literal 'PROGRESSIVE' representing the blur type. Always check the blurType before
+ * reading other properties.
+ */
+ blurType: 'PROGRESSIVE'
+
+ /**
+ * The starting radius of the progressive blur
+ */
+ startRadius: number
+
+ /**
+ * The starting offset of the progressive blur
+ */
+ startOffset: Vector
+
+ /**
+ * The ending offset of the progressive blur
+ */
+ endOffset: Vector
+} & BaseBlurEffect
+
+/**
+ * A texture effect
+ */
+export type TextureEffect = {
+ /**
+ * The string literal 'TEXTURE' representing the effect's type. Always check the type before reading
+ * other properties.
+ */
+ type: 'TEXTURE'
+
+ /**
+ * The size of the texture effect
+ */
+ noiseSize: number
+
+ /**
+ * The radius of the texture effect
+ */
+ radius: number
+
+ /**
+ * Whether the texture is clipped to the shape
+ */
+ clipToShape: boolean
+}
+
+export type MonotoneNoiseEffect = {
+ /**
+ * The string literal 'MONOTONE' representing the noise type.
+ */
+ noiseType: 'MONOTONE'
+} & BaseNoiseEffect
+
+export type MultitoneNoiseEffect = {
+ /**
+ * The string literal 'MULTITONE' representing the noise type.
+ */
+ noiseType: 'MULTITONE'
+
+ /**
+ * The opacity of the noise effect
+ */
+ opacity: number
+} & BaseNoiseEffect
+
+export type DuotoneNoiseEffect = {
+ /**
+ * The string literal 'DUOTONE' representing the noise type.
+ */
+ noiseType: 'DUOTONE'
+
+ /**
+ * The secondary color of the noise effect
+ */
+ secondaryColor: RGBA
+} & BaseNoiseEffect
+
+/**
+ * A noise effect
+ */
+export type BaseNoiseEffect = {
+ /**
+ * The string literal 'NOISE' representing the effect's type. Always check the type before reading
+ * other properties.
+ */
+ type: 'NOISE'
+
+ /**
+ * Blend mode of the noise effect
+ */
+ blendMode: BlendMode
+
+ /**
+ * The size of the noise effect
+ */
+ noiseSize: number
+
+ /**
+ * The density of the noise effect
+ */
+ density: number
+}
+
+export type NoiseEffect = MonotoneNoiseEffect | MultitoneNoiseEffect | DuotoneNoiseEffect
+
+export type Effect = DropShadowEffect | InnerShadowEffect | BlurEffect | TextureEffect | NoiseEffect
+
+/**
+ * A set of properties that can be applied to nodes and published. Styles for a property can be
+ * created in the corresponding property's panel while editing a file.
+ */
+export type Style = {
+ /**
+ * The key of the style
+ */
+ key: string
+
+ /**
+ * Name of the style
+ */
+ name: string
+
+ /**
+ * Description of the style
+ */
+ description: string
+
+ /**
+ * Whether this style is a remote style that doesn't live in this file
+ */
+ remote: boolean
+
+ styleType: StyleType
+}
+
+/**
+ * This type is a string enum with the following possible values:
+ *
+ * - `EASE_IN`: Ease in with an animation curve similar to CSS ease-in.
+ * - `EASE_OUT`: Ease out with an animation curve similar to CSS ease-out.
+ * - `EASE_IN_AND_OUT`: Ease in and then out with an animation curve similar to CSS ease-in-out.
+ * - `LINEAR`: No easing, similar to CSS linear.
+ * - `EASE_IN_BACK`: Ease in with an animation curve that moves past the initial keyframe's value and
+ * then accelerates as it reaches the end.
+ * - `EASE_OUT_BACK`: Ease out with an animation curve that starts fast, then slows and goes past the
+ * ending keyframe's value.
+ * - `EASE_IN_AND_OUT_BACK`: Ease in and then out with an animation curve that overshoots the initial
+ * keyframe's value, then accelerates quickly before it slows and overshoots the ending keyframes
+ * value.
+ * - `CUSTOM_CUBIC_BEZIER`: User-defined cubic bezier curve.
+ * - `GENTLE`: Gentle animation similar to react-spring.
+ * - `QUICK`: Quick spring animation, great for toasts and notifications.
+ * - `BOUNCY`: Bouncy spring, for delightful animations like a heart bounce.
+ * - `SLOW`: Slow spring, useful as a steady, natural way to scale up fullscreen content.
+ * - `CUSTOM_SPRING`: User-defined spring animation.
+ */
+export type EasingType =
+ | 'EASE_IN'
+ | 'EASE_OUT'
+ | 'EASE_IN_AND_OUT'
+ | 'LINEAR'
+ | 'EASE_IN_BACK'
+ | 'EASE_OUT_BACK'
+ | 'EASE_IN_AND_OUT_BACK'
+ | 'CUSTOM_CUBIC_BEZIER'
+ | 'GENTLE'
+ | 'QUICK'
+ | 'BOUNCY'
+ | 'SLOW'
+ | 'CUSTOM_SPRING'
+
+/**
+ * Individual stroke weights
+ */
+export type StrokeWeights = {
+ /**
+ * The top stroke weight.
+ */
+ top: number
+
+ /**
+ * The right stroke weight.
+ */
+ right: number
+
+ /**
+ * The bottom stroke weight.
+ */
+ bottom: number
+
+ /**
+ * The left stroke weight.
+ */
+ left: number
+}
+
+/**
+ * Paint metadata to override default paints.
+ */
+export type PaintOverride = {
+ /**
+ * Paints applied to characters.
+ */
+ fills?: Paint[]
+
+ /**
+ * ID of style node, if any, that this inherits fill data from.
+ */
+ inheritFillStyleId?: string
+}
+
+/**
+ * Defines a single path
+ */
+export type Path = {
+ /**
+ * A series of path commands that encodes how to draw the path.
+ */
+ path: string
+
+ /**
+ * The winding rule for the path (same as in SVGs). This determines whether a given point in space
+ * is inside or outside the path.
+ */
+ windingRule: 'NONZERO' | 'EVENODD'
+
+ /**
+ * If there is a per-region fill, this refers to an ID in the `fillOverrideTable`.
+ */
+ overrideID?: number
+}
+
+/**
+ * Information about the arc properties of an ellipse. 0° is the x axis and increasing angles rotate
+ * clockwise.
+ */
+export type ArcData = {
+ /**
+ * Start of the sweep in radians.
+ */
+ startingAngle: number
+
+ /**
+ * End of the sweep in radians.
+ */
+ endingAngle: number
+
+ /**
+ * Inner radius value between 0 and 1
+ */
+ innerRadius: number
+}
+
+/**
+ * A link to either a URL or another frame (node) in the document.
+ */
+export type Hyperlink = {
+ /**
+ * The type of hyperlink. Can be either `URL` or `NODE`.
+ */
+ type: 'URL' | 'NODE'
+
+ /**
+ * The URL that the hyperlink points to, if `type` is `URL`.
+ */
+ url?: string
+
+ /**
+ * The ID of the node that the hyperlink points to, if `type` is `NODE`.
+ */
+ nodeID?: string
+}
+
+export type BaseTypeStyle = {
+ /**
+ * Font family of text (standard name).
+ */
+ fontFamily?: string
+
+ /**
+ * PostScript font name.
+ */
+ fontPostScriptName?: string | null
+
+ /**
+ * Describes visual weight or emphasis, such as Bold or Italic.
+ */
+ fontStyle?: string
+
+ /**
+ * Whether or not text is italicized.
+ */
+ italic?: boolean
+
+ /**
+ * Numeric font weight.
+ */
+ fontWeight?: number
+
+ /**
+ * Font size in px.
+ */
+ fontSize?: number
+
+ /**
+ * Text casing applied to the node, default is the original casing.
+ */
+ textCase?: 'UPPER' | 'LOWER' | 'TITLE' | 'SMALL_CAPS' | 'SMALL_CAPS_FORCED'
+
+ /**
+ * Horizontal text alignment as string enum.
+ */
+ textAlignHorizontal?: 'LEFT' | 'RIGHT' | 'CENTER' | 'JUSTIFIED'
+
+ /**
+ * Vertical text alignment as string enum.
+ */
+ textAlignVertical?: 'TOP' | 'CENTER' | 'BOTTOM'
+
+ /**
+ * Space between characters in px.
+ */
+ letterSpacing?: number
+
+ /**
+ * An array of fill paints applied to the characters.
+ */
+ fills?: Paint[]
+
+ /**
+ * Link to a URL or frame.
+ */
+ hyperlink?: Hyperlink
+
+ /**
+ * A map of OpenType feature flags to 1 or 0, 1 if it is enabled and 0 if it is disabled. Note that
+ * some flags aren't reflected here. For example, SMCP (small caps) is still represented by the
+ * `textCase` field.
+ */
+ opentypeFlags?: { [key: string]: number }
+
+ /**
+ * Indicates how the font weight was overridden when there is a text style override.
+ */
+ semanticWeight?: 'BOLD' | 'NORMAL'
+
+ /**
+ * Indicates how the font style was overridden when there is a text style override.
+ */
+ semanticItalic?: 'ITALIC' | 'NORMAL'
+}
+
+export type TypeStyle = {
+ /**
+ * Space between paragraphs in px, 0 if not present.
+ */
+ paragraphSpacing?: number
+
+ /**
+ * Paragraph indentation in px, 0 if not present.
+ */
+ paragraphIndent?: number
+
+ /**
+ * Space between list items in px, 0 if not present.
+ */
+ listSpacing?: number
+
+ /**
+ * Text decoration applied to the node, default is none.
+ */
+ textDecoration?: 'NONE' | 'STRIKETHROUGH' | 'UNDERLINE'
+
+ /**
+ * Dimensions along which text will auto resize, default is that the text does not auto-resize.
+ * TRUNCATE means that the text will be shortened and trailing text will be replaced with "…" if the
+ * text contents is larger than the bounds. `TRUNCATE` as a return value is deprecated and will be
+ * removed in a future version. Read from `textTruncation` instead.
+ */
+ textAutoResize?: 'NONE' | 'WIDTH_AND_HEIGHT' | 'HEIGHT' | 'TRUNCATE'
+
+ /**
+ * Whether this text node will truncate with an ellipsis when the text contents is larger than the
+ * text node.
+ */
+ textTruncation?: 'DISABLED' | 'ENDING'
+
+ /**
+ * When `textTruncation: "ENDING"` is set, `maxLines` determines how many lines a text node can grow
+ * to before it truncates.
+ */
+ maxLines?: number
+
+ /**
+ * Line height in px.
+ */
+ lineHeightPx?: number
+
+ /**
+ * Line height as a percentage of normal line height. This is deprecated; in a future version of the
+ * API only lineHeightPx and lineHeightPercentFontSize will be returned.
+ */
+ lineHeightPercent?: number
+
+ /**
+ * Line height as a percentage of the font size. Only returned when `lineHeightPercent` (deprecated)
+ * is not 100.
+ */
+ lineHeightPercentFontSize?: number
+
+ /**
+ * The unit of the line height value specified by the user.
+ */
+ lineHeightUnit?: 'PIXELS' | 'FONT_SIZE_%' | 'INTRINSIC_%'
+
+ /**
+ * Whether or not this style has overrides over a text style. The possible fields to override are
+ * semanticWeight, semanticItalic, hyperlink, and textDecoration. If this is true, then those fields
+ * are overrides if present.
+ */
+ isOverrideOverTextStyle?: boolean
+
+ /**
+ * The variables bound to a particular field on this style
+ */
+ boundVariables?: {
+ fontFamily?: VariableAlias
+
+ fontSize?: VariableAlias
+
+ fontStyle?: VariableAlias
+
+ fontWeight?: VariableAlias
+
+ letterSpacing?: VariableAlias
+
+ lineHeight?: VariableAlias
+
+ paragraphSpacing?: VariableAlias
+
+ paragraphIndent?: VariableAlias
+ }
+} & BaseTypeStyle
+
+export type TextPathTypeStyle = {
+ /**
+ * Whether or not this style has overrides over a text style. The possible fields to override are
+ * semanticWeight, semanticItalic, and hyperlink. If this is true, then those fields are overrides
+ * if present.
+ */
+ isOverrideOverTextStyle?: boolean
+
+ /**
+ * The variables bound to a particular field on this style
+ */
+ boundVariables?: {
+ fontFamily?: VariableAlias
+
+ fontSize?: VariableAlias
+
+ fontStyle?: VariableAlias
+
+ fontWeight?: VariableAlias
+
+ letterSpacing?: VariableAlias
+ }
+} & BaseTypeStyle
+
+/**
+ * Component property type.
+ */
+export type ComponentPropertyType = 'BOOLEAN' | 'INSTANCE_SWAP' | 'TEXT' | 'VARIANT'
+
+/**
+ * Instance swap preferred value.
+ */
+export type InstanceSwapPreferredValue = {
+ /**
+ * Type of node for this preferred value.
+ */
+ type: 'COMPONENT' | 'COMPONENT_SET'
+
+ /**
+ * Key of this component or component set.
+ */
+ key: string
+}
+
+/**
+ * A property of a component.
+ */
+export type ComponentPropertyDefinition = {
+ /**
+ * Type of this component property.
+ */
+ type: ComponentPropertyType
+
+ /**
+ * Initial value of this property for instances.
+ */
+ defaultValue: boolean | string
+
+ /**
+ * All possible values for this property. Only exists on VARIANT properties.
+ */
+ variantOptions?: string[]
+
+ /**
+ * Preferred values for this property. Only applicable if type is `INSTANCE_SWAP`.
+ */
+ preferredValues?: InstanceSwapPreferredValue[]
+}
+
+/**
+ * A property of a component.
+ */
+export type ComponentProperty = {
+ /**
+ * Type of this component property.
+ */
+ type: ComponentPropertyType
+
+ /**
+ * Value of the property for this component instance.
+ */
+ value: boolean | string
+
+ /**
+ * Preferred values for this property. Only applicable if type is `INSTANCE_SWAP`.
+ */
+ preferredValues?: InstanceSwapPreferredValue[]
+
+ /**
+ * The variables bound to a particular field on this component property
+ */
+ boundVariables?: { value?: VariableAlias }
+}
+
+/**
+ * Fields directly overridden on an instance. Inherited overrides are not included.
+ */
+export type Overrides = {
+ /**
+ * A unique ID for a node.
+ */
+ id: string
+
+ /**
+ * An array of properties.
+ */
+ overriddenFields: string[]
+}
+
+/**
+ * Geometric shape type.
+ */
+export type ShapeType =
+ | 'SQUARE'
+ | 'ELLIPSE'
+ | 'ROUNDED_RECTANGLE'
+ | 'DIAMOND'
+ | 'TRIANGLE_UP'
+ | 'TRIANGLE_DOWN'
+ | 'PARALLELOGRAM_RIGHT'
+ | 'PARALLELOGRAM_LEFT'
+ | 'ENG_DATABASE'
+ | 'ENG_QUEUE'
+ | 'ENG_FILE'
+ | 'ENG_FOLDER'
+ | 'TRAPEZOID'
+ | 'PREDEFINED_PROCESS'
+ | 'SHIELD'
+ | 'DOCUMENT_SINGLE'
+ | 'DOCUMENT_MULTIPLE'
+ | 'MANUAL_INPUT'
+ | 'HEXAGON'
+ | 'CHEVRON'
+ | 'PENTAGON'
+ | 'OCTAGON'
+ | 'STAR'
+ | 'PLUS'
+ | 'ARROW_LEFT'
+ | 'ARROW_RIGHT'
+ | 'SUMMING_JUNCTION'
+ | 'OR'
+ | 'SPEECH_BUBBLE'
+ | 'INTERNAL_STORAGE'
+
+/**
+ * Stores canvas location for a connector start/end point.
+ */
+export type ConnectorEndpoint =
+ | {
+ /**
+ * Node ID that this endpoint attaches to.
+ */
+ endpointNodeId?: string
+
+ /**
+ * The position of the endpoint relative to the node.
+ */
+ position?: Vector
+ }
+ | {
+ /**
+ * Node ID that this endpoint attaches to.
+ */
+ endpointNodeId?: string
+
+ /**
+ * The magnet type is a string enum.
+ */
+ magnet?: 'AUTO' | 'TOP' | 'BOTTOM' | 'LEFT' | 'RIGHT' | 'CENTER'
+ }
+
+/**
+ * Connector line type.
+ */
+export type ConnectorLineType = 'STRAIGHT' | 'ELBOWED'
+
+export type ConnectorTextBackground = CornerTrait & MinimalFillsTrait
+
+/**
+ * A description of a main component. Helps you identify which component instances are attached to.
+ */
+export type Component = {
+ /**
+ * The key of the component
+ */
+ key: string
+
+ /**
+ * Name of the component
+ */
+ name: string
+
+ /**
+ * The description of the component as entered in the editor
+ */
+ description: string
+
+ /**
+ * The ID of the component set if the component belongs to one
+ */
+ componentSetId?: string
+
+ /**
+ * An array of documentation links attached to this component
+ */
+ documentationLinks: DocumentationLink[]
+
+ /**
+ * Whether this component is a remote component that doesn't live in this file
+ */
+ remote: boolean
+}
+
+/**
+ * A description of a component set, which is a node containing a set of variants of a component.
+ */
+export type ComponentSet = {
+ /**
+ * The key of the component set
+ */
+ key: string
+
+ /**
+ * Name of the component set
+ */
+ name: string
+
+ /**
+ * The description of the component set as entered in the editor
+ */
+ description: string
+
+ /**
+ * An array of documentation links attached to this component set
+ */
+ documentationLinks?: DocumentationLink[]
+
+ /**
+ * Whether this component set is a remote component set that doesn't live in this file
+ */
+ remote?: boolean
+}
+
+/**
+ * Represents a link to documentation for a component or component set.
+ */
+export type DocumentationLink = {
+ /**
+ * Should be a valid URI (e.g. https://www.figma.com).
+ */
+ uri: string
+}
+
+/**
+ * Contains a variable alias
+ */
+export type VariableAlias = {
+ type: 'VARIABLE_ALIAS'
+
+ /**
+ * The id of the variable that the current variable is aliased to. This variable can be a local or
+ * remote variable, and both can be retrieved via the GET /v1/files/:file_key/variables/local
+ * endpoint.
+ */
+ id: string
+}
+
+/**
+ * An interaction in the Figma viewer, containing a trigger and one or more actions.
+ */
+export type Interaction = {
+ /**
+ * The user event that initiates the interaction.
+ */
+ trigger: Trigger | null
+
+ /**
+ * The actions that are performed when the trigger is activated.
+ */
+ actions?: Action[]
+}
+
+/**
+ * The `"ON_HOVER"` and `"ON_PRESS"` trigger types revert the navigation when the trigger is
+ * finished (the result is temporary). `"MOUSE_ENTER"`, `"MOUSE_LEAVE"`, `"MOUSE_UP"` and
+ * `"MOUSE_DOWN"` are permanent, one-way navigation. The `delay` parameter requires the trigger to
+ * be held for a certain duration of time before the action occurs. Both `timeout` and `delay`
+ * values are in milliseconds. The `"ON_MEDIA_HIT"` and `"ON_MEDIA_END"` trigger types can only
+ * trigger from a video. They fire when a video reaches a certain time or ends. The `timestamp`
+ * value is in seconds.
+ */
+export type Trigger =
+ | { type: 'ON_CLICK' | 'ON_HOVER' | 'ON_PRESS' | 'ON_DRAG' }
+ | AfterTimeoutTrigger
+ | {
+ type: 'MOUSE_ENTER' | 'MOUSE_LEAVE' | 'MOUSE_UP' | 'MOUSE_DOWN'
+
+ delay: number
+
+ /**
+ * Whether this is a [deprecated
+ * version](https://help.figma.com/hc/en-us/articles/360040035834-Prototype-triggers#h_01HHN04REHJNP168R26P1CMP0A)
+ * of the trigger that was left unchanged for backwards compatibility. If not present, the trigger
+ * is the latest version.
+ */
+ deprecatedVersion?: boolean
+ }
+ | OnKeyDownTrigger
+ | OnMediaHitTrigger
+ | { type: 'ON_MEDIA_END' }
+
+export type AfterTimeoutTrigger = {
+ type: 'AFTER_TIMEOUT'
+
+ timeout: number
+}
+
+export type OnKeyDownTrigger = {
+ type: 'ON_KEY_DOWN'
+
+ device: 'KEYBOARD' | 'XBOX_ONE' | 'PS4' | 'SWITCH_PRO' | 'UNKNOWN_CONTROLLER'
+
+ keyCodes: number[]
+}
+
+export type OnMediaHitTrigger = {
+ type: 'ON_MEDIA_HIT'
+
+ mediaHitTime: number
+}
+
+/**
+ * An action that is performed when a trigger is activated.
+ */
+export type Action =
+ | { type: 'BACK' | 'CLOSE' }
+ | OpenURLAction
+ | UpdateMediaRuntimeAction
+ | SetVariableAction
+ | SetVariableModeAction
+ | ConditionalAction
+ | NodeAction
+
+/**
+ * An action that opens a URL.
+ */
+export type OpenURLAction = {
+ type: 'URL'
+
+ url: string
+}
+
+/**
+ * An action that affects a video node in the Figma viewer. For example, to play, pause, or skip.
+ */
+export type UpdateMediaRuntimeAction =
+ | {
+ type: 'UPDATE_MEDIA_RUNTIME'
+
+ destinationId: string | null
+
+ mediaAction: 'PLAY' | 'PAUSE' | 'TOGGLE_PLAY_PAUSE' | 'MUTE' | 'UNMUTE' | 'TOGGLE_MUTE_UNMUTE'
+ }
+ | {
+ type: 'UPDATE_MEDIA_RUNTIME'
+
+ destinationId?: string | null
+
+ mediaAction: 'SKIP_FORWARD' | 'SKIP_BACKWARD'
+
+ amountToSkip: number
+ }
+ | {
+ type: 'UPDATE_MEDIA_RUNTIME'
+
+ destinationId?: string | null
+
+ mediaAction: 'SKIP_TO'
+
+ newTimestamp: number
+ }
+
+/**
+ * An action that navigates to a specific node in the Figma viewer.
+ */
+export type NodeAction = {
+ type: 'NODE'
+
+ destinationId: string | null
+
+ navigation: Navigation
+
+ transition: Transition | null
+
+ /**
+ * Whether the scroll offsets of any scrollable elements in the current screen or overlay are
+ * preserved when navigating to the destination. This is applicable only if the layout of both the
+ * current frame and its destination are the same.
+ */
+ preserveScrollPosition?: boolean
+
+ /**
+ * Applicable only when `navigation` is `"OVERLAY"` and the destination is a frame with
+ * `overlayPosition` equal to `"MANUAL"`. This value represents the offset by which the overlay is
+ * opened relative to this node.
+ */
+ overlayRelativePosition?: Vector
+
+ /**
+ * When true, all videos within the destination frame will reset their memorized playback position
+ * to 00:00 before starting to play.
+ */
+ resetVideoPosition?: boolean
+
+ /**
+ * Whether the scroll offsets of any scrollable elements in the current screen or overlay reset when
+ * navigating to the destination. This is applicable only if the layout of both the current frame
+ * and its destination are the same.
+ */
+ resetScrollPosition?: boolean
+
+ /**
+ * Whether the state of any interactive components in the current screen or overlay reset when
+ * navigating to the destination. This is applicable if there are interactive components in the
+ * destination frame.
+ */
+ resetInteractiveComponents?: boolean
+}
+
+/**
+ * The method of navigation. The possible values are:
+ *
+ * - `"NAVIGATE"`: Replaces the current screen with the destination, also closing all overlays.
+ * - `"OVERLAY"`: Opens the destination as an overlay on the current screen.
+ * - `"SWAP"`: On an overlay, replaces the current (topmost) overlay with the destination. On a
+ * top-level frame, behaves the same as `"NAVIGATE"` except that no entry is added to the
+ * navigation history.
+ * - `"SCROLL_TO"`: Scrolls to the destination on the current screen.
+ * - `"CHANGE_TO"`: Changes the closest ancestor instance of source node to the specified variant.
+ */
+export type Navigation = 'NAVIGATE' | 'SWAP' | 'OVERLAY' | 'SCROLL_TO' | 'CHANGE_TO'
+
+export type Transition = SimpleTransition | DirectionalTransition
+
+/**
+ * Describes an animation used when navigating in a prototype.
+ */
+export type SimpleTransition = {
+ type: 'DISSOLVE' | 'SMART_ANIMATE' | 'SCROLL_ANIMATE'
+
+ /**
+ * The duration of the transition in milliseconds.
+ */
+ duration: number
+
+ /**
+ * The easing curve of the transition.
+ */
+ easing: Easing
+}
+
+/**
+ * Describes an animation used when navigating in a prototype.
+ */
+export type DirectionalTransition = {
+ type: 'MOVE_IN' | 'MOVE_OUT' | 'PUSH' | 'SLIDE_IN' | 'SLIDE_OUT'
+
+ direction: 'LEFT' | 'RIGHT' | 'TOP' | 'BOTTOM'
+
+ /**
+ * The duration of the transition in milliseconds.
+ */
+ duration: number
+
+ /**
+ * The easing curve of the transition.
+ */
+ easing: Easing
+
+ /**
+ * When the transition `type` is `"SMART_ANIMATE"` or when `matchLayers` is `true`, then the
+ * transition will be performed using smart animate, which attempts to match corresponding layers an
+ * interpolate other properties during the animation.
+ */
+ matchLayers?: boolean
+}
+
+/**
+ * Describes an easing curve.
+ */
+export type Easing = {
+ /**
+ * The type of easing curve.
+ */
+ type: EasingType
+
+ /**
+ * A cubic bezier curve that defines the easing.
+ */
+ easingFunctionCubicBezier?: {
+ /**
+ * The x component of the first control point.
+ */
+ x1: number
+
+ /**
+ * The y component of the first control point.
+ */
+ y1: number
+
+ /**
+ * The x component of the second control point.
+ */
+ x2: number
+
+ /**
+ * The y component of the second control point.
+ */
+ y2: number
+ }
+
+ /**
+ * A spring function that defines the easing.
+ */
+ easingFunctionSpring?: {
+ mass: number
+
+ stiffness: number
+
+ damping: number
+ }
+}
+
+/**
+ * Sets a variable to a specific value.
+ */
+export type SetVariableAction = {
+ type: 'SET_VARIABLE'
+
+ variableId: string | null
+
+ variableValue?: VariableData
+}
+
+/**
+ * Sets a variable to a specific mode.
+ */
+export type SetVariableModeAction = {
+ type: 'SET_VARIABLE_MODE'
+
+ variableCollectionId?: string | null
+
+ variableModeId?: string | null
+}
+
+/**
+ * Checks if a condition is met before performing certain actions by using an if/else conditional
+ * statement.
+ */
+export type ConditionalAction = {
+ type: 'CONDITIONAL'
+
+ conditionalBlocks: ConditionalBlock[]
+}
+
+/**
+ * A value to set a variable to during prototyping.
+ */
+export type VariableData = {
+ type?: VariableDataType
+
+ resolvedType?: VariableResolvedDataType
+
+ value?: boolean | number | string | RGB | RGBA | VariableAlias | Expression
+}
+
+/**
+ * Defines the types of data a VariableData object can hold
+ */
+export type VariableDataType =
+ | 'BOOLEAN'
+ | 'FLOAT'
+ | 'STRING'
+ | 'COLOR'
+ | 'VARIABLE_ALIAS'
+ | 'EXPRESSION'
+
+/**
+ * Defines the types of data a VariableData object can eventually equal
+ */
+export type VariableResolvedDataType = 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR'
+
+/**
+ * Defines the [Expression](https://help.figma.com/hc/en-us/articles/15253194385943) object, which
+ * contains a list of `VariableData` objects strung together by operators (`ExpressionFunction`).
+ */
+export type Expression = {
+ expressionFunction: ExpressionFunction
+
+ expressionArguments: VariableData[]
+}
+
+/**
+ * Defines the list of operators available to use in an Expression.
+ */
+export type ExpressionFunction =
+ | 'ADDITION'
+ | 'SUBTRACTION'
+ | 'MULTIPLICATION'
+ | 'DIVISION'
+ | 'EQUALS'
+ | 'NOT_EQUAL'
+ | 'LESS_THAN'
+ | 'LESS_THAN_OR_EQUAL'
+ | 'GREATER_THAN'
+ | 'GREATER_THAN_OR_EQUAL'
+ | 'AND'
+ | 'OR'
+ | 'VAR_MODE_LOOKUP'
+ | 'NEGATE'
+ | 'NOT'
+
+/**
+ * Either the if or else conditional blocks. The if block contains a condition to check. If that
+ * condition is met then it will run those list of actions, else it will run the actions in the else
+ * block.
+ */
+export type ConditionalBlock = {
+ condition?: VariableData
+
+ actions: Action[]
+}
+
+/**
+ * A pinned distance between two nodes in Dev Mode
+ */
+export type Measurement = {
+ id: string
+
+ start: MeasurementStartEnd
+
+ end: MeasurementStartEnd
+
+ offset: MeasurementOffsetInner | MeasurementOffsetOuter
+
+ /**
+ * When manually overridden, the displayed value of the measurement
+ */
+ freeText?: string
+}
+
+/**
+ * The node and side a measurement is pinned to
+ */
+export type MeasurementStartEnd = {
+ nodeId: string
+
+ side: 'TOP' | 'RIGHT' | 'BOTTOM' | 'LEFT'
+}
+
+/**
+ * Measurement offset relative to the inside of the start node
+ */
+export type MeasurementOffsetInner = {
+ type: 'INNER'
+
+ relative: number
+}
+
+/**
+ * Measurement offset relative to the outside of the start node
+ */
+export type MeasurementOffsetOuter = {
+ type: 'OUTER'
+
+ fixed: number
+}
+
+/**
+ * Position of a comment relative to the frame to which it is attached.
+ */
+export type FrameOffset = {
+ /**
+ * Unique id specifying the frame.
+ */
+ node_id: string
+
+ /**
+ * 2D vector offset within the frame from the top-left corner.
+ */
+ node_offset: Vector
+}
+
+/**
+ * Position of a region comment on the canvas.
+ */
+export type Region = {
+ /**
+ * X coordinate of the position.
+ */
+ x: number
+
+ /**
+ * Y coordinate of the position.
+ */
+ y: number
+
+ /**
+ * The height of the comment region. Must be greater than 0.
+ */
+ region_height: number
+
+ /**
+ * The width of the comment region. Must be greater than 0.
+ */
+ region_width: number
+
+ /**
+ * The corner of the comment region to pin to the node's corner as a string enum.
+ */
+ comment_pin_corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
+}
+
+/**
+ * Position of a region comment relative to the frame to which it is attached.
+ */
+export type FrameOffsetRegion = {
+ /**
+ * Unique id specifying the frame.
+ */
+ node_id: string
+
+ /**
+ * 2D vector offset within the frame from the top-left corner.
+ */
+ node_offset: Vector
+
+ /**
+ * The height of the comment region. Must be greater than 0.
+ */
+ region_height: number
+
+ /**
+ * The width of the comment region. Must be greater than 0.
+ */
+ region_width: number
+
+ /**
+ * The corner of the comment region to pin to the node's corner as a string enum.
+ */
+ comment_pin_corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
+}
+
+/**
+ * A comment or reply left by a user.
+ */
+export type Comment = {
+ /**
+ * Unique identifier for comment.
+ */
+ id: string
+
+ /**
+ * Positioning information of the comment. Includes information on the location of the comment pin,
+ * which is either the absolute coordinates on the canvas or a relative offset within a frame. If
+ * the comment is a region, it will also contain the region height, width, and position of the
+ * anchor in regards to the region.
+ */
+ client_meta: Vector | FrameOffset | Region | FrameOffsetRegion
+
+ /**
+ * The file in which the comment lives
+ */
+ file_key: string
+
+ /**
+ * If present, the id of the comment to which this is the reply
+ */
+ parent_id?: string
+
+ /**
+ * The user who left the comment
+ */
+ user: User
+
+ /**
+ * The UTC ISO 8601 time at which the comment was left
+ */
+ created_at: string
+
+ /**
+ * If set, the UTC ISO 8601 time the comment was resolved
+ */
+ resolved_at?: string | null
+
+ /**
+ * The content of the comment
+ */
+ message: string
+
+ /**
+ * Only set for top level comments. The number displayed with the comment in the UI
+ */
+ order_id: string | null
+
+ /**
+ * An array of reactions to the comment
+ */
+ reactions: Reaction[]
+}
+
+/**
+ * A reaction left by a user.
+ */
+export type Reaction = {
+ /**
+ * The user who left the reaction.
+ */
+ user: User
+
+ emoji: Emoji
+
+ /**
+ * The UTC ISO 8601 time at which the reaction was left.
+ */
+ created_at: string
+}
+
+/**
+ * The emoji type of reaction as shortcode (e.g. `:heart:`, `:+1::skin-tone-2:`). The list of
+ * accepted emoji shortcodes can be found in [this
+ * file](https://raw.githubusercontent.com/missive/emoji-mart/main/packages/emoji-mart-data/sets/14/native.json)
+ * under the top-level emojis and aliases fields, with optional skin tone modifiers when
+ * applicable.
+ */
+export type Emoji = string
+
+/**
+ * A description of a user.
+ */
+export type User = {
+ /**
+ * Unique stable id of the user.
+ */
+ id: string
+
+ /**
+ * Name of the user.
+ */
+ handle: string
+
+ /**
+ * URL link to the user's profile image.
+ */
+ img_url: string
+}
+
+/**
+ * Data on the frame a component resides in.
+ */
+export type FrameInfo = {
+ /**
+ * The ID of the frame node within the file.
+ */
+ nodeId?: string
+
+ /**
+ * The name of the frame node.
+ */
+ name?: string
+
+ /**
+ * The background color of the frame node.
+ */
+ backgroundColor?: string
+
+ /**
+ * The ID of the page containing the frame node.
+ */
+ pageId: string
+
+ /**
+ * The name of the page containing the frame node.
+ */
+ pageName: string
+
+ /**
+ * Deprecated - Use containingComponentSet instead.
+ *
+ * @deprecated
+ */
+ containingStateGroup?: {
+ /**
+ * The ID of the state group node.
+ */
+ nodeId?: string
+
+ /**
+ * The name of the state group node.
+ */
+ name?: string
+ } | null
+
+ /**
+ * The component set node that contains the frame node.
+ */
+ containingComponentSet?: {
+ /**
+ * The ID of the component set node.
+ */
+ nodeId?: string
+
+ /**
+ * The name of the component set node.
+ */
+ name?: string
+ } | null
+}
+
+/**
+ * An arrangement of published UI elements that can be instantiated across figma files.
+ */
+export type PublishedComponent = {
+ /**
+ * The unique identifier for the component.
+ */
+ key: string
+
+ /**
+ * The unique identifier of the Figma file that contains the component.
+ */
+ file_key: string
+
+ /**
+ * The unique identifier of the component node within the Figma file.
+ */
+ node_id: string
+
+ /**
+ * A URL to a thumbnail image of the component.
+ */
+ thumbnail_url?: string
+
+ /**
+ * The name of the component.
+ */
+ name: string
+
+ /**
+ * The description of the component as entered by the publisher.
+ */
+ description: string
+
+ /**
+ * The UTC ISO 8601 time when the component was created.
+ */
+ created_at: string
+
+ /**
+ * The UTC ISO 8601 time when the component was last updated.
+ */
+ updated_at: string
+
+ /**
+ * The user who last updated the component.
+ */
+ user: User
+
+ /**
+ * The containing frame of the component.
+ */
+ containing_frame?: FrameInfo
+}
+
+/**
+ * A node containing a set of variants of a component.
+ */
+export type PublishedComponentSet = {
+ /**
+ * The unique identifier for the component set.
+ */
+ key: string
+
+ /**
+ * The unique identifier of the Figma file that contains the component set.
+ */
+ file_key: string
+
+ /**
+ * The unique identifier of the component set node within the Figma file.
+ */
+ node_id: string
+
+ /**
+ * A URL to a thumbnail image of the component set.
+ */
+ thumbnail_url?: string
+
+ /**
+ * The name of the component set.
+ */
+ name: string
+
+ /**
+ * The description of the component set as entered by the publisher.
+ */
+ description: string
+
+ /**
+ * The UTC ISO 8601 time when the component set was created.
+ */
+ created_at: string
+
+ /**
+ * The UTC ISO 8601 time when the component set was last updated.
+ */
+ updated_at: string
+
+ /**
+ * The user who last updated the component set.
+ */
+ user: User
+
+ /**
+ * The containing frame of the component set.
+ */
+ containing_frame?: FrameInfo
+}
+
+/**
+ * The type of style
+ */
+export type StyleType = 'FILL' | 'TEXT' | 'EFFECT' | 'GRID'
+
+/**
+ * A set of published properties that can be applied to nodes.
+ */
+export type PublishedStyle = {
+ /**
+ * The unique identifier for the style
+ */
+ key: string
+
+ /**
+ * The unique identifier of the Figma file that contains the style.
+ */
+ file_key: string
+
+ /**
+ * ID of the style node within the figma file
+ */
+ node_id: string
+
+ style_type: StyleType
+
+ /**
+ * A URL to a thumbnail image of the style.
+ */
+ thumbnail_url?: string
+
+ /**
+ * The name of the style.
+ */
+ name: string
+
+ /**
+ * The description of the style as entered by the publisher.
+ */
+ description: string
+
+ /**
+ * The UTC ISO 8601 time when the style was created.
+ */
+ created_at: string
+
+ /**
+ * The UTC ISO 8601 time when the style was last updated.
+ */
+ updated_at: string
+
+ /**
+ * The user who last updated the style.
+ */
+ user: User
+
+ /**
+ * A user specified order number by which the style can be sorted.
+ */
+ sort_position: string
+}
+
+/**
+ * A Project can be identified by both the Project name, and the Project ID.
+ */
+export type Project = {
+ /**
+ * The ID of the project.
+ */
+ id: string
+
+ /**
+ * The name of the project.
+ */
+ name: string
+}
+
+/**
+ * A version of a file
+ */
+export type Version = {
+ /**
+ * Unique identifier for version
+ */
+ id: string
+
+ /**
+ * The UTC ISO 8601 time at which the version was created
+ */
+ created_at: string
+
+ /**
+ * The label given to the version in the editor
+ */
+ label: string | null
+
+ /**
+ * The description of the version as entered in the editor
+ */
+ description: string | null
+
+ /**
+ * The user that created the version
+ */
+ user: User
+
+ /**
+ * A URL to a thumbnail image of the file version.
+ */
+ thumbnail_url?: string
+}
+
+/**
+ * A description of an HTTP webhook (from Figma back to your application)
+ */
+export type WebhookV2 = {
+ /**
+ * The ID of the webhook
+ */
+ id: string
+
+ /**
+ * The event this webhook triggers on
+ */
+ event_type: WebhookV2Event
+
+ /**
+ * The team id you are subscribed to for updates. This is deprecated, use context and context_id
+ * instead
+ *
+ * @deprecated
+ */
+ team_id: string
+
+ /**
+ * The type of context this webhook is attached to. The value will be "PROJECT", "TEAM", or "FILE"
+ */
+ context: string
+
+ /**
+ * The ID of the context this webhook is attached to
+ */
+ context_id: string
+
+ /**
+ * The plan API ID of the team or organization where this webhook was created
+ */
+ plan_api_id: string
+
+ /**
+ * The current status of the webhook
+ */
+ status: WebhookV2Status
+
+ /**
+ * The client ID of the OAuth application that registered this webhook, if any
+ */
+ client_id: string | null
+
+ /**
+ * The passcode that will be passed back to the webhook endpoint. For security, when using the GET
+ * endpoints, the value is an empty string
+ */
+ passcode: string
+
+ /**
+ * The endpoint that will be hit when the webhook is triggered
+ */
+ endpoint: string
+
+ /**
+ * Optional user-provided description or name for the webhook. This is provided to help make
+ * maintaining a number of webhooks more convenient. Max length 140 characters.
+ */
+ description: string | null
+}
+
+/**
+ * An enum representing the possible events that a webhook can subscribe to
+ */
+export type WebhookV2Event =
+ | 'PING'
+ | 'FILE_UPDATE'
+ | 'FILE_VERSION_UPDATE'
+ | 'FILE_DELETE'
+ | 'LIBRARY_PUBLISH'
+ | 'FILE_COMMENT'
+ | 'DEV_MODE_STATUS_UPDATE'
+
+/**
+ * An enum representing the possible statuses you can set a webhook to:
+ *
+ * - `ACTIVE`: The webhook is healthy and receive all events
+ * - `PAUSED`: The webhook is paused and will not receive any events
+ */
+export type WebhookV2Status = 'ACTIVE' | 'PAUSED'
+
+/**
+ * Information regarding the most recent interactions sent to a webhook endpoint
+ */
+export type WebhookV2Request = {
+ /**
+ * The ID of the webhook the requests were sent to
+ */
+ webhook_id: string
+
+ request_info: WebhookV2RequestInfo
+
+ response_info: WebhookV2ResponseInfo
+
+ /**
+ * Error message for this request. NULL if no error occurred
+ */
+ error_msg: string | null
+}
+
+/**
+ * Information regarding the request sent to a webhook endpoint
+ */
+export type WebhookV2RequestInfo = {
+ /**
+ * The ID of the webhook
+ */
+ id: string
+
+ /**
+ * The actual endpoint the request was sent to
+ */
+ endpoint: string
+
+ /**
+ * The contents of the request that was sent to the endpoint
+ */
+ payload: object
+
+ /**
+ * UTC ISO 8601 timestamp of when the request was sent
+ */
+ sent_at: string
+}
+
+/**
+ * Information regarding the reply sent back from a webhook endpoint
+ */
+export type WebhookV2ResponseInfo = object | null
+
+/**
+ * An object representing the library item information in the payload of the `LIBRARY_PUBLISH` event
+ */
+export type LibraryItemData = {
+ /**
+ * Unique identifier for the library item
+ */
+ key: string
+
+ /**
+ * Name of the library item
+ */
+ name: string
+}
+
+/**
+ * An object representing a fragment of a comment left by a user, used in the payload of the
+ * `FILE_COMMENT` event. Note only ONE of the fields below will be set
+ */
+export type CommentFragment = {
+ /**
+ * Comment text that is set if a fragment is text based
+ */
+ text?: string
+
+ /**
+ * User id that is set if a fragment refers to a user mention
+ */
+ mention?: string
+}
+
+export type WebhookBasePayload = {
+ /**
+ * The passcode specified when the webhook was created, should match what was initially provided
+ */
+ passcode: string
+
+ /**
+ * UTC ISO 8601 timestamp of when the event was triggered.
+ */
+ timestamp: string
+
+ /**
+ * The id of the webhook that caused the callback
+ */
+ webhook_id: string
+}
+
+export type WebhookPingPayload = WebhookBasePayload & { event_type: 'PING' }
+
+export type WebhookFileUpdatePayload = WebhookBasePayload & {
+ event_type: 'FILE_UPDATE'
+
+ /**
+ * The key of the file that was updated
+ */
+ file_key: string
+
+ /**
+ * The name of the file that was updated
+ */
+ file_name: string
+}
+
+export type WebhookFileDeletePayload = WebhookBasePayload & {
+ event_type: 'FILE_DELETE'
+
+ /**
+ * The key of the file that was deleted
+ */
+ file_key: string
+
+ /**
+ * The name of the file that was deleted
+ */
+ file_name: string
+
+ /**
+ * The user that deleted the file and triggered this event
+ */
+ triggered_by: User
+}
+
+export type WebhookFileVersionUpdatePayload = WebhookBasePayload & {
+ event_type: 'FILE_VERSION_UPDATE'
+
+ /**
+ * UTC ISO 8601 timestamp of when the version was created
+ */
+ created_at: string
+
+ /**
+ * Description of the version in the version history
+ */
+ description?: string
+
+ /**
+ * The key of the file that was updated
+ */
+ file_key: string
+
+ /**
+ * The name of the file that was updated
+ */
+ file_name: string
+
+ /**
+ * The user that created the named version and triggered this event
+ */
+ triggered_by: User
+
+ /**
+ * ID of the published version
+ */
+ version_id: string
+}
+
+export type WebhookLibraryPublishPayload = WebhookBasePayload & {
+ event_type: 'LIBRARY_PUBLISH'
+
+ /**
+ * Components that were created by the library publish
+ */
+ created_components: LibraryItemData[]
+
+ /**
+ * Styles that were created by the library publish
+ */
+ created_styles: LibraryItemData[]
+
+ /**
+ * Variables that were created by the library publish
+ */
+ created_variables: LibraryItemData[]
+
+ /**
+ * Components that were modified by the library publish
+ */
+ modified_components: LibraryItemData[]
+
+ /**
+ * Styles that were modified by the library publish
+ */
+ modified_styles: LibraryItemData[]
+
+ /**
+ * Variables that were modified by the library publish
+ */
+ modified_variables: LibraryItemData[]
+
+ /**
+ * Components that were deleted by the library publish
+ */
+ deleted_components: LibraryItemData[]
+
+ /**
+ * Styles that were deleted by the library publish
+ */
+ deleted_styles: LibraryItemData[]
+
+ /**
+ * Variables that were deleted by the library publish
+ */
+ deleted_variables: LibraryItemData[]
+
+ /**
+ * Description of the library publish
+ */
+ description?: string
+
+ /**
+ * The key of the file that was published
+ */
+ file_key: string
+
+ /**
+ * The name of the file that was published
+ */
+ file_name: string
+
+ /**
+ * The library item that was published
+ */
+ library_item: LibraryItemData
+
+ /**
+ * The user that published the library and triggered this event
+ */
+ triggered_by: User
+}
+
+export type WebhookFileCommentPayload = WebhookBasePayload & {
+ event_type: 'FILE_COMMENT'
+
+ /**
+ * Contents of the comment itself
+ */
+ comment: CommentFragment[]
+
+ /**
+ * Unique identifier for comment
+ */
+ comment_id: string
+
+ /**
+ * The UTC ISO 8601 time at which the comment was left
+ */
+ created_at: string
+
+ /**
+ * The key of the file that was commented on
+ */
+ file_key: string
+
+ /**
+ * The name of the file that was commented on
+ */
+ file_name: string
+
+ /**
+ * Users that were mentioned in the comment
+ */
+ mentions?: User[]
+
+ /**
+ * The user that made the comment and triggered this event
+ */
+ triggered_by: User
+}
+
+export type WebhookDevModeStatusUpdatePayload = WebhookBasePayload & {
+ event_type: 'DEV_MODE_STATUS_UPDATE'
+
+ /**
+ * The key of the file that was updated
+ */
+ file_key: string
+
+ /**
+ * The name of the file that was updated
+ */
+ file_name: string
+
+ /**
+ * The id of the node where the Dev Mode status changed. For example, "43:2"
+ */
+ node_id: string
+
+ /**
+ * An array of related links that have been applied to the layer in the file
+ */
+ related_links: DevResource[]
+
+ /**
+ * The Dev Mode status. Either "NONE", "READY_FOR_DEV", or "COMPLETED"
+ */
+ status: string
+
+ /**
+ * The user that made the status change and triggered the event
+ */
+ triggered_by: User
+}
+
+/**
+ * A Figma user
+ */
+export type ActivityLogUserEntity = {
+ /**
+ * The type of entity.
+ */
+ type: 'user'
+
+ /**
+ * Unique stable id of the user.
+ */
+ id: string
+
+ /**
+ * Name of the user.
+ */
+ name: string
+
+ /**
+ * Email associated with the user's account.
+ */
+ email: string
+}
+
+/**
+ * A Figma Design or FigJam file
+ */
+export type ActivityLogFileEntity = {
+ /**
+ * The type of entity.
+ */
+ type: 'file'
+
+ /**
+ * Unique identifier of the file.
+ */
+ key: string
+
+ /**
+ * Name of the file.
+ */
+ name: string
+
+ /**
+ * Indicates if the object is a file on Figma Design or FigJam.
+ */
+ editor_type: 'figma' | 'figjam'
+
+ /**
+ * Access policy for users who have the link to the file.
+ */
+ link_access: 'view' | 'edit' | 'org_view' | 'org_edit' | 'inherit'
+
+ /**
+ * Access policy for users who have the link to the file's prototype.
+ */
+ proto_link_access: 'view' | 'org_view' | 'inherit'
+}
+
+/**
+ * A file branch that diverges from and can be merged back into the main file
+ */
+export type ActivityLogFileRepoEntity = {
+ /**
+ * The type of entity.
+ */
+ type: 'file_repo'
+
+ /**
+ * Unique identifier of the file branch.
+ */
+ id: string
+
+ /**
+ * Name of the file.
+ */
+ name: string
+
+ /**
+ * Key of the main file.
+ */
+ main_file_key: string
+}
+
+/**
+ * A project that a collection of Figma files are grouped under
+ */
+export type ActivityLogProjectEntity = {
+ /**
+ * The type of entity.
+ */
+ type: 'project'
+
+ /**
+ * Unique identifier of the project.
+ */
+ id: string
+
+ /**
+ * Name of the project.
+ */
+ name: string
+}
+
+/**
+ * A Figma team that contains multiple users and projects
+ */
+export type ActivityLogTeamEntity = {
+ /**
+ * The type of entity.
+ */
+ type: 'team'
+
+ /**
+ * Unique identifier of the team.
+ */
+ id: string
+
+ /**
+ * Name of the team.
+ */
+ name: string
+}
+
+/**
+ * Part of the organizational hierarchy of managing files and users within Figma, only available on
+ * the Enterprise Plan
+ */
+export type ActivityLogWorkspaceEntity = {
+ /**
+ * The type of entity.
+ */
+ type: 'workspace'
+
+ /**
+ * Unique identifier of the workspace.
+ */
+ id: string
+
+ /**
+ * Name of the workspace.
+ */
+ name: string
+}
+
+/**
+ * A Figma organization
+ */
+export type ActivityLogOrgEntity = {
+ /**
+ * The type of entity.
+ */
+ type: 'org'
+
+ /**
+ * Unique identifier of the organization.
+ */
+ id: string
+
+ /**
+ * Name of the organization.
+ */
+ name: string
+}
+
+/**
+ * A Figma plugin
+ */
+export type ActivityLogPluginEntity = {
+ /**
+ * The type of entity.
+ */
+ type: 'plugin'
+
+ /**
+ * Unique identifier of the plugin.
+ */
+ id: string
+
+ /**
+ * Name of the plugin.
+ */
+ name: string
+
+ /**
+ * Indicates if the object is a plugin is available on Figma Design or FigJam.
+ */
+ editor_type: 'figma' | 'figjam'
+}
+
+/**
+ * A Figma widget
+ */
+export type ActivityLogWidgetEntity = {
+ /**
+ * The type of entity.
+ */
+ type: 'widget'
+
+ /**
+ * Unique identifier of the widget.
+ */
+ id: string
+
+ /**
+ * Name of the widget.
+ */
+ name: string
+
+ /**
+ * Indicates if the object is a widget available on Figma Design or FigJam.
+ */
+ editor_type: 'figma' | 'figjam'
+}
+
+/**
+ * An event returned by the Activity Logs API.
+ */
+export type ActivityLog = {
+ /**
+ * The ID of the event.
+ */
+ id: string
+
+ /**
+ * The timestamp of the event in seconds since the Unix epoch.
+ */
+ timestamp: number
+
+ /**
+ * The user who performed the action.
+ */
+ actor: object | null
+
+ /**
+ * The task or activity the actor performed.
+ */
+ action: {
+ /**
+ * The type of the action.
+ */
+ type: string
+
+ /**
+ * Metadata of the action. Each action type supports its own metadata attributes.
+ */
+ details: object | null
+ }
+
+ /**
+ * The resource the actor took the action on. It can be a user, file, project or other resource
+ * types.
+ */
+ entity:
+ | ActivityLogUserEntity
+ | ActivityLogFileEntity
+ | ActivityLogFileRepoEntity
+ | ActivityLogProjectEntity
+ | ActivityLogTeamEntity
+ | ActivityLogWorkspaceEntity
+ | ActivityLogOrgEntity
+ | ActivityLogPluginEntity
+ | ActivityLogWidgetEntity
+
+ /**
+ * Contextual information about the event.
+ */
+ context: {
+ /**
+ * The third-party application that triggered the event, if applicable.
+ */
+ client_name: string | null
+
+ /**
+ * The IP address from of the client that sent the event request.
+ */
+ ip_address: string
+
+ /**
+ * If Figma's Support team triggered the event. This is either true or false.
+ */
+ is_figma_support_team_action: boolean
+
+ /**
+ * The id of the organization where the event took place.
+ */
+ org_id: string
+
+ /**
+ * The id of the team where the event took place -- if this took place in a specific team.
+ */
+ team_id: string | null
+ }
+}
+
+/**
+ * An object describing the user's payment status.
+ */
+export type PaymentStatus = {
+ /**
+ * The current payment status of the user on the resource, as a string enum:
+ *
+ * - `UNPAID`: user has not paid for the resource
+ * - `PAID`: user has an active purchase on the resource
+ * - `TRIAL`: user is in the trial period for a subscription resource
+ */
+ type?: 'UNPAID' | 'PAID' | 'TRIAL'
+}
+
+/**
+ * An object describing a user's payment information for a plugin, widget, or Community file.
+ */
+export type PaymentInformation = {
+ /**
+ * The ID of the user whose payment information was queried. Can be used to verify the validity of a
+ * response.
+ */
+ user_id: string
+
+ /**
+ * The ID of the plugin, widget, or Community file that was queried. Can be used to verify the
+ * validity of a response.
+ */
+ resource_id: string
+
+ /**
+ * The type of the resource.
+ */
+ resource_type: 'PLUGIN' | 'WIDGET' | 'COMMUNITY_FILE'
+
+ payment_status: PaymentStatus
+
+ /**
+ * The UTC ISO 8601 timestamp indicating when the user purchased the resource. No value is given if
+ * the user has never purchased the resource.
+ *
+ * Note that a value will still be returned if the user had purchased the resource, but no longer
+ * has active access to it (e.g. purchase refunded, subscription ended).
+ */
+ date_of_purchase?: string
+}
+
+/**
+ * Scopes allow a variable to be shown or hidden in the variable picker for various fields. This
+ * declutters the Figma UI if you have a large number of variables. Variable scopes are currently
+ * supported on `FLOAT`, `STRING`, and `COLOR` variables.
+ *
+ * `ALL_SCOPES` is a special scope that means that the variable will be shown in the variable picker
+ * for all variable fields. If `ALL_SCOPES` is set, no additional scopes can be set.
+ *
+ * `ALL_FILLS` is a special scope that means that the variable will be shown in the variable picker
+ * for all fill fields. If `ALL_FILLS` is set, no additional fill scopes can be set.
+ *
+ * Valid scopes for `FLOAT` variables:
+ *
+ * - `ALL_SCOPES`
+ * - `TEXT_CONTENT`
+ * - `WIDTH_HEIGHT`
+ * - `GAP`
+ * - `STROKE_FLOAT`
+ * - `EFFECT_FLOAT`
+ * - `OPACITY`
+ * - `FONT_WEIGHT`
+ * - `FONT_SIZE`
+ * - `LINE_HEIGHT`
+ * - `LETTER_SPACING`
+ * - `PARAGRAPH_SPACING`
+ * - `PARAGRAPH_INDENT`
+ *
+ * Valid scopes for `STRING` variables:
+ *
+ * - `ALL_SCOPES`
+ * - `TEXT_CONTENT`
+ * - `FONT_FAMILY`
+ * - `FONT_STYLE`
+ *
+ * Valid scopes for `COLOR` variables:
+ *
+ * - `ALL_SCOPES`
+ * - `ALL_FILLS`
+ * - `FRAME_FILL`
+ * - `SHAPE_FILL`
+ * - `TEXT_FILL`
+ * - `STROKE_COLOR`
+ * - `EFFECT_COLOR`
+ */
+export type VariableScope =
+ | 'ALL_SCOPES'
+ | 'TEXT_CONTENT'
+ | 'CORNER_RADIUS'
+ | 'WIDTH_HEIGHT'
+ | 'GAP'
+ | 'ALL_FILLS'
+ | 'FRAME_FILL'
+ | 'SHAPE_FILL'
+ | 'TEXT_FILL'
+ | 'STROKE_COLOR'
+ | 'STROKE_FLOAT'
+ | 'EFFECT_FLOAT'
+ | 'EFFECT_COLOR'
+ | 'OPACITY'
+ | 'FONT_FAMILY'
+ | 'FONT_STYLE'
+ | 'FONT_WEIGHT'
+ | 'FONT_SIZE'
+ | 'LINE_HEIGHT'
+ | 'LETTER_SPACING'
+ | 'PARAGRAPH_SPACING'
+ | 'PARAGRAPH_INDENT'
+ | 'FONT_VARIATIONS'
+
+/**
+ * An object containing platform-specific code syntax definitions for a variable. All platforms are
+ * optional.
+ */
+export type VariableCodeSyntax = {
+ WEB?: string
+
+ ANDROID?: string
+
+ iOS?: string
+}
+
+/**
+ * A grouping of related Variable objects each with the same modes.
+ */
+export type LocalVariableCollection = {
+ /**
+ * The unique identifier of this variable collection.
+ */
+ id: string
+
+ /**
+ * The name of this variable collection.
+ */
+ name: string
+
+ /**
+ * The key of this variable collection.
+ */
+ key: string
+
+ /**
+ * The modes of this variable collection.
+ */
+ modes: {
+ /**
+ * The unique identifier of this mode.
+ */
+ modeId: string
+
+ /**
+ * The name of this mode.
+ */
+ name: string
+ }[]
+
+ /**
+ * The id of the default mode.
+ */
+ defaultModeId: string
+
+ /**
+ * Whether this variable collection is remote.
+ */
+ remote: boolean
+
+ /**
+ * Whether this variable collection is hidden when publishing the current file as a library.
+ */
+ hiddenFromPublishing: boolean
+
+ /**
+ * The ids of the variables in the collection. Note that the order of these variables is roughly the
+ * same as what is shown in Figma Design, however it does not account for groups. As a result, the
+ * order of these variables may not exactly reflect the exact ordering and grouping shown in the
+ * authoring UI.
+ */
+ variableIds: string[]
+}
+
+/**
+ * A Variable is a single design token that defines values for each of the modes in its
+ * VariableCollection. These values can be applied to various kinds of design properties.
+ */
+export type LocalVariable = {
+ /**
+ * The unique identifier of this variable.
+ */
+ id: string
+
+ /**
+ * The name of this variable.
+ */
+ name: string
+
+ /**
+ * The key of this variable.
+ */
+ key: string
+
+ /**
+ * The id of the variable collection that contains this variable.
+ */
+ variableCollectionId: string
+
+ /**
+ * The resolved type of the variable.
+ */
+ resolvedType: 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR'
+
+ /**
+ * The values for each mode of this variable.
+ */
+ valuesByMode: { [key: string]: boolean | number | string | RGBA | VariableAlias }
+
+ /**
+ * Whether this variable is remote.
+ */
+ remote: boolean
+
+ /**
+ * The description of this variable.
+ */
+ description: string
+
+ /**
+ * Whether this variable is hidden when publishing the current file as a library.
+ *
+ * If the parent `VariableCollection` is marked as `hiddenFromPublishing`, then this variable will
+ * also be hidden from publishing via the UI. `hiddenFromPublishing` is independently toggled for a
+ * variable and collection. However, both must be true for a given variable to be publishable.
+ */
+ hiddenFromPublishing: boolean
+
+ /**
+ * An array of scopes in the UI where this variable is shown. Setting this property will show/hide
+ * this variable in the variable picker UI for different fields.
+ *
+ * Setting scopes for a variable does not prevent that variable from being bound in other scopes
+ * (for example, via the Plugin API). This only limits the variables that are shown in pickers
+ * within the Figma UI.
+ */
+ scopes: VariableScope[]
+
+ codeSyntax: VariableCodeSyntax
+
+ /**
+ * Indicates that the variable was deleted in the editor, but the document may still contain
+ * references to the variable. References to the variable may exist through bound values or variable
+ * aliases.
+ */
+ deletedButReferenced?: boolean
+}
+
+/**
+ * A grouping of related Variable objects each with the same modes.
+ */
+export type PublishedVariableCollection = {
+ /**
+ * The unique identifier of this variable collection.
+ */
+ id: string
+
+ /**
+ * The ID of the variable collection that is used by subscribing files. This ID changes every time
+ * the variable collection is modified and published.
+ */
+ subscribed_id: string
+
+ /**
+ * The name of this variable collection.
+ */
+ name: string
+
+ /**
+ * The key of this variable collection.
+ */
+ key: string
+
+ /**
+ * The UTC ISO 8601 time at which the variable collection was last updated.
+ *
+ * This timestamp will change any time a variable in the collection is changed.
+ */
+ updatedAt: string
+}
+
+/**
+ * A Variable is a single design token that defines values for each of the modes in its
+ * VariableCollection. These values can be applied to various kinds of design properties.
+ */
+export type PublishedVariable = {
+ /**
+ * The unique identifier of this variable.
+ */
+ id: string
+
+ /**
+ * The ID of the variable that is used by subscribing files. This ID changes every time the variable
+ * is modified and published.
+ */
+ subscribed_id: string
+
+ /**
+ * The name of this variable.
+ */
+ name: string
+
+ /**
+ * The key of this variable.
+ */
+ key: string
+
+ /**
+ * The id of the variable collection that contains this variable.
+ */
+ variableCollectionId: string
+
+ /**
+ * The resolved type of the variable.
+ */
+ resolvedDataType: 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR'
+
+ /**
+ * The UTC ISO 8601 time at which the variable was last updated.
+ */
+ updatedAt: string
+}
+
+/**
+ * An object that contains details about creating a `VariableCollection`.
+ */
+export type VariableCollectionCreate = {
+ /**
+ * The action to perform for the variable collection.
+ */
+ action: 'CREATE'
+
+ /**
+ * A temporary id for this variable collection.
+ */
+ id?: string
+
+ /**
+ * The name of this variable collection.
+ */
+ name: string
+
+ /**
+ * The initial mode refers to the mode that is created by default. You can set a temporary id here,
+ * in order to reference this mode later in this request.
+ */
+ initialModeId?: string
+
+ /**
+ * Whether this variable collection is hidden when publishing the current file as a library.
+ */
+ hiddenFromPublishing?: boolean
+}
+
+/**
+ * An object that contains details about updating a `VariableCollection`.
+ */
+export type VariableCollectionUpdate = {
+ /**
+ * The action to perform for the variable collection.
+ */
+ action: 'UPDATE'
+
+ /**
+ * The id of the variable collection to update.
+ */
+ id: string
+
+ /**
+ * The name of this variable collection.
+ */
+ name?: string
+
+ /**
+ * Whether this variable collection is hidden when publishing the current file as a library.
+ */
+ hiddenFromPublishing?: boolean
+}
+
+/**
+ * An object that contains details about deleting a `VariableCollection`.
+ */
+export type VariableCollectionDelete = {
+ /**
+ * The action to perform for the variable collection.
+ */
+ action: 'DELETE'
+
+ /**
+ * The id of the variable collection to delete.
+ */
+ id: string
+}
+
+export type VariableCollectionChange =
+ | VariableCollectionCreate
+ | VariableCollectionUpdate
+ | VariableCollectionDelete
+
+/**
+ * An object that contains details about creating a `VariableMode`.
+ */
+export type VariableModeCreate = {
+ /**
+ * The action to perform for the variable mode.
+ */
+ action: 'CREATE'
+
+ /**
+ * A temporary id for this variable mode.
+ */
+ id?: string
+
+ /**
+ * The name of this variable mode.
+ */
+ name: string
+
+ /**
+ * The variable collection that will contain the mode. You can use the temporary id of a variable
+ * collection.
+ */
+ variableCollectionId: string
+}
+
+/**
+ * An object that contains details about updating a `VariableMode`.
+ */
+export type VariableModeUpdate = {
+ /**
+ * The action to perform for the variable mode.
+ */
+ action: 'UPDATE'
+
+ /**
+ * The id of the variable mode to update.
+ */
+ id: string
+
+ /**
+ * The name of this variable mode.
+ */
+ name?: string
+
+ /**
+ * The variable collection that contains the mode.
+ */
+ variableCollectionId: string
+}
+
+/**
+ * An object that contains details about deleting a `VariableMode`.
+ */
+export type VariableModeDelete = {
+ /**
+ * The action to perform for the variable mode.
+ */
+ action: 'DELETE'
+
+ /**
+ * The id of the variable mode to delete.
+ */
+ id: string
+}
+
+export type VariableModeChange = VariableModeCreate | VariableModeUpdate | VariableModeDelete
+
+/**
+ * An object that contains details about creating a `Variable`.
+ */
+export type VariableCreate = {
+ /**
+ * The action to perform for the variable.
+ */
+ action: 'CREATE'
+
+ /**
+ * A temporary id for this variable.
+ */
+ id?: string
+
+ /**
+ * The name of this variable.
+ */
+ name: string
+
+ /**
+ * The variable collection that will contain the variable. You can use the temporary id of a
+ * variable collection.
+ */
+ variableCollectionId: string
+
+ /**
+ * The resolved type of the variable.
+ */
+ resolvedType: 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR'
+
+ /**
+ * The description of this variable.
+ */
+ description?: string
+
+ /**
+ * Whether this variable is hidden when publishing the current file as a library.
+ */
+ hiddenFromPublishing?: boolean
+
+ /**
+ * An array of scopes in the UI where this variable is shown. Setting this property will show/hide
+ * this variable in the variable picker UI for different fields.
+ */
+ scopes?: VariableScope[]
+
+ codeSyntax?: VariableCodeSyntax
+}
+
+/**
+ * An object that contains details about updating a `Variable`.
+ */
+export type VariableUpdate = {
+ /**
+ * The action to perform for the variable.
+ */
+ action: 'UPDATE'
+
+ /**
+ * The id of the variable to update.
+ */
+ id: string
+
+ /**
+ * The name of this variable.
+ */
+ name?: string
+
+ /**
+ * The description of this variable.
+ */
+ description?: string
+
+ /**
+ * Whether this variable is hidden when publishing the current file as a library.
+ */
+ hiddenFromPublishing?: boolean
+
+ /**
+ * An array of scopes in the UI where this variable is shown. Setting this property will show/hide
+ * this variable in the variable picker UI for different fields.
+ */
+ scopes?: VariableScope[]
+
+ codeSyntax?: VariableCodeSyntax
+}
+
+/**
+ * An object that contains details about deleting a `Variable`.
+ */
+export type VariableDelete = {
+ /**
+ * The action to perform for the variable.
+ */
+ action: 'DELETE'
+
+ /**
+ * The id of the variable to delete.
+ */
+ id: string
+}
+
+export type VariableChange = VariableCreate | VariableUpdate | VariableDelete
+
+/**
+ * An object that represents a value for a given mode of a variable. All properties are required.
+ */
+export type VariableModeValue = {
+ /**
+ * The target variable. You can use the temporary id of a variable.
+ */
+ variableId: string
+
+ /**
+ * Must correspond to a mode in the variable collection that contains the target variable.
+ */
+ modeId: string
+
+ value: VariableValue
+}
+
+/**
+ * The value for the variable. The value must match the variable's type. If setting to a variable
+ * alias, the alias must resolve to this type.
+ */
+export type VariableValue = boolean | number | string | RGB | RGBA | VariableAlias
+
+/**
+ * A dev resource in a file
+ */
+export type DevResource = {
+ /**
+ * Unique identifier of the dev resource
+ */
+ id: string
+
+ /**
+ * The name of the dev resource.
+ */
+ name: string
+
+ /**
+ * The URL of the dev resource.
+ */
+ url: string
+
+ /**
+ * The file key where the dev resource belongs.
+ */
+ file_key: string
+
+ /**
+ * The target node to attach the dev resource to.
+ */
+ node_id: string
+}
+
+/**
+ * Library analytics component actions data broken down by asset.
+ */
+export type LibraryAnalyticsComponentActionsByAsset = {
+ /**
+ * The date in ISO 8601 format. e.g. 2023-12-13
+ */
+ week: string
+
+ /**
+ * Unique, stable id of the component.
+ */
+ component_key: string
+
+ /**
+ * Name of the component.
+ */
+ component_name: string
+
+ /**
+ * Unique, stable id of the component set that this component belongs to.
+ */
+ component_set_key?: string
+
+ /**
+ * Name of the component set that this component belongs to.
+ */
+ component_set_name?: string
+
+ /**
+ * The number of detach events for this period.
+ */
+ detachments: number
+
+ /**
+ * The number of insertion events for this period.
+ */
+ insertions: number
+}
+
+/**
+ * Library analytics action data broken down by team.
+ */
+export type LibraryAnalyticsComponentActionsByTeam = {
+ /**
+ * The date in ISO 8601 format. e.g. 2023-12-13
+ */
+ week: string
+
+ /**
+ * The name of the team using the library.
+ */
+ team_name: string
+
+ /**
+ * The name of the workspace that the team belongs to.
+ */
+ workspace_name?: string
+
+ /**
+ * The number of detach events for this period.
+ */
+ detachments: number
+
+ /**
+ * The number of insertion events for this period.
+ */
+ insertions: number
+}
+
+/**
+ * Library analytics component usage data broken down by component.
+ */
+export type LibraryAnalyticsComponentUsagesByAsset = {
+ /**
+ * Unique, stable id of the component.
+ */
+ component_key: string
+
+ /**
+ * Name of the component.
+ */
+ component_name: string
+
+ /**
+ * Unique, stable id of the component set that this component belongs to.
+ */
+ component_set_key?: string
+
+ /**
+ * Name of the component set that this component belongs to.
+ */
+ component_set_name?: string
+
+ /**
+ * The number of instances of the component within the organization.
+ */
+ usages: number
+
+ /**
+ * The number of teams using the component within the organization.
+ */
+ teams_using: number
+
+ /**
+ * The number of files using the component within the organization.
+ */
+ files_using: number
+}
+
+/**
+ * Library analytics component usage data broken down by file.
+ */
+export type LibraryAnalyticsComponentUsagesByFile = {
+ /**
+ * The name of the file using the library.
+ */
+ file_name: string
+
+ /**
+ * The name of the team the file belongs to.
+ */
+ team_name: string
+
+ /**
+ * The name of the workspace that the file belongs to.
+ */
+ workspace_name?: string
+
+ /**
+ * The number of component instances from the library used within the file.
+ */
+ usages: number
+}
+
+/**
+ * Library analytics style actions data broken down by asset.
+ */
+export type LibraryAnalyticsStyleActionsByAsset = {
+ /**
+ * The date in ISO 8601 format. e.g. 2023-12-13
+ */
+ week: string
+
+ /**
+ * Unique, stable id of the style.
+ */
+ style_key: string
+
+ /**
+ * The name of the style.
+ */
+ style_name: string
+
+ /**
+ * The type of the style.
+ */
+ style_type: string
+
+ /**
+ * The number of detach events for this period.
+ */
+ detachments: number
+
+ /**
+ * The number of insertion events for this period.
+ */
+ insertions: number
+}
+
+/**
+ * Library analytics style action data broken down by team.
+ */
+export type LibraryAnalyticsStyleActionsByTeam = {
+ /**
+ * The date in ISO 8601 format. e.g. 2023-12-13
+ */
+ week: string
+
+ /**
+ * The name of the team using the library.
+ */
+ team_name: string
+
+ /**
+ * The name of the workspace that the team belongs to.
+ */
+ workspace_name?: string
+
+ /**
+ * The number of detach events for this period.
+ */
+ detachments: number
+
+ /**
+ * The number of insertion events for this period.
+ */
+ insertions: number
+}
+
+/**
+ * Library analytics style usage data broken down by component.
+ */
+export type LibraryAnalyticsStyleUsagesByAsset = {
+ /**
+ * Unique, stable id of the style.
+ */
+ style_key: string
+
+ /**
+ * The name of the style.
+ */
+ style_name: string
+
+ /**
+ * The type of the style.
+ */
+ style_type: string
+
+ /**
+ * The number of usages of the style within the organization.
+ */
+ usages: number
+
+ /**
+ * The number of teams using the style within the organization.
+ */
+ teams_using: number
+
+ /**
+ * The number of files using the style within the organization.
+ */
+ files_using: number
+}
+
+/**
+ * Library analytics style usage data broken down by file.
+ */
+export type LibraryAnalyticsStyleUsagesByFile = {
+ /**
+ * The name of the file using the library.
+ */
+ file_name: string
+
+ /**
+ * The name of the team the file belongs to.
+ */
+ team_name: string
+
+ /**
+ * The name of the workspace that the file belongs to.
+ */
+ workspace_name?: string
+
+ /**
+ * The number of times styles from this library are used within the file.
+ */
+ usages: number
+}
+
+/**
+ * Library analytics variable actions data broken down by asset.
+ */
+export type LibraryAnalyticsVariableActionsByAsset = {
+ /**
+ * The date in ISO 8601 format. e.g. 2023-12-13
+ */
+ week: string
+
+ /**
+ * Unique, stable id of the variable.
+ */
+ variable_key: string
+
+ /**
+ * The name of the variable.
+ */
+ variable_name: string
+
+ /**
+ * The type of the variable.
+ */
+ variable_type: string
+
+ /**
+ * Unique, stable id of the collection the variable belongs to.
+ */
+ collection_key: string
+
+ /**
+ * The name of the collection the variable belongs to.
+ */
+ collection_name: string
+
+ /**
+ * The number of detach events for this period.
+ */
+ detachments: number
+
+ /**
+ * The number of insertion events for this period.
+ */
+ insertions: number
+}
+
+/**
+ * Library analytics variable action data broken down by team.
+ */
+export type LibraryAnalyticsVariableActionsByTeam = {
+ /**
+ * The date in ISO 8601 format. e.g. 2023-12-13
+ */
+ week: string
+
+ /**
+ * The name of the team using the library.
+ */
+ team_name: string
+
+ /**
+ * The name of the workspace that the team belongs to.
+ */
+ workspace_name?: string
+
+ /**
+ * The number of detach events for this period.
+ */
+ detachments: number
+
+ /**
+ * The number of insertion events for this period.
+ */
+ insertions: number
+}
+
+/**
+ * Library analytics variable usage data broken down by component.
+ */
+export type LibraryAnalyticsVariableUsagesByAsset = {
+ /**
+ * Unique, stable id of the variable.
+ */
+ variable_key: string
+
+ /**
+ * The name of the variable.
+ */
+ variable_name: string
+
+ /**
+ * The type of the variable.
+ */
+ variable_type: string
+
+ /**
+ * Unique, stable id of the collection the variable belongs to.
+ */
+ collection_key: string
+
+ /**
+ * The name of the collection the variable belongs to.
+ */
+ collection_name: string
+
+ /**
+ * The number of usages of the variable within the organization.
+ */
+ usages: number
+
+ /**
+ * The number of teams using the variable within the organization.
+ */
+ teams_using: number
+
+ /**
+ * The number of files using the variable within the organization.
+ */
+ files_using: number
+}
+
+/**
+ * Library analytics variable usage data broken down by file.
+ */
+export type LibraryAnalyticsVariableUsagesByFile = {
+ /**
+ * The name of the file using the library.
+ */
+ file_name: string
+
+ /**
+ * The name of the team the file belongs to.
+ */
+ team_name: string
+
+ /**
+ * The name of the workspace that the file belongs to.
+ */
+ workspace_name?: string
+
+ /**
+ * The number of times variables from this library are used within the file.
+ */
+ usages: number
+}
+
+/**
+ * If pagination is needed due to the length of the response, identifies the next and previous
+ * pages.
+ */
+export type ResponsePagination = {
+ /**
+ * A URL that calls the previous page of the response.
+ */
+ prev_page?: string
+
+ /**
+ * A URL that calls the next page of the response.
+ */
+ next_page?: string
+}
+
+/**
+ * Pagination cursor
+ */
+export type ResponseCursor = {
+ before?: number
+
+ after?: number
+}
+
+/**
+ * A response indicating an error occurred.
+ */
+export type ErrorResponsePayloadWithErrMessage = {
+ /**
+ * Status code
+ */
+ status: number
+
+ /**
+ * A string describing the error
+ */
+ err: string
+}
+
+/**
+ * A response indicating an error occurred.
+ */
+export type ErrorResponsePayloadWithErrorBoolean = {
+ /**
+ * For erroneous requests, this value is always `true`.
+ */
+ error: true
+
+ /**
+ * Status code
+ */
+ status: number
+
+ /**
+ * A string describing the error
+ */
+ message: string
+}
+
+/**
+ * Response from the GET /v1/files/{file_key} endpoint.
+ */
+export type GetFileResponse = {
+ /**
+ * The name of the file as it appears in the editor.
+ */
+ name: string
+
+ /**
+ * The role of the user making the API request in relation to the file.
+ */
+ role: 'owner' | 'editor' | 'viewer'
+
+ /**
+ * The UTC ISO 8601 time at which the file was last modified.
+ */
+ lastModified: string
+
+ /**
+ * The type of editor associated with this file.
+ */
+ editorType: 'figma' | 'figjam'
+
+ /**
+ * A URL to a thumbnail image of the file.
+ */
+ thumbnailUrl?: string
+
+ /**
+ * The version number of the file. This number is incremented when a file is modified and can be
+ * used to check if the file has changed between requests.
+ */
+ version: string
+
+ document: DocumentNode
+
+ /**
+ * A mapping from component IDs to component metadata.
+ */
+ components: { [key: string]: Component }
+
+ /**
+ * A mapping from component set IDs to component set metadata.
+ */
+ componentSets: { [key: string]: ComponentSet }
+
+ /**
+ * The version of the file schema that this file uses.
+ */
+ schemaVersion: number
+
+ /**
+ * A mapping from style IDs to style metadata.
+ */
+ styles: { [key: string]: Style }
+
+ /**
+ * The share permission level of the file link.
+ */
+ linkAccess?: string
+
+ /**
+ * The key of the main file for this file. If present, this file is a component or component set.
+ */
+ mainFileKey?: string
+
+ /**
+ * A list of branches for this file.
+ */
+ branches?: {
+ /**
+ * The key of the branch.
+ */
+ key: string
+
+ /**
+ * The name of the branch.
+ */
+ name: string
+
+ /**
+ * A URL to a thumbnail image of the branch.
+ */
+ thumbnail_url: string
+
+ /**
+ * The UTC ISO 8601 time at which the branch was last modified.
+ */
+ last_modified: string
+ }[]
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/nodes endpoint.
+ */
+export type GetFileNodesResponse = {
+ /**
+ * The name of the file as it appears in the editor.
+ */
+ name: string
+
+ /**
+ * The role of the user making the API request in relation to the file.
+ */
+ role: 'owner' | 'editor' | 'viewer'
+
+ /**
+ * The UTC ISO 8601 time at which the file was last modified.
+ */
+ lastModified: string
+
+ /**
+ * The type of editor associated with this file.
+ */
+ editorType: 'figma' | 'figjam'
+
+ /**
+ * A URL to a thumbnail image of the file.
+ */
+ thumbnailUrl: string
+
+ /**
+ * The version number of the file. This number is incremented when a file is modified and can be
+ * used to check if the file has changed between requests.
+ */
+ version: string
+
+ /**
+ * A mapping from node IDs to node metadata.
+ */
+ nodes: {
+ [key: string]: {
+ document: Node
+
+ /**
+ * A mapping from component IDs to component metadata.
+ */
+ components: { [key: string]: Component }
+
+ /**
+ * A mapping from component set IDs to component set metadata.
+ */
+ componentSets: { [key: string]: ComponentSet }
+
+ /**
+ * The version of the file schema that this file uses.
+ */
+ schemaVersion: number
+
+ /**
+ * A mapping from style IDs to style metadata.
+ */
+ styles: { [key: string]: Style }
+ }
+ }
+}
+
+/**
+ * Response from the GET /v1/images/{file_key} endpoint.
+ */
+export type GetImagesResponse = {
+ /**
+ * For successful requests, this value is always `null`.
+ */
+ err: null
+
+ /**
+ * A map from node IDs to URLs of the rendered images.
+ */
+ images: { [key: string]: string | null }
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/images endpoint.
+ */
+export type GetImageFillsResponse = {
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ /**
+ * Status code
+ */
+ status: 200
+
+ meta: {
+ /**
+ * A map of image references to URLs of the image fills.
+ */
+ images: { [key: string]: string }
+ }
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/meta endpoint.
+ */
+export type GetFileMetaResponse = {
+ /**
+ * The name of the file.
+ */
+ name: string
+
+ /**
+ * The name of the project containing the file.
+ */
+ folder_name?: string
+
+ /**
+ * The UTC ISO 8601 time at which the file content was last modified.
+ */
+ last_touched_at: string
+
+ /**
+ * The user who created the file.
+ */
+ creator: User
+
+ /**
+ * The user who last modified the file contents.
+ */
+ last_touched_by?: User
+
+ /**
+ * A URL to a thumbnail image of the file.
+ */
+ thumbnail_url?: string
+
+ /**
+ * The type of editor associated with this file.
+ */
+ editorType: 'figma' | 'figjam' | 'slides'
+
+ /**
+ * The role of the user making the API request in relation to the file.
+ */
+ role?: 'owner' | 'editor' | 'viewer'
+
+ /**
+ * Access policy for users who have the link to the file.
+ */
+ link_access?: 'view' | 'edit' | 'org_view' | 'org_edit' | 'inherit'
+
+ /**
+ * The URL of the file.
+ */
+ url?: string
+
+ /**
+ * The version number of the file. This number is incremented when a file is modified and can be
+ * used to check if the file has changed between requests.
+ */
+ version?: string
+}
+
+/**
+ * Response from the GET /v1/teams/{team_id}/projects endpoint.
+ */
+export type GetTeamProjectsResponse = {
+ /**
+ * The team's name.
+ */
+ name: string
+
+ /**
+ * An array of projects.
+ */
+ projects: Project[]
+}
+
+/**
+ * Response from the GET /v1/projects/{project_id}/files endpoint.
+ */
+export type GetProjectFilesResponse = {
+ /**
+ * The project's name.
+ */
+ name: string
+
+ /**
+ * An array of files.
+ */
+ files: {
+ /**
+ * The file's key.
+ */
+ key: string
+
+ /**
+ * The file's name.
+ */
+ name: string
+
+ /**
+ * The file's thumbnail URL.
+ */
+ thumbnail_url?: string
+
+ /**
+ * The UTC ISO 8601 time at which the file was last modified.
+ */
+ last_modified: string
+ }[]
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/versions endpoint.
+ */
+export type GetFileVersionsResponse = {
+ /**
+ * An array of versions.
+ */
+ versions: Version[]
+
+ pagination: ResponsePagination
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/comments endpoint.
+ */
+export type GetCommentsResponse = {
+ /**
+ * An array of comments.
+ */
+ comments: Comment[]
+}
+
+/**
+ * Response from the POST /v1/files/{file_key}/comments endpoint.
+ */
+export type PostCommentResponse = Comment
+
+/**
+ * Response from the DELETE /v1/files/{file_key}/comments/{comment_id} endpoint.
+ */
+export type DeleteCommentResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/comments/{comment_id}/reactions endpoint.
+ */
+export type GetCommentReactionsResponse = {
+ /**
+ * An array of reactions.
+ */
+ reactions: Reaction[]
+
+ pagination: ResponsePagination
+}
+
+/**
+ * Response from the POST /v1/files/{file_key}/comments/{comment_id}/reactions endpoint.
+ */
+export type PostCommentReactionResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+}
+
+/**
+ * Response from the DELETE /v1/files/{file_key}/comments/{comment_id}/reactions endpoint.
+ */
+export type DeleteCommentReactionResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+}
+
+/**
+ * Response from the GET /v1/me endpoint.
+ */
+export type GetMeResponse = User & {
+ /**
+ * Email associated with the user's account. This property is only present on the /v1/me endpoint.
+ */
+ email: string
+}
+
+/**
+ * Response from the GET /v1/teams/{team_id}/components endpoint.
+ */
+export type GetTeamComponentsResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: {
+ components: PublishedComponent[]
+
+ cursor?: ResponseCursor
+ }
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/components endpoint.
+ */
+export type GetFileComponentsResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: { components: PublishedComponent[] }
+}
+
+/**
+ * Response from the GET /v1/components/{key} endpoint.
+ */
+export type GetComponentResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: PublishedComponent
+}
+
+/**
+ * Response from the GET /v1/teams/{team_id}/component_sets endpoint.
+ */
+export type GetTeamComponentSetsResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: {
+ component_sets: PublishedComponentSet[]
+
+ cursor?: ResponseCursor
+ }
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/component_sets endpoint.
+ */
+export type GetFileComponentSetsResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: { component_sets: PublishedComponentSet[] }
+}
+
+/**
+ * Response from the GET /v1/component_sets/{key} endpoint.
+ */
+export type GetComponentSetResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: PublishedComponentSet
+}
+
+/**
+ * Response from the GET /v1/teams/{team_id}/styles endpoint.
+ */
+export type GetTeamStylesResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: {
+ styles: PublishedStyle[]
+
+ cursor?: ResponseCursor
+ }
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/styles endpoint.
+ */
+export type GetFileStylesResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: { styles: PublishedStyle[] }
+}
+
+/**
+ * Response from the GET /v1/styles/{key} endpoint.
+ */
+export type GetStyleResponse = {
+ /**
+ * The status of the request.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: PublishedStyle
+}
+
+/**
+ * Response from the POST /v2/webhooks endpoint.
+ */
+export type PostWebhookResponse = WebhookV2
+
+/**
+ * Response from the GET /v2/webhooks/{webhook_id} endpoint.
+ */
+export type GetWebhookResponse = WebhookV2
+
+/**
+ * Response from the GET /v2/webhooks endpoint.
+ */
+export type GetWebhooksResponse = {
+ /**
+ * An array of webhooks.
+ */
+ webhooks: WebhookV2[]
+
+ pagination?: ResponsePagination
+}
+
+/**
+ * Response from the PUT /v2/webhooks/{webhook_id} endpoint.
+ */
+export type PutWebhookResponse = WebhookV2
+
+/**
+ * Response from the DELETE /v2/webhooks/{webhook_id} endpoint.
+ */
+export type DeleteWebhookResponse = WebhookV2
+
+/**
+ * Response from the GET /v2/teams/{team_id}/webhooks endpoint.
+ */
+export type GetTeamWebhooksResponse = {
+ /**
+ * An array of webhooks.
+ */
+ webhooks: WebhookV2[]
+}
+
+/**
+ * Response from the GET /v2/webhooks/{webhook_id}/requests endpoint.
+ */
+export type GetWebhookRequestsResponse = {
+ /**
+ * An array of webhook requests.
+ */
+ requests: WebhookV2Request[]
+}
+
+/**
+ * Response from the GET /v1/activity_logs endpoint.
+ */
+export type GetActivityLogsResponse = {
+ /**
+ * The response status code.
+ */
+ status?: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error?: false
+
+ meta?: {
+ /**
+ * An array of activity logs sorted by timestamp in ascending order by default.
+ */
+ activity_logs?: ActivityLog[]
+
+ /**
+ * Encodes the last event (the most recent event)
+ */
+ cursor?: string
+
+ /**
+ * Whether there is a next page of events
+ */
+ next_page?: boolean
+ }
+}
+
+/**
+ * Response from the GET /v1/payments endpoint.
+ */
+export type GetPaymentsResponse = {
+ /**
+ * The response status code.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: PaymentInformation
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/variables/local endpoint.
+ */
+export type GetLocalVariablesResponse = {
+ /**
+ * The response status code.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: {
+ /**
+ * A map of variable ids to variables
+ */
+ variables: { [key: string]: LocalVariable }
+
+ /**
+ * A map of variable collection ids to variable collections
+ */
+ variableCollections: { [key: string]: LocalVariableCollection }
+ }
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/variables/published endpoint.
+ */
+export type GetPublishedVariablesResponse = {
+ /**
+ * The response status code.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: {
+ /**
+ * A map of variable ids to variables
+ */
+ variables: { [key: string]: PublishedVariable }
+
+ /**
+ * A map of variable collection ids to variable collections
+ */
+ variableCollections: { [key: string]: PublishedVariableCollection }
+ }
+}
+
+/**
+ * Response from the POST /v1/files/{file_key}/variables endpoint.
+ */
+export type PostVariablesResponse = {
+ /**
+ * The response status code.
+ */
+ status: 200
+
+ /**
+ * For successful requests, this value is always `false`.
+ */
+ error: false
+
+ meta: {
+ /**
+ * A map of temporary ids in the request to the real ids of the newly created objects
+ */
+ tempIdToRealId: { [key: string]: string }
+ }
+}
+
+/**
+ * Response from the GET /v1/files/{file_key}/dev_resources endpoint.
+ */
+export type GetDevResourcesResponse = {
+ /**
+ * An array of dev resources.
+ */
+ dev_resources: DevResource[]
+}
+
+/**
+ * Response from the POST /v1/dev_resources endpoint.
+ */
+export type PostDevResourcesResponse = {
+ /**
+ * An array of links created.
+ */
+ links_created: DevResource[]
+
+ /**
+ * An array of errors.
+ */
+ errors?: {
+ /**
+ * The file key.
+ */
+ file_key?: string | null
+
+ /**
+ * The node id.
+ */
+ node_id?: string | null
+
+ /**
+ * The error message.
+ */
+ error: string
+ }[]
+}
+
+/**
+ * Response from the PUT /v1/dev_resources endpoint.
+ */
+export type PutDevResourcesResponse = {
+ /**
+ * An array of links updated.
+ */
+ links_updated?: DevResource[]
+
+ /**
+ * An array of errors.
+ */
+ errors?: {
+ /**
+ * The id of the dev resource.
+ */
+ id?: string
+
+ /**
+ * The error message.
+ */
+ error: string
+ }[]
+}
+
+/**
+ * Response from the DELETE /v1/files/{file_key}/dev_resources/{dev_resource_id} endpoint.
+ */
+export type DeleteDevResourceResponse = void
+
+/**
+ * Response from the GET /v1/analytics/libraries/{file_key}/component/actions.
+ */
+export type GetLibraryAnalyticsComponentActionsResponse = {
+ /**
+ * An array of analytics data.
+ */
+ rows: LibraryAnalyticsComponentActionsByAsset[] | LibraryAnalyticsComponentActionsByTeam[]
+
+ /**
+ * Whether there is a next page of data that can be fetched.
+ */
+ next_page: boolean
+
+ /**
+ * The cursor to use to fetch the next page of data. Not present if next_page is false.
+ */
+ cursor?: string
+}
+
+/**
+ * Response from the PUT /v1/analytics/libraries/{file_key}/component/usages.
+ */
+export type GetLibraryAnalyticsComponentUsagesResponse = {
+ /**
+ * An array of analytics data.
+ */
+ rows: LibraryAnalyticsComponentUsagesByAsset[] | LibraryAnalyticsComponentUsagesByFile[]
+
+ /**
+ * Whether there is a next page of data that can be fetched.
+ */
+ next_page: boolean
+
+ /**
+ * The cursor to use to fetch the next page of data. Not present if next_page is false.
+ */
+ cursor?: string
+}
+
+/**
+ * Response from the GET /v1/analytics/libraries/{file_key}/style/actions.
+ */
+export type GetLibraryAnalyticsStyleActionsResponse = {
+ /**
+ * An array of analytics data.
+ */
+ rows: LibraryAnalyticsStyleActionsByAsset[] | LibraryAnalyticsStyleActionsByTeam[]
+
+ /**
+ * Whether there is a next page of data that can be fetched.
+ */
+ next_page: boolean
+
+ /**
+ * The cursor to use to fetch the next page of data. Not present if next_page is false.
+ */
+ cursor?: string
+}
+
+/**
+ * Response from the PUT /v1/analytics/libraries/{file_key}/style/usages.
+ */
+export type GetLibraryAnalyticsStyleUsagesResponse = {
+ /**
+ * An array of analytics data.
+ */
+ rows: LibraryAnalyticsStyleUsagesByAsset[] | LibraryAnalyticsStyleUsagesByFile[]
+
+ /**
+ * Whether there is a next page of data that can be fetched.
+ */
+ next_page: boolean
+
+ /**
+ * The cursor to use to fetch the next page of data. Not present if next_page is false.
+ */
+ cursor?: string
+}
+
+/**
+ * Response from the GET /v1/analytics/libraries/{file_key}/variable/actions.
+ */
+export type GetLibraryAnalyticsVariableActionsResponse = {
+ /**
+ * An array of analytics data.
+ */
+ rows: LibraryAnalyticsVariableActionsByAsset[] | LibraryAnalyticsVariableActionsByTeam[]
+
+ /**
+ * Whether there is a next page of data that can be fetched.
+ */
+ next_page: boolean
+
+ /**
+ * The cursor to use to fetch the next page of data. Not present if next_page is false.
+ */
+ cursor?: string
+}
+
+/**
+ * Response from the PUT /v1/analytics/libraries/{file_key}/variable/usages.
+ */
+export type GetLibraryAnalyticsVariableUsagesResponse = {
+ /**
+ * An array of analytics data.
+ */
+ rows: LibraryAnalyticsVariableUsagesByAsset[] | LibraryAnalyticsVariableUsagesByFile[]
+
+ /**
+ * Whether there is a next page of data that can be fetched.
+ */
+ next_page: boolean
+
+ /**
+ * The cursor to use to fetch the next page of data. Not present if next_page is false.
+ */
+ cursor?: string
+}
+
+/**
+ * Bad request. Parameters are invalid or malformed. Please check the input formats. This error can
+ * also happen if the requested resources are too large to complete the request, which results in a
+ * timeout. Please reduce the number and size of objects requested.
+ */
+export type BadRequestErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & {
+ /**
+ * Status code
+ */
+ status: 400
+}
+
+/**
+ * Bad request. Parameters are invalid or malformed. Please check the input formats. This error can
+ * also happen if the requested resources are too large to complete the request, which results in a
+ * timeout. Please reduce the number and size of objects requested.
+ */
+export type BadRequestErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & {
+ /**
+ * Status code
+ */
+ status: 400
+}
+
+/**
+ * Token is missing or incorrect.
+ */
+export type UnauthorizedErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & {
+ /**
+ * Status code
+ */
+ status: 401
+}
+
+/**
+ * The request was valid, but the server is refusing action. The user might not have the necessary
+ * permissions for a resource, or may need an account of some sort.
+ */
+export type ForbiddenErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & {
+ /**
+ * Status code
+ */
+ status: 403
+}
+
+/**
+ * The request was valid, but the server is refusing action. The user might not have the necessary
+ * permissions for a resource, or may need an account of some sort.
+ */
+export type ForbiddenErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & {
+ /**
+ * Status code
+ */
+ status: 403
+}
+
+/**
+ * The requested file or resource was not found.
+ */
+export type NotFoundErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & {
+ /**
+ * Status code
+ */
+ status: 404
+}
+
+/**
+ * The requested file or resource was not found.
+ */
+export type NotFoundErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & {
+ /**
+ * Status code
+ */
+ status: 404
+}
+
+/**
+ * In some cases API requests may be throttled or rate limited. Please wait a while before
+ * attempting the request again (typically a minute).
+ */
+export type TooManyRequestsErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & {
+ /**
+ * Status code
+ */
+ status: 429
+}
+
+/**
+ * In some cases API requests may be throttled or rate limited. Please wait a while before
+ * attempting the request again (typically a minute).
+ */
+export type TooManyRequestsErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & {
+ /**
+ * Status code
+ */
+ status: 429
+}
+
+/**
+ * An internal server error occurred.
+ */
+export type InternalServerErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & {
+ /**
+ * Status code
+ */
+ status: 500
+}
+
+/**
+ * An internal server error occurred.
+ */
+export type InternalServerErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & {
+ /**
+ * Status code
+ */
+ status: 500
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}
+ */
+export type GetFilePathParams = {
+ /**
+ * File to export JSON from. This can be a file key or branch key. Use `GET /v1/files/:key` with
+ * the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/files/{file_key}
+ */
+export type GetFileQueryParams = {
+ /**
+ * A specific version ID to get. Omitting this will get the current version of the file.
+ */
+ version?: string
+ /**
+ * Comma separated list of nodes that you care about in the document. If specified, only a subset of
+ * the document will be returned corresponding to the nodes listed, their children, and everything
+ * between the root node and the listed nodes.
+ *
+ * Note: There may be other nodes included in the returned JSON that are outside the ancestor chains
+ * of the desired nodes. The response may also include dependencies of anything in the nodes'
+ * subtrees. For example, if a node subtree contains an instance of a local component that lives
+ * elsewhere in that file, that component and its ancestor chain will also be included.
+ *
+ * For historical reasons, top-level canvas nodes are always returned, regardless of whether they
+ * are listed in the `ids` parameter. This quirk may be removed in a future version of the API.
+ */
+ ids?: string
+ /**
+ * Positive integer representing how deep into the document tree to traverse. For example, setting
+ * this to 1 returns only Pages, setting it to 2 returns Pages and all top level objects on each
+ * page. Not setting this parameter returns all nodes.
+ */
+ depth?: number
+ /**
+ * Set to "paths" to export vector data.
+ */
+ geometry?: string
+ /**
+ * A comma separated list of plugin IDs and/or the string "shared". Any data present in the document
+ * written by those plugins will be included in the result in the `pluginData` and
+ * `sharedPluginData` properties.
+ */
+ plugin_data?: string
+ /**
+ * Returns branch metadata for the requested file. If the file is a branch, the main file's key will
+ * be included in the returned response. If the file has branches, their metadata will be included
+ * in the returned response. Default: false.
+ */
+ branch_data?: boolean
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/nodes
+ */
+export type GetFileNodesPathParams = {
+ /**
+ * File to export JSON from. This can be a file key or branch key. Use `GET /v1/files/:key` with
+ * the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/files/{file_key}/nodes
+ */
+export type GetFileNodesQueryParams = {
+ /**
+ * A comma separated list of node IDs to retrieve and convert.
+ */
+ ids: string
+ /**
+ * A specific version ID to get. Omitting this will get the current version of the file.
+ */
+ version?: string
+ /**
+ * Positive integer representing how deep into the node tree to traverse. For example, setting this
+ * to 1 will return only the children directly underneath the desired nodes. Not setting this
+ * parameter returns all nodes.
+ *
+ * Note: this parameter behaves differently from the same parameter in the `GET /v1/files/:key`
+ * endpoint. In this endpoint, the depth will be counted starting from the desired node rather than
+ * the document root node.
+ */
+ depth?: number
+ /**
+ * Set to "paths" to export vector data.
+ */
+ geometry?: string
+ /**
+ * A comma separated list of plugin IDs and/or the string "shared". Any data present in the document
+ * written by those plugins will be included in the result in the `pluginData` and
+ * `sharedPluginData` properties.
+ */
+ plugin_data?: string
+}
+
+/**
+ * Path parameters for GET /v1/images/{file_key}
+ */
+export type GetImagesPathParams = {
+ /**
+ * File to export images from. This can be a file key or branch key. Use `GET /v1/files/:key` with
+ * the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/images/{file_key}
+ */
+export type GetImagesQueryParams = {
+ /**
+ * A comma separated list of node IDs to render.
+ */
+ ids: string
+ /**
+ * A specific version ID to get. Omitting this will get the current version of the file.
+ */
+ version?: string
+ /**
+ * A number between 0.01 and 4, the image scaling factor.
+ */
+ scale?: number
+ /**
+ * A string enum for the image output format.
+ */
+ format?: 'jpg' | 'png' | 'svg' | 'pdf'
+ /**
+ * Whether text elements are rendered as outlines (vector paths) or as `` elements in SVGs.
+ *
+ * Rendering text elements as outlines guarantees that the text looks exactly the same in the SVG as
+ * it does in the browser/inside Figma.
+ *
+ * Exporting as `` allows text to be selectable inside SVGs and generally makes the SVG easier
+ * to read. However, this relies on the browser's rendering engine which can vary between browsers
+ * and/or operating systems. As such, visual accuracy is not guaranteed as the result could look
+ * different than in Figma.
+ */
+ svg_outline_text?: boolean
+ /**
+ * Whether to include id attributes for all SVG elements. Adds the layer name to the `id` attribute
+ * of an svg element.
+ */
+ svg_include_id?: boolean
+ /**
+ * Whether to include node id attributes for all SVG elements. Adds the node id to a `data-node-id`
+ * attribute of an svg element.
+ */
+ svg_include_node_id?: boolean
+ /**
+ * Whether to simplify inside/outside strokes and use stroke attribute if possible instead of
+ * ``.
+ */
+ svg_simplify_stroke?: boolean
+ /**
+ * Whether content that overlaps the node should be excluded from rendering. Passing false (i.e.,
+ * rendering overlaps) may increase processing time, since more of the document must be included in
+ * rendering.
+ */
+ contents_only?: boolean
+ /**
+ * Use the full dimensions of the node regardless of whether or not it is cropped or the space
+ * around it is empty. Use this to export text nodes without cropping.
+ */
+ use_absolute_bounds?: boolean
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/images
+ */
+export type GetImageFillsPathParams = {
+ /**
+ * File to get image URLs from. This can be a file key or branch key. Use `GET /v1/files/:key` with
+ * the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/meta
+ */
+export type GetFileMetaPathParams = {
+ /**
+ * File to get metadata for. This can be a file key or branch key. Use `GET /v1/files/:key` with
+ * the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Path parameters for GET /v1/teams/{team_id}/projects
+ */
+export type GetTeamProjectsPathParams = {
+ /**
+ * ID of the team to list projects from
+ */
+ team_id: string
+}
+
+/**
+ * Path parameters for GET /v1/projects/{project_id}/files
+ */
+export type GetProjectFilesPathParams = {
+ /**
+ * ID of the project to list files from
+ */
+ project_id: string
+}
+
+/**
+ * Query parameters for GET /v1/projects/{project_id}/files
+ */
+export type GetProjectFilesQueryParams = {
+ /**
+ * Returns branch metadata in the response for each main file with a branch inside the project.
+ */
+ branch_data?: boolean
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/versions
+ */
+export type GetFileVersionsPathParams = {
+ /**
+ * File to get version history from. This can be a file key or branch key. Use `GET /v1/files/:key`
+ * with the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/files/{file_key}/versions
+ */
+export type GetFileVersionsQueryParams = {
+ /**
+ * The number of items returned in a page of the response. If not included, `page_size` is `30`.
+ */
+ page_size?: number
+ /**
+ * A version ID for one of the versions in the history. Gets versions before this ID. Used for
+ * paginating. If the response is not paginated, this link returns the same data in the current
+ * response.
+ */
+ before?: number
+ /**
+ * A version ID for one of the versions in the history. Gets versions after this ID. Used for
+ * paginating. If the response is not paginated, this property is not included.
+ */
+ after?: number
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/comments
+ */
+export type GetCommentsPathParams = {
+ /**
+ * File to get comments from. This can be a file key or branch key. Use `GET /v1/files/:key` with
+ * the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/files/{file_key}/comments
+ */
+export type GetCommentsQueryParams = {
+ /**
+ * If enabled, will return comments as their markdown equivalents when applicable.
+ */
+ as_md?: boolean
+}
+
+/**
+ * Path parameters for POST /v1/files/{file_key}/comments
+ */
+export type PostCommentPathParams = {
+ /**
+ * File to add comments in. This can be a file key or branch key. Use `GET /v1/files/:key` with the
+ * `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Request body parameters for POST /v1/files/{file_key}/comments
+ */
+export type PostCommentRequestBody = {
+ /**
+ * The text contents of the comment to post.
+ */
+ message: string
+
+ /**
+ * The ID of the comment to reply to, if any. This must be a root comment. You cannot reply to other
+ * replies (a comment that has a parent_id).
+ */
+ comment_id?: string
+
+ /**
+ * The position where to place the comment.
+ */
+ client_meta?: Vector | FrameOffset | Region | FrameOffsetRegion
+}
+
+/**
+ * Path parameters for DELETE /v1/files/{file_key}/comments/{comment_id}
+ */
+export type DeleteCommentPathParams = {
+ /**
+ * File to delete comment from. This can be a file key or branch key. Use `GET /v1/files/:key` with
+ * the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+ /**
+ * Comment id of comment to delete
+ */
+ comment_id: string
+}
+
+/**
+ * Path parameters for DELETE /v1/files/{file_key}/comments/{comment_id}/reactions
+ */
+export type DeleteCommentReactionPathParams = {
+ /**
+ * File to delete comment reaction from. This can be a file key or branch key. Use `GET
+ * /v1/files/:key` with the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+ /**
+ * ID of comment to delete reaction from.
+ */
+ comment_id: string
+}
+
+/**
+ * Query parameters for DELETE /v1/files/{file_key}/comments/{comment_id}/reactions
+ */
+export type DeleteCommentReactionQueryParams = { emoji: Emoji }
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/comments/{comment_id}/reactions
+ */
+export type GetCommentReactionsPathParams = {
+ /**
+ * File to get comment containing reactions from. This can be a file key or branch key. Use `GET
+ * /v1/files/:key` with the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+ /**
+ * ID of comment to get reactions from.
+ */
+ comment_id: string
+}
+
+/**
+ * Query parameters for GET /v1/files/{file_key}/comments/{comment_id}/reactions
+ */
+export type GetCommentReactionsQueryParams = {
+ /**
+ * Cursor for pagination, retrieved from the response of the previous call.
+ */
+ cursor?: string
+}
+
+/**
+ * Path parameters for POST /v1/files/{file_key}/comments/{comment_id}/reactions
+ */
+export type PostCommentReactionPathParams = {
+ /**
+ * File to post comment reactions to. This can be a file key or branch key. Use `GET
+ * /v1/files/:key` with the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+ /**
+ * ID of comment to react to.
+ */
+ comment_id: string
+}
+
+/**
+ * Request body parameters for POST /v1/files/{file_key}/comments/{comment_id}/reactions
+ */
+export type PostCommentReactionRequestBody = { emoji: Emoji }
+
+/**
+ * Path parameters for GET /v1/teams/{team_id}/components
+ */
+export type GetTeamComponentsPathParams = {
+ /**
+ * Id of the team to list components from.
+ */
+ team_id: string
+}
+
+/**
+ * Query parameters for GET /v1/teams/{team_id}/components
+ */
+export type GetTeamComponentsQueryParams = {
+ /**
+ * Number of items to return in a paged list of results. Defaults to 30.
+ */
+ page_size?: number
+ /**
+ * Cursor indicating which id after which to start retrieving components for. Exclusive with before.
+ * The cursor value is an internally tracked integer that doesn't correspond to any Ids.
+ */
+ after?: number
+ /**
+ * Cursor indicating which id before which to start retrieving components for. Exclusive with after.
+ * The cursor value is an internally tracked integer that doesn't correspond to any Ids.
+ */
+ before?: number
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/components
+ */
+export type GetFileComponentsPathParams = {
+ /**
+ * File to list components from. This must be a main file key, not a branch key, as it is not
+ * possible to publish from branches.
+ */
+ file_key: string
+}
+
+/**
+ * Path parameters for GET /v1/components/{key}
+ */
+export type GetComponentPathParams = {
+ /**
+ * The unique identifier of the component.
+ */
+ key: string
+}
+
+/**
+ * Path parameters for GET /v1/teams/{team_id}/component_sets
+ */
+export type GetTeamComponentSetsPathParams = {
+ /**
+ * Id of the team to list component sets from.
+ */
+ team_id: string
+}
+
+/**
+ * Query parameters for GET /v1/teams/{team_id}/component_sets
+ */
+export type GetTeamComponentSetsQueryParams = {
+ /**
+ * Number of items to return in a paged list of results. Defaults to 30.
+ */
+ page_size?: number
+ /**
+ * Cursor indicating which id after which to start retrieving component sets for. Exclusive with
+ * before. The cursor value is an internally tracked integer that doesn't correspond to any Ids.
+ */
+ after?: number
+ /**
+ * Cursor indicating which id before which to start retrieving component sets for. Exclusive with
+ * after. The cursor value is an internally tracked integer that doesn't correspond to any Ids.
+ */
+ before?: number
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/component_sets
+ */
+export type GetFileComponentSetsPathParams = {
+ /**
+ * File to list component sets from. This must be a main file key, not a branch key, as it is not
+ * possible to publish from branches.
+ */
+ file_key: string
+}
+
+/**
+ * Path parameters for GET /v1/component_sets/{key}
+ */
+export type GetComponentSetPathParams = {
+ /**
+ * The unique identifier of the component set.
+ */
+ key: string
+}
+
+/**
+ * Path parameters for GET /v1/teams/{team_id}/styles
+ */
+export type GetTeamStylesPathParams = {
+ /**
+ * Id of the team to list styles from.
+ */
+ team_id: string
+}
+
+/**
+ * Query parameters for GET /v1/teams/{team_id}/styles
+ */
+export type GetTeamStylesQueryParams = {
+ /**
+ * Number of items to return in a paged list of results. Defaults to 30.
+ */
+ page_size?: number
+ /**
+ * Cursor indicating which id after which to start retrieving styles for. Exclusive with before. The
+ * cursor value is an internally tracked integer that doesn't correspond to any Ids.
+ */
+ after?: number
+ /**
+ * Cursor indicating which id before which to start retrieving styles for. Exclusive with after. The
+ * cursor value is an internally tracked integer that doesn't correspond to any Ids.
+ */
+ before?: number
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/styles
+ */
+export type GetFileStylesPathParams = {
+ /**
+ * File to list styles from. This must be a main file key, not a branch key, as it is not possible
+ * to publish from branches.
+ */
+ file_key: string
+}
+
+/**
+ * Path parameters for GET /v1/styles/{key}
+ */
+export type GetStylePathParams = {
+ /**
+ * The unique identifier of the style.
+ */
+ key: string
+}
+
+/**
+ * Query parameters for GET /v2/webhooks
+ */
+export type GetWebhooksQueryParams = {
+ /**
+ * Context to create the resource on. Should be "team", "project", or "file".
+ */
+ context?: string
+ /**
+ * The id of the context that you want to get attached webhooks for. If you're using context_id, you
+ * cannot use plan_api_id.
+ */
+ context_id?: string
+ /**
+ * The id of your plan. Use this to get all webhooks for all contexts you have access to. If you're
+ * using plan_api_id, you cannot use context or context_id. When you use plan_api_id, the response
+ * is paginated.
+ */
+ plan_api_id?: string
+ /**
+ * If you're using plan_api_id, this is the cursor to use for pagination. If you're using context or
+ * context_id, this parameter is ignored. Provide the next_page or prev_page value from the previous
+ * response to get the next or previous page of results.
+ */
+ cursor?: string
+}
+
+/**
+ * Request body parameters for POST /v2/webhooks
+ */
+export type PostWebhookRequestBody = {
+ event_type: WebhookV2Event
+
+ /**
+ * Team id to receive updates about. This is deprecated, use 'context' and 'context_id' instead.
+ *
+ * @deprecated
+ */
+ team_id?: string
+
+ /**
+ * Context to create the webhook for. Must be "team", "project", or "file".
+ */
+ context: string
+
+ /**
+ * The id of the context you want to receive updates about.
+ */
+ context_id: string
+
+ /**
+ * The HTTP endpoint that will receive a POST request when the event triggers. Max length 2048
+ * characters.
+ */
+ endpoint: string
+
+ /**
+ * String that will be passed back to your webhook endpoint to verify that it is being called by
+ * Figma. Max length 100 characters.
+ */
+ passcode: string
+
+ /**
+ * State of the webhook, including any error state it may be in
+ */
+ status?: WebhookV2Status
+
+ /**
+ * User provided description or name for the webhook. Max length 150 characters.
+ */
+ description?: string
+}
+
+/**
+ * Path parameters for DELETE /v2/webhooks/{webhook_id}
+ */
+export type DeleteWebhookPathParams = {
+ /**
+ * ID of webhook to delete
+ */
+ webhook_id: string
+}
+
+/**
+ * Path parameters for GET /v2/webhooks/{webhook_id}
+ */
+export type GetWebhookPathParams = {
+ /**
+ * ID of webhook to get
+ */
+ webhook_id: string
+}
+
+/**
+ * Path parameters for PUT /v2/webhooks/{webhook_id}
+ */
+export type PutWebhookPathParams = {
+ /**
+ * ID of webhook to update
+ */
+ webhook_id: string
+}
+
+/**
+ * Request body parameters for PUT /v2/webhooks/{webhook_id}
+ */
+export type PutWebhookRequestBody = {
+ event_type: WebhookV2Event
+
+ /**
+ * The HTTP endpoint that will receive a POST request when the event triggers. Max length 2048
+ * characters.
+ */
+ endpoint: string
+
+ /**
+ * String that will be passed back to your webhook endpoint to verify that it is being called by
+ * Figma. Max length 100 characters.
+ */
+ passcode: string
+
+ /**
+ * State of the webhook, including any error state it may be in
+ */
+ status?: WebhookV2Status
+
+ /**
+ * User provided description or name for the webhook. Max length 150 characters.
+ */
+ description?: string
+}
+
+/**
+ * Path parameters for GET /v2/teams/{team_id}/webhooks
+ */
+export type GetTeamWebhooksPathParams = {
+ /**
+ * ID of team to get webhooks for
+ */
+ team_id: string
+}
+
+/**
+ * Path parameters for GET /v2/webhooks/{webhook_id}/requests
+ */
+export type GetWebhookRequestsPathParams = {
+ /**
+ * The id of the webhook subscription you want to see events from
+ */
+ webhook_id: string
+}
+
+/**
+ * Query parameters for GET /v1/activity_logs
+ */
+export type GetActivityLogsQueryParams = {
+ /**
+ * Event type(s) to include in the response. Can have multiple values separated by comma. All
+ * events are returned by default.
+ */
+ events?: string
+ /**
+ * Unix timestamp of the least recent event to include. This param defaults to one year ago if
+ * unspecified.
+ */
+ start_time?: number
+ /**
+ * Unix timestamp of the most recent event to include. This param defaults to the current timestamp
+ * if unspecified.
+ */
+ end_time?: number
+ /**
+ * Maximum number of events to return. This param defaults to 1000 if unspecified.
+ */
+ limit?: number
+ /**
+ * Event order by timestamp. This param can be either "asc" (default) or "desc".
+ */
+ order?: 'asc' | 'desc'
+}
+
+/**
+ * Query parameters for GET /v1/payments
+ */
+export type GetPaymentsQueryParams = {
+ /**
+ * Short-lived token returned from "getPluginPaymentTokenAsync" in the plugin payments API and used
+ * to authenticate to this endpoint. Read more about generating this token through "Calling the
+ * Payments REST API from a plugin or widget" below.
+ */
+ plugin_payment_token?: string
+ /**
+ * The ID of the user to query payment information about. You can get the user ID by having the user
+ * OAuth2 to the Figma REST API.
+ */
+ user_id?: string
+ /**
+ * The ID of the Community file to query a user's payment information on. You can get the Community
+ * file ID from the file's Community page (look for the number after "file/" in the URL). Provide
+ * exactly one of "community_file_id", "plugin_id", or "widget_id".
+ */
+ community_file_id?: string
+ /**
+ * The ID of the plugin to query a user's payment information on. You can get the plugin ID from the
+ * plugin's manifest, or from the plugin's Community page (look for the number after "plugin/" in
+ * the URL). Provide exactly one of "community_file_id", "plugin_id", or "widget_id".
+ */
+ plugin_id?: string
+ /**
+ * The ID of the widget to query a user's payment information on. You can get the widget ID from the
+ * widget's manifest, or from the widget's Community page (look for the number after "widget/" in
+ * the URL). Provide exactly one of "community_file_id", "plugin_id", or "widget_id".
+ */
+ widget_id?: string
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/variables/local
+ */
+export type GetLocalVariablesPathParams = {
+ /**
+ * File to get variables from. This can be a file key or branch key. Use `GET /v1/files/:key` with
+ * the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/variables/published
+ */
+export type GetPublishedVariablesPathParams = {
+ /**
+ * File to get variables from. This must be a main file key, not a branch key, as it is not
+ * possible to publish from branches.
+ */
+ file_key: string
+}
+
+/**
+ * Path parameters for POST /v1/files/{file_key}/variables
+ */
+export type PostVariablesPathParams = {
+ /**
+ * File to modify variables in. This can be a file key or branch key. Use `GET /v1/files/:key` with
+ * the `branch_data` query param to get the branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Request body parameters for POST /v1/files/{file_key}/variables
+ */
+export type PostVariablesRequestBody = {
+ /**
+ * For creating, updating, and deleting variable collections.
+ */
+ variableCollections?: VariableCollectionChange[]
+
+ /**
+ * For creating, updating, and deleting modes within variable collections.
+ */
+ variableModes?: VariableModeChange[]
+
+ /**
+ * For creating, updating, and deleting variables.
+ */
+ variables?: VariableChange[]
+
+ /**
+ * For setting a specific value, given a variable and a mode.
+ */
+ variableModeValues?: VariableModeValue[]
+}
+
+/**
+ * Path parameters for GET /v1/files/{file_key}/dev_resources
+ */
+export type GetDevResourcesPathParams = {
+ /**
+ * The file to get the dev resources from. This must be a main file key, not a branch key.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/files/{file_key}/dev_resources
+ */
+export type GetDevResourcesQueryParams = {
+ /**
+ * Comma separated list of nodes that you care about in the document. If specified, only dev
+ * resources attached to these nodes will be returned. If not specified, all dev resources in the
+ * file will be returned.
+ */
+ node_ids?: string
+}
+
+/**
+ * Request body parameters for POST /v1/dev_resources
+ */
+export type PostDevResourcesRequestBody = {
+ /**
+ * An array of dev resources.
+ */
+ dev_resources: {
+ /**
+ * The name of the dev resource.
+ */
+ name: string
+
+ /**
+ * The URL of the dev resource.
+ */
+ url: string
+
+ /**
+ * The file key where the dev resource belongs.
+ */
+ file_key: string
+
+ /**
+ * The target node to attach the dev resource to.
+ */
+ node_id: string
+ }[]
+}
+
+/**
+ * Request body parameters for PUT /v1/dev_resources
+ */
+export type PutDevResourcesRequestBody = {
+ /**
+ * An array of dev resources.
+ */
+ dev_resources: {
+ /**
+ * Unique identifier of the dev resource
+ */
+ id: string
+
+ /**
+ * The name of the dev resource.
+ */
+ name?: string
+
+ /**
+ * The URL of the dev resource.
+ */
+ url?: string
+ }[]
+}
+
+/**
+ * Path parameters for DELETE /v1/files/{file_key}/dev_resources/{dev_resource_id}
+ */
+export type DeleteDevResourcePathParams = {
+ /**
+ * The file to delete the dev resource from. This must be a main file key, not a branch key.
+ */
+ file_key: string
+ /**
+ * The id of the dev resource to delete.
+ */
+ dev_resource_id: string
+}
+
+/**
+ * Path parameters for GET /v1/analytics/libraries/{file_key}/component/actions
+ */
+export type GetLibraryAnalyticsComponentActionsPathParams = {
+ /**
+ * File key of the library to fetch analytics data for.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/analytics/libraries/{file_key}/component/actions
+ */
+export type GetLibraryAnalyticsComponentActionsQueryParams = {
+ /**
+ * Cursor indicating what page of data to fetch. Obtained from prior API call.
+ */
+ cursor?: string
+ /**
+ * A dimension to group returned analytics data by.
+ */
+ group_by: 'component' | 'team'
+ /**
+ * ISO 8601 date string (YYYY-MM-DD) of the earliest week to include. Dates are rounded back to the
+ * nearest start of a week. Defaults to one year prior.
+ */
+ start_date?: string
+ /**
+ * ISO 8601 date string (YYYY-MM-DD) of the latest week to include. Dates are rounded forward to the
+ * nearest end of a week. Defaults to the latest computed week.
+ */
+ end_date?: string
+}
+
+/**
+ * Path parameters for GET /v1/analytics/libraries/{file_key}/component/usages
+ */
+export type GetLibraryAnalyticsComponentUsagesPathParams = {
+ /**
+ * File key of the library to fetch analytics data for.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/analytics/libraries/{file_key}/component/usages
+ */
+export type GetLibraryAnalyticsComponentUsagesQueryParams = {
+ /**
+ * Cursor indicating what page of data to fetch. Obtained from prior API call.
+ */
+ cursor?: string
+ /**
+ * A dimension to group returned analytics data by.
+ */
+ group_by: 'component' | 'file'
+}
+
+/**
+ * Path parameters for GET /v1/analytics/libraries/{file_key}/style/actions
+ */
+export type GetLibraryAnalyticsStyleActionsPathParams = {
+ /**
+ * File key of the library to fetch analytics data for.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/analytics/libraries/{file_key}/style/actions
+ */
+export type GetLibraryAnalyticsStyleActionsQueryParams = {
+ /**
+ * Cursor indicating what page of data to fetch. Obtained from prior API call.
+ */
+ cursor?: string
+ /**
+ * A dimension to group returned analytics data by.
+ */
+ group_by: 'style' | 'team'
+ /**
+ * ISO 8601 date string (YYYY-MM-DD) of the earliest week to include. Dates are rounded back to the
+ * nearest start of a week. Defaults to one year prior.
+ */
+ start_date?: string
+ /**
+ * ISO 8601 date string (YYYY-MM-DD) of the latest week to include. Dates are rounded forward to the
+ * nearest end of a week. Defaults to the latest computed week.
+ */
+ end_date?: string
+}
+
+/**
+ * Path parameters for GET /v1/analytics/libraries/{file_key}/style/usages
+ */
+export type GetLibraryAnalyticsStyleUsagesPathParams = {
+ /**
+ * File key of the library to fetch analytics data for.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/analytics/libraries/{file_key}/style/usages
+ */
+export type GetLibraryAnalyticsStyleUsagesQueryParams = {
+ /**
+ * Cursor indicating what page of data to fetch. Obtained from prior API call.
+ */
+ cursor?: string
+ /**
+ * A dimension to group returned analytics data by.
+ */
+ group_by: 'style' | 'file'
+}
+
+/**
+ * Path parameters for GET /v1/analytics/libraries/{file_key}/variable/actions
+ */
+export type GetLibraryAnalyticsVariableActionsPathParams = {
+ /**
+ * File key of the library to fetch analytics data for.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/analytics/libraries/{file_key}/variable/actions
+ */
+export type GetLibraryAnalyticsVariableActionsQueryParams = {
+ /**
+ * Cursor indicating what page of data to fetch. Obtained from prior API call.
+ */
+ cursor?: string
+ /**
+ * A dimension to group returned analytics data by.
+ */
+ group_by: 'variable' | 'team'
+ /**
+ * ISO 8601 date string (YYYY-MM-DD) of the earliest week to include. Dates are rounded back to the
+ * nearest start of a week. Defaults to one year prior.
+ */
+ start_date?: string
+ /**
+ * ISO 8601 date string (YYYY-MM-DD) of the latest week to include. Dates are rounded forward to the
+ * nearest end of a week. Defaults to the latest computed week.
+ */
+ end_date?: string
+}
+
+/**
+ * Path parameters for GET /v1/analytics/libraries/{file_key}/variable/usages
+ */
+export type GetLibraryAnalyticsVariableUsagesPathParams = {
+ /**
+ * File key of the library to fetch analytics data for.
+ */
+ file_key: string
+}
+
+/**
+ * Query parameters for GET /v1/analytics/libraries/{file_key}/variable/usages
+ */
+export type GetLibraryAnalyticsVariableUsagesQueryParams = {
+ /**
+ * Cursor indicating what page of data to fetch. Obtained from prior API call.
+ */
+ cursor?: string
+ /**
+ * A dimension to group returned analytics data by.
+ */
+ group_by: 'variable' | 'file'
+}
diff --git a/.ref/figma/figma-rest-api.min.d.ts b/.ref/figma/figma-rest-api.min.d.ts
new file mode 100644
index 0000000000..06c69dda6a
--- /dev/null
+++ b/.ref/figma/figma-rest-api.min.d.ts
@@ -0,0 +1,2080 @@
+export type IsLayerTrait = {
+ /**
+ * A string uniquely identifying this node within the document.
+ */
+ id: string;
+
+ /**
+ * The name given to the node by the user in the tool.
+ */
+ name: string;
+
+ /**
+ * The type of the node
+ */
+ type: string;
+
+ /**
+ * Whether or not the node is visible on the canvas.
+ */
+ visible?: boolean;
+
+ /**
+ * If true, layer is locked and cannot be edited
+ */
+ locked?: boolean;
+
+ /**
+ * Whether the layer is fixed while the parent is scrolling
+ *
+ * @deprecated
+ */
+ isFixed?: boolean;
+
+ /**
+ * How layer should be treated when the frame is resized
+ */
+ scrollBehavior: "SCROLLS" | "FIXED" | "STICKY_SCROLLS";
+
+ /**
+ * The rotation of the node, if not 0.
+ */
+ rotation?: number;
+
+ /**
+ * A mapping of a layer's property to component property name of component properties attached to
+ * this node. The component property name can be used to look up more information on the
+ * corresponding component's or component set's componentPropertyDefinitions.
+ */
+ componentPropertyReferences?: { [key: string]: string };
+
+ /**
+ * Data written by plugins that is visible only to the plugin that wrote it. Requires the
+ * `pluginData` to include the ID of the plugin.
+ */
+ pluginData?: unknown;
+
+ /**
+ * Data written by plugins that is visible to all plugins. Requires the `pluginData` parameter to
+ * include the string "shared".
+ */
+ sharedPluginData?: unknown;
+
+ /**
+ * A mapping of variable collection ID to mode ID representing the explicitly set modes for this
+ * node.
+ */
+ explicitVariableModes?: { [key: string]: string };
+};
+
+export type HasChildrenTrait = {
+ /**
+ * An array of nodes that are direct children of this node
+ */
+ children: SubcanvasNode[];
+};
+
+export type HasLayoutTrait = {
+ /**
+ * Bounding box of the node in absolute space coordinates.
+ */
+ absoluteBoundingBox: Rectangle | null;
+
+ /**
+ * The actual bounds of a node accounting for drop shadows, thick strokes, and anything else that
+ * may fall outside the node's regular bounding box defined in `x`, `y`, `width`, and `height`. The
+ * `x` and `y` inside this property represent the absolute position of the node on the page. This
+ * value will be `null` if the node is invisible.
+ */
+ absoluteRenderBounds: Rectangle | null;
+
+ /**
+ * Keep height and width constrained to same ratio.
+ */
+ preserveRatio?: boolean;
+
+ /**
+ * Horizontal and vertical layout constraints for node.
+ */
+ constraints?: LayoutConstraint;
+
+ /**
+ * The top two rows of a matrix that represents the 2D transform of this node relative to its
+ * parent. The bottom row of the matrix is implicitly always (0, 0, 1). Use to transform coordinates
+ * in geometry. Only present if `geometry=paths` is passed.
+ */
+ relativeTransform?: Transform;
+
+ /**
+ * Width and height of element. This is different from the width and height of the bounding box in
+ * that the absolute bounding box represents the element after scaling and rotation. Only present if
+ * `geometry=paths` is passed.
+ */
+ size?: Vector;
+
+ /**
+ * Determines if the layer should stretch along the parent's counter axis. This property is only
+ * provided for direct children of auto-layout frames.
+ *
+ * - `INHERIT`
+ * - `STRETCH`
+ *
+ * In previous versions of auto layout, determined how the layer is aligned inside an auto-layout
+ * frame. This property is only provided for direct children of auto-layout frames.
+ *
+ * - `MIN`
+ * - `CENTER`
+ * - `MAX`
+ * - `STRETCH`
+ *
+ * In horizontal auto-layout frames, "MIN" and "MAX" correspond to "TOP" and "BOTTOM". In vertical
+ * auto-layout frames, "MIN" and "MAX" correspond to "LEFT" and "RIGHT".
+ */
+ layoutAlign?: "INHERIT" | "STRETCH" | "MIN" | "CENTER" | "MAX";
+
+ /**
+ * This property is applicable only for direct children of auto-layout frames, ignored otherwise.
+ * Determines whether a layer should stretch along the parent's primary axis. A `0` corresponds to a
+ * fixed size and `1` corresponds to stretch.
+ */
+ layoutGrow?: 0 | 1;
+
+ /**
+ * Determines whether a layer's size and position should be determined by auto-layout settings or
+ * manually adjustable.
+ */
+ layoutPositioning?: "AUTO" | "ABSOLUTE";
+
+ /**
+ * The minimum width of the frame. This property is only applicable for auto-layout frames or direct
+ * children of auto-layout frames.
+ */
+ minWidth?: number;
+
+ /**
+ * The maximum width of the frame. This property is only applicable for auto-layout frames or direct
+ * children of auto-layout frames.
+ */
+ maxWidth?: number;
+
+ /**
+ * The minimum height of the frame. This property is only applicable for auto-layout frames or
+ * direct children of auto-layout frames.
+ */
+ minHeight?: number;
+
+ /**
+ * The maximum height of the frame. This property is only applicable for auto-layout frames or
+ * direct children of auto-layout frames.
+ */
+ maxHeight?: number;
+
+ /**
+ * The horizontal sizing setting on this auto-layout frame or frame child.
+ *
+ * - `FIXED`
+ * - `HUG`: only valid on auto-layout frames and text nodes
+ * - `FILL`: only valid on auto-layout frame children
+ */
+ layoutSizingHorizontal?: "FIXED" | "HUG" | "FILL";
+
+ /**
+ * The vertical sizing setting on this auto-layout frame or frame child.
+ *
+ * - `FIXED`
+ * - `HUG`: only valid on auto-layout frames and text nodes
+ * - `FILL`: only valid on auto-layout frame children
+ */
+ layoutSizingVertical?: "FIXED" | "HUG" | "FILL";
+};
+
+export type HasFramePropertiesTrait = {
+ /**
+ * Whether or not this node clip content outside of its bounds
+ */
+ clipsContent: boolean;
+
+ /**
+ * Background of the node. This is deprecated, as backgrounds for frames are now in the `fills`
+ * field.
+ *
+ * @deprecated
+ */
+ background?: Paint[];
+
+ /**
+ * Background color of the node. This is deprecated, as frames now support more than a solid color
+ * as a background. Please use the `fills` field instead.
+ *
+ * @deprecated
+ */
+ backgroundColor?: RGBA;
+
+ /**
+ * An array of layout grids attached to this node (see layout grids section for more details). GROUP
+ * nodes do not have this attribute
+ */
+ layoutGrids?: LayoutGrid[];
+
+ /**
+ * Whether a node has primary axis scrolling, horizontal or vertical.
+ */
+ overflowDirection?:
+ | "HORIZONTAL_SCROLLING"
+ | "VERTICAL_SCROLLING"
+ | "HORIZONTAL_AND_VERTICAL_SCROLLING"
+ | "NONE";
+
+ /**
+ * Whether this layer uses auto-layout to position its children.
+ */
+ layoutMode?: "NONE" | "HORIZONTAL" | "VERTICAL";
+
+ /**
+ * Whether the primary axis has a fixed length (determined by the user) or an automatic length
+ * (determined by the layout engine). This property is only applicable for auto-layout frames.
+ */
+ primaryAxisSizingMode?: "FIXED" | "AUTO";
+
+ /**
+ * Whether the counter axis has a fixed length (determined by the user) or an automatic length
+ * (determined by the layout engine). This property is only applicable for auto-layout frames.
+ */
+ counterAxisSizingMode?: "FIXED" | "AUTO";
+
+ /**
+ * Determines how the auto-layout frame's children should be aligned in the primary axis direction.
+ * This property is only applicable for auto-layout frames.
+ */
+ primaryAxisAlignItems?: "MIN" | "CENTER" | "MAX" | "SPACE_BETWEEN";
+
+ /**
+ * Determines how the auto-layout frame's children should be aligned in the counter axis direction.
+ * This property is only applicable for auto-layout frames.
+ */
+ counterAxisAlignItems?: "MIN" | "CENTER" | "MAX" | "BASELINE";
+
+ /**
+ * The padding between the left border of the frame and its children. This property is only
+ * applicable for auto-layout frames.
+ */
+ paddingLeft?: number;
+
+ /**
+ * The padding between the right border of the frame and its children. This property is only
+ * applicable for auto-layout frames.
+ */
+ paddingRight?: number;
+
+ /**
+ * The padding between the top border of the frame and its children. This property is only
+ * applicable for auto-layout frames.
+ */
+ paddingTop?: number;
+
+ /**
+ * The padding between the bottom border of the frame and its children. This property is only
+ * applicable for auto-layout frames.
+ */
+ paddingBottom?: number;
+
+ /**
+ * The distance between children of the frame. Can be negative. This property is only applicable for
+ * auto-layout frames.
+ */
+ itemSpacing?: number;
+
+ /**
+ * Determines the canvas stacking order of layers in this frame. When true, the first layer will be
+ * draw on top. This property is only applicable for auto-layout frames.
+ */
+ itemReverseZIndex?: boolean;
+
+ /**
+ * Determines whether strokes are included in layout calculations. When true, auto-layout frames
+ * behave like css "box-sizing: border-box". This property is only applicable for auto-layout
+ * frames.
+ */
+ strokesIncludedInLayout?: boolean;
+
+ /**
+ * Whether this auto-layout frame has wrapping enabled.
+ */
+ layoutWrap?: "NO_WRAP" | "WRAP";
+
+ /**
+ * The distance between wrapped tracks of an auto-layout frame. This property is only applicable for
+ * auto-layout frames with `layoutWrap: "WRAP"`
+ */
+ counterAxisSpacing?: number;
+
+ /**
+ * Determines how the auto-layout frame’s wrapped tracks should be aligned in the counter axis
+ * direction. This property is only applicable for auto-layout frames with `layoutWrap: "WRAP"`.
+ */
+ counterAxisAlignContent?: "AUTO" | "SPACE_BETWEEN";
+};
+
+export type HasBlendModeAndOpacityTrait = {
+ /**
+ * How this node blends with nodes behind it in the scene (see blend mode section for more details)
+ */
+ blendMode: BlendMode;
+
+ /**
+ * Opacity of the node
+ */
+ opacity?: number;
+};
+
+export type HasExportSettingsTrait = {
+ /**
+ * An array of export settings representing images to export from the node.
+ */
+ exportSettings?: ExportSetting[];
+};
+
+export type HasGeometryTrait = MinimalFillsTrait &
+ MinimalStrokesTrait & {
+ /**
+ * Map from ID to PaintOverride for looking up fill overrides. To see which regions are overriden,
+ * you must use the `geometry=paths` option. Each path returned may have an `overrideID` which maps
+ * to this table.
+ */
+ fillOverrideTable?: { [key: string]: PaintOverride | null };
+
+ /**
+ * Only specified if parameter `geometry=paths` is used. An array of paths representing the object
+ * fill.
+ */
+ fillGeometry?: Path[];
+
+ /**
+ * Only specified if parameter `geometry=paths` is used. An array of paths representing the object
+ * stroke.
+ */
+ strokeGeometry?: Path[];
+
+ /**
+ * A string enum describing the end caps of vector paths.
+ */
+ strokeCap?:
+ | "NONE"
+ | "ROUND"
+ | "SQUARE"
+ | "LINE_ARROW"
+ | "TRIANGLE_ARROW"
+ | "DIAMOND_FILLED"
+ | "CIRCLE_FILLED"
+ | "TRIANGLE_FILLED"
+ | "WASHI_TAPE_1"
+ | "WASHI_TAPE_2"
+ | "WASHI_TAPE_3"
+ | "WASHI_TAPE_4"
+ | "WASHI_TAPE_5"
+ | "WASHI_TAPE_6";
+
+ /**
+ * Only valid if `strokeJoin` is "MITER". The corner angle, in degrees, below which `strokeJoin`
+ * will be set to "BEVEL" to avoid super sharp corners. By default this is 28.96 degrees.
+ */
+ strokeMiterAngle?: number;
+ };
+
+export type MinimalFillsTrait = {
+ /**
+ * An array of fill paints applied to the node.
+ */
+ fills: Paint[];
+
+ /**
+ * A mapping of a StyleType to style ID (see Style) of styles present on this node. The style ID can
+ * be used to look up more information about the style in the top-level styles field.
+ */
+ styles?: { [key: string]: string };
+};
+
+export type MinimalStrokesTrait = {
+ /**
+ * An array of stroke paints applied to the node.
+ */
+ strokes?: Paint[];
+
+ /**
+ * The weight of strokes on the node.
+ */
+ strokeWeight?: number;
+
+ /**
+ * Position of stroke relative to vector outline, as a string enum
+ *
+ * - `INSIDE`: stroke drawn inside the shape boundary
+ * - `OUTSIDE`: stroke drawn outside the shape boundary
+ * - `CENTER`: stroke drawn centered along the shape boundary
+ */
+ strokeAlign?: "INSIDE" | "OUTSIDE" | "CENTER";
+
+ /**
+ * A string enum with value of "MITER", "BEVEL", or "ROUND", describing how corners in vector paths
+ * are rendered.
+ */
+ strokeJoin?: "MITER" | "BEVEL" | "ROUND";
+
+ /**
+ * An array of floating point numbers describing the pattern of dash length and gap lengths that the
+ * vector stroke will use when drawn.
+ *
+ * For example a value of [1, 2] indicates that the stroke will be drawn with a dash of length 1
+ * followed by a gap of length 2, repeated.
+ */
+ strokeDashes?: number[];
+};
+
+export type IndividualStrokesTrait = {
+ /**
+ * An object including the top, bottom, left, and right stroke weights. Only returned if individual
+ * stroke weights are used.
+ */
+ individualStrokeWeights?: StrokeWeights;
+};
+
+export type CornerTrait = {
+ /**
+ * Radius of each corner if a single radius is set for all corners
+ */
+ cornerRadius?: number;
+
+ /**
+ * A value that lets you control how "smooth" the corners are. Ranges from 0 to 1. 0 is the default
+ * and means that the corner is perfectly circular. A value of 0.6 means the corner matches the iOS
+ * 7 "squircle" icon shape. Other values produce various other curves.
+ */
+ cornerSmoothing?: number;
+
+ /**
+ * Array of length 4 of the radius of each corner of the frame, starting in the top left and
+ * proceeding clockwise.
+ *
+ * Values are given in the order top-left, top-right, bottom-right, bottom-left.
+ */
+ rectangleCornerRadii?: number[];
+};
+
+export type HasEffectsTrait = {
+ /**
+ * An array of effects attached to this node (see effects section for more details)
+ */
+ effects: Effect[];
+};
+
+export type HasMaskTrait = {
+ /**
+ * Does this node mask sibling nodes in front of it?
+ */
+ isMask?: boolean;
+
+ /**
+ * If this layer is a mask, this property describes the operation used to mask the layer's siblings.
+ * The value may be one of the following:
+ *
+ * - ALPHA: the mask node's alpha channel will be used to determine the opacity of each pixel in the
+ * masked result.
+ * - VECTOR: if the mask node has visible fill paints, every pixel inside the node's fill regions will
+ * be fully visible in the masked result. If the mask has visible stroke paints, every pixel
+ * inside the node's stroke regions will be fully visible in the masked result.
+ * - LUMINANCE: the luminance value of each pixel of the mask node will be used to determine the
+ * opacity of that pixel in the masked result.
+ */
+ maskType?: "ALPHA" | "VECTOR" | "LUMINANCE";
+
+ /**
+ * True if maskType is VECTOR. This field is deprecated; use maskType instead.
+ *
+ * @deprecated
+ */
+ isMaskOutline?: boolean;
+};
+
+export type ComponentPropertiesTrait = {
+ /**
+ * A mapping of name to `ComponentPropertyDefinition` for every component property on this
+ * component. Each property has a type, defaultValue, and other optional values.
+ */
+ componentPropertyDefinitions?: { [key: string]: ComponentPropertyDefinition };
+};
+
+export type TypePropertiesTrait = {
+ /**
+ * The raw characters in the text node.
+ */
+ characters: string;
+
+ /**
+ * Style of text including font family and weight.
+ */
+ style: TypeStyle;
+
+ /**
+ * The array corresponds to characters in the text box, where each element references the
+ * 'styleOverrideTable' to apply specific styles to each character. The array's length can be less
+ * than or equal to the number of characters due to the removal of trailing zeros. Elements with a
+ * value of 0 indicate characters that use the default type style. If the array is shorter than the
+ * total number of characters, the characters beyond the array's length also use the default style.
+ */
+ characterStyleOverrides: number[];
+
+ /**
+ * Internal property, preserved for backward compatibility. Avoid using this value.
+ */
+ layoutVersion?: number;
+
+ /**
+ * Map from ID to TypeStyle for looking up style overrides.
+ */
+ styleOverrideTable: { [key: string]: TypeStyle };
+
+ /**
+ * An array with the same number of elements as lines in the text node, where lines are delimited by
+ * newline or paragraph separator characters. Each element in the array corresponds to the list type
+ * of a specific line. List types are represented as string enums with one of these possible
+ * values:
+ *
+ * - `NONE`: Not a list item.
+ * - `ORDERED`: Text is an ordered list (numbered).
+ * - `UNORDERED`: Text is an unordered list (bulleted).
+ */
+ lineTypes: ("NONE" | "ORDERED" | "UNORDERED")[];
+
+ /**
+ * An array with the same number of elements as lines in the text node, where lines are delimited by
+ * newline or paragraph separator characters. Each element in the array corresponds to the
+ * indentation level of a specific line.
+ */
+ lineIndentations: number[];
+};
+
+export type TextPathPropertiesTrait = {
+ /**
+ * The raw characters in the text path node.
+ */
+ characters: string;
+
+ /**
+ * Style of text including font family and weight.
+ */
+ style: TextPathTypeStyle;
+
+ /**
+ * The array corresponds to characters in the text box, where each element references the
+ * 'styleOverrideTable' to apply specific styles to each character. The array's length can be less
+ * than or equal to the number of characters due to the removal of trailing zeros. Elements with a
+ * value of 0 indicate characters that use the default type style. If the array is shorter than the
+ * total number of characters, the characters beyond the array's length also use the default style.
+ */
+ characterStyleOverrides: number[];
+
+ /**
+ * Internal property, preserved for backward compatibility. Avoid using this value.
+ */
+ layoutVersion?: number;
+
+ /**
+ * Map from ID to TextPathTypeStyle for looking up style overrides.
+ */
+ styleOverrideTable: { [key: string]: TextPathTypeStyle };
+};
+
+export type HasTextSublayerTrait = {
+ /**
+ * Text contained within a text box.
+ */
+ characters: string;
+};
+
+export type DevStatusTrait = {
+ /**
+ * Represents whether or not a node has a particular handoff (or dev) status applied to it.
+ */
+ devStatus?: {
+ type: "NONE" | "READY_FOR_DEV" | "COMPLETED";
+
+ /**
+ * An optional field where the designer can add more information about the design and what has
+ * changed.
+ */
+ description?: string;
+ };
+};
+
+export type AnnotationsTrait = object;
+
+export type FrameTraits = IsLayerTrait &
+ HasBlendModeAndOpacityTrait &
+ HasChildrenTrait &
+ HasLayoutTrait &
+ HasFramePropertiesTrait &
+ CornerTrait &
+ HasGeometryTrait &
+ HasExportSettingsTrait &
+ HasEffectsTrait &
+ HasMaskTrait &
+ IndividualStrokesTrait &
+ DevStatusTrait &
+ AnnotationsTrait;
+
+export type DefaultShapeTraits = IsLayerTrait &
+ HasBlendModeAndOpacityTrait &
+ HasLayoutTrait &
+ HasGeometryTrait &
+ HasExportSettingsTrait &
+ HasEffectsTrait &
+ HasMaskTrait;
+
+export type CornerRadiusShapeTraits = DefaultShapeTraits & CornerTrait;
+
+export type RectangularShapeTraits = DefaultShapeTraits &
+ CornerTrait &
+ IndividualStrokesTrait &
+ AnnotationsTrait;
+
+export type Node =
+ | BooleanOperationNode
+ | ComponentNode
+ | ComponentSetNode
+ | EllipseNode
+ | FrameNode
+ | GroupNode
+ | InstanceNode
+ | LineNode
+ | RectangleNode
+ | RegularPolygonNode
+ | SectionNode
+ | SliceNode
+ | StarNode
+ | StickyNode
+ | TableNode
+ | TableCellNode
+ | TextNode
+ | TextPathNode
+ | TransformGroupNode
+ | VectorNode
+ | DocumentNode
+ | CanvasNode;
+
+export type DocumentNode = {
+ type: "DOCUMENT";
+
+ children: CanvasNode[];
+} & IsLayerTrait;
+
+export type CanvasNode = {
+ type: "CANVAS";
+
+ children: SubcanvasNode[];
+
+ /**
+ * Background color of the canvas.
+ */
+ backgroundColor: RGBA;
+
+ /**
+ * Node ID that corresponds to the start frame for prototypes. This is deprecated with the
+ * introduction of multiple flows. Please use the `flowStartingPoints` field.
+ *
+ * @deprecated
+ */
+ prototypeStartNodeID: string | null;
+
+ /**
+ * An array of flow starting points sorted by its position in the prototype settings panel.
+ */
+ flowStartingPoints: FlowStartingPoint[];
+
+ /**
+ * The device used to view a prototype.
+ */
+ prototypeDevice: PrototypeDevice;
+
+ /**
+ * The background color of the prototype (currently only supports a single solid color paint).
+ */
+ prototypeBackgrounds?: RGBA[];
+} & IsLayerTrait &
+ HasExportSettingsTrait;
+
+export type SubcanvasNode =
+ | BooleanOperationNode
+ | ComponentNode
+ | ComponentSetNode
+ | EllipseNode
+ | FrameNode
+ | GroupNode
+ | InstanceNode
+ | LineNode
+ | RectangleNode
+ | RegularPolygonNode
+ | SectionNode
+ | SliceNode
+ | StarNode
+ | StickyNode
+ | TableNode
+ | TableCellNode
+ | TextNode
+ | TextPathNode
+ | TransformGroupNode
+ | VectorNode;
+
+export type BooleanOperationNode = {
+ /**
+ * The type of this node, represented by the string literal "BOOLEAN_OPERATION"
+ */
+ type: "BOOLEAN_OPERATION";
+
+ /**
+ * A string enum indicating the type of boolean operation applied.
+ */
+ booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE";
+} & IsLayerTrait &
+ HasBlendModeAndOpacityTrait &
+ HasChildrenTrait &
+ HasLayoutTrait &
+ HasGeometryTrait &
+ HasExportSettingsTrait &
+ HasEffectsTrait &
+ HasMaskTrait;
+
+export type SectionNode = {
+ /**
+ * The type of this node, represented by the string literal "SECTION"
+ */
+ type: "SECTION";
+
+ /**
+ * Whether the contents of the section are visible
+ */
+ sectionContentsHidden: boolean;
+} & IsLayerTrait &
+ HasGeometryTrait &
+ HasChildrenTrait &
+ HasLayoutTrait &
+ DevStatusTrait;
+
+export type FrameNode = {
+ /**
+ * The type of this node, represented by the string literal "FRAME"
+ */
+ type: "FRAME";
+} & FrameTraits;
+
+export type GroupNode = {
+ /**
+ * The type of this node, represented by the string literal "GROUP"
+ */
+ type: "GROUP";
+} & FrameTraits;
+
+export type ComponentNode = {
+ /**
+ * The type of this node, represented by the string literal "COMPONENT"
+ */
+ type: "COMPONENT";
+} & FrameTraits &
+ ComponentPropertiesTrait;
+
+export type ComponentSetNode = {
+ /**
+ * The type of this node, represented by the string literal "COMPONENT_SET"
+ */
+ type: "COMPONENT_SET";
+} & FrameTraits &
+ ComponentPropertiesTrait;
+
+export type VectorNode = {
+ /**
+ * The type of this node, represented by the string literal "VECTOR"
+ */
+ type: "VECTOR";
+} & CornerRadiusShapeTraits &
+ AnnotationsTrait;
+
+export type StarNode = {
+ /**
+ * The type of this node, represented by the string literal "STAR"
+ */
+ type: "STAR";
+} & CornerRadiusShapeTraits &
+ AnnotationsTrait;
+
+export type LineNode = {
+ /**
+ * The type of this node, represented by the string literal "LINE"
+ */
+ type: "LINE";
+} & DefaultShapeTraits &
+ AnnotationsTrait;
+
+export type EllipseNode = {
+ /**
+ * The type of this node, represented by the string literal "ELLIPSE"
+ */
+ type: "ELLIPSE";
+
+ arcData: ArcData;
+} & DefaultShapeTraits &
+ AnnotationsTrait;
+
+export type RegularPolygonNode = {
+ /**
+ * The type of this node, represented by the string literal "REGULAR_POLYGON"
+ */
+ type: "REGULAR_POLYGON";
+} & CornerRadiusShapeTraits &
+ AnnotationsTrait;
+
+export type RectangleNode = {
+ /**
+ * The type of this node, represented by the string literal "RECTANGLE"
+ */
+ type: "RECTANGLE";
+} & RectangularShapeTraits;
+
+export type TextNode = {
+ /**
+ * The type of this node, represented by the string literal "TEXT"
+ */
+ type: "TEXT";
+} & DefaultShapeTraits &
+ TypePropertiesTrait &
+ AnnotationsTrait;
+
+export type TextPathNode = {
+ /**
+ * The type of this node, represented by the string literal "TEXT_PATH"
+ */
+ type: "TEXT_PATH";
+} & DefaultShapeTraits &
+ TextPathPropertiesTrait;
+
+export type TableNode = {
+ /**
+ * The type of this node, represented by the string literal "TABLE"
+ */
+ type: "TABLE";
+} & IsLayerTrait &
+ HasChildrenTrait &
+ HasLayoutTrait &
+ MinimalStrokesTrait &
+ HasEffectsTrait &
+ HasBlendModeAndOpacityTrait &
+ HasExportSettingsTrait;
+
+export type TableCellNode = {
+ /**
+ * The type of this node, represented by the string literal "TABLE_CELL"
+ */
+ type: "TABLE_CELL";
+} & IsLayerTrait &
+ MinimalFillsTrait &
+ HasLayoutTrait &
+ HasTextSublayerTrait;
+
+export type TransformGroupNode = {
+ /**
+ * The type of this node, represented by the string literal "TRANSFORM_GROUP"
+ */
+ type: "TRANSFORM_GROUP";
+} & FrameTraits;
+
+export type SliceNode = {
+ /**
+ * The type of this node, represented by the string literal "SLICE"
+ */
+ type: "SLICE";
+} & IsLayerTrait;
+
+export type InstanceNode = {
+ /**
+ * The type of this node, represented by the string literal "INSTANCE"
+ */
+ type: "INSTANCE";
+
+ /**
+ * ID of component that this instance came from.
+ */
+ componentId: string;
+
+ /**
+ * If true, this node has been marked as exposed to its containing component or component set.
+ */
+ isExposedInstance?: boolean;
+
+ /**
+ * IDs of instances that have been exposed to this node's level.
+ */
+ exposedInstances?: string[];
+
+ /**
+ * A mapping of name to `ComponentProperty` for all component properties on this instance. Each
+ * property has a type, value, and other optional values.
+ */
+ componentProperties?: { [key: string]: ComponentProperty };
+
+ /**
+ * An array of all of the fields directly overridden on this instance. Inherited overrides are not
+ * included.
+ */
+ overrides: Overrides[];
+} & FrameTraits;
+
+export type StickyNode = {
+ /**
+ * The type of this node, represented by the string literal "STICKY"
+ */
+ type: "STICKY";
+
+ /**
+ * If true, author name is visible.
+ */
+ authorVisible?: boolean;
+} & IsLayerTrait &
+ HasLayoutTrait &
+ HasBlendModeAndOpacityTrait &
+ MinimalFillsTrait &
+ HasMaskTrait &
+ HasEffectsTrait &
+ HasExportSettingsTrait &
+ HasTextSublayerTrait;
+
+/**
+ * An RGB color
+ */
+export type RGB = {
+ /**
+ * Red channel value, between 0 and 1.
+ */
+ r: number;
+
+ /**
+ * Green channel value, between 0 and 1.
+ */
+ g: number;
+
+ /**
+ * Blue channel value, between 0 and 1.
+ */
+ b: number;
+};
+
+/**
+ * An RGBA color
+ */
+export type RGBA = {
+ /**
+ * Red channel value, between 0 and 1.
+ */
+ r: number;
+
+ /**
+ * Green channel value, between 0 and 1.
+ */
+ g: number;
+
+ /**
+ * Blue channel value, between 0 and 1.
+ */
+ b: number;
+
+ /**
+ * Alpha channel value, between 0 and 1.
+ */
+ a: number;
+};
+
+/**
+ * A flow starting point used when launching a prototype to enter Presentation view.
+ */
+export type FlowStartingPoint = {
+ /**
+ * Unique identifier specifying the frame.
+ */
+ nodeId: string;
+
+ /**
+ * Name of flow.
+ */
+ name: string;
+};
+
+/**
+ * A width and a height.
+ */
+export type Size = {
+ /**
+ * The width of a size.
+ */
+ width: number;
+
+ /**
+ * The height of a size.
+ */
+ height: number;
+};
+
+/**
+ * The device used to view a prototype.
+ */
+export type PrototypeDevice = {
+ type: "NONE" | "PRESET" | "CUSTOM" | "PRESENTATION";
+
+ size?: Size;
+
+ presetIdentifier?: string;
+
+ rotation: "NONE" | "CCW_90";
+};
+
+/**
+ * Sizing constraint for exports.
+ */
+export type Constraint = {
+ /**
+ * Type of constraint to apply:
+ *
+ * - `SCALE`: Scale by `value`.
+ * - `WIDTH`: Scale proportionally and set width to `value`.
+ * - `HEIGHT`: Scale proportionally and set height to `value`.
+ */
+ type: "SCALE" | "WIDTH" | "HEIGHT";
+
+ /**
+ * See type property for effect of this field.
+ */
+ value: number;
+};
+
+/**
+ * An export setting.
+ */
+export type ExportSetting = {
+ suffix: string;
+
+ format: "JPG" | "PNG" | "SVG" | "PDF";
+
+ constraint: Constraint;
+};
+
+/**
+ * This type is a string enum with the following possible values
+ *
+ * Normal blends:
+ *
+ * - `PASS_THROUGH` (only applicable to objects with children)
+ * - `NORMAL`
+ *
+ * Darken:
+ *
+ * - `DARKEN`
+ * - `MULTIPLY`
+ * - `LINEAR_BURN`
+ * - `COLOR_BURN`
+ *
+ * Lighten:
+ *
+ * - `LIGHTEN`
+ * - `SCREEN`
+ * - `LINEAR_DODGE`
+ * - `COLOR_DODGE`
+ *
+ * Contrast:
+ *
+ * - `OVERLAY`
+ * - `SOFT_LIGHT`
+ * - `HARD_LIGHT`
+ *
+ * Inversion:
+ *
+ * - `DIFFERENCE`
+ * - `EXCLUSION`
+ *
+ * Component:
+ *
+ * - `HUE`
+ * - `SATURATION`
+ * - `COLOR`
+ * - `LUMINOSITY`
+ */
+export type BlendMode =
+ | "PASS_THROUGH"
+ | "NORMAL"
+ | "DARKEN"
+ | "MULTIPLY"
+ | "LINEAR_BURN"
+ | "COLOR_BURN"
+ | "LIGHTEN"
+ | "SCREEN"
+ | "LINEAR_DODGE"
+ | "COLOR_DODGE"
+ | "OVERLAY"
+ | "SOFT_LIGHT"
+ | "HARD_LIGHT"
+ | "DIFFERENCE"
+ | "EXCLUSION"
+ | "HUE"
+ | "SATURATION"
+ | "COLOR"
+ | "LUMINOSITY";
+
+/**
+ * A 2d vector.
+ */
+export type Vector = {
+ /**
+ * X coordinate of the vector.
+ */
+ x: number;
+
+ /**
+ * Y coordinate of the vector.
+ */
+ y: number;
+};
+
+/**
+ * A single color stop with its position along the gradient axis, color, and bound variables if any
+ */
+export type ColorStop = {
+ /**
+ * Value between 0 and 1 representing position along gradient axis.
+ */
+ position: number;
+
+ /**
+ * Color attached to corresponding position.
+ */
+ color: RGBA;
+};
+
+/**
+ * A transformation matrix is standard way in computer graphics to represent translation and
+ * rotation. These are the top two rows of a 3x3 matrix. The bottom row of the matrix is assumed to
+ * be [0, 0, 1]. This is known as an affine transform and is enough to represent translation,
+ * rotation, and skew.
+ *
+ * The identity transform is [[1, 0, 0], [0, 1, 0]].
+ *
+ * A translation matrix will typically look like:
+ *
+ * ;[
+ * [1, 0, tx],
+ * [0, 1, ty],
+ * ]
+ *
+ * And a rotation matrix will typically look like:
+ *
+ * ;[
+ * [cos(angle), sin(angle), 0],
+ * [-sin(angle), cos(angle), 0],
+ * ]
+ *
+ * Another way to think about this transform is as three vectors:
+ *
+ * - The x axis (t[0][0], t[1][0])
+ * - The y axis (t[0][1], t[1][1])
+ * - The translation offset (t[0][2], t[1][2])
+ *
+ * The most common usage of the Transform matrix is the `relativeTransform property`. This
+ * particular usage of the matrix has a few additional restrictions. The translation offset can take
+ * on any value but we do enforce that the axis vectors are unit vectors (i.e. have length 1). The
+ * axes are not required to be at 90° angles to each other.
+ */
+export type Transform = number[][];
+
+/**
+ * Image filters to apply to the node.
+ */
+export type ImageFilters = {
+ exposure?: number;
+
+ contrast?: number;
+
+ saturation?: number;
+
+ temperature?: number;
+
+ tint?: number;
+
+ highlights?: number;
+
+ shadows?: number;
+};
+
+export type BasePaint = {
+ /**
+ * Is the paint enabled?
+ */
+ visible?: boolean;
+
+ /**
+ * Overall opacity of paint (colors within the paint can also have opacity values which would blend
+ * with this)
+ */
+ opacity?: number;
+
+ /**
+ * How this node blends with nodes behind it in the scene
+ */
+ blendMode: BlendMode;
+};
+
+export type SolidPaint = {
+ /**
+ * The string literal "SOLID" representing the paint's type. Always check the `type` before reading
+ * other properties.
+ */
+ type: "SOLID";
+
+ /**
+ * Solid color of the paint
+ */
+ color: RGBA;
+} & BasePaint;
+
+export type GradientPaint = {
+ /**
+ * The string literal representing the paint's type. Always check the `type` before reading other
+ * properties.
+ */
+ type:
+ | "GRADIENT_LINEAR"
+ | "GRADIENT_RADIAL"
+ | "GRADIENT_ANGULAR"
+ | "GRADIENT_DIAMOND";
+
+ /**
+ * This field contains three vectors, each of which are a position in normalized object space
+ * (normalized object space is if the top left corner of the bounding box of the object is (0, 0)
+ * and the bottom right is (1,1)). The first position corresponds to the start of the gradient
+ * (value 0 for the purposes of calculating gradient stops), the second position is the end of the
+ * gradient (value 1), and the third handle position determines the width of the gradient.
+ */
+ gradientHandlePositions: Vector[];
+
+ /**
+ * Positions of key points along the gradient axis with the colors anchored there. Colors along the
+ * gradient are interpolated smoothly between neighboring gradient stops.
+ */
+ gradientStops: ColorStop[];
+} & BasePaint;
+
+export type ImagePaint = {
+ /**
+ * The string literal "IMAGE" representing the paint's type. Always check the `type` before reading
+ * other properties.
+ */
+ type: "IMAGE";
+
+ /**
+ * Image scaling mode.
+ */
+ scaleMode: "FILL" | "FIT" | "TILE" | "STRETCH";
+
+ /**
+ * A reference to an image embedded in this node. To download the image using this reference, use
+ * the `GET file images` endpoint to retrieve the mapping from image references to image URLs.
+ */
+ imageRef: string;
+
+ /**
+ * Affine transform applied to the image, only present if `scaleMode` is `STRETCH`
+ */
+ imageTransform?: Transform;
+
+ /**
+ * Amount image is scaled by in tiling, only present if scaleMode is `TILE`.
+ */
+ scalingFactor?: number;
+
+ /**
+ * Defines what image filters have been applied to this paint, if any. If this property is not
+ * defined, no filters have been applied.
+ */
+ filters?: ImageFilters;
+
+ /**
+ * Image rotation, in degrees.
+ */
+ rotation?: number;
+
+ /**
+ * A reference to an animated GIF embedded in this node. To download the image using this reference,
+ * use the `GET file images` endpoint to retrieve the mapping from image references to image URLs.
+ */
+ gifRef?: string;
+} & BasePaint;
+
+export type PatternPaint = {
+ /**
+ * The string literal "PATTERN" representing the paint's type. Always check the `type` before
+ * reading other properties.
+ */
+ type: "PATTERN";
+
+ /**
+ * The node id of the source node for the pattern
+ */
+ sourceNodeId: string;
+
+ /**
+ * The tile type for the pattern
+ */
+ tileType: "RECTANGULAR" | "HORIZONTAL_HEXAGONAL" | "VERTICAL_HEXAGONAL";
+
+ /**
+ * The scaling factor for the pattern
+ */
+ scalingFactor: number;
+
+ /**
+ * The spacing for the pattern
+ */
+ spacing: Vector;
+
+ /**
+ * The horizontal alignment for the pattern
+ */
+ horizontalAlignment: "START" | "CENTER" | "END";
+
+ /**
+ * The vertical alignment for the pattern
+ */
+ verticalAlignment: "START" | "CENTER" | "END";
+} & BasePaint;
+
+export type Paint = SolidPaint | GradientPaint | ImagePaint | PatternPaint;
+
+/**
+ * Layout constraint relative to containing Frame
+ */
+export type LayoutConstraint = {
+ /**
+ * Vertical constraint (relative to containing frame) as an enum:
+ *
+ * - `TOP`: Node is laid out relative to top of the containing frame
+ * - `BOTTOM`: Node is laid out relative to bottom of the containing frame
+ * - `CENTER`: Node is vertically centered relative to containing frame
+ * - `TOP_BOTTOM`: Both top and bottom of node are constrained relative to containing frame (node
+ * stretches with frame)
+ * - `SCALE`: Node scales vertically with containing frame
+ */
+ vertical: "TOP" | "BOTTOM" | "CENTER" | "TOP_BOTTOM" | "SCALE";
+
+ /**
+ * Horizontal constraint (relative to containing frame) as an enum:
+ *
+ * - `LEFT`: Node is laid out relative to left of the containing frame
+ * - `RIGHT`: Node is laid out relative to right of the containing frame
+ * - `CENTER`: Node is horizontally centered relative to containing frame
+ * - `LEFT_RIGHT`: Both left and right of node are constrained relative to containing frame (node
+ * stretches with frame)
+ * - `SCALE`: Node scales horizontally with containing frame
+ */
+ horizontal: "LEFT" | "RIGHT" | "CENTER" | "LEFT_RIGHT" | "SCALE";
+};
+
+/**
+ * A rectangle that expresses a bounding box in absolute coordinates.
+ */
+export type Rectangle = {
+ /**
+ * X coordinate of top left corner of the rectangle.
+ */
+ x: number;
+
+ /**
+ * Y coordinate of top left corner of the rectangle.
+ */
+ y: number;
+
+ /**
+ * Width of the rectangle.
+ */
+ width: number;
+
+ /**
+ * Height of the rectangle.
+ */
+ height: number;
+};
+
+/**
+ * Guides to align and place objects within a frames.
+ */
+export type LayoutGrid = {
+ /**
+ * Orientation of the grid as a string enum
+ *
+ * - `COLUMNS`: Vertical grid
+ * - `ROWS`: Horizontal grid
+ * - `GRID`: Square grid
+ */
+ pattern: "COLUMNS" | "ROWS" | "GRID";
+
+ /**
+ * Width of column grid or height of row grid or square grid spacing.
+ */
+ sectionSize: number;
+
+ /**
+ * Is the grid currently visible?
+ */
+ visible: boolean;
+
+ /**
+ * Color of the grid
+ */
+ color: RGBA;
+
+ /**
+ * Positioning of grid as a string enum
+ *
+ * - `MIN`: Grid starts at the left or top of the frame
+ * - `MAX`: Grid starts at the right or bottom of the frame
+ * - `STRETCH`: Grid is stretched to fit the frame
+ * - `CENTER`: Grid is center aligned
+ */
+ alignment: "MIN" | "MAX" | "STRETCH" | "CENTER";
+
+ /**
+ * Spacing in between columns and rows
+ */
+ gutterSize: number;
+
+ /**
+ * Spacing before the first column or row
+ */
+ offset: number;
+
+ /**
+ * Number of columns or rows
+ */
+ count: number;
+};
+
+/**
+ * Base properties shared by all shadow effects
+ */
+export type BaseShadowEffect = {
+ /**
+ * The color of the shadow
+ */
+ color: RGBA;
+
+ /**
+ * Blend mode of the shadow
+ */
+ blendMode: BlendMode;
+
+ /**
+ * How far the shadow is projected in the x and y directions
+ */
+ offset: Vector;
+
+ /**
+ * Radius of the blur effect (applies to shadows as well)
+ */
+ radius: number;
+
+ /**
+ * The distance by which to expand (or contract) the shadow.
+ *
+ * For drop shadows, a positive `spread` value creates a shadow larger than the node, whereas a
+ * negative value creates a shadow smaller than the node.
+ *
+ * For inner shadows, a positive `spread` value contracts the shadow. Spread values are only
+ * accepted on rectangles and ellipses, or on frames, components, and instances with visible fill
+ * paints and `clipsContent` enabled. When left unspecified, the default value is 0.
+ */
+ spread?: number;
+
+ /**
+ * Whether this shadow is visible.
+ */
+ visible: boolean;
+};
+
+export type DropShadowEffect = {
+ /**
+ * A string literal representing the effect's type. Always check the type before reading other
+ * properties.
+ */
+ type: "DROP_SHADOW";
+
+ /**
+ * Whether to show the shadow behind translucent or transparent pixels
+ */
+ showShadowBehindNode: boolean;
+} & BaseShadowEffect;
+
+export type InnerShadowEffect = {
+ /**
+ * A string literal representing the effect's type. Always check the type before reading other
+ * properties.
+ */
+ type?: "INNER_SHADOW";
+} & BaseShadowEffect;
+
+export type BlurEffect = NormalBlurEffect | ProgressiveBlurEffect;
+
+/**
+ * Base properties shared by all blur effects
+ */
+export type BaseBlurEffect = {
+ /**
+ * A string literal representing the effect's type. Always check the type before reading other
+ * properties.
+ */
+ type: "LAYER_BLUR" | "BACKGROUND_BLUR";
+
+ /**
+ * Whether this blur is active.
+ */
+ visible: boolean;
+
+ /**
+ * Radius of the blur effect
+ */
+ radius: number;
+};
+
+export type NormalBlurEffect = {
+ /**
+ * The string literal 'NORMAL' representing the blur type. Always check the blurType before reading
+ * other properties.
+ */
+ blurType?: "NORMAL";
+} & BaseBlurEffect;
+
+export type ProgressiveBlurEffect = {
+ /**
+ * The string literal 'PROGRESSIVE' representing the blur type. Always check the blurType before
+ * reading other properties.
+ */
+ blurType: "PROGRESSIVE";
+
+ /**
+ * The starting radius of the progressive blur
+ */
+ startRadius: number;
+
+ /**
+ * The starting offset of the progressive blur
+ */
+ startOffset: Vector;
+
+ /**
+ * The ending offset of the progressive blur
+ */
+ endOffset: Vector;
+} & BaseBlurEffect;
+
+/**
+ * A texture effect
+ */
+export type TextureEffect = {
+ /**
+ * The string literal 'TEXTURE' representing the effect's type. Always check the type before reading
+ * other properties.
+ */
+ type: "TEXTURE";
+
+ /**
+ * The size of the texture effect
+ */
+ noiseSize: number;
+
+ /**
+ * The radius of the texture effect
+ */
+ radius: number;
+
+ /**
+ * Whether the texture is clipped to the shape
+ */
+ clipToShape: boolean;
+};
+
+export type MonotoneNoiseEffect = {
+ /**
+ * The string literal 'MONOTONE' representing the noise type.
+ */
+ noiseType: "MONOTONE";
+} & BaseNoiseEffect;
+
+export type MultitoneNoiseEffect = {
+ /**
+ * The string literal 'MULTITONE' representing the noise type.
+ */
+ noiseType: "MULTITONE";
+
+ /**
+ * The opacity of the noise effect
+ */
+ opacity: number;
+} & BaseNoiseEffect;
+
+export type DuotoneNoiseEffect = {
+ /**
+ * The string literal 'DUOTONE' representing the noise type.
+ */
+ noiseType: "DUOTONE";
+
+ /**
+ * The secondary color of the noise effect
+ */
+ secondaryColor: RGBA;
+} & BaseNoiseEffect;
+
+/**
+ * A noise effect
+ */
+export type BaseNoiseEffect = {
+ /**
+ * The string literal 'NOISE' representing the effect's type. Always check the type before reading
+ * other properties.
+ */
+ type: "NOISE";
+
+ /**
+ * Blend mode of the noise effect
+ */
+ blendMode: BlendMode;
+
+ /**
+ * The size of the noise effect
+ */
+ noiseSize: number;
+
+ /**
+ * The density of the noise effect
+ */
+ density: number;
+};
+
+export type NoiseEffect =
+ | MonotoneNoiseEffect
+ | MultitoneNoiseEffect
+ | DuotoneNoiseEffect;
+
+export type Effect =
+ | DropShadowEffect
+ | InnerShadowEffect
+ | BlurEffect
+ | TextureEffect
+ | NoiseEffect;
+
+/**
+ * A set of properties that can be applied to nodes and published. Styles for a property can be
+ * created in the corresponding property's panel while editing a file.
+ */
+export type Style = {
+ /**
+ * The key of the style
+ */
+ key: string;
+
+ /**
+ * Name of the style
+ */
+ name: string;
+
+ /**
+ * Description of the style
+ */
+ description: string;
+
+ /**
+ * Whether this style is a remote style that doesn't live in this file
+ */
+ remote: boolean;
+};
+
+/**
+ * This type is a string enum with the following possible values:
+ *
+ * - `EASE_IN`: Ease in with an animation curve similar to CSS ease-in.
+ * - `EASE_OUT`: Ease out with an animation curve similar to CSS ease-out.
+ * - `EASE_IN_AND_OUT`: Ease in and then out with an animation curve similar to CSS ease-in-out.
+ * - `LINEAR`: No easing, similar to CSS linear.
+ * - `EASE_IN_BACK`: Ease in with an animation curve that moves past the initial keyframe's value and
+ * then accelerates as it reaches the end.
+ * - `EASE_OUT_BACK`: Ease out with an animation curve that starts fast, then slows and goes past the
+ * ending keyframe's value.
+ * - `EASE_IN_AND_OUT_BACK`: Ease in and then out with an animation curve that overshoots the initial
+ * keyframe's value, then accelerates quickly before it slows and overshoots the ending keyframes
+ * value.
+ * - `CUSTOM_CUBIC_BEZIER`: User-defined cubic bezier curve.
+ * - `GENTLE`: Gentle animation similar to react-spring.
+ * - `QUICK`: Quick spring animation, great for toasts and notifications.
+ * - `BOUNCY`: Bouncy spring, for delightful animations like a heart bounce.
+ * - `SLOW`: Slow spring, useful as a steady, natural way to scale up fullscreen content.
+ * - `CUSTOM_SPRING`: User-defined spring animation.
+ */
+export type EasingType =
+ | "EASE_IN"
+ | "EASE_OUT"
+ | "EASE_IN_AND_OUT"
+ | "LINEAR"
+ | "EASE_IN_BACK"
+ | "EASE_OUT_BACK"
+ | "EASE_IN_AND_OUT_BACK"
+ | "CUSTOM_CUBIC_BEZIER"
+ | "GENTLE"
+ | "QUICK"
+ | "BOUNCY"
+ | "SLOW"
+ | "CUSTOM_SPRING";
+
+/**
+ * Individual stroke weights
+ */
+export type StrokeWeights = {
+ /**
+ * The top stroke weight.
+ */
+ top: number;
+
+ /**
+ * The right stroke weight.
+ */
+ right: number;
+
+ /**
+ * The bottom stroke weight.
+ */
+ bottom: number;
+
+ /**
+ * The left stroke weight.
+ */
+ left: number;
+};
+
+/**
+ * Paint metadata to override default paints.
+ */
+export type PaintOverride = {
+ /**
+ * Paints applied to characters.
+ */
+ fills?: Paint[];
+
+ /**
+ * ID of style node, if any, that this inherits fill data from.
+ */
+ inheritFillStyleId?: string;
+};
+
+/**
+ * Defines a single path
+ */
+export type Path = {
+ /**
+ * A series of path commands that encodes how to draw the path.
+ */
+ path: string;
+
+ /**
+ * The winding rule for the path (same as in SVGs). This determines whether a given point in space
+ * is inside or outside the path.
+ */
+ windingRule: "NONZERO" | "EVENODD";
+
+ /**
+ * If there is a per-region fill, this refers to an ID in the `fillOverrideTable`.
+ */
+ overrideID?: number;
+};
+
+/**
+ * Information about the arc properties of an ellipse. 0° is the x axis and increasing angles rotate
+ * clockwise.
+ */
+export type ArcData = {
+ /**
+ * Start of the sweep in radians.
+ */
+ startingAngle: number;
+
+ /**
+ * End of the sweep in radians.
+ */
+ endingAngle: number;
+
+ /**
+ * Inner radius value between 0 and 1
+ */
+ innerRadius: number;
+};
+
+/**
+ * A link to either a URL or another frame (node) in the document.
+ */
+export type Hyperlink = {
+ /**
+ * The type of hyperlink. Can be either `URL` or `NODE`.
+ */
+ type: "URL" | "NODE";
+
+ /**
+ * The URL that the hyperlink points to, if `type` is `URL`.
+ */
+ url?: string;
+
+ /**
+ * The ID of the node that the hyperlink points to, if `type` is `NODE`.
+ */
+ nodeID?: string;
+};
+
+export type BaseTypeStyle = {
+ /**
+ * Font family of text (standard name).
+ */
+ fontFamily?: string;
+
+ /**
+ * PostScript font name.
+ */
+ fontPostScriptName?: string | null;
+
+ /**
+ * Describes visual weight or emphasis, such as Bold or Italic.
+ */
+ fontStyle?: string;
+
+ /**
+ * Whether or not text is italicized.
+ */
+ italic?: boolean;
+
+ /**
+ * Numeric font weight.
+ */
+ fontWeight?: number;
+
+ /**
+ * Font size in px.
+ */
+ fontSize?: number;
+
+ /**
+ * Text casing applied to the node, default is the original casing.
+ */
+ textCase?: "UPPER" | "LOWER" | "TITLE" | "SMALL_CAPS" | "SMALL_CAPS_FORCED";
+
+ /**
+ * Horizontal text alignment as string enum.
+ */
+ textAlignHorizontal?: "LEFT" | "RIGHT" | "CENTER" | "JUSTIFIED";
+
+ /**
+ * Vertical text alignment as string enum.
+ */
+ textAlignVertical?: "TOP" | "CENTER" | "BOTTOM";
+
+ /**
+ * Space between characters in px.
+ */
+ letterSpacing?: number;
+
+ /**
+ * An array of fill paints applied to the characters.
+ */
+ fills?: Paint[];
+
+ /**
+ * Link to a URL or frame.
+ */
+ hyperlink?: Hyperlink;
+
+ /**
+ * A map of OpenType feature flags to 1 or 0, 1 if it is enabled and 0 if it is disabled. Note that
+ * some flags aren't reflected here. For example, SMCP (small caps) is still represented by the
+ * `textCase` field.
+ */
+ opentypeFlags?: { [key: string]: number };
+
+ /**
+ * Indicates how the font weight was overridden when there is a text style override.
+ */
+ semanticWeight?: "BOLD" | "NORMAL";
+
+ /**
+ * Indicates how the font style was overridden when there is a text style override.
+ */
+ semanticItalic?: "ITALIC" | "NORMAL";
+};
+
+export type TypeStyle = {
+ /**
+ * Space between paragraphs in px, 0 if not present.
+ */
+ paragraphSpacing?: number;
+
+ /**
+ * Paragraph indentation in px, 0 if not present.
+ */
+ paragraphIndent?: number;
+
+ /**
+ * Space between list items in px, 0 if not present.
+ */
+ listSpacing?: number;
+
+ /**
+ * Text decoration applied to the node, default is none.
+ */
+ textDecoration?: "NONE" | "STRIKETHROUGH" | "UNDERLINE";
+
+ /**
+ * Dimensions along which text will auto resize, default is that the text does not auto-resize.
+ * TRUNCATE means that the text will be shortened and trailing text will be replaced with "…" if the
+ * text contents is larger than the bounds. `TRUNCATE` as a return value is deprecated and will be
+ * removed in a future version. Read from `textTruncation` instead.
+ */
+ textAutoResize?: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" | "TRUNCATE";
+
+ /**
+ * Whether this text node will truncate with an ellipsis when the text contents is larger than the
+ * text node.
+ */
+ textTruncation?: "DISABLED" | "ENDING";
+
+ /**
+ * When `textTruncation: "ENDING"` is set, `maxLines` determines how many lines a text node can grow
+ * to before it truncates.
+ */
+ maxLines?: number;
+
+ /**
+ * Line height in px.
+ */
+ lineHeightPx?: number;
+
+ /**
+ * Line height as a percentage of normal line height. This is deprecated; in a future version of the
+ * API only lineHeightPx and lineHeightPercentFontSize will be returned.
+ */
+ lineHeightPercent?: number;
+
+ /**
+ * Line height as a percentage of the font size. Only returned when `lineHeightPercent` (deprecated)
+ * is not 100.
+ */
+ lineHeightPercentFontSize?: number;
+
+ /**
+ * The unit of the line height value specified by the user.
+ */
+ lineHeightUnit?: "PIXELS" | "FONT_SIZE_%" | "INTRINSIC_%";
+
+ /**
+ * Whether or not this style has overrides over a text style. The possible fields to override are
+ * semanticWeight, semanticItalic, hyperlink, and textDecoration. If this is true, then those fields
+ * are overrides if present.
+ */
+ isOverrideOverTextStyle?: boolean;
+} & BaseTypeStyle;
+
+export type TextPathTypeStyle = {
+ /**
+ * Whether or not this style has overrides over a text style. The possible fields to override are
+ * semanticWeight, semanticItalic, and hyperlink. If this is true, then those fields are overrides
+ * if present.
+ */
+ isOverrideOverTextStyle?: boolean;
+} & BaseTypeStyle;
+
+/**
+ * Component property type.
+ */
+export type ComponentPropertyType =
+ | "BOOLEAN"
+ | "INSTANCE_SWAP"
+ | "TEXT"
+ | "VARIANT";
+
+/**
+ * Instance swap preferred value.
+ */
+export type InstanceSwapPreferredValue = {
+ /**
+ * Type of node for this preferred value.
+ */
+ type: "COMPONENT" | "COMPONENT_SET";
+
+ /**
+ * Key of this component or component set.
+ */
+ key: string;
+};
+
+/**
+ * A property of a component.
+ */
+export type ComponentPropertyDefinition = {
+ /**
+ * Type of this component property.
+ */
+ type: ComponentPropertyType;
+
+ /**
+ * Initial value of this property for instances.
+ */
+ defaultValue: boolean | string;
+
+ /**
+ * All possible values for this property. Only exists on VARIANT properties.
+ */
+ variantOptions?: string[];
+
+ /**
+ * Preferred values for this property. Only applicable if type is `INSTANCE_SWAP`.
+ */
+ preferredValues?: InstanceSwapPreferredValue[];
+};
+
+/**
+ * A property of a component.
+ */
+export type ComponentProperty = {
+ /**
+ * Type of this component property.
+ */
+ type: ComponentPropertyType;
+
+ /**
+ * Value of the property for this component instance.
+ */
+ value: boolean | string;
+
+ /**
+ * Preferred values for this property. Only applicable if type is `INSTANCE_SWAP`.
+ */
+ preferredValues?: InstanceSwapPreferredValue[];
+};
+
+/**
+ * Fields directly overridden on an instance. Inherited overrides are not included.
+ */
+export type Overrides = {
+ /**
+ * A unique ID for a node.
+ */
+ id: string;
+
+ /**
+ * An array of properties.
+ */
+ overriddenFields: string[];
+};
diff --git a/AGENTS.md b/AGENTS.md
index 9331946146..82b8f66416 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -16,6 +16,7 @@ Currently, we have below features / modules.
- [docs](./docs) - the docs directory
- [editor](./editor) - the editor directory
+- [crates](./crates) - the rust crates directory
- [packages](./packages) - shared packages
- [desktop](./desktop) - the electron desktop app
- [supabase](./supabase) - the supabase project
@@ -32,10 +33,11 @@ Currently, we have below features / modules.
- TypeScript 5 - main language for most apps
- Python 3.12 - partially used for tasks / jobs, that are independent, e.g. `/library`
- Deno - partially used for tasks / jobs, that shares the codebase, e.g. `/jobs`
+- Rust (2024 edition) - used for wasm builds, mostly for graphics core.
**Database**
-Grida heavily relies on Supabase.
+Grida heavily relies on Supabase (PostgreSQL).
- Supabase
@@ -50,6 +52,11 @@ Grida heavily relies on Supabase.
- Shadcn UI
- Lucide / Radix Icons
+**Graphics Backend**
+
+- DOM - plain dom as canvas - for website builder canvas. (binded with react)
+- Skia - the graphics backend - for 2D graphics. (binded with skia-safe)
+
**Desktop**
- electron with electron-forge
@@ -61,11 +68,19 @@ Documentation files are located in the `./docs` directory.
This directory contains the docs as-is, the deployment of the docs are handled by [apps/docs](./apps/docs). A docusaurus project that syncs the docs content to its directory. When writing docs, the root `./docs` directory is the source of truth.
+## `/crates/*`
+
+Importance: **High**
+
+monorepo rust crates.
+
+The rust implementation of the Grida Canvas. this is rapidly under development. - it will serve as our new rendering backend once it is stable.
+
## `/editor`
Importance: **Very high**
-The editor is a monorepo project that contains the codebase for the editor.
+The editor is a monorepo nextjs project that contains the codebase for the editor.
grida.co and \[tenant\].grida.site domains are connected.
@@ -128,10 +143,14 @@ Importance: **Low**
Library workers are hosted on railway.com
-## `/packages/grida-canvas-*`
+## `/packages/*`
Importance: **High**
+monorepo node packages
+
+**`/packages/grida-canvas-*`**
+
Packages that powers the canvas. (some are published to npm, some are not)
Since our project is in a rapid development, some large modules still lives under the `/editor` directory. Which will progressively move to `/packages` directory, once things are sorted out and fully defined with the good models.
@@ -146,16 +165,31 @@ To run test, build, and dev, use below commands.
```sh
# run tests
-pnpm turbo test
+turbo test
+
+# run tests for packages
+turbo test --filter='./packages/*'
+
+# run tests except for rust crates
+turbo test --filter='!./crates/*'
# run build
-pnpm turbo build
+turbo build
# run dev
-pnpm turbo dev
+turbo dev
# run typecheck
-pnpm turbo typecheck # fallback when build fails due to network issues (nextjs package might fail due to font fetching issues)
+turbo typecheck # fallback when build fails due to network issues (nextjs package might fail due to font fetching issues)
+
+# for crates specific tests
+cargo test
+
+# for crates specific build
+cargo build
+
+# format crates
+cargo fmt --all
```
Note: `typecheck` still rely on packages build artifacts, so it will fail if the build fails.
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000000..17fa339078
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,4032 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ab_glyph"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0"
+dependencies = [
+ "ab_glyph_rasterizer",
+ "owned_ttf_parser",
+]
+
+[[package]]
+name = "ab_glyph_rasterizer"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.3",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-activity"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
+dependencies = [
+ "android-properties",
+ "bitflags 2.9.1",
+ "cc",
+ "cesu8",
+ "jni",
+ "jni-sys",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "num_enum",
+ "thiserror",
+]
+
+[[package]]
+name = "android-properties"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "as-raw-xcb-connection"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
+
+[[package]]
+name = "async-trait"
+version = "0.1.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bindgen"
+version = "0.71.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
+dependencies = [
+ "bitflags 2.9.1",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
+[[package]]
+name = "bytemuck"
+version = "1.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "calloop"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
+dependencies = [
+ "bitflags 2.9.1",
+ "log",
+ "polling",
+ "rustix 0.38.44",
+ "slab",
+ "thiserror",
+]
+
+[[package]]
+name = "calloop-wayland-source"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
+dependencies = [
+ "calloop",
+ "rustix 0.38.44",
+ "wayland-backend",
+ "wayland-client",
+]
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
+name = "cc"
+version = "1.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
+dependencies = [
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "cg"
+version = "0.0.0"
+dependencies = [
+ "async-trait",
+ "clap",
+ "console_error_panic_hook",
+ "criterion",
+ "figma-api",
+ "futures",
+ "gl",
+ "glutin",
+ "glutin-winit",
+ "math2",
+ "rand",
+ "raw-window-handle",
+ "reqwest",
+ "rstar",
+ "serde",
+ "serde_json",
+ "skia-safe",
+ "tokio",
+ "uuid",
+ "wasm-bindgen",
+ "winit",
+]
+
+[[package]]
+name = "cgl"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link",
+]
+
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types 0.5.0",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "criterion"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+dependencies = [
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "is-terminal",
+ "itertools 0.10.5",
+ "num-traits",
+ "once_cell",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools 0.10.5",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+
+[[package]]
+name = "cursor-icon"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
+
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deranged"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dispatch2"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dlib"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "figma-api"
+version = "0.31.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23256077c8160e009f3668de3dc68c41af8a97eee423fc09fd9396471f4da090"
+dependencies = [
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serde_with",
+ "url",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared 0.3.1",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gethostname"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
+dependencies = [
+ "libc",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "gl"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404"
+dependencies = [
+ "gl_generator",
+]
+
+[[package]]
+name = "gl_generator"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
+dependencies = [
+ "khronos_api",
+ "log",
+ "xml-rs",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "glutin"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg_aliases",
+ "cgl",
+ "dispatch2",
+ "glutin_egl_sys",
+ "glutin_glx_sys",
+ "glutin_wgl_sys",
+ "libloading",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+ "once_cell",
+ "raw-window-handle",
+ "wayland-sys",
+ "windows-sys 0.52.0",
+ "x11-dl",
+]
+
+[[package]]
+name = "glutin-winit"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f"
+dependencies = [
+ "cfg_aliases",
+ "glutin",
+ "raw-window-handle",
+ "winit",
+]
+
+[[package]]
+name = "glutin_egl_sys"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2"
+dependencies = [
+ "gl_generator",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "glutin_glx_sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185"
+dependencies = [
+ "gl_generator",
+ "x11-dl",
+]
+
+[[package]]
+name = "glutin_wgl_sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
+dependencies = [
+ "gl_generator",
+]
+
+[[package]]
+name = "grida"
+version = "0.0.0"
+dependencies = [
+ "gl",
+ "skia-safe",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.9.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
+
+[[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
+
+[[package]]
+name = "heapless"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
+dependencies = [
+ "hash32",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.3",
+ "serde",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "jobserver"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+dependencies = [
+ "getrandom 0.3.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "khronos_api"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "libloading"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.53.0",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.9.1",
+ "libc",
+ "redox_syscall 0.5.12",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "math2"
+version = "0.0.2"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memmap2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.9.1",
+ "jni-sys",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2",
+ "libc",
+ "objc2 0.5.2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-contacts"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
+dependencies = [
+ "bitflags 2.9.1",
+ "dispatch2",
+ "objc2 0.6.1",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-contacts",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2",
+ "dispatch",
+ "libc",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-link-presentation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-symbols"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
+dependencies = [
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+ "objc2-link-presentation",
+ "objc2-quartz-core",
+ "objc2-symbols",
+ "objc2-uniform-type-identifiers",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-uniform-type-identifiers"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "orbclient"
+version = "0.3.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43"
+dependencies = [
+ "libredox",
+]
+
+[[package]]
+name = "owned_ttf_parser"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
+dependencies = [
+ "ttf-parser",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "plotters"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
+name = "polling"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.0.7",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "rayon"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "reqwest"
+version = "0.12.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rstar"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb"
+dependencies = [
+ "heapless",
+ "num-traits",
+ "smallvec",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.9.1",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags 2.9.1",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.4",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "sctk-adwaita"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
+dependencies = [
+ "ab_glyph",
+ "log",
+ "memmap2",
+ "smithay-client-toolkit",
+ "tiny-skia",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
+dependencies = [
+ "base64",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.9.0",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "skia-bindings"
+version = "0.86.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbcd02d7008cdc4ac86b7d7461874c7ac1d2c38cad96629d7617c5d4b848acd0"
+dependencies = [
+ "bindgen",
+ "cc",
+ "flate2",
+ "heck",
+ "lazy_static",
+ "regex",
+ "serde_json",
+ "tar",
+ "toml",
+]
+
+[[package]]
+name = "skia-safe"
+version = "0.86.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "008dec8a6b69f03b2a0bc4520dc06a7a8efc844e59b2a9bc024f0cb02fb60b63"
+dependencies = [
+ "bitflags 2.9.1",
+ "lazy_static",
+ "skia-bindings",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
+dependencies = [
+ "bitflags 2.9.1",
+ "calloop",
+ "calloop-wayland-source",
+ "cursor-icon",
+ "libc",
+ "log",
+ "memmap2",
+ "rustix 0.38.44",
+ "thiserror",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-csd-frame",
+ "wayland-cursor",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
+ "wayland-scanner",
+ "xkeysym",
+]
+
+[[package]]
+name = "smol_str"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "strict-num"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix 1.0.7",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
+[[package]]
+name = "time-macros"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tiny-skia"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "bytemuck",
+ "cfg-if",
+ "log",
+ "tiny-skia-path",
+]
+
+[[package]]
+name = "tiny-skia-path"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
+dependencies = [
+ "arrayref",
+ "bytemuck",
+ "strict-num",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "tokio"
+version = "1.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
+dependencies = [
+ "indexmap 2.9.0",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+dependencies = [
+ "bitflags 2.9.1",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "ttf-parser"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wayland-backend"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "rustix 0.38.44",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
+dependencies = [
+ "bitflags 2.9.1",
+ "rustix 0.38.44",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-csd-frame"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
+dependencies = [
+ "bitflags 2.9.1",
+ "cursor-icon",
+ "wayland-backend",
+]
+
+[[package]]
+name = "wayland-cursor"
+version = "0.31.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182"
+dependencies = [
+ "rustix 0.38.44",
+ "wayland-client",
+ "xcursor",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.32.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a"
+dependencies = [
+ "bitflags 2.9.1",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-plasma"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175"
+dependencies = [
+ "bitflags 2.9.1",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-wlr"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf"
+dependencies = [
+ "bitflags 2.9.1",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
+dependencies = [
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
+dependencies = [
+ "dlib",
+ "log",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+
+[[package]]
+name = "windows-registry"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
+dependencies = [
+ "windows-result",
+ "windows-strings 0.3.1",
+ "windows-targets 0.53.0",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winit"
+version = "0.30.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
+dependencies = [
+ "ahash",
+ "android-activity",
+ "atomic-waker",
+ "bitflags 2.9.1",
+ "block2",
+ "bytemuck",
+ "calloop",
+ "cfg_aliases",
+ "concurrent-queue",
+ "core-foundation",
+ "core-graphics",
+ "cursor-icon",
+ "dpi",
+ "js-sys",
+ "libc",
+ "memmap2",
+ "ndk",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-ui-kit",
+ "orbclient",
+ "percent-encoding",
+ "pin-project",
+ "raw-window-handle",
+ "redox_syscall 0.4.1",
+ "rustix 0.38.44",
+ "sctk-adwaita",
+ "smithay-client-toolkit",
+ "smol_str",
+ "tracing",
+ "unicode-segmentation",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-plasma",
+ "web-sys",
+ "web-time",
+ "windows-sys 0.52.0",
+ "x11-dl",
+ "x11rb",
+ "xkbcommon-dl",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11rb"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
+dependencies = [
+ "as-raw-xcb-connection",
+ "gethostname",
+ "libc",
+ "libloading",
+ "once_cell",
+ "rustix 0.38.44",
+ "x11rb-protocol",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
+
+[[package]]
+name = "xattr"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
+dependencies = [
+ "libc",
+ "rustix 1.0.7",
+]
+
+[[package]]
+name = "xcursor"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61"
+
+[[package]]
+name = "xkbcommon-dl"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
+dependencies = [
+ "bitflags 2.9.1",
+ "dlib",
+ "log",
+ "once_cell",
+ "xkeysym",
+]
+
+[[package]]
+name = "xkeysym"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
+
+[[package]]
+name = "xml-rs"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda"
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000000..0663057cd2
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,7 @@
+[workspace]
+resolver = "3"
+members = [
+ "crates/grida",
+ "crates/grida-canvas",
+ "crates/grida-math2",
+]
\ No newline at end of file
diff --git a/crates/.gitignore b/crates/.gitignore
new file mode 100644
index 0000000000..87f6532f0a
--- /dev/null
+++ b/crates/.gitignore
@@ -0,0 +1,21 @@
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
+
+# Generated by cargo mutants
+# Contains mutation testing data
+**/mutants.out*/
+
+# RustRover
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
\ No newline at end of file
diff --git a/crates/Dockerfile b/crates/Dockerfile
new file mode 100644
index 0000000000..6462b71b5e
--- /dev/null
+++ b/crates/Dockerfile
@@ -0,0 +1,18 @@
+FROM ghcr.io/pragmatrix/rust-skia-linux:latest
+
+# Prepare Rust toolchain
+RUN rustup update && \
+ rustup default stable && \
+ rustup target add wasm32-unknown-emscripten
+
+# ENV EMCC_CFLAGS="-s ERROR_ON_UNDEFINED_SYMBOLS=0 -s MAX_WEBGL_VERSION=2"
+ENV EMCC_CFLAGS="-s ERROR_ON_UNDEFINED_SYMBOLS=0 -s MAX_WEBGL_VERSION=2 -s MODULARIZE=1 -s EXPORT_NAME=createRustSkiaModule -s EXPORTED_RUNTIME_METHODS=GL"
+
+WORKDIR /work
+COPY . /work
+
+RUN source /emsdk/emsdk_env.sh && \
+ cd grida && \
+ cargo build --release --target wasm32-unknown-emscripten
+
+CMD ["bash"]
diff --git a/crates/Makefile b/crates/Makefile
new file mode 100644
index 0000000000..a5e7d4f3e1
--- /dev/null
+++ b/crates/Makefile
@@ -0,0 +1,22 @@
+IMAGE_NAME=grida-wasm-builder
+CONTAINER_NAME=grida-build-container
+WASM_TARGET=target/wasm32-unknown-emscripten
+OUT_DIR=grida/target
+
+.PHONY: all build extract clean
+
+all: build extract
+
+build:
+ docker build -f Dockerfile -t $(IMAGE_NAME) .
+
+extract:
+ docker build -f Dockerfile -t $(IMAGE_NAME) .
+ -@docker rm -f $(CONTAINER_NAME) 2>/dev/null || true
+ mkdir -p $(OUT_DIR)
+ docker create --name $(CONTAINER_NAME) $(IMAGE_NAME)
+ docker cp $(CONTAINER_NAME):/work/grida/$(WASM_TARGET)/ $(OUT_DIR)/
+ docker rm -f $(CONTAINER_NAME)
+
+clean:
+ rm -rf $(OUT_DIR)/*
diff --git a/crates/docs/optimization.md b/crates/docs/optimization.md
new file mode 100644
index 0000000000..67dd57c9cb
--- /dev/null
+++ b/crates/docs/optimization.md
@@ -0,0 +1,226 @@
+# Rendering Optimization Strategies
+
+A summary of all discussed optimization techniques for achieving high-performance rendering (e.g., 144fps with large design documents).
+
+---
+
+## Transform & Geometry
+
+1. **Transform Cache**
+
+ - Store `local_transform` and derived `world_transform`.
+ - Use dirty flags and top-down updates.
+
+2. **Geometry Cache**
+
+ - Cache `local_bounds`, `world_bounds`.
+ - Used for culling, layout, and hit-testing.
+
+3. **Flat Scene Graph + Parent Pointers**
+
+ - Flat arena with parent/children relationships.
+ - Enables O(1) access and traversal.
+
+---
+
+## Rendering Pipeline
+
+4. **GPU Acceleration (Skia Backend::GL/Vulkan)**
+
+ - Use hardware compositing, filters, transforms.
+
+5. **Scene-Level Picture Caching**
+
+ - Use `SkPicture` to record full-scene vector draw ops.
+ - Serves as the always-up-to-date canonical snapshot.
+ - Resolution-independent; ideal for rerendering or tile regeneration.
+
+6. **Tile-Based Raster Cache (Hybrid Rendering)**
+
+ - Render the full viewport, take snapshot. debounced (after no more changes. e.g. 150ms)
+ - Divide the snapshot into fixed-size tiles (e.g., 512×512).
+ - When new area discovered, render the cached, non-overlapping parts with tile cache. only render newly discovered area.
+ - Repeat step 1.
+ - Optional padding per tile to account for effects (blur, shadows).
+
+7. **Dynamic Mode Switching (Picture vs Tile)**
+
+ - Render from `SkPicture` directly during normal zoom or active edits.
+ - Fallback to raster tiles for zoomed-out or complex views.
+ - Tile invalidation/redraw is driven by zoom level, camera transform, or frame budget.
+
+8. **Dirty & Re-Cache Strategy**
+
+ - Nodes marked dirty will trigger re-recording of affected picture regions or tiles.
+ - Use change tracking to only re-record minimum needed areas.
+ - Recording large subtrees is expensive—optimize granularity based on tree structure.
+
+9. **Scene Cache Config / Strategy**
+
+ - Defines how scene caching is organized.
+ - Properties include:
+
+ - `depth`:
+
+ - `0` → Entire scene is one cache.
+ - `1` → Cache per top-level container.
+ - `n` → Cache at depth `n`, chunking deeper layers.
+
+ - `mode`: `AlwaysPicture`, `Hybrid`, `AlwaysTile`
+
+ - `tile_size`, `tile_padding`
+
+ - `zoom_threshold_for_tiles`
+
+ - `frame_budget_threshold_ms`
+
+ - `use_bbh`, `enable_lod`, etc.
+
+ - Cache accessors like `get_picture_cache_by_id()` support scoped re-rendering.
+
+10. **Will-Change Optimization**
+
+ - Nodes marked with "will-change" are expected to become dirty soon.
+ - Examples:
+
+ - Image node waiting on async src resolution
+ - Text node waiting on font availability
+
+ - Tree holders of such nodes are chunked for localized re-recording.
+ - Prevents re-recording full subtrees—minimizes recording cost.
+
+11. **Flattened Render Command List**
+
+ - Scene is compiled into a flat list of `RenderCommand` structs with resolved:
+
+ - Transform
+ - Clip bounds
+ - Opacity
+ - Z-order
+
+ - Enables non-recursive rendering and independent layer recording.
+ - Required for tiling at arbitrary depths and for caching subtrees.
+
+ **Example:**
+
+ ```text
+ Logical Tree:
+ Frame
+ └── Group
+ ├── Rect1
+ ├── Rect2
+ └── Rect3
+
+ Flattened:
+ [
+ RenderCommand { node_id: Rect1, transform: ..., clip: ..., z: ... },
+ RenderCommand { node_id: Rect2, transform: ..., clip: ..., z: ... },
+ RenderCommand { node_id: Rect3, transform: ..., clip: ..., z: ... },
+ ]
+ ```
+
+ - Each command can be grouped and recorded separately into its own `SkPicture`.
+ - Nesting is preserved logically via sort order, but rendering is flat.
+ - This model is essential for dynamic caching, parallel planning, and GPU-aware scheduling.
+
+12. **Dirty-Region Culling**
+
+ - Use camera’s `visible_rect` to cull `world_bounds`.
+ - Optional: accelerate with quadtree or BVH.
+
+13. **Minimize Canvas State Changes**
+
+ - Reuse transforms and paints.
+ - Precompute common values like DPI × Zoom × ViewMatrix.
+
+14. **Text & Path Caching**
+
+ - Cache laid-out paragraphs and SkPaths.
+ - Avoid layout recomputation every frame.
+
+15. **Render Pass Flattening**
+
+ - Group nodes with same blend/composite states.
+ - Sort draw calls for fewer GPU flushes.
+
+---
+
+## Image Optimization
+
+16. **LoD / Mipmapped Image Swapping**
+
+ - Use lower-res versions of images at low zoom.
+ - Prevents high GPU bandwidth use at low visibility.
+
+17. **ImageRepository with Transform-Aware Access**
+
+ - Pick image resolution based on projected screen size.
+ - _TODO_: currently we select mipmap levels solely by the size of the
+ drawing rectangle. This is a temporary strategy until a proper cache
+ invalidation mechanism based on zoom is introduced.
+
+---
+
+## Text & Glyph Optimization
+
+18. **Glyph Cache (Atlas or Paragraph Caching)**
+
+ - Cache rasterized or vector glyphs used across the document.
+ - Prevents redundant layout or rendering of text.
+ - Essential for high-DPI or frequently zoomed views.
+
+---
+
+## Engine-Level
+
+19. **Precomputed World Transforms**
+
+ - Avoid recalculating transforms per draw call.
+ - Essential for random-access rendering.
+
+20. **Flat Table Architecture**
+
+ - All node data (transforms, bounds, styles) stored in flat maps.
+ - Enables fast diffing, syncing, and concurrent access.
+
+21. **Callback-Based Traversal with Fn/FnMut**
+
+ - Owner controls child behavior via inlined, zero-cost closures.
+
+22. **Scene Planner & Scheduler**
+
+ - A dynamic system that builds the flat render list per frame.
+ - Reacts to scene changes, memory pressure, or frame budget changes.
+ - Drives the decision to re-record, cache, evict, or downgrade fidelity.
+
+---
+
+## Optional Advanced
+
+23. **Multithreaded Scene Update**
+
+ - Parallelize transform/bounds resolution.
+
+24. **CRDT-Ready Data Stores**
+
+ - Flat table model enables future collaboration support.
+
+25. **BVH or Quadtree Spatial Index**
+
+ - Build dynamic index from `world_bounds` for fast spatial queries.
+
+---
+
+## With Compromises
+
+> Practical, UX-safe tradeoffs that simplify implementation and improve performance, especially under load. These techniques sacrifice exactness for speed — but in ways users won’t notice.
+
+---
+
+- **Quantize Camera Transform**
+
+ Instead of using fully continuous float precision for the camera position and zoom, round them to the nearest N units (e.g., 0.1 for position, 0.01 for zoom):
+
+---
+
+This list is designed to help evolve a renderer from minimal single-threaded mode to scalable, GPU-friendly real-time performance.
diff --git a/crates/fixtures/test-figma/766822741396935685.zip b/crates/fixtures/test-figma/766822741396935685.zip
new file mode 100644
index 0000000000..33cdbb0108
Binary files /dev/null and b/crates/fixtures/test-figma/766822741396935685.zip differ
diff --git a/crates/fixtures/test-figma/767108022358877208.zip b/crates/fixtures/test-figma/767108022358877208.zip
new file mode 100644
index 0000000000..a37d6fb23e
Binary files /dev/null and b/crates/fixtures/test-figma/767108022358877208.zip differ
diff --git a/crates/fixtures/test-figma/767127152320102433.zip b/crates/fixtures/test-figma/767127152320102433.zip
new file mode 100644
index 0000000000..06b13aa6b4
Binary files /dev/null and b/crates/fixtures/test-figma/767127152320102433.zip differ
diff --git a/crates/fixtures/test-figma/767135333966182437.zip b/crates/fixtures/test-figma/767135333966182437.zip
new file mode 100644
index 0000000000..66e1c39eb1
Binary files /dev/null and b/crates/fixtures/test-figma/767135333966182437.zip differ
diff --git a/crates/fixtures/test-figma/767544357629075510.zip b/crates/fixtures/test-figma/767544357629075510.zip
new file mode 100644
index 0000000000..7a1045b291
Binary files /dev/null and b/crates/fixtures/test-figma/767544357629075510.zip differ
diff --git a/crates/fixtures/test-figma/768233500885703748.zip b/crates/fixtures/test-figma/768233500885703748.zip
new file mode 100644
index 0000000000..2716afa838
Binary files /dev/null and b/crates/fixtures/test-figma/768233500885703748.zip differ
diff --git a/crates/fixtures/test-figma/768273489419236430.zip b/crates/fixtures/test-figma/768273489419236430.zip
new file mode 100644
index 0000000000..1819b15692
Binary files /dev/null and b/crates/fixtures/test-figma/768273489419236430.zip differ
diff --git a/crates/grida-canvas/AGENTS.md b/crates/grida-canvas/AGENTS.md
new file mode 100644
index 0000000000..bc337c3606
--- /dev/null
+++ b/crates/grida-canvas/AGENTS.md
@@ -0,0 +1,20 @@
+safe, high-performance, 2D real-time rendering engine.
+
+- uses [`skia-safe`](https://rust-skia.github.io/doc/skia_safe/) for painting
+- uses [`math2`](../grida-math2/README.md) for geometry & common math operations
+
+## Testing & Development
+
+```sh
+# run tests
+cargo test
+
+# run fmt
+cargo fmt
+
+# run build
+cargo build
+
+# run dev (mostly requires window)
+cargo run --example
+```
diff --git a/crates/grida-canvas/Cargo.lock b/crates/grida-canvas/Cargo.lock
new file mode 100644
index 0000000000..04ad23b8dd
--- /dev/null
+++ b/crates/grida-canvas/Cargo.lock
@@ -0,0 +1,784 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.71.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
+[[package]]
+name = "cc"
+version = "1.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cg"
+version = "0.1.0"
+dependencies = [
+ "console_error_panic_hook",
+ "skia-safe",
+ "wasm-bindgen",
+ "wee_alloc",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "libredox",
+ "windows-sys",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "libloading"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
+dependencies = [
+ "cfg-if 1.0.0",
+ "windows-targets 0.53.0",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags",
+ "libc",
+ "redox_syscall",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memory_units"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "skia-bindings"
+version = "0.86.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbcd02d7008cdc4ac86b7d7461874c7ac1d2c38cad96629d7617c5d4b848acd0"
+dependencies = [
+ "bindgen",
+ "cc",
+ "flate2",
+ "heck",
+ "lazy_static",
+ "regex",
+ "serde_json",
+ "tar",
+ "toml",
+]
+
+[[package]]
+name = "skia-safe"
+version = "0.86.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "008dec8a6b69f03b2a0bc4520dc06a7a8efc844e59b2a9bc024f0cb02fb60b63"
+dependencies = [
+ "bitflags",
+ "lazy_static",
+ "skia-bindings",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if 1.0.0",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wee_alloc"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "memory_units",
+ "winapi",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winnow"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "xattr"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
+dependencies = [
+ "libc",
+ "rustix",
+]
diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml
new file mode 100644
index 0000000000..c3cb1b6258
--- /dev/null
+++ b/crates/grida-canvas/Cargo.toml
@@ -0,0 +1,50 @@
+[package]
+name = "cg"
+version = "0.0.0"
+edition = "2024"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+# Core dependencies
+serde = "1.0.219"
+serde_json = "1.0.140"
+uuid = { version = "1.17.0", features = ["v4", "js"] }
+math2 = { path = "../grida-math2" }
+skia-safe = { version = "0.86.0", features = ["gpu", "gl", "textlayout"] }
+async-trait = "0.1"
+rstar = "0.12"
+
+# WASM-specific dependencies
+wasm-bindgen = { version = "0.2.100", optional = true }
+console_error_panic_hook = { version = "0.1.7", optional = true }
+figma-api = "0.31.3"
+rand = "0.9.1"
+futures = "0.3.31"
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+# Native-only dependencies
+tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+reqwest = "0.12.19"
+glutin = "0.32.0"
+glutin-winit = "0.5.0"
+raw-window-handle = "0.6.0"
+gl-rs = { version = "0.14.0", package = "gl" }
+winit = "0.30.0"
+clap = { version = "4.5.39", features = ["derive"] }
+
+[features]
+default = ["wasm-bindgen", "console_error_panic_hook"]
+wasm = ["wasm-bindgen", "console_error_panic_hook"]
+
+[dev-dependencies]
+criterion = "0.5"
+
+[[bench]]
+name = "bench_rectangles"
+harness = false
+
+[[bench]]
+name = "bench_mipmap"
+harness = false
diff --git a/crates/grida-canvas/Makefile b/crates/grida-canvas/Makefile
new file mode 100644
index 0000000000..4dad84a152
--- /dev/null
+++ b/crates/grida-canvas/Makefile
@@ -0,0 +1,10 @@
+EMSDK ?= ~/.asdf/installs/emsdk/$(shell asdf current emsdk | tr -s ' ' | cut -d ' ' -f 2)
+BUILD = EMCC_CFLAGS="-s ERROR_ON_UNDEFINED_SYMBOLS=0 -s MAX_WEBGL_VERSION=2 -s MODULARIZE=1 -s EXPORT_NAME=createRustSkiaModule -s EXPORTED_RUNTIME_METHODS=GL" EMSDK=$(EMSDK) cargo build --target wasm32-unknown-emscripten
+
+.PHONY: build
+build:
+ $(BUILD)
+
+.PHONY: build_release
+build_release:
+ $(BUILD) --release
diff --git a/crates/grida-canvas/README.md b/crates/grida-canvas/README.md
new file mode 100644
index 0000000000..0a9b0335ec
--- /dev/null
+++ b/crates/grida-canvas/README.md
@@ -0,0 +1,58 @@
+# grida-canvas (`cg`)
+
+
+
+## Build
+
+```bash
+make build
+make build_release
+```
+
+## Rendering
+
+**2D Nodes**
+
+- [ ] TextSpan
+- [ ] Text (Text with mixed styles)
+- [ ] Image
+- [ ] Bitmap (for bitmap drawing)
+- [ ] Group
+- [ ] Container (Frame)
+- [ ] Rectangle
+- [ ] Ellipse
+- [ ] Polygon
+- [ ] RegularPolygon
+- [ ] RegularStarPolygon
+- [ ] Path (SVG Path)
+- [ ] Vector (Vector Network)
+- [ ] Line
+
+**Meta**
+
+- [ ] Mask
+- [ ] Clip
+
+**Styles & Effects**
+
+- [ ] SolidPaint
+- [ ] LinearGradientPaint
+- [ ] RadialGradientPaint
+- [ ] DropShadow
+- [ ] BoxShadow
+- [ ] BlendMode
+
+## API
+
+**Camera**
+
+- [ ] 2D Camera
+
+**Pipeline & API**
+
+- [ ] load font
+- [ ] load image
+
+## Interactivity
+
+- [ ] Hit testing
diff --git a/crates/grida-canvas/benches/bench_mipmap.rs b/crates/grida-canvas/benches/bench_mipmap.rs
new file mode 100644
index 0000000000..b6905f01d1
--- /dev/null
+++ b/crates/grida-canvas/benches/bench_mipmap.rs
@@ -0,0 +1,35 @@
+use cg::mipmap::{ImageMipmaps, MipmapConfig, MipmapLevels};
+use criterion::{Criterion, black_box, criterion_group, criterion_main};
+use skia_safe::surfaces;
+
+fn bench_mipmap_generation(c: &mut Criterion) {
+ let mut surface = surfaces::raster_n32_premul((2048, 2048)).unwrap();
+ let image = surface.image_snapshot();
+
+ let mut group = c.benchmark_group("mipmap_gen");
+
+ let chained = MipmapConfig {
+ levels: MipmapLevels::FullChain,
+ chained: true,
+ };
+ group.bench_function("chained", |b| {
+ b.iter(|| {
+ let _ = ImageMipmaps::from_image(black_box(image.clone()), &chained);
+ })
+ });
+
+ let direct = MipmapConfig {
+ levels: MipmapLevels::FullChain,
+ chained: false,
+ };
+ group.bench_function("direct", |b| {
+ b.iter(|| {
+ let _ = ImageMipmaps::from_image(black_box(image.clone()), &direct);
+ })
+ });
+
+ group.finish();
+}
+
+criterion_group!(benches, bench_mipmap_generation);
+criterion_main!(benches);
diff --git a/crates/grida-canvas/benches/bench_rectangles.rs b/crates/grida-canvas/benches/bench_rectangles.rs
new file mode 100644
index 0000000000..b6df145db3
--- /dev/null
+++ b/crates/grida-canvas/benches/bench_rectangles.rs
@@ -0,0 +1,220 @@
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::runtime::scene::{Backend, Renderer};
+use criterion::{Criterion, black_box, criterion_group, criterion_main};
+use math2::transform::AffineTransform;
+
+fn create_rectangles(count: usize, with_effects: bool) -> Scene {
+ let mut repository = NodeRepository::new();
+ let mut ids = Vec::new();
+
+ // Create rectangles
+ for i in 0..count {
+ let id = format!("rect-{}", i);
+ ids.push(id.clone());
+
+ let rect = RectangleNode {
+ base: BaseNode {
+ id: id.clone(),
+ name: format!("Rectangle {}", i),
+ active: true,
+ },
+ transform: AffineTransform::identity(),
+ size: Size {
+ width: 100.0,
+ height: 100.0,
+ },
+ corner_radius: RectangularCornerRadius::zero(),
+ fill: Paint::Solid(SolidPaint {
+ color: Color(255, 0, 0, 255),
+ opacity: 1.0,
+ }),
+ stroke: Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ }),
+ stroke_width: 1.0,
+ stroke_align: StrokeAlign::Inside,
+ stroke_dash_array: None,
+ opacity: 1.0,
+ blend_mode: BlendMode::Normal,
+ effect: if with_effects {
+ Some(FilterEffect::DropShadow(FeDropShadow {
+ dx: 2.0,
+ dy: 2.0,
+ blur: 4.0,
+ color: Color(0, 0, 0, 128),
+ }))
+ } else {
+ None
+ },
+ };
+
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Create root group
+ let root_group = GroupNode {
+ base: BaseNode {
+ id: "root".to_string(),
+ name: "Root Group".to_string(),
+ active: true,
+ },
+ transform: AffineTransform::identity(),
+ children: ids.clone(),
+ opacity: 1.0,
+ blend_mode: BlendMode::Normal,
+ };
+
+ repository.insert(Node::Group(root_group));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Test Scene".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec!["root".to_string()],
+ nodes: repository,
+ background_color: None,
+ }
+}
+
+fn bench_rectangles(c: &mut Criterion) {
+ let width = 1000;
+ let height = 1000;
+
+ let mut group = c.benchmark_group("rectangles");
+ group.sample_size(100);
+ group.measurement_time(std::time::Duration::from_secs(10));
+
+ // 1K rectangles
+ group.bench_function("1k_basic", |b| {
+ b.iter(|| {
+ let mut renderer = Renderer::new();
+ let surface_ptr = Renderer::init_raster(width, height);
+ renderer.set_backend(Backend::Raster(surface_ptr));
+
+ let scene = create_rectangles(black_box(1_000), false);
+
+ // Clear canvas
+ let surface = unsafe { &mut *surface_ptr };
+ let canvas = surface.canvas();
+ let mut paint = skia_safe::Paint::default();
+ paint.set_color(skia_safe::Color::WHITE);
+ canvas.draw_rect(
+ skia_safe::Rect::from_xywh(0.0, 0.0, width as f32, height as f32),
+ &paint,
+ );
+
+ renderer.load_scene(scene);
+ renderer.queue();
+ renderer.free();
+ })
+ });
+
+ // 10K rectangles
+ group.bench_function("10k_basic", |b| {
+ b.iter(|| {
+ let mut renderer = Renderer::new();
+
+ let surface_ptr = Renderer::init_raster(width, height);
+ renderer.set_backend(Backend::Raster(surface_ptr));
+
+ let scene = create_rectangles(black_box(10_000), false);
+
+ // Clear canvas
+ let surface = unsafe { &mut *surface_ptr };
+ let canvas = surface.canvas();
+ let mut paint = skia_safe::Paint::default();
+ paint.set_color(skia_safe::Color::WHITE);
+ canvas.draw_rect(
+ skia_safe::Rect::from_xywh(0.0, 0.0, width as f32, height as f32),
+ &paint,
+ );
+
+ renderer.load_scene(scene);
+ renderer.queue();
+ renderer.free();
+ })
+ });
+
+ group.bench_function("10k_with_effects", |b| {
+ b.iter(|| {
+ let mut renderer = Renderer::new();
+
+ let surface_ptr = Renderer::init_raster(width, height);
+ renderer.set_backend(Backend::Raster(surface_ptr));
+
+ let scene = create_rectangles(black_box(10_000), true);
+
+ // Clear canvas
+ let surface = unsafe { &mut *surface_ptr };
+ let canvas = surface.canvas();
+ let mut paint = skia_safe::Paint::default();
+ paint.set_color(skia_safe::Color::WHITE);
+ canvas.draw_rect(
+ skia_safe::Rect::from_xywh(0.0, 0.0, width as f32, height as f32),
+ &paint,
+ );
+
+ renderer.load_scene(scene);
+ renderer.queue();
+ renderer.free();
+ })
+ });
+
+ // 50K rectangles
+ group.bench_function("50k_basic", |b| {
+ b.iter(|| {
+ let mut renderer = Renderer::new();
+
+ let surface_ptr = Renderer::init_raster(width, height);
+ renderer.set_backend(Backend::Raster(surface_ptr));
+
+ let scene = create_rectangles(black_box(50_000), false);
+
+ // Clear canvas
+ let surface = unsafe { &mut *surface_ptr };
+ let canvas = surface.canvas();
+ let mut paint = skia_safe::Paint::default();
+ paint.set_color(skia_safe::Color::WHITE);
+ canvas.draw_rect(
+ skia_safe::Rect::from_xywh(0.0, 0.0, width as f32, height as f32),
+ &paint,
+ );
+
+ renderer.load_scene(scene);
+ renderer.queue();
+ renderer.free();
+ })
+ });
+
+ group.bench_function("50k_with_effects", |b| {
+ b.iter(|| {
+ let mut renderer = Renderer::new();
+
+ let surface_ptr = Renderer::init_raster(width, height);
+ renderer.set_backend(Backend::Raster(surface_ptr));
+
+ let scene = create_rectangles(black_box(50_000), true);
+
+ // Clear canvas
+ let surface = unsafe { &mut *surface_ptr };
+ let canvas = surface.canvas();
+ let mut paint = skia_safe::Paint::default();
+ paint.set_color(skia_safe::Color::WHITE);
+ canvas.draw_rect(
+ skia_safe::Rect::from_xywh(0.0, 0.0, width as f32, height as f32),
+ &paint,
+ );
+
+ renderer.load_scene(scene);
+ renderer.queue();
+ renderer.free();
+ })
+ });
+
+ group.finish();
+}
+
+criterion_group!(benches, bench_rectangles);
+criterion_main!(benches);
diff --git a/crates/grida-canvas/cover.png b/crates/grida-canvas/cover.png
new file mode 100644
index 0000000000..5e6146d33e
Binary files /dev/null and b/crates/grida-canvas/cover.png differ
diff --git a/crates/grida-canvas/examples/100k.rs b/crates/grida-canvas/examples/100k.rs
new file mode 100644
index 0000000000..31fdcbcb70
--- /dev/null
+++ b/crates/grida-canvas/examples/100k.rs
@@ -0,0 +1,104 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_n_shapes(n: usize) -> Scene {
+ let nf = NodeFactory::new();
+
+ let mut repository = NodeRepository::new();
+ let mut all_shape_ids = Vec::new();
+
+ // Grid parameters
+ let shape_size = 100.0; // Fixed size of 100x100 per shape
+ let spacing = 10.0; // Space between shapes
+
+ // Calculate grid dimensions to make it as square as possible
+ let grid_width = (n as f32).sqrt().ceil() as i32;
+ let grid_height = (n as f32 / grid_width as f32).ceil() as i32;
+
+ // Calculate starting position (top-left)
+ let start_x = 0.0;
+ let start_y = 0.0;
+
+ // Generate shapes in a grid pattern
+ for i in 0..n {
+ let row = (i as i32) / grid_width;
+ let col = (i as i32) % grid_width;
+
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Shape_{}", i);
+ rect.transform = AffineTransform::new(
+ start_x + (col as f32 * (shape_size + spacing)),
+ start_y + (row as f32 * (shape_size + spacing)),
+ 0.0,
+ );
+ rect.size = Size {
+ width: shape_size,
+ height: shape_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(10.0);
+
+ // Create rainbow effect from top-left to bottom-right
+ // Calculate diagonal position (0.0 to 1.0 across the diagonal)
+ let diagonal_progress = (row + col) as f32 / (grid_height + grid_width - 2) as f32;
+
+ // Convert to hue (0-360 degrees)
+ let hue = diagonal_progress * 360.0;
+
+ // Convert HSV to RGB
+ let (r, g, b) = hsv_to_rgb(hue, 1.0, 1.0);
+
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(r, g, b, 255),
+ opacity: 1.0,
+ });
+
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ Scene {
+ id: "scene".to_string(),
+ name: format!("{} Shapes Performance Test", n),
+ transform: AffineTransform::identity(),
+ children: all_shape_ids,
+ nodes: repository,
+ background_color: None,
+ }
+}
+
+// Helper function to convert HSV to RGB
+fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
+ let h = h % 360.0;
+ let c = v * s;
+ let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
+ let m = v - c;
+
+ let (r, g, b) = if h < 60.0 {
+ (c, x, 0.0)
+ } else if h < 120.0 {
+ (x, c, 0.0)
+ } else if h < 180.0 {
+ (0.0, c, x)
+ } else if h < 240.0 {
+ (0.0, x, c)
+ } else if h < 300.0 {
+ (x, 0.0, c)
+ } else {
+ (c, 0.0, x)
+ };
+
+ (
+ ((r + m) * 255.0) as u8,
+ ((g + m) * 255.0) as u8,
+ ((b + m) * 255.0) as u8,
+ )
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_n_shapes(100000).await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/basic.rs b/crates/grida-canvas/examples/basic.rs
new file mode 100644
index 0000000000..8a157193c6
--- /dev/null
+++ b/crates/grida-canvas/examples/basic.rs
@@ -0,0 +1,240 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_basic() -> Scene {
+ let font_caveat_family = "Caveat".to_string();
+
+ let nf = NodeFactory::new();
+
+ // Preload image before timing
+ let demo_image_id = "demo_image";
+
+ // Create a test image node with URL
+ let mut image_node = nf.create_image_node();
+ image_node.base.name = "Test Image".to_string();
+ image_node.transform = AffineTransform::new(50.0, 50.0, 0.0);
+ image_node.size = Size {
+ width: 200.0,
+ height: 200.0,
+ };
+ image_node.corner_radius = RectangularCornerRadius::all(20.0);
+ image_node.stroke_width = 2.0;
+ image_node.effect = Some(FilterEffect::DropShadow(FeDropShadow {
+ dx: 4.0,
+ dy: 4.0,
+ blur: 8.0,
+ color: Color(0, 0, 0, 77),
+ }));
+ image_node._ref = demo_image_id.to_string();
+
+ // Create a test rectangle node with linear gradient
+ let mut rect_node = nf.create_rectangle_node();
+ rect_node.base.name = "Test Rectangle".to_string();
+ rect_node.transform = AffineTransform::new(300.0, 50.0, 0.0);
+ rect_node.size = Size {
+ width: 200.0,
+ height: 100.0,
+ };
+ rect_node.corner_radius = RectangularCornerRadius::all(10.0);
+ rect_node.fill = Paint::Solid(SolidPaint {
+ color: Color(255, 0, 0, 255), // Red fill
+ opacity: 1.0,
+ });
+ rect_node.stroke_width = 2.0;
+ rect_node.effect = Some(FilterEffect::DropShadow(FeDropShadow {
+ dx: 4.0,
+ dy: 4.0,
+ blur: 8.0,
+ color: Color(0, 0, 0, 77),
+ }));
+
+ // Create a test ellipse node with radial gradient and a visible stroke
+ let mut ellipse_node = nf.create_ellipse_node();
+ ellipse_node.base.name = "Test Ellipse".to_string();
+ ellipse_node.blend_mode = BlendMode::Multiply;
+ ellipse_node.transform = AffineTransform::new(550.0, 50.0, 0.0);
+ ellipse_node.size = Size {
+ width: 200.0,
+ height: 200.0,
+ };
+ ellipse_node.fill = Paint::RadialGradient(RadialGradientPaint {
+ transform: AffineTransform::identity(),
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(0, 255, 0, 255), // Green
+ },
+ GradientStop {
+ offset: 0.5,
+ color: Color(255, 255, 0, 255), // Yellow
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(255, 0, 255, 255), // Magenta
+ },
+ ],
+ opacity: 1.0,
+ });
+ ellipse_node.stroke_width = 6.0;
+
+ // Create a test polygon node (pentagon)
+ let pentagon_points = (0..5)
+ .map(|i| {
+ let angle = std::f32::consts::PI * 2.0 * (i as f32) / 5.0 - std::f32::consts::FRAC_PI_2;
+ let radius = 100.0;
+ let x = radius * angle.cos();
+ let y = radius * angle.sin();
+ Point { x, y }
+ })
+ .collect::>();
+
+ let mut polygon_node = nf.create_polygon_node();
+ polygon_node.base.name = "Test Polygon".to_string();
+ polygon_node.blend_mode = BlendMode::Screen;
+ polygon_node.transform = AffineTransform::new(800.0, 50.0, 0.0);
+ polygon_node.points = pentagon_points;
+ polygon_node.fill = Paint::Solid(SolidPaint {
+ color: Color(255, 200, 0, 255), // Orange fill
+ opacity: 1.0,
+ });
+ polygon_node.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255), // Black stroke
+ opacity: 1.0,
+ });
+ polygon_node.stroke_width = 5.0;
+
+ // Create a test regular polygon node (hexagon)
+ let mut regular_polygon_node = nf.create_regular_polygon_node();
+ regular_polygon_node.base.name = "Test Regular Polygon".to_string();
+ regular_polygon_node.blend_mode = BlendMode::Overlay;
+ regular_polygon_node.transform = AffineTransform::new(50.0, 300.0, 0.0);
+ regular_polygon_node.size = Size {
+ width: 200.0,
+ height: 200.0,
+ };
+ regular_polygon_node.point_count = 6; // hexagon
+ regular_polygon_node.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 200, 255, 255), // Cyan fill
+ opacity: 1.0,
+ });
+ regular_polygon_node.stroke_width = 4.0;
+ regular_polygon_node.opacity = 0.5;
+
+ // Create a test text span node
+ let mut text_span_node = nf.create_text_span_node();
+ text_span_node.base.name = "Test Text".to_string();
+ text_span_node.transform = AffineTransform::new(300.0, 300.0, 0.0);
+ text_span_node.size = Size {
+ width: 300.0,
+ height: 200.0,
+ };
+ text_span_node.text = "Grida Canvas SKIA Bindings Backend".to_string();
+ text_span_node.text_style = TextStyle {
+ text_decoration: TextDecoration::LineThrough,
+ font_family: font_caveat_family.clone(),
+ font_size: 32.0,
+ font_weight: FontWeight::new(900),
+ italic: false,
+ letter_spacing: None,
+ line_height: None,
+ text_transform: TextTransform::None,
+ };
+ text_span_node.text_align = TextAlign::Center;
+ text_span_node.text_align_vertical = TextAlignVertical::Center;
+ text_span_node.stroke = Some(Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255), // Black stroke
+ opacity: 1.0,
+ }));
+ text_span_node.stroke_width = Some(4.0);
+
+ // Create a test path node
+ let mut path_node = nf.create_path_node();
+ path_node.base.name = "Test Path".to_string();
+ path_node.transform = AffineTransform::new(550.0, 300.0, 0.0);
+ path_node.data = "M50 150H0v-50h50v50ZM150 150h-50v-50h50v50ZM100 100H50V50h50v50ZM50 50H0V0h50v50ZM150 50h-50V0h50v50Z".to_string();
+ path_node.stroke = Paint::Solid(SolidPaint {
+ color: Color(255, 0, 0, 255), // Red stroke
+ opacity: 1.0,
+ });
+ path_node.stroke_width = 4.0;
+
+ // Create a test line node with solid color
+ let mut line_node = nf.create_line_node();
+ line_node.base.name = "Test Line".to_string();
+ line_node.opacity = 0.8;
+ line_node.transform = AffineTransform::new(800.0, 300.0, 0.0);
+ line_node.size = Size {
+ width: 200.0,
+ height: 0.0, // ignored
+ };
+ line_node.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 255, 0, 255), // Green color
+ opacity: 1.0,
+ });
+ line_node.stroke_width = 4.0;
+
+ // Create a group node for the shapes (rectangle, ellipse, polygon)
+ let mut shapes_group_node = nf.create_group_node();
+ shapes_group_node.base.name = "Shapes Group".to_string();
+ shapes_group_node.transform = AffineTransform::new(0.0, 0.0, 0.0);
+
+ // Create a root container node containing the shapes group, text, and line
+ let mut root_container_node = nf.create_container_node();
+ root_container_node.size = Size {
+ width: 1080.0,
+ height: 1080.0,
+ };
+ root_container_node.base.name = "Root Container".to_string();
+
+ // Create a node map and add all nodes
+ let mut repository = NodeRepository::new();
+
+ // First, collect all the IDs we'll need
+ let rect_id = rect_node.base.id.clone();
+ let ellipse_id = ellipse_node.base.id.clone();
+ let polygon_id = polygon_node.base.id.clone();
+ let regular_polygon_id = regular_polygon_node.base.id.clone();
+ let text_span_id = text_span_node.base.id.clone();
+ let line_id = line_node.base.id.clone();
+ let image_id = image_node.base.id.clone();
+ let path_id = path_node.base.id.clone();
+
+ // Now add all nodes to the map
+ repository.insert(Node::Rectangle(rect_node));
+ repository.insert(Node::Ellipse(ellipse_node));
+ repository.insert(Node::Polygon(polygon_node));
+ repository.insert(Node::RegularPolygon(regular_polygon_node));
+ repository.insert(Node::TextSpan(text_span_node));
+ repository.insert(Node::Line(line_node));
+ repository.insert(Node::Image(image_node));
+ repository.insert(Node::Path(path_node));
+
+ // Now set up the shapes group with the IDs we collected
+ shapes_group_node.children = vec![rect_id, ellipse_id, polygon_id, regular_polygon_id];
+ let shapes_group_id = shapes_group_node.base.id.clone();
+ repository.insert(Node::Group(shapes_group_node));
+
+ // Finally set up the root container with all IDs
+ root_container_node.children = vec![shapes_group_id, text_span_id, line_id, path_id, image_id];
+ let root_container_id = root_container_node.base.id.clone();
+ repository.insert(Node::Container(root_container_node));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_container_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_basic().await;
+
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/booleans.rs b/crates/grida-canvas/examples/booleans.rs
new file mode 100644
index 0000000000..33a578f369
--- /dev/null
+++ b/crates/grida-canvas/examples/booleans.rs
@@ -0,0 +1,375 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_booleans() -> Scene {
+ let nf = NodeFactory::new();
+ let mut repository = NodeRepository::new();
+
+ // Create a root container node
+ let mut root_container_node = nf.create_container_node();
+ root_container_node.base.name = "Root Container".to_string();
+ root_container_node.size = Size {
+ width: 1080.0,
+ height: 1080.0,
+ };
+
+ let mut all_shape_ids = Vec::new();
+ let spacing = 200.0;
+ let start_x = 100.0;
+ let base_size = 100.0;
+
+ // Example 1: Rectangle and Circle Union
+ {
+ let y_offset = 150.0; // Increased from 100.0 to give more room at top
+
+ // Create shapes
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = "Rectangle".to_string();
+ rect.transform = AffineTransform::new(start_x, y_offset, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255),
+ opacity: 1.0,
+ });
+
+ let mut circle = nf.create_ellipse_node();
+ circle.base.name = "Circle".to_string();
+ circle.transform = AffineTransform::new(start_x + spacing, y_offset, 0.0);
+ circle.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ circle.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255),
+ opacity: 1.0,
+ });
+
+ // Add description text
+ let mut text = nf.create_text_span_node();
+ text.base.name = "Description".to_string();
+ text.transform = AffineTransform::new(start_x, y_offset - 40.0, 0.0); // Moved text up slightly
+ text.size = Size {
+ width: 500.0, // Increased width for better text display
+ height: 20.0,
+ };
+ text.text = "Union (A ∪ B): Combines two shapes into one".to_string();
+ text.text_style.font_size = 16.0;
+ text.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+
+ // Create boolean operation
+ let mut bool_node = BooleanPathOperationNode {
+ base: BaseNode {
+ id: "bool_union_1".to_string(),
+ name: "Union Operation".to_string(),
+ active: true,
+ },
+ transform: AffineTransform::new(start_x + spacing * 2.0, y_offset, 0.0),
+ op: BooleanPathOperation::Union,
+ children: vec![rect.base.id.clone(), circle.base.id.clone()],
+ fill: Paint::Solid(SolidPaint {
+ color: Color(100, 100, 200, 255),
+ opacity: 1.0,
+ }),
+ stroke: Some(Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ })),
+ stroke_width: 2.0,
+ stroke_align: StrokeAlign::Center,
+ stroke_dash_array: None,
+ opacity: 1.0,
+ blend_mode: BlendMode::Normal,
+ effect: None,
+ };
+
+ // Collect IDs before moving nodes
+ all_shape_ids.push(rect.base.id.clone());
+ all_shape_ids.push(circle.base.id.clone());
+ all_shape_ids.push(text.base.id.clone());
+ all_shape_ids.push(bool_node.base.id.clone());
+
+ // Insert all nodes
+ repository.insert(Node::Rectangle(rect));
+ repository.insert(Node::Ellipse(circle));
+ repository.insert(Node::TextSpan(text));
+ repository.insert(Node::BooleanOperation(bool_node));
+ }
+
+ // Example 2: Two Circles Intersection
+ {
+ let y_offset = 400.0; // Increased from 300.0
+
+ // Create shapes
+ let mut circle1 = nf.create_ellipse_node();
+ circle1.base.name = "Circle 1".to_string();
+ circle1.transform = AffineTransform::new(start_x, y_offset, 0.0);
+ circle1.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ circle1.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255),
+ opacity: 1.0,
+ });
+
+ let mut circle2 = nf.create_ellipse_node();
+ circle2.base.name = "Circle 2".to_string();
+ circle2.transform = AffineTransform::new(start_x + 100.0, y_offset, 0.0);
+ circle2.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ circle2.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255),
+ opacity: 1.0,
+ });
+
+ // Add description text
+ let mut text = nf.create_text_span_node();
+ text.base.name = "Description".to_string();
+ text.transform = AffineTransform::new(start_x, y_offset - 40.0, 0.0);
+ text.size = Size {
+ width: 500.0,
+ height: 20.0,
+ };
+ text.text = "Intersection (A ∩ B): Shows only the overlapping area".to_string();
+ text.text_style.font_size = 16.0;
+ text.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+
+ // Create boolean operation
+ let mut bool_node = BooleanPathOperationNode {
+ base: BaseNode {
+ id: "bool_intersection_1".to_string(),
+ name: "Intersection Operation".to_string(),
+ active: true,
+ },
+ transform: AffineTransform::new(start_x + spacing * 2.0, y_offset, 0.0),
+ op: BooleanPathOperation::Intersection,
+ children: vec![circle1.base.id.clone(), circle2.base.id.clone()],
+ fill: Paint::Solid(SolidPaint {
+ color: Color(100, 100, 200, 255),
+ opacity: 1.0,
+ }),
+ stroke: Some(Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ })),
+ stroke_width: 2.0,
+ stroke_align: StrokeAlign::Center,
+ stroke_dash_array: None,
+ opacity: 1.0,
+ blend_mode: BlendMode::Normal,
+ effect: None,
+ };
+
+ // Collect IDs before moving nodes
+ all_shape_ids.push(circle1.base.id.clone());
+ all_shape_ids.push(circle2.base.id.clone());
+ all_shape_ids.push(text.base.id.clone());
+ all_shape_ids.push(bool_node.base.id.clone());
+
+ // Insert all nodes
+ repository.insert(Node::Ellipse(circle1));
+ repository.insert(Node::Ellipse(circle2));
+ repository.insert(Node::TextSpan(text));
+ repository.insert(Node::BooleanOperation(bool_node));
+ }
+
+ // Example 3: Star and Rectangle Difference
+ {
+ let y_offset = 650.0; // Increased from 500.0
+
+ // Create shapes
+ let mut star = nf.create_regular_star_polygon_node();
+ star.base.name = "Star".to_string();
+ star.transform = AffineTransform::new(start_x, y_offset, 0.0);
+ star.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ star.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255),
+ opacity: 1.0,
+ });
+
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = "Rectangle".to_string();
+ rect.transform = AffineTransform::new(start_x + spacing * 0.5, y_offset, 0.0);
+ rect.size = Size {
+ width: base_size * 0.8,
+ height: base_size * 0.8,
+ };
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255),
+ opacity: 1.0,
+ });
+
+ // Add description text
+ let mut text = nf.create_text_span_node();
+ text.base.name = "Description".to_string();
+ text.transform = AffineTransform::new(start_x, y_offset - 40.0, 0.0);
+ text.size = Size {
+ width: 500.0,
+ height: 20.0,
+ };
+ text.text = "Difference (A - B): Removes the second shape from the first".to_string();
+ text.text_style.font_size = 16.0;
+ text.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+
+ // Create boolean operation
+ let mut bool_node = BooleanPathOperationNode {
+ base: BaseNode {
+ id: "bool_difference_1".to_string(),
+ name: "Difference Operation".to_string(),
+ active: true,
+ },
+ transform: AffineTransform::new(start_x + spacing * 2.0, y_offset, 0.0),
+ op: BooleanPathOperation::Difference,
+ children: vec![star.base.id.clone(), rect.base.id.clone()],
+ fill: Paint::Solid(SolidPaint {
+ color: Color(100, 100, 200, 255),
+ opacity: 1.0,
+ }),
+ stroke: Some(Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ })),
+ stroke_width: 2.0,
+ stroke_align: StrokeAlign::Center,
+ stroke_dash_array: None,
+ opacity: 1.0,
+ blend_mode: BlendMode::Normal,
+ effect: None,
+ };
+
+ // Collect IDs before moving nodes
+ all_shape_ids.push(star.base.id.clone());
+ all_shape_ids.push(rect.base.id.clone());
+ all_shape_ids.push(text.base.id.clone());
+ all_shape_ids.push(bool_node.base.id.clone());
+
+ // Insert all nodes
+ repository.insert(Node::RegularStarPolygon(star));
+ repository.insert(Node::Rectangle(rect));
+ repository.insert(Node::TextSpan(text));
+ repository.insert(Node::BooleanOperation(bool_node));
+ }
+
+ // Example 4: Two Squares XOR
+ {
+ let y_offset = 900.0; // Increased from 700.0
+
+ // Create shapes
+ let mut square1 = nf.create_rectangle_node();
+ square1.base.name = "Square 1".to_string();
+ square1.transform = AffineTransform::new(start_x, y_offset, 0.0);
+ square1.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ square1.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255),
+ opacity: 1.0,
+ });
+
+ let mut square2 = nf.create_rectangle_node();
+ square2.base.name = "Square 2".to_string();
+ square2.transform = AffineTransform::new(start_x + spacing * 0.5, y_offset, 0.0);
+ square2.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ square2.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255),
+ opacity: 1.0,
+ });
+
+ // Add description text
+ let mut text = nf.create_text_span_node();
+ text.base.name = "Description".to_string();
+ text.transform = AffineTransform::new(start_x, y_offset - 40.0, 0.0);
+ text.size = Size {
+ width: 500.0,
+ height: 20.0,
+ };
+ text.text = "XOR (A ⊕ B): Shows areas that don't overlap".to_string();
+ text.text_style.font_size = 16.0;
+ text.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+
+ // Create boolean operation
+ let mut bool_node = BooleanPathOperationNode {
+ base: BaseNode {
+ id: "bool_xor_1".to_string(),
+ name: "XOR Operation".to_string(),
+ active: true,
+ },
+ transform: AffineTransform::new(start_x + spacing * 2.0, y_offset, 0.0),
+ op: BooleanPathOperation::Xor,
+ children: vec![square1.base.id.clone(), square2.base.id.clone()],
+ fill: Paint::Solid(SolidPaint {
+ color: Color(100, 100, 200, 255),
+ opacity: 1.0,
+ }),
+ stroke: Some(Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ })),
+ stroke_width: 2.0,
+ stroke_align: StrokeAlign::Center,
+ stroke_dash_array: None,
+ opacity: 1.0,
+ blend_mode: BlendMode::Normal,
+ effect: None,
+ };
+
+ // Collect IDs before moving nodes
+ all_shape_ids.push(square1.base.id.clone());
+ all_shape_ids.push(square2.base.id.clone());
+ all_shape_ids.push(text.base.id.clone());
+ all_shape_ids.push(bool_node.base.id.clone());
+
+ // Insert all nodes
+ repository.insert(Node::Rectangle(square1));
+ repository.insert(Node::Rectangle(square2));
+ repository.insert(Node::TextSpan(text));
+ repository.insert(Node::BooleanOperation(bool_node));
+ }
+
+ // Set up the root container
+ root_container_node.children.extend(all_shape_ids);
+ let root_container_id = root_container_node.base.id.clone();
+ repository.insert(Node::Container(root_container_node));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Boolean Operations Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_container_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_booleans().await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/cache_image.rs b/crates/grida-canvas/examples/cache_image.rs
new file mode 100644
index 0000000000..e77132427d
--- /dev/null
+++ b/crates/grida-canvas/examples/cache_image.rs
@@ -0,0 +1,314 @@
+//! This example demonstrates the performance benefits of image caching in Skia across different
+//! device pixel ratios (DPR) and canvas sizes. It benchmarks the following scenarios:
+//!
+//! 1. Drawing Performance:
+//! - Compares cached vs uncached drawing performance
+//! - Measures performance across various device sizes and DPRs
+//! - Shows how caching affects rendering speed
+//!
+//! 2. DPR Handling:
+//! - Demonstrates proper handling of device pixel ratios
+//! - Creates surfaces at logical size and applies DPR scaling during drawing
+//! - Shows how caching works with high-DPI displays
+//!
+//! 3. Device Coverage:
+//! - Tests various device sizes from mobile to desktop
+//! - Includes different DPR values (1.0, 2.0, 3.0)
+//! - Covers common display resolutions and device types
+//!
+//! The benchmark draws a complex pattern of circles and measures:
+//! - Average, minimum, and maximum drawing times
+//! - Performance improvement ratio between cached and uncached drawing
+//! - Impact of DPR scaling on performance
+//!
+//! This is particularly useful for understanding:
+//! - When to use image caching
+//! - How DPR affects rendering performance
+//! - Performance characteristics across different device types
+
+use skia_safe::*;
+use std::time::{Duration, Instant};
+
+struct CanvasSpec {
+ name: String,
+ logical: (i32, i32),
+ dpr: f32,
+ physical: (i32, i32),
+}
+
+fn get_canvas_specs() -> Vec {
+ vec![
+ CanvasSpec {
+ name: "iPhone SE".to_string(),
+ logical: (375, 667),
+ dpr: 2.0,
+ physical: (750, 1334),
+ },
+ CanvasSpec {
+ name: "iPhone 14 Pro".to_string(),
+ logical: (430, 932),
+ dpr: 3.0,
+ physical: (1290, 2796),
+ },
+ CanvasSpec {
+ name: "iPad Pro 11\"".to_string(),
+ logical: (834, 1194),
+ dpr: 2.0,
+ physical: (1668, 2388),
+ },
+ CanvasSpec {
+ name: "MacBook Air 13\"".to_string(),
+ logical: (1440, 900),
+ dpr: 2.0,
+ physical: (2880, 1800),
+ },
+ CanvasSpec {
+ name: "MacBook Pro 16\"".to_string(),
+ logical: (1728, 1117),
+ dpr: 2.0,
+ physical: (3456, 2234),
+ },
+ CanvasSpec {
+ name: "Studio Display".to_string(),
+ logical: (2560, 1440),
+ dpr: 2.0,
+ physical: (5120, 2880),
+ },
+ CanvasSpec {
+ name: "4K Monitor".to_string(),
+ logical: (1920, 1080),
+ dpr: 2.0,
+ physical: (3840, 2160),
+ },
+ CanvasSpec {
+ name: "5K Monitor".to_string(),
+ logical: (2560, 1440),
+ dpr: 2.0,
+ physical: (5120, 2880),
+ },
+ CanvasSpec {
+ name: "1080p Display".to_string(),
+ logical: (1920, 1080),
+ dpr: 1.0,
+ physical: (1920, 1080),
+ },
+ CanvasSpec {
+ name: "Small Canvas".to_string(),
+ logical: (512, 512),
+ dpr: 1.0,
+ physical: (512, 512),
+ },
+ ]
+}
+
+fn draw_complex(canvas: &Canvas) {
+ let width = canvas.image_info().width() as f32;
+ let height = canvas.image_info().height() as f32;
+
+ // Calculate grid size based on canvas dimensions
+ let grid_size = (width * height).sqrt() / 20.0; // Adjust divisor to control density
+ let cols = (width / grid_size).ceil() as i32;
+ let rows = (height / grid_size).ceil() as i32;
+ let circle_radius = grid_size * 0.4; // Circle size relative to grid cell
+
+ // Create a gradient shader
+ let colors = [
+ Color::from_argb(0xFF, 0xFF, 0x00, 0x00), // Red
+ Color::from_argb(0xFF, 0x00, 0xFF, 0x00), // Green
+ Color::from_argb(0xFF, 0x00, 0x00, 0xFF), // Blue
+ ];
+ let positions = [0.0, 0.5, 1.0];
+ let shader = Shader::linear_gradient(
+ (Point::new(0.0, 0.0), Point::new(width, height)),
+ &colors[..],
+ Some(&positions[..]),
+ TileMode::Clamp,
+ None,
+ None,
+ )
+ .unwrap();
+
+ // Create multiple layers with different effects
+ for layer in 0..3 {
+ let mut paint = Paint::default();
+ paint.set_anti_alias(true);
+ paint.set_shader(shader.clone());
+
+ // Apply different blend modes for each layer
+ let blend_mode = match layer {
+ 0 => BlendMode::Multiply,
+ 1 => BlendMode::Screen,
+ 2 => BlendMode::Overlay,
+ _ => unreachable!(),
+ };
+ paint.set_blend_mode(blend_mode);
+
+ // Draw circles with blur effect
+ for row in 0..rows {
+ for col in 0..cols {
+ let x = col as f32 * grid_size + grid_size / 2.0;
+ let y = row as f32 * grid_size + grid_size / 2.0;
+
+ // Apply different blur radius for each layer
+ let blur_radius = match layer {
+ 0 => grid_size * 0.1, // Scale blur with grid size
+ 1 => grid_size * 0.15,
+ 2 => grid_size * 0.2,
+ _ => unreachable!(),
+ };
+
+ // Create a temporary surface for the blur effect
+ let temp_size = (circle_radius * 2.0) as i32;
+ let mut temp_surface =
+ Surface::new_raster_n32_premul((temp_size, temp_size)).unwrap();
+ let temp_canvas = temp_surface.canvas();
+
+ // Draw the circle on the temporary surface
+ temp_canvas.draw_circle(
+ (temp_size as f32 / 2.0, temp_size as f32 / 2.0),
+ circle_radius,
+ &paint,
+ );
+
+ // Apply blur to the temporary surface
+ let image = temp_surface.image_snapshot();
+ let mut blur_paint = Paint::default();
+ blur_paint.set_image_filter(image_filters::blur(
+ (blur_radius, blur_radius),
+ None,
+ None,
+ None,
+ ));
+
+ // Draw the blurred circle
+ canvas.draw_image(
+ image,
+ (x - circle_radius, y - circle_radius),
+ Some(&blur_paint),
+ );
+ }
+ }
+ }
+
+ // Add a final overlay with a radial gradient
+ let mut overlay_paint = Paint::default();
+ let radial_colors = [
+ Color::from_argb(0x40, 0xFF, 0xFF, 0xFF), // Semi-transparent white
+ Color::from_argb(0x00, 0xFF, 0xFF, 0xFF), // Transparent
+ ];
+ let radial_positions = [0.0, 1.0];
+ let radial_shader = Shader::radial_gradient(
+ Point::new(width / 2.0, height / 2.0),
+ width.min(height) / 2.0,
+ &radial_colors[..],
+ Some(&radial_positions[..]),
+ TileMode::Clamp,
+ None,
+ None,
+ )
+ .unwrap();
+ overlay_paint.set_shader(radial_shader);
+ overlay_paint.set_blend_mode(BlendMode::SoftLight);
+ canvas.draw_rect(Rect::new(0.0, 0.0, width, height), &overlay_paint);
+}
+
+fn make_cached_image(surface: &mut Surface, dpr: f32) -> Image {
+ let canvas = surface.canvas();
+ canvas.clear(Color::WHITE);
+
+ // Apply DPR scaling
+ canvas.save();
+ canvas.scale((dpr, dpr));
+ draw_complex(canvas);
+ canvas.restore();
+
+ // Create a new surface with the same size for the cached image
+ let mut cached_surface =
+ Surface::new_raster_n32_premul((surface.width(), surface.height())).unwrap();
+ let cached_canvas = cached_surface.canvas();
+ cached_canvas.draw_image(surface.image_snapshot(), (0, 0), None);
+ cached_surface.image_snapshot()
+}
+
+fn run_benchmark(use_cache: bool, surface: &mut Surface, cached: &Image, dpr: f32) -> Duration {
+ let canvas = surface.canvas();
+ canvas.clear(Color::WHITE);
+
+ let start = Instant::now();
+ if use_cache {
+ let mut paint = Paint::default();
+ canvas.draw_image(cached, (0, 0), Some(&paint));
+ } else {
+ // Apply DPR scaling for uncached drawing
+ canvas.save();
+ canvas.scale((dpr, dpr));
+ draw_complex(canvas);
+ canvas.restore();
+ }
+ start.elapsed()
+}
+
+fn run_benchmark_for_size(spec: &CanvasSpec) {
+ println!("\nRunning benchmark for {}:", spec.name);
+ println!("Logical size: {}x{}", spec.logical.0, spec.logical.1);
+ println!("Physical size: {}x{}", spec.physical.0, spec.physical.1);
+ println!("DPR: {}", spec.dpr);
+
+ // Create surface at logical size
+ let mut surface = Surface::new_raster_n32_premul(spec.logical).unwrap();
+ let cached = make_cached_image(&mut surface, spec.dpr);
+
+ const FRAMES: usize = 100;
+ let mut cached_times = Vec::with_capacity(FRAMES);
+ let mut uncached_times = Vec::with_capacity(FRAMES);
+
+ // Warm up
+ for _ in 0..10 {
+ run_benchmark(true, &mut surface, &cached, spec.dpr);
+ run_benchmark(false, &mut surface, &cached, spec.dpr);
+ }
+
+ // Collect measurements
+ for _ in 0..FRAMES {
+ cached_times.push(run_benchmark(true, &mut surface, &cached, spec.dpr));
+ uncached_times.push(run_benchmark(false, &mut surface, &cached, spec.dpr));
+ }
+
+ // Calculate statistics
+ let cached_avg: Duration = cached_times.iter().sum::() / FRAMES as u32;
+ let uncached_avg: Duration = uncached_times.iter().sum::() / FRAMES as u32;
+
+ let cached_min = cached_times.iter().min().unwrap();
+ let cached_max = cached_times.iter().max().unwrap();
+ let uncached_min = uncached_times.iter().min().unwrap();
+ let uncached_max = uncached_times.iter().max().unwrap();
+
+ println!("\nPerformance Results:");
+ println!("-------------------");
+ println!("Cached drawing:");
+ println!(" Average: {:?}", cached_avg);
+ println!(" Min: {:?}", cached_min);
+ println!(" Max: {:?}", cached_max);
+ println!("\nUncached drawing:");
+ println!(" Average: {:?}", uncached_avg);
+ println!(" Min: {:?}", uncached_min);
+ println!(" Max: {:?}", uncached_max);
+ println!("\nPerformance improvement:");
+ println!(
+ " Speedup: {:.2}x",
+ uncached_avg.as_secs_f64() / cached_avg.as_secs_f64()
+ );
+}
+
+fn main() {
+ let specs = get_canvas_specs();
+
+ println!(
+ "Starting benchmarks for {} different canvas sizes...",
+ specs.len()
+ );
+
+ for spec in specs {
+ run_benchmark_for_size(&spec);
+ }
+}
diff --git a/crates/grida-canvas/examples/cache_picture.rs b/crates/grida-canvas/examples/cache_picture.rs
new file mode 100644
index 0000000000..7b4dd76cce
--- /dev/null
+++ b/crates/grida-canvas/examples/cache_picture.rs
@@ -0,0 +1,338 @@
+use cg::window::scheduler::FrameScheduler;
+use gl_rs as gl;
+use glutin::{
+ config::{ConfigTemplateBuilder, GlConfig},
+ context::{ContextApi, ContextAttributesBuilder, PossiblyCurrentContext},
+ display::{GetGlDisplay, GlDisplay},
+ prelude::{GlSurface, NotCurrentGlContext},
+ surface::{Surface as GlutinSurface, SurfaceAttributesBuilder, SwapInterval, WindowSurface},
+};
+use glutin_winit::DisplayBuilder;
+use raw_window_handle::HasRawWindowHandle;
+use skia_safe::{Canvas, Color, Paint, Picture, PictureRecorder, Rect, Surface};
+use std::{ffi::CString, num::NonZeroU32, time::Instant};
+use winit::{
+ event::{Event, WindowEvent},
+ event_loop::EventLoop,
+};
+
+struct CachedScene {
+ picture: Picture,
+}
+
+impl CachedScene {
+ fn new(width: f32, height: f32) -> Self {
+ // Create a recorder to capture the static scene
+ let mut recorder = PictureRecorder::new();
+ let bounds = Rect::new(0.0, 0.0, width, height);
+ let canvas = recorder.begin_recording(bounds, None);
+
+ // Draw some static content
+ Self::draw_static_content(canvas);
+
+ // End recording and create the picture
+ let picture = recorder.finish_recording_as_picture(None).unwrap();
+
+ Self { picture }
+ }
+
+ fn draw_static_content(canvas: &Canvas) {
+ println!("[cache] Drawing static content (should only appear once)");
+ // Draw a grid of rectangles
+ for i in 0..10 {
+ for j in 0..10 {
+ let mut paint = Paint::default();
+ paint.set_color(Color::from_argb(255, 100, 100, 100));
+
+ let x = i as f32 * 100.0;
+ let y = j as f32 * 100.0;
+ let rect = Rect::new(x, y, x + 50.0, y + 50.0);
+ canvas.draw_rect(rect, &paint);
+ }
+ }
+ }
+
+ fn render(&self, canvas: &Canvas, camera_x: f32, camera_y: f32, zoom: f32) {
+ canvas.save();
+
+ // Apply camera transform
+ canvas.translate((camera_x, camera_y));
+ canvas.scale((zoom, zoom));
+
+ // Draw the cached picture
+ canvas.draw_picture(&self.picture, None, None);
+
+ canvas.restore();
+ }
+}
+
+fn init_window(
+ width: i32,
+ height: i32,
+) -> (
+ *mut Surface,
+ EventLoop<()>,
+ winit::window::Window,
+ GlutinSurface,
+ PossiblyCurrentContext,
+ glutin::config::Config,
+ skia_safe::gpu::gl::FramebufferInfo,
+ skia_safe::gpu::DirectContext,
+ f64,
+) {
+ let el = EventLoop::new().expect("Failed to create event loop");
+ let window_attributes = winit::window::WindowAttributes::default()
+ .with_title("Skia Picture Cache Demo")
+ .with_inner_size(winit::dpi::LogicalSize::new(width, height));
+ let template = ConfigTemplateBuilder::new()
+ .with_alpha_size(8)
+ .with_transparency(true);
+ let display_builder = DisplayBuilder::new().with_window_attributes(window_attributes.into());
+ let (window, gl_config) = display_builder
+ .build(&el, template, |configs| {
+ configs
+ .reduce(|accum, config| {
+ let transparency_check = config.supports_transparency().unwrap_or(false)
+ & !accum.supports_transparency().unwrap_or(false);
+ if transparency_check || config.num_samples() < accum.num_samples() {
+ config
+ } else {
+ accum
+ }
+ })
+ .unwrap()
+ })
+ .unwrap();
+ let window = window.expect("Could not create window with OpenGL context");
+ let raw_window_handle = window
+ .raw_window_handle()
+ .expect("Failed to get window handle");
+ let scale_factor = window.scale_factor();
+ let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle));
+ let fallback_context_attributes = ContextAttributesBuilder::new()
+ .with_context_api(ContextApi::Gles(None))
+ .build(Some(raw_window_handle));
+ let not_current_gl_context = unsafe {
+ gl_config
+ .display()
+ .create_context(&gl_config, &context_attributes)
+ .unwrap_or_else(|_| {
+ gl_config
+ .display()
+ .create_context(&gl_config, &fallback_context_attributes)
+ .expect("failed to create context")
+ })
+ };
+ let (width, height): (u32, u32) = window.inner_size().into();
+ let attrs = SurfaceAttributesBuilder::::new().build(
+ raw_window_handle,
+ NonZeroU32::new(width).unwrap(),
+ NonZeroU32::new(height).unwrap(),
+ );
+ let gl_surface = unsafe {
+ gl_config
+ .display()
+ .create_window_surface(&gl_config, &attrs)
+ .expect("Could not create gl window surface")
+ };
+ let gl_context = not_current_gl_context
+ .make_current(&gl_surface)
+ .expect("Could not make GL context current");
+
+ gl::load_with(|s| {
+ gl_config
+ .display()
+ .get_proc_address(CString::new(s).unwrap().as_c_str())
+ });
+
+ // Enable VSync
+ gl_surface
+ .set_swap_interval(&gl_context, SwapInterval::Wait(NonZeroU32::new(1).unwrap()))
+ .expect("Failed to set swap interval");
+
+ let interface = skia_safe::gpu::gl::Interface::new_load_with(|name| {
+ if name == "eglGetCurrentDisplay" {
+ return std::ptr::null();
+ }
+ gl_config
+ .display()
+ .get_proc_address(CString::new(name).unwrap().as_c_str())
+ })
+ .expect("Could not create interface");
+ let mut gr_context = skia_safe::gpu::direct_contexts::make_gl(interface, None)
+ .expect("Could not create direct context");
+ let fb_info = {
+ let mut fboid: gl::types::GLint = 0;
+ unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
+ skia_safe::gpu::gl::FramebufferInfo {
+ fboid: fboid.try_into().unwrap(),
+ format: skia_safe::gpu::gl::Format::RGBA8.into(),
+ ..Default::default()
+ }
+ };
+ let backend_render_target = skia_safe::gpu::backend_render_targets::make_gl(
+ (width as i32, height as i32),
+ gl_config.num_samples() as usize,
+ gl_config.stencil_size() as usize,
+ fb_info,
+ );
+ let surface = skia_safe::gpu::surfaces::wrap_backend_render_target(
+ &mut gr_context,
+ &backend_render_target,
+ skia_safe::gpu::SurfaceOrigin::BottomLeft,
+ skia_safe::ColorType::RGBA8888,
+ None,
+ None,
+ )
+ .expect("Could not create skia surface");
+ (
+ Box::into_raw(Box::new(surface)),
+ el,
+ window,
+ gl_surface,
+ gl_context,
+ gl_config,
+ fb_info,
+ gr_context,
+ scale_factor,
+ )
+}
+
+fn main() {
+ let (
+ surface_ptr,
+ el,
+ window,
+ mut gl_surface,
+ gl_context,
+ gl_config,
+ fb_info,
+ mut gr_context,
+ scale_factor,
+ ) = init_window(800, 600);
+
+ // SAFETY: We own the surface pointer
+ let surface = unsafe { &mut *surface_ptr };
+ let size = window.inner_size();
+ let scene = CachedScene::new(size.width as f32, size.height as f32);
+ let mut camera_x = 0.0;
+ let mut camera_y = 0.0;
+ let mut zoom = 1.0;
+ let start_time = Instant::now();
+ let mut frame_count = 0;
+ let mut last_fps_time = Instant::now();
+ let mut last_frame_time = Instant::now();
+
+ // Create frame scheduler with 120 FPS target and 144 FPS max
+ let mut scheduler = FrameScheduler::new(120).with_max_fps(144);
+
+ // Enable pre-present notification for better frame timing
+ window.pre_present_notify();
+
+ el.run(move |event, elwt| {
+ match event {
+ Event::WindowEvent {
+ event: WindowEvent::CloseRequested,
+ ..
+ } => {
+ elwt.exit();
+ }
+ Event::WindowEvent {
+ event: WindowEvent::Resized(size),
+ ..
+ } => {
+ // Recreate GL surface and Skia surface on resize
+ let attrs = SurfaceAttributesBuilder::::new().build(
+ window
+ .raw_window_handle()
+ .expect("Failed to get window handle"),
+ NonZeroU32::new(size.width).unwrap(),
+ NonZeroU32::new(size.height).unwrap(),
+ );
+ gl_surface = unsafe {
+ gl_config
+ .display()
+ .create_window_surface(&gl_config, &attrs)
+ .expect("Could not create gl window surface")
+ };
+ let backend_render_target = skia_safe::gpu::backend_render_targets::make_gl(
+ (size.width as i32, size.height as i32),
+ gl_config.num_samples() as usize,
+ gl_config.stencil_size() as usize,
+ fb_info,
+ );
+ let new_surface = skia_safe::gpu::surfaces::wrap_backend_render_target(
+ &mut gr_context,
+ &backend_render_target,
+ skia_safe::gpu::SurfaceOrigin::BottomLeft,
+ skia_safe::ColorType::RGBA8888,
+ None,
+ None,
+ )
+ .expect("Could not create skia surface");
+ unsafe { _ = Box::from_raw(surface_ptr) };
+ let new_surface_ptr = Box::into_raw(Box::new(new_surface));
+ // SAFETY: update surface pointer
+ let surface = unsafe { &mut *new_surface_ptr };
+ // Optionally, re-record the scene if you want to match new size
+ // scene = CachedScene::new(size.width as f32, size.height as f32);
+ }
+ Event::WindowEvent {
+ event: WindowEvent::RedrawRequested,
+ ..
+ } => {
+ let now = Instant::now();
+ let elapsed = now.duration_since(start_time).as_secs_f32();
+ let angle = elapsed * 2.0;
+ camera_x = angle.cos() * 100.0;
+ camera_y = angle.sin() * 100.0;
+
+ // Add zoom animation
+ let zoom_angle = elapsed * 1.0; // Slower zoom cycle
+ zoom = 1.0 + zoom_angle.sin() * 0.5; // Oscillate between 0.5 and 1.5
+
+ // Clear and render
+ let canvas = surface.canvas();
+ canvas.clear(Color::from_argb(255, 255, 255, 255));
+ scene.render(canvas, camera_x, camera_y, zoom);
+
+ // Flush GPU commands
+ if let Some(mut gr_ctx) = surface.recording_context() {
+ if let Some(mut direct_ctx) = gr_ctx.as_direct_context() {
+ direct_ctx.flush_and_submit();
+ // Wait for GPU to finish using flush_and_submit
+ direct_ctx.flush_and_submit();
+ }
+ }
+
+ // Swap buffers
+ if let Err(e) = gl_surface.swap_buffers(&gl_context) {
+ eprintln!("Error swapping buffers: {:?}", e);
+ }
+
+ // Frame timing and pacing
+ let frame_time = now.duration_since(last_frame_time);
+ scheduler.sleep_to_maintain_fps();
+ last_frame_time = now;
+
+ // FPS calculation
+ frame_count += 1;
+ if now.duration_since(last_fps_time).as_secs_f32() >= 1.0 {
+ println!(
+ "FPS: {} (Target: {:.1}, Frame Time: {:.2}ms)",
+ frame_count,
+ scheduler.average_fps(),
+ frame_time.as_secs_f32() * 1000.0
+ );
+ frame_count = 0;
+ last_fps_time = now;
+ }
+
+ // Request next frame
+ window.request_redraw();
+ }
+ _ => {}
+ }
+ })
+ .unwrap();
+}
diff --git a/crates/grida-canvas/examples/cache_text_picture.rs b/crates/grida-canvas/examples/cache_text_picture.rs
new file mode 100644
index 0000000000..c540f077d3
--- /dev/null
+++ b/crates/grida-canvas/examples/cache_text_picture.rs
@@ -0,0 +1,124 @@
+use skia_safe::textlayout::{
+ FontCollection, Paragraph, ParagraphBuilder, ParagraphStyle, TextStyle,
+};
+use skia_safe::{Color, Picture, PictureRecorder, Point, Rect, Surface};
+use std::time::{Duration, Instant};
+
+fn make_paragraph(fc: &FontCollection, text: &str, width: f32, font_size: f32) -> Paragraph {
+ let mut ps = ParagraphStyle::new();
+ ps.set_text_align(skia_safe::textlayout::TextAlign::Left);
+ let mut builder = ParagraphBuilder::new(&ps, fc);
+ let mut ts = TextStyle::new();
+ ts.set_font_size(font_size);
+ builder.push_style(&ts);
+ builder.add_text(text);
+ let mut paragraph = builder.build();
+ paragraph.layout(width);
+ paragraph
+}
+
+fn make_picture(fc: &FontCollection, texts: &[(&str, f32)], width: f32, height: f32) -> Picture {
+ let mut recorder = PictureRecorder::new();
+ let canvas = recorder.begin_recording(Rect::from_xywh(0.0, 0.0, width, height), None);
+
+ let mut y_offset = 0.0;
+ for (text, font_size) in texts {
+ let para = make_paragraph(fc, text, width, *font_size);
+ para.paint(canvas, Point::new(0.0, y_offset));
+ y_offset += para.height() + 20.0; // Add some spacing between paragraphs
+ }
+
+ recorder.finish_recording_as_picture(None).unwrap()
+}
+
+fn run(
+ use_cache: bool,
+ surface: &mut Surface,
+ fc: &FontCollection,
+ pic: &Picture,
+ texts: &[(&str, f32)],
+) -> Duration {
+ let width = surface.width() as f32;
+ let canvas = surface.canvas();
+ canvas.clear(Color::WHITE);
+ let start = Instant::now();
+
+ if use_cache {
+ canvas.draw_picture(pic, None, None);
+ } else {
+ let mut y_offset = 0.0;
+ for (text, font_size) in texts {
+ let para = make_paragraph(fc, text, width, *font_size);
+ para.paint(canvas, Point::new(0.0, y_offset));
+ y_offset += para.height() + 20.0;
+ }
+ }
+
+ start.elapsed()
+}
+
+fn main() {
+ let fc = FontCollection::new();
+
+ // Create a more complex text content with multiple paragraphs
+ let text1 = "The quick brown fox jumps over the lazy dog. ".repeat(20);
+ let text2 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(15);
+ let text3 = "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ".repeat(10);
+ let text4 = "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. ".repeat(8);
+
+ let texts: Vec<(&str, f32)> = vec![
+ (text1.as_str(), 16.0),
+ (text2.as_str(), 20.0),
+ (text3.as_str(), 24.0),
+ (text4.as_str(), 18.0),
+ ];
+
+ let width = 1200;
+ let height = 2000;
+ let mut surface = Surface::new_raster_n32_premul((width, height)).unwrap();
+ let pic = make_picture(&fc, &texts, width as f32, height as f32);
+
+ const FRAMES: usize = 500; // Increased number of frames for better statistics
+ let mut cached_times = Vec::with_capacity(FRAMES);
+ let mut uncached_times = Vec::with_capacity(FRAMES);
+
+ // Warmup phase
+ for _ in 0..20 {
+ // Increased warmup iterations
+ run(true, &mut surface, &fc, &pic, &texts);
+ run(false, &mut surface, &fc, &pic, &texts);
+ }
+
+ // Benchmark phase
+ for _ in 0..FRAMES {
+ cached_times.push(run(true, &mut surface, &fc, &pic, &texts));
+ uncached_times.push(run(false, &mut surface, &fc, &pic, &texts));
+ }
+
+ // Calculate statistics
+ let avg_cached: Duration = cached_times.iter().sum::() / FRAMES as u32;
+ let avg_uncached: Duration = uncached_times.iter().sum::() / FRAMES as u32;
+
+ // Calculate standard deviation
+ let cached_std: f64 = cached_times
+ .iter()
+ .map(|&d| (d.as_nanos() as f64 - avg_cached.as_nanos() as f64).powi(2))
+ .sum::()
+ .sqrt()
+ / (FRAMES as f64).sqrt();
+
+ let uncached_std: f64 = uncached_times
+ .iter()
+ .map(|&d| (d.as_nanos() as f64 - avg_uncached.as_nanos() as f64).powi(2))
+ .sum::()
+ .sqrt()
+ / (FRAMES as f64).sqrt();
+
+ println!("Benchmark Results:");
+ println!("Cached avg: {:?} (±{:.2}ns)", avg_cached, cached_std);
+ println!("Uncached avg: {:?} (±{:.2}ns)", avg_uncached, uncached_std);
+ println!(
+ "Speedup: {:.2}x",
+ avg_uncached.as_secs_f64() / avg_cached.as_secs_f64()
+ );
+}
diff --git a/crates/grida-canvas/examples/camera.rs b/crates/grida-canvas/examples/camera.rs
new file mode 100644
index 0000000000..968f424e18
--- /dev/null
+++ b/crates/grida-canvas/examples/camera.rs
@@ -0,0 +1,351 @@
+use cg::node::factory::NodeFactory;
+use cg::{
+ node::schema::*,
+ runtime::camera::Camera2D,
+ runtime::scene::{Backend, Renderer},
+};
+use gl_rs as gl;
+use glutin::{
+ config::{ConfigTemplateBuilder, GlConfig},
+ context::{ContextApi, ContextAttributesBuilder, PossiblyCurrentContext},
+ display::{GetGlDisplay, GlDisplay},
+ prelude::{GlSurface, NotCurrentGlContext},
+ surface::{Surface as GlutinSurface, SurfaceAttributesBuilder, WindowSurface},
+};
+use glutin_winit::DisplayBuilder;
+use math2::transform::AffineTransform;
+use raw_window_handle::HasRawWindowHandle;
+use skia_safe::{Surface, gpu};
+use std::{ffi::CString, num::NonZeroU32};
+use winit::{
+ event::{Event, WindowEvent},
+ event_loop::EventLoop,
+ window::{Window, WindowAttributes},
+};
+
+fn create_static_scene() -> Scene {
+ let mut repository = cg::node::repository::NodeRepository::new();
+ let nf = NodeFactory::new();
+
+ // Create a grid of rectangles
+ let mut ids = Vec::new();
+ for i in 0..10 {
+ for j in 0..10 {
+ let mut rect = nf.create_rectangle_node();
+ let id = rect.base.id.clone();
+ rect.base.name = format!("Rectangle {}-{}", i, j);
+ rect.transform = AffineTransform::new(i as f32 * 100.0, j as f32 * 100.0, 0.0);
+ rect.size = Size {
+ width: 50.0,
+ height: 50.0,
+ };
+ repository.insert(Node::Rectangle(rect));
+ ids.push(id);
+ }
+ }
+
+ // Create a root group containing all rectangles
+ let root_group = GroupNode {
+ base: BaseNode {
+ id: "root".to_string(),
+ name: "Root Group".to_string(),
+ active: true,
+ },
+ transform: AffineTransform::identity(),
+ children: ids,
+ opacity: 1.0,
+ blend_mode: BlendMode::Normal,
+ };
+
+ repository.insert(Node::Group(root_group));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Test Scene".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec!["root".to_string()],
+ nodes: repository,
+ background_color: Some(Color(255, 255, 255, 255)),
+ }
+}
+
+fn init_window(
+ width: i32,
+ height: i32,
+) -> (
+ *mut Surface,
+ EventLoop<()>,
+ Window,
+ GlutinSurface,
+ PossiblyCurrentContext,
+ glutin::config::Config,
+ gpu::gl::FramebufferInfo,
+ skia_safe::gpu::DirectContext,
+ f64,
+) {
+ // Create event loop and window
+ let el = EventLoop::new().expect("Failed to create event loop");
+ let window_attributes = WindowAttributes::default()
+ .with_title("Grida Canvas - Caching Demo")
+ .with_inner_size(winit::dpi::LogicalSize::new(width, height));
+
+ // Create GL config template
+ let template = ConfigTemplateBuilder::new()
+ .with_alpha_size(8)
+ .with_transparency(true);
+
+ // Build display and get window
+ let display_builder = DisplayBuilder::new().with_window_attributes(window_attributes.into());
+ let (window, gl_config) = display_builder
+ .build(&el, template, |configs| {
+ configs
+ .reduce(|accum, config| {
+ let transparency_check = config.supports_transparency().unwrap_or(false)
+ & !accum.supports_transparency().unwrap_or(false);
+
+ if transparency_check || config.num_samples() < accum.num_samples() {
+ config
+ } else {
+ accum
+ }
+ })
+ .unwrap()
+ })
+ .unwrap();
+
+ let window = window.expect("Could not create window with OpenGL context");
+ let raw_window_handle = window
+ .raw_window_handle()
+ .expect("Failed to get window handle");
+ let scale_factor = window.scale_factor();
+
+ // Create context
+ let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle));
+ let fallback_context_attributes = ContextAttributesBuilder::new()
+ .with_context_api(ContextApi::Gles(None))
+ .build(Some(raw_window_handle));
+
+ let not_current_gl_context = unsafe {
+ gl_config
+ .display()
+ .create_context(&gl_config, &context_attributes)
+ .unwrap_or_else(|_| {
+ gl_config
+ .display()
+ .create_context(&gl_config, &fallback_context_attributes)
+ .expect("failed to create context")
+ })
+ };
+
+ let (width, height): (u32, u32) = window.inner_size().into();
+ let attrs = SurfaceAttributesBuilder::::new().build(
+ raw_window_handle,
+ NonZeroU32::new(width).unwrap(),
+ NonZeroU32::new(height).unwrap(),
+ );
+
+ let gl_surface = unsafe {
+ gl_config
+ .display()
+ .create_window_surface(&gl_config, &attrs)
+ .expect("Could not create gl window surface")
+ };
+
+ let gl_context = not_current_gl_context
+ .make_current(&gl_surface)
+ .expect("Could not make GL context current");
+
+ // Initialize GL
+ gl::load_with(|s| {
+ gl_config
+ .display()
+ .get_proc_address(CString::new(s).unwrap().as_c_str())
+ });
+
+ // Create Skia interface
+ let interface = skia_safe::gpu::gl::Interface::new_load_with(|name| {
+ if name == "eglGetCurrentDisplay" {
+ return std::ptr::null();
+ }
+ gl_config
+ .display()
+ .get_proc_address(CString::new(name).unwrap().as_c_str())
+ })
+ .expect("Could not create interface");
+
+ let mut gr_context = skia_safe::gpu::direct_contexts::make_gl(interface, None)
+ .expect("Could not create direct context");
+
+ // Get framebuffer info
+ let fb_info = {
+ let mut fboid: gl::types::GLint = 0;
+ unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
+ gpu::gl::FramebufferInfo {
+ fboid: fboid.try_into().unwrap(),
+ format: skia_safe::gpu::gl::Format::RGBA8.into(),
+ ..Default::default()
+ }
+ };
+
+ // Create Skia surface
+ let backend_render_target = gpu::backend_render_targets::make_gl(
+ (width as i32, height as i32),
+ gl_config.num_samples() as usize,
+ gl_config.stencil_size() as usize,
+ fb_info,
+ );
+
+ let surface = gpu::surfaces::wrap_backend_render_target(
+ &mut gr_context,
+ &backend_render_target,
+ skia_safe::gpu::SurfaceOrigin::BottomLeft,
+ skia_safe::ColorType::RGBA8888,
+ None,
+ None,
+ )
+ .expect("Could not create skia surface");
+
+ (
+ Box::into_raw(Box::new(surface)),
+ el,
+ window,
+ gl_surface,
+ gl_context,
+ gl_config,
+ fb_info,
+ gr_context,
+ scale_factor,
+ )
+}
+
+fn main() {
+ let (
+ surface_ptr,
+ el,
+ window,
+ gl_surface,
+ gl_context,
+ gl_config,
+ fb_info,
+ mut gr_context,
+ scale_factor,
+ ) = init_window(800, 600);
+
+ // Create renderer
+ let mut renderer = Renderer::new();
+ renderer.set_backend(Backend::GL(surface_ptr));
+
+ // Create static scene
+ let scene = create_static_scene();
+
+ // Create camera
+ let mut camera = Camera2D::new(Size {
+ width: 800.0,
+ height: 600.0,
+ });
+ camera.set_position(400.0, 300.0);
+ camera.set_zoom(1.0);
+ renderer.set_camera(camera);
+
+ // Load and warm up the scene cache
+ renderer.load_scene(scene.clone());
+ renderer.queue();
+
+ // Benchmark rendering with camera transformations
+ let mut frame_count = 0;
+ let mut total_time = 0.0;
+ let start_time = std::time::Instant::now();
+
+ el.run(move |event, elwt| {
+ match event {
+ Event::WindowEvent {
+ event: WindowEvent::CloseRequested,
+ ..
+ } => {
+ elwt.exit();
+ }
+ Event::WindowEvent {
+ event: WindowEvent::Resized(size),
+ ..
+ } => {
+ // Recreate GL surface
+ let attrs = SurfaceAttributesBuilder::::new().build(
+ window
+ .raw_window_handle()
+ .expect("Failed to get window handle"),
+ NonZeroU32::new(size.width).unwrap(),
+ NonZeroU32::new(size.height).unwrap(),
+ );
+ let new_gl_surface = unsafe {
+ gl_config
+ .display()
+ .create_window_surface(&gl_config, &attrs)
+ .expect("Could not create gl window surface")
+ };
+
+ // Recreate Skia surface
+ let backend_render_target = gpu::backend_render_targets::make_gl(
+ (size.width as i32, size.height as i32),
+ gl_config.num_samples() as usize,
+ gl_config.stencil_size() as usize,
+ fb_info,
+ );
+ let surface = gpu::surfaces::wrap_backend_render_target(
+ &mut gr_context,
+ &backend_render_target,
+ skia_safe::gpu::SurfaceOrigin::BottomLeft,
+ skia_safe::ColorType::RGBA8888,
+ None,
+ None,
+ )
+ .expect("Could not create skia surface");
+
+ // Update surface pointer
+ unsafe { _ = Box::from_raw(surface_ptr) };
+ let new_surface_ptr = Box::into_raw(Box::new(surface));
+ renderer.set_backend(Backend::GL(new_surface_ptr));
+ }
+ Event::AboutToWait => {
+ let frame_start = std::time::Instant::now();
+
+ // Update camera position in a circular motion
+ let elapsed = start_time.elapsed().as_secs_f32();
+ let angle = elapsed * 2.0;
+ let x = 400.0 + angle.cos() * 100.0;
+ let y = 300.0 + angle.sin() * 100.0;
+ renderer.camera.as_mut().unwrap().set_position(x, y);
+
+ // Render the scene
+ renderer.queue();
+
+ if let Err(e) = gl_surface.swap_buffers(&gl_context) {
+ eprintln!("Error swapping buffers: {:?}", e);
+ }
+
+ let frame_time = frame_start.elapsed().as_secs_f32();
+ total_time += frame_time;
+ frame_count += 1;
+
+ if frame_count % 60 == 0 {
+ let avg_time = total_time / frame_count as f32;
+ println!(
+ "Frame {}: {:.3}ms (avg: {:.3}ms, FPS: {:.1})",
+ frame_count,
+ frame_time * 1000.0,
+ avg_time * 1000.0,
+ 1.0 / avg_time
+ );
+ }
+
+ window.request_redraw();
+ }
+ _ => {}
+ }
+ })
+ .unwrap();
+
+ // Clean up
+ unsafe {
+ _ = Box::from_raw(surface_ptr);
+ }
+}
diff --git a/crates/grida-canvas/examples/container.rs b/crates/grida-canvas/examples/container.rs
new file mode 100644
index 0000000000..27e115954c
--- /dev/null
+++ b/crates/grida-canvas/examples/container.rs
@@ -0,0 +1,79 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_clip() -> Scene {
+ let nf = NodeFactory::new();
+ let mut repository = NodeRepository::new();
+
+ // Create a single container with solid fill
+ let mut container = nf.create_container_node();
+ container.base.name = "Simple Container".to_string();
+ container.transform = AffineTransform::new(100.0, 100.0, 0.0);
+ container.size = Size {
+ width: 300.0,
+ height: 300.0,
+ };
+ container.corner_radius = RectangularCornerRadius::all(20.0);
+ container.fill = Paint::Solid(SolidPaint {
+ color: Color(240, 100, 100, 255), // Light red
+ opacity: 1.0,
+ });
+ container.stroke = Some(Paint::Solid(SolidPaint {
+ color: Color(200, 50, 50, 255), // Darker red
+ opacity: 1.0,
+ }));
+ container.effect = Some(FilterEffect::DropShadow(FeDropShadow {
+ dx: 0.0,
+ dy: 0.0,
+ blur: 10.0,
+ color: Color(0, 0, 0, 255),
+ }));
+ container.clip = true;
+ container.stroke_width = 2.0;
+
+ // Create an ellipse
+ let mut ellipse = nf.create_ellipse_node();
+ ellipse.base.name = "Simple Ellipse".to_string();
+ ellipse.transform = AffineTransform::new(100.0, 150.0, 0.0); // Position below container
+ ellipse.size = Size {
+ width: 300.0,
+ height: 200.0,
+ };
+ ellipse.fill = Paint::Solid(SolidPaint {
+ color: Color(100, 200, 100, 255), // Light green
+ opacity: 1.0,
+ });
+ ellipse.stroke = Paint::Solid(SolidPaint {
+ color: Color(50, 150, 50, 255), // Darker green
+ opacity: 1.0,
+ });
+ ellipse.stroke_width = 2.0;
+
+ // Add nodes to repository and collect their IDs
+ let ellipse_id = ellipse.base.id.clone();
+ repository.insert(Node::Ellipse(ellipse));
+
+ // Add ellipse as child of container
+ container.children = vec![ellipse_id];
+
+ let container_id = container.base.id.clone();
+ repository.insert(Node::Container(container));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Simple Container Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![container_id],
+ nodes: repository,
+ background_color: None,
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_clip().await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/effects.rs b/crates/grida-canvas/examples/effects.rs
new file mode 100644
index 0000000000..e0773b5f65
--- /dev/null
+++ b/crates/grida-canvas/examples/effects.rs
@@ -0,0 +1,208 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_effects() -> Scene {
+ let nf = NodeFactory::new();
+ let mut repository = NodeRepository::new();
+
+ // Create a root container node
+ let mut root_container_node = nf.create_container_node();
+ root_container_node.size = Size {
+ width: 2000.0,
+ height: 2000.0,
+ };
+ root_container_node.base.name = "Root Container".to_string();
+
+ let mut all_effect_ids = Vec::new();
+ let spacing = 200.0;
+ let start_x = 50.0;
+ let base_size = 150.0;
+
+ // Row 1: Drop Shadow Variations
+ for i in 0..6 {
+ if i < 3 {
+ // First three shapes as rectangles
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Drop Shadow Rect {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 100.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(20.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(255, 255, 255, 255), // White
+ opacity: 1.0,
+ });
+ rect.effect = Some(FilterEffect::DropShadow(FeDropShadow {
+ dx: 4.0,
+ dy: 4.0,
+ blur: 4.0 * (i + 1) as f32,
+ color: Color(0, 0, 0, 128),
+ }));
+ all_effect_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ } else {
+ // Last two shapes as regular polygons
+ let mut polygon = nf.create_regular_polygon_node();
+ polygon.base.name = format!("Drop Shadow Polygon {}", i + 1);
+ polygon.transform = AffineTransform::new(start_x + spacing * i as f32, 100.0, 0.0);
+ polygon.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ polygon.point_count = i + 3;
+ polygon.fill = Paint::Solid(SolidPaint {
+ color: Color(255, 255, 255, 255), // White
+ opacity: 1.0,
+ });
+ polygon.effect = Some(FilterEffect::DropShadow(FeDropShadow {
+ dx: 4.0,
+ dy: 4.0,
+ blur: 4.0 * (i + 1) as f32,
+ color: Color(0, 0, 0, 128),
+ }));
+ all_effect_ids.push(polygon.base.id.clone());
+ repository.insert(Node::RegularPolygon(polygon));
+ }
+ }
+
+ // Row 2: Gaussian Blur Variations
+ for i in 0..6 {
+ if i < 3 {
+ // First three shapes as rectangles
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Gaussian Blur Rect {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 300.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(20.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255), // White
+ opacity: 1.0,
+ });
+ rect.effect = Some(FilterEffect::GaussianBlur(FeGaussianBlur {
+ radius: 4.0 * (i + 1) as f32,
+ }));
+ all_effect_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ } else {
+ // Last two shapes as regular polygons
+ let mut polygon = nf.create_regular_polygon_node();
+ polygon.base.name = format!("Gaussian Blur Polygon {}", i + 1);
+ polygon.transform = AffineTransform::new(start_x + spacing * i as f32, 300.0, 0.0);
+ polygon.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ polygon.point_count = i + 3;
+ polygon.fill = Paint::Solid(SolidPaint {
+ color: Color(200, 200, 200, 255), // White
+ opacity: 1.0,
+ });
+ polygon.effect = Some(FilterEffect::GaussianBlur(FeGaussianBlur {
+ radius: 4.0 * (i + 1) as f32,
+ }));
+ all_effect_ids.push(polygon.base.id.clone());
+ repository.insert(Node::RegularPolygon(polygon));
+ }
+ }
+
+ // Row 3: Backdrop Blur Variations
+ // Add a vivid gradient background behind Row 2 (Backdrop Blur Variations)
+ let mut vivid_gradient_rect = nf.create_rectangle_node();
+ vivid_gradient_rect.base.name = "Vivid Gradient Row2".to_string();
+ vivid_gradient_rect.transform = AffineTransform::new(0.0, 530.0, 0.0); // y middle of row 2
+ vivid_gradient_rect.size = Size {
+ width: 2000.0,
+ height: 90.0,
+ };
+ vivid_gradient_rect.fill = Paint::LinearGradient(LinearGradientPaint {
+ transform: AffineTransform::identity(),
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 0, 128, 255),
+ }, // Pink
+ GradientStop {
+ offset: 0.5,
+ color: Color(0, 255, 255, 255),
+ }, // Cyan
+ GradientStop {
+ offset: 1.0,
+ color: Color(255, 255, 0, 255),
+ }, // Yellow
+ ],
+ opacity: 1.0,
+ });
+ let vivid_gradient_rect_id = vivid_gradient_rect.base.id.clone();
+ repository.insert(Node::Rectangle(vivid_gradient_rect));
+
+ for i in 0..6 {
+ if i < 3 {
+ // First three shapes as rectangles
+ let mut blur_rect = nf.create_rectangle_node();
+ blur_rect.base.name = format!("Backdrop Blur Rect {}", i + 1);
+ blur_rect.transform = AffineTransform::new(start_x + spacing * i as f32, 500.0, 0.0);
+ blur_rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ blur_rect.corner_radius = RectangularCornerRadius::all(20.0);
+ blur_rect.fill = Paint::Solid(SolidPaint {
+ color: Color(255, 255, 255, 128), // Semi-transparent white
+ opacity: 1.0,
+ });
+ blur_rect.effect = Some(FilterEffect::BackdropBlur(FeBackdropBlur {
+ radius: 8.0 * (i + 1) as f32,
+ }));
+ all_effect_ids.push(blur_rect.base.id.clone());
+ repository.insert(Node::Rectangle(blur_rect));
+ } else {
+ // Last two shapes as regular polygons
+ let mut blur_polygon = nf.create_regular_polygon_node();
+ blur_polygon.base.name = format!("Backdrop Blur Polygon {}", i + 1);
+ blur_polygon.transform = AffineTransform::new(start_x + spacing * i as f32, 500.0, 0.0);
+ blur_polygon.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ blur_polygon.point_count = i + 3;
+ blur_polygon.fill = Paint::Solid(SolidPaint {
+ color: Color(255, 255, 255, 128), // Semi-transparent white
+ opacity: 1.0,
+ });
+ blur_polygon.effect = Some(FilterEffect::BackdropBlur(FeBackdropBlur {
+ radius: 8.0 * (i + 1) as f32,
+ }));
+ all_effect_ids.push(blur_polygon.base.id.clone());
+ repository.insert(Node::RegularPolygon(blur_polygon));
+ }
+ }
+
+ // Set up the root container
+ root_container_node.children = vec![vivid_gradient_rect_id];
+ root_container_node.children.extend(all_effect_ids);
+ let root_container_id = root_container_node.base.id.clone();
+ repository.insert(Node::Container(root_container_node));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Effects Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_container_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_effects().await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/figma.rs b/crates/grida-canvas/examples/figma.rs
new file mode 100644
index 0000000000..4bcfee7500
--- /dev/null
+++ b/crates/grida-canvas/examples/figma.rs
@@ -0,0 +1,232 @@
+use cg::font_loader::FontLoader;
+use cg::image_loader::{ImageLoader, load_scene_images};
+use cg::webfont_helper::{find_font_files, load_webfonts_metadata};
+use cg::window;
+use cg::{io::io_figma::FigmaConverter, node::schema::Scene};
+use clap::Parser;
+use figma_api::apis::{
+ configuration::{ApiKey, Configuration},
+ files_api::{get_file, get_image_fills},
+};
+use futures::future::join_all;
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+ #[arg(long = "file-key")]
+ file_key: Option,
+ #[arg(long = "api-key")]
+ api_key: Option,
+ #[arg(long = "scene-index")]
+ scene_index: usize,
+ #[arg(long = "no-image")]
+ no_image: bool,
+ #[arg(long = "file")]
+ file: Option,
+ #[arg(long = "images")]
+ images_dir: Option,
+}
+
+async fn load_scene_from_url(
+ file_key: Option<&str>,
+ api_key: Option<&str>,
+ scene_index: usize,
+ no_image: bool,
+ file_path: Option<&str>,
+ images_dir: Option<&str>,
+) -> Result<(Scene, FigmaConverter), String> {
+ let file = if let Some(file_path) = file_path {
+ // Load from local file
+ let file_content = std::fs::read_to_string(file_path)
+ .map_err(|e| format!("Failed to read file: {}", e))?;
+ serde_json::from_str(&file_content).map_err(|e| format!("Failed to parse JSON: {}", e))?
+ } else {
+ // Load from Figma API
+ let file_key = file_key.ok_or("file-key is required when not using --file")?;
+ let api_key = api_key.ok_or("api-key is required when not using --file")?;
+
+ let configuration = Configuration {
+ base_path: "https://api.figma.com".to_string(),
+ user_agent: None,
+ client: reqwest::Client::new(),
+ basic_auth: None,
+ oauth_access_token: None,
+ bearer_access_token: None,
+ api_key: Some(ApiKey {
+ key: api_key.to_string(),
+ prefix: None,
+ }),
+ };
+
+ get_file(
+ &configuration,
+ file_key,
+ None,
+ None,
+ None,
+ Some("paths"),
+ None,
+ None,
+ )
+ .await
+ .expect("Failed to load file")
+ };
+
+ let images = if no_image {
+ println!("Skipping image loading (--no-image flag)");
+ std::collections::HashMap::new()
+ } else if let Some(images_dir) = images_dir {
+ // Load images from local directory
+ let mut images = std::collections::HashMap::new();
+ let dir = std::fs::read_dir(images_dir)
+ .map_err(|e| format!("Failed to read images directory: {}", e))?;
+
+ for entry in dir {
+ let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
+ let path = entry.path();
+ if path.is_file() {
+ let key = path
+ .file_stem()
+ .ok_or_else(|| format!("Invalid filename: {:?}", path))?
+ .to_string_lossy()
+ .to_string();
+ let url = path.to_string_lossy().to_string();
+ images.insert(key, url);
+ }
+ }
+
+ println!("Loaded {} images from directory", images.len());
+ images
+ } else if file_path.is_some() {
+ // When loading from local file without --images, skip image loading
+ println!("Skipping image loading (loading from local file without --images directory)");
+ std::collections::HashMap::new()
+ } else {
+ // Load from Figma API (only when not loading from local file)
+ println!("Loading images from Figma API");
+ let file_key = file_key.ok_or("file-key is required when not using --file")?;
+ let api_key = api_key.ok_or("api-key is required when not using --file")?;
+
+ let configuration = Configuration {
+ base_path: "https://api.figma.com".to_string(),
+ user_agent: None,
+ client: reqwest::Client::new(),
+ basic_auth: None,
+ oauth_access_token: None,
+ bearer_access_token: None,
+ api_key: Some(ApiKey {
+ key: api_key.to_string(),
+ prefix: None,
+ }),
+ };
+
+ let images_response = get_image_fills(&configuration, file_key)
+ .await
+ .expect("Failed to load images");
+ images_response.meta.images
+ };
+
+ let mut converter = FigmaConverter::new().with_image_urls(images);
+
+ let document = converter
+ .convert_document(&file.document)
+ .expect("Failed to convert document");
+
+ Ok((document[scene_index].clone(), converter))
+}
+
+#[tokio::main]
+async fn main() {
+ let cli = Cli::parse();
+ let (scene, converter) = load_scene_from_url(
+ cli.file_key.as_deref(),
+ cli.api_key.as_deref(),
+ cli.scene_index,
+ cli.no_image,
+ cli.file.as_deref(),
+ cli.images_dir.as_deref(),
+ )
+ .await
+ .expect("Failed to load scene");
+
+ println!("Rendering scene: {}", scene.name);
+ println!("Scene ID: {}", scene.id);
+ println!("Number of children: {}", scene.children.len());
+ println!("Total nodes in repository: {}", scene.nodes.len());
+
+ // Load webfonts metadata and find matching font files
+ let webfonts_metadata = load_webfonts_metadata()
+ .await
+ .expect("Failed to load webfonts metadata");
+ let font_files = find_font_files(&webfonts_metadata, &converter.get_discovered_fonts());
+ println!("\nFound {} matching font files:", font_files.len());
+ for font_file in &font_files {
+ println!("Font: {} ({})", font_file.family, font_file.postscript_name);
+ println!(" Style: {}", font_file.style);
+ println!(" URL: {}", font_file.url);
+ println!();
+ }
+
+ // Clone the scene before passing it to run_demo_window_with
+ let scene_for_window = scene.clone();
+ let scene_for_loader = scene;
+
+ // Use the window module's run_demo_window_with to handle image loading and font loading
+ window::run_demo_window_with(scene_for_window, |_renderer, tx, font_tx, proxy| {
+ // Initialize the image loader in lifecycle mode
+ println!("📸 Initializing image loader...");
+ let mut image_loader = ImageLoader::new_lifecycle(tx, proxy.clone());
+
+ // Initialize the font loader in lifecycle mode
+ println!("📝 Initializing font loader...");
+ let font_tx_clone = font_tx.clone();
+ let proxy_clone = proxy.clone();
+
+ // Load all images in the scene - non-blocking
+ let should_load_images = !cli.no_image && (cli.file.is_none() || cli.images_dir.is_some());
+ if should_load_images {
+ println!("🔄 Starting to load scene images in background...");
+ let scene_for_images = scene_for_loader.clone();
+ tokio::spawn(async move {
+ load_scene_images(&mut image_loader, &scene_for_images).await;
+ println!("✅ Scene images loading completed in background");
+ });
+ } else {
+ if cli.no_image {
+ println!("⏭️ Skipping image loading as --no-image flag is set");
+ } else if cli.file.is_some() && cli.images_dir.is_none() {
+ println!(
+ "⏭️ Skipping image loading (loading from local file without --images directory)"
+ );
+ }
+ }
+
+ // Load all fonts in the scene - non-blocking
+ println!("🔄 Starting to load scene fonts in background...");
+ let font_files_clone = font_files.clone();
+ let font_tx = font_tx_clone;
+ let proxy = proxy_clone;
+ tokio::spawn(async move {
+ let font_loading_futures: Vec<_> = font_files_clone
+ .into_iter()
+ .map(|font_file| {
+ let font_tx = font_tx.clone();
+ let proxy = proxy.clone();
+ async move {
+ let family = font_file.family;
+ let url = font_file.url;
+ let postscript_name = font_file.postscript_name;
+ println!("Loading font: {} ({})", family, postscript_name);
+ let mut font_loader = FontLoader::new_lifecycle(font_tx, proxy);
+ font_loader.load_font(&family, &url).await;
+ println!("✅ Font loaded: {} ({})", family, postscript_name);
+ }
+ })
+ .collect();
+
+ join_all(font_loading_futures).await;
+ println!("✅ Scene fonts loading completed in background");
+ });
+ })
+ .await;
+}
diff --git a/crates/grida-canvas/examples/fonts.rs b/crates/grida-canvas/examples/fonts.rs
new file mode 100644
index 0000000000..d6d587b905
--- /dev/null
+++ b/crates/grida-canvas/examples/fonts.rs
@@ -0,0 +1,120 @@
+use skia_safe::textlayout::FontCollection;
+use skia_safe::textlayout::{
+ ParagraphBuilder, ParagraphStyle, TextAlign, TextDirection, TextStyle, TypefaceFontProvider,
+};
+use skia_safe::{Color, Font, FontMgr, Paint, Point, Surface};
+use std::fs;
+
+fn main() {
+ // Create a surface to draw on
+ #[allow(deprecated)]
+ let mut surface = Surface::new_raster_n32_premul((400, 800)).unwrap();
+ let canvas = surface.canvas();
+
+ // Clear the canvas with white background
+ canvas.clear(Color::WHITE);
+
+ // Create a paint for text
+ let mut paint = Paint::default();
+ paint.set_anti_alias(true);
+ paint.set_color(Color::BLACK);
+
+ // Load the Caveat font from local resources
+ let font_data = fs::read("resources/Caveat-VariableFont_wght.ttf").unwrap();
+ let font_mgr = FontMgr::new();
+ let typeface = font_mgr.new_from_data(&font_data, None).unwrap();
+ let font = Font::new(typeface, 24.0);
+
+ // Draw text with Caveat
+ let text = "Hello, Skia!";
+ let point = Point::new(50.0, 100.0);
+ canvas.draw_str(text, point, &font, &paint);
+
+ // Try to load Bungee font
+ if let Ok(bungee_data) = fs::read("resources/Bungee-Regular.ttf") {
+ if let Some(bungee_typeface) = font_mgr.new_from_data(&bungee_data, None) {
+ let bungee_font = Font::new(bungee_typeface, 24.0);
+ // Draw text with Bungee
+ let bungee_point = Point::new(50.0, 200.0);
+ canvas.draw_str("Bungee Font!", bungee_point, &bungee_font, &paint);
+ }
+ } else {
+ // If Bungee font is not found, draw a message
+ let fallback_point = Point::new(50.0, 200.0);
+ canvas.draw_str("Bungee font not found", fallback_point, &font, &paint);
+ }
+
+ // Try to load Fruktur font
+ if let Ok(fruktur_data) = fs::read("resources/Fruktur-Regular.ttf") {
+ if let Some(fruktur_typeface) = font_mgr.new_from_data(&fruktur_data, None) {
+ let fruktur_font = Font::new(fruktur_typeface, 24.0);
+ // Draw text with Fruktur
+ let fruktur_point = Point::new(50.0, 300.0);
+ canvas.draw_str("Fruktur Font!", fruktur_point, &fruktur_font, &paint);
+ }
+ } else {
+ // If Fruktur font is not found, draw a message
+ let fallback_point = Point::new(50.0, 300.0);
+ canvas.draw_str("Fruktur font not found", fallback_point, &font, &paint);
+ }
+
+ // Try to load VT323 font
+ if let Ok(vt323_data) = fs::read("resources/VT323-Regular.ttf") {
+ if let Some(vt323_typeface) = font_mgr.new_from_data(&vt323_data, None) {
+ let vt323_font = Font::new(vt323_typeface.clone(), 24.0);
+ // Draw text with VT323
+ let vt323_point = Point::new(50.0, 400.0);
+ canvas.draw_str("VT323 Font!", vt323_point, &vt323_font, &paint);
+
+ // Add paragraph demo with VT323
+ let paragraph_text = "Welcome to the VT323 font demo! This is a monospace font that's perfect for coding and retro-style interfaces. It has a distinctive pixelated look that makes it stand out. The font was designed by Peter Hull and is inspired by the classic VT320 terminal.";
+ let paragraph_point = Point::new(50.0, 450.0);
+
+ // Create a paragraph style
+ let mut paragraph_style = ParagraphStyle::new();
+ paragraph_style.set_text_direction(TextDirection::LTR);
+ paragraph_style.set_text_align(TextAlign::Left);
+
+ // Create a font collection and add the VT323 font
+ let mut font_collection = FontCollection::new();
+ let mut provider = TypefaceFontProvider::new();
+ provider.register_typeface(
+ font_mgr.new_from_data(&vt323_data, None).unwrap(),
+ Some("VT323"),
+ );
+ font_collection.set_asset_font_manager(Some(provider.into()));
+ font_collection.set_default_font_manager(font_mgr.clone(), None);
+
+ // Create a paragraph builder
+ let mut para_builder = ParagraphBuilder::new(¶graph_style, &font_collection);
+
+ // Add text with the VT323 font
+ let mut text_style = TextStyle::new();
+ text_style.set_foreground_paint(&paint);
+ text_style.set_font_size(20.0);
+ text_style.set_font_families(&["VT323"]);
+ para_builder.push_style(&text_style);
+ para_builder.add_text(paragraph_text);
+
+ // Build and layout the paragraph
+ let mut paragraph = para_builder.build();
+ paragraph.layout(300.0); // Set width to 300 pixels
+
+ // Draw the paragraph
+ paragraph.paint(canvas, paragraph_point);
+ }
+ } else {
+ // If VT323 font is not found, draw a message
+ let fallback_point = Point::new(50.0, 400.0);
+ canvas.draw_str("VT323 font not found", fallback_point, &font, &paint);
+ }
+
+ // Save the result to a PNG file
+ let image = surface.image_snapshot();
+ #[allow(deprecated)]
+ let data = image
+ .encode_to_data(skia_safe::EncodedImageFormat::PNG)
+ .unwrap();
+ let bytes = data.as_bytes();
+ std::fs::write("text_output.png", bytes).unwrap();
+}
diff --git a/crates/grida-canvas/examples/gradients.rs b/crates/grida-canvas/examples/gradients.rs
new file mode 100644
index 0000000000..bb6b08c4be
--- /dev/null
+++ b/crates/grida-canvas/examples/gradients.rs
@@ -0,0 +1,172 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_gradients() -> Scene {
+ let nf = NodeFactory::new();
+ let mut repository = NodeRepository::new();
+
+ // root container
+ let mut root = nf.create_container_node();
+ root.base.name = "Root".to_string();
+ root.size = Size {
+ width: 1200.0,
+ height: 800.0,
+ };
+
+ let mut ids = Vec::new();
+ let spacing = 160.0;
+ let start_x = 60.0;
+ let base = 120.0;
+
+ // Linear gradient fills
+ for i in 0..5 {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Linear Fill {}", i);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 80.0, 0.0);
+ rect.size = Size {
+ width: base,
+ height: base,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ let angle = (i as f32) * 45.0;
+ rect.fill = Paint::LinearGradient(LinearGradientPaint {
+ transform: AffineTransform::from_rotatation(angle),
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 0, 0, 255),
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(0, 0, 255, 255),
+ },
+ ],
+ opacity: 1.0,
+ });
+ ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Radial gradient fills
+ for i in 0..5 {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Radial Fill {}", i);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 280.0, 0.0);
+ rect.size = Size {
+ width: base,
+ height: base,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ let offset = -0.25 + 0.125 * i as f32;
+ rect.fill = Paint::RadialGradient(RadialGradientPaint {
+ transform: AffineTransform {
+ matrix: [[1.0, 0.0, offset], [0.0, 1.0, offset]],
+ },
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 255, 0, 255),
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(0, 255, 0, 255),
+ },
+ ],
+ opacity: 1.0,
+ });
+ ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Linear gradient strokes
+ for i in 0..5 {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Linear Stroke {}", i);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 480.0, 0.0);
+ rect.size = Size {
+ width: base,
+ height: base,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ let angle = (i as f32) * 45.0;
+ rect.stroke = Paint::LinearGradient(LinearGradientPaint {
+ transform: AffineTransform::from_rotatation(angle),
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 0, 255, 255),
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(0, 255, 255, 255),
+ },
+ ],
+ opacity: 1.0,
+ });
+ rect.stroke_width = 8.0;
+ ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Radial gradient strokes
+ for i in 0..5 {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Radial Stroke {}", i);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 680.0, 0.0);
+ rect.size = Size {
+ width: base,
+ height: base,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ let offset = -0.25 + 0.125 * i as f32;
+ rect.stroke = Paint::RadialGradient(RadialGradientPaint {
+ transform: AffineTransform {
+ matrix: [[1.0, 0.0, offset], [0.0, 1.0, offset]],
+ },
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 128, 0, 255),
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(0, 128, 255, 255),
+ },
+ ],
+ opacity: 1.0,
+ });
+ rect.stroke_width = 8.0;
+ ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ root.children = ids.clone();
+ let root_id = root.base.id.clone();
+ repository.insert(Node::Container(root));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Gradients Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_gradients().await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/image.rs b/crates/grida-canvas/examples/image.rs
new file mode 100644
index 0000000000..1a949f469f
--- /dev/null
+++ b/crates/grida-canvas/examples/image.rs
@@ -0,0 +1,66 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::{box_fit::BoxFit, transform::AffineTransform};
+
+async fn demo_image() -> Scene {
+ let nf = NodeFactory::new();
+ // let image4k = "resources/4k.jpg".to_string();
+ // let image4ksize = Size {
+ // width: 4000.0,
+ // height: 6000.0,
+ // };
+ let image8k = "resources/8k.jpg".to_string();
+ let image8ksize = Size {
+ width: 8070.0,
+ height: 5196.0,
+ };
+
+ // Root container
+ let mut root = nf.create_container_node();
+ root.base.name = "Root".to_string();
+ root.size = image8ksize.clone();
+
+ // First example: Rectangle with ImagePaint fill
+ let mut rect1 = nf.create_rectangle_node();
+ rect1.base.name = "ImageFillRect".to_string();
+ rect1.transform = AffineTransform::identity();
+ rect1.size = image8ksize.clone();
+ rect1.fill = Paint::Image(ImagePaint {
+ _ref: image8k.clone(),
+ opacity: 1.0,
+ transform: AffineTransform::identity(),
+ fit: BoxFit::Cover,
+ });
+ rect1.stroke = Paint::Solid(SolidPaint {
+ color: Color(255, 0, 0, 255),
+ opacity: 1.0,
+ });
+ rect1.stroke_width = 2.0;
+
+ let mut repository = NodeRepository::new();
+
+ let rect1_id = rect1.base.id.clone();
+
+ repository.insert(Node::Rectangle(rect1));
+
+ root.children = vec![rect1_id];
+ let root_id = root.base.id.clone();
+ repository.insert(Node::Container(root));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Images Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_image().await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/images.rs b/crates/grida-canvas/examples/images.rs
new file mode 100644
index 0000000000..f533cf7586
--- /dev/null
+++ b/crates/grida-canvas/examples/images.rs
@@ -0,0 +1,130 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::{box_fit::BoxFit, transform::AffineTransform};
+
+async fn demo_images() -> Scene {
+ let nf = NodeFactory::new();
+ let image_url = "https://grida.co/images/abstract-placeholder.jpg".to_string();
+
+ // Root container
+ let mut root = nf.create_container_node();
+ root.base.name = "Root".to_string();
+ root.size = Size {
+ width: 800.0,
+ height: 600.0,
+ };
+
+ // First example: Rectangle with ImagePaint fill
+ let mut rect1 = nf.create_rectangle_node();
+ rect1.base.name = "ImageFillRect".to_string();
+ rect1.transform = AffineTransform::new(50.0, 50.0, 0.0);
+ rect1.size = Size {
+ width: 200.0,
+ height: 200.0,
+ };
+ rect1.fill = Paint::Image(ImagePaint {
+ _ref: image_url.clone(),
+ opacity: 1.0,
+ transform: AffineTransform::identity(),
+ fit: BoxFit::Cover,
+ });
+ rect1.stroke = Paint::Solid(SolidPaint {
+ color: Color(255, 0, 0, 255),
+ opacity: 1.0,
+ });
+ rect1.stroke_width = 2.0;
+
+ // Second example: Rectangle with ImagePaint fill and stroke
+ let mut rect2 = nf.create_rectangle_node();
+ rect2.base.name = "ImageFillAndStrokeRect".to_string();
+ rect2.transform = AffineTransform::new(300.0, 50.0, 0.0);
+ rect2.size = Size {
+ width: 200.0,
+ height: 200.0,
+ };
+ rect2.fill = Paint::Image(ImagePaint {
+ _ref: image_url.clone(),
+ opacity: 1.0,
+ transform: AffineTransform::identity(),
+ fit: BoxFit::Cover,
+ });
+ rect2.stroke = Paint::Image(ImagePaint {
+ _ref: image_url.clone(),
+ opacity: 1.0,
+ transform: AffineTransform::identity(),
+ fit: BoxFit::Cover,
+ });
+ rect2.stroke_width = 10.0;
+
+ // Third example: Rectangle with ImagePaint stroke only
+ let mut rect3 = nf.create_rectangle_node();
+ rect3.base.name = "ImageStrokeRect".to_string();
+ rect3.transform = AffineTransform::new(550.0, 50.0, 0.0);
+ rect3.size = Size {
+ width: 200.0,
+ height: 200.0,
+ };
+ rect3.corner_radius = RectangularCornerRadius::all(40.0);
+ rect3.fill = Paint::Solid(SolidPaint {
+ color: Color(240, 240, 240, 255),
+ opacity: 1.0,
+ });
+ rect3.stroke = Paint::Image(ImagePaint {
+ _ref: image_url.clone(),
+ opacity: 1.0,
+ transform: AffineTransform::identity(),
+ fit: BoxFit::Cover,
+ });
+ rect3.stroke_width = 10.0;
+
+ // Fourth example: Rectangle with ImagePaint fill using a custom transform
+ let mut rect4 = nf.create_rectangle_node();
+ rect4.base.name = "ImageTransformFillRect".to_string();
+ rect4.transform = AffineTransform::new(50.0, 300.0, 0.0);
+ rect4.size = Size {
+ width: 200.0,
+ height: 200.0,
+ };
+ rect4.fill = Paint::Image(ImagePaint {
+ _ref: image_url.clone(),
+ opacity: 1.0,
+ // Rotate the image 45 degrees with BoxFit::None to showcase the paint transform
+ transform: AffineTransform {
+ matrix: [[0.7071, -0.7071, 100.0], [0.7071, 0.7071, 0.0]],
+ },
+ fit: BoxFit::None,
+ });
+
+ let mut repository = NodeRepository::new();
+
+ let rect1_id = rect1.base.id.clone();
+ let rect2_id = rect2.base.id.clone();
+ let rect3_id = rect3.base.id.clone();
+ let rect4_id = rect4.base.id.clone();
+
+ repository.insert(Node::Rectangle(rect1));
+ repository.insert(Node::Rectangle(rect2));
+ repository.insert(Node::Rectangle(rect3));
+ repository.insert(Node::Rectangle(rect4));
+
+ root.children = vec![rect1_id, rect2_id, rect3_id, rect4_id];
+ let root_id = root.base.id.clone();
+ repository.insert(Node::Container(root));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Images Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_images().await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/json.rs b/crates/grida-canvas/examples/json.rs
new file mode 100644
index 0000000000..ff736a955d
--- /dev/null
+++ b/crates/grida-canvas/examples/json.rs
@@ -0,0 +1,46 @@
+use cg::io::io_json::parse;
+use cg::node::schema::*;
+use cg::window;
+use clap::Parser;
+use math2::transform::AffineTransform;
+use std::fs;
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+ // take path to json file
+ path: String,
+}
+
+async fn load_scene_from_file(file_path: &str) -> Scene {
+ let file: String = fs::read_to_string(file_path).expect("failed to read file");
+ let canvas_file = parse(&file).expect("failed to parse file");
+ let nodes = canvas_file.document.nodes;
+ // entry_scene_id or scenes[0]
+ let scene_id = canvas_file.document.entry_scene_id.unwrap_or(
+ canvas_file
+ .document
+ .scenes
+ .keys()
+ .next()
+ .unwrap()
+ .to_string(),
+ );
+ let scene = canvas_file.document.scenes.get(&scene_id).unwrap();
+ Scene {
+ nodes: nodes.into_iter().map(|(k, v)| (k, v.into())).collect(),
+ id: scene_id,
+ name: scene.name.clone(),
+ transform: AffineTransform::identity(),
+ children: scene.children.clone(),
+ background_color: Some(Color(230, 230, 230, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let cli = Cli::parse();
+ let scene = load_scene_from_file(&cli.path).await;
+
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/nested.rs b/crates/grida-canvas/examples/nested.rs
new file mode 100644
index 0000000000..8b53589b95
--- /dev/null
+++ b/crates/grida-canvas/examples/nested.rs
@@ -0,0 +1,100 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_nested() -> Scene {
+ let nf = NodeFactory::new();
+ let mut repository = NodeRepository::new();
+ let n = 5; // number of nesting levels
+
+ // Create innermost rectangle
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = "Inner Rect".to_string();
+ rect.size = Size {
+ width: 100.0,
+ height: 100.0,
+ };
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(255, 0, 0, 255),
+ opacity: 1.0,
+ });
+ let mut current_id = rect.base.id.clone();
+ repository.insert(Node::Rectangle(rect));
+
+ // Create nested structure
+ for i in 0..n {
+ if i % 2 == 0 {
+ // Create group with rotation transform
+ let mut group = nf.create_group_node();
+ group.base.name = format!("Group {}", i);
+ group.transform = AffineTransform::new(
+ 50.0 * (i as f32 + 1.0), // x offset
+ 50.0 * (i as f32 + 1.0), // y offset
+ 0.0,
+ );
+
+ // Add a rectangle to the group
+ let mut group_rect = nf.create_rectangle_node();
+ group_rect.base.name = format!("Group {} Rect", i);
+ group_rect.size = Size {
+ width: 100.0,
+ height: 100.0,
+ };
+ group_rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 255, 0, 255), // Green
+ opacity: 1.0,
+ });
+ let group_rect_id = group_rect.base.id.clone();
+ repository.insert(Node::Rectangle(group_rect));
+
+ group.children = vec![current_id, group_rect_id];
+ current_id = group.base.id.clone();
+ repository.insert(Node::Group(group));
+ } else {
+ // Create container with scale transform
+ let mut container = nf.create_container_node();
+ container.base.name = format!("Container {}", i);
+ container.transform = AffineTransform::new(
+ -30.0 * (i as f32 + 1.0), // x offset
+ -30.0 * (i as f32 + 1.0), // y offset
+ 0.0,
+ );
+
+ // Add a rectangle to the container
+ let mut container_rect = nf.create_rectangle_node();
+ container_rect.base.name = format!("Container {} Rect", i);
+ container_rect.size = Size {
+ width: 100.0,
+ height: 100.0,
+ };
+ container_rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 255, 255), // Blue
+ opacity: 1.0,
+ });
+ let container_rect_id = container_rect.base.id.clone();
+ repository.insert(Node::Rectangle(container_rect));
+
+ container.children = vec![current_id, container_rect_id];
+ current_id = container.base.id.clone();
+ repository.insert(Node::Container(container));
+ }
+ }
+
+ Scene {
+ id: "nested".to_string(),
+ name: "Nested Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![current_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_nested().await;
+
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/paint.rs b/crates/grida-canvas/examples/paint.rs
new file mode 100644
index 0000000000..594127f283
--- /dev/null
+++ b/crates/grida-canvas/examples/paint.rs
@@ -0,0 +1,250 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_paints() -> Scene {
+ let nf = NodeFactory::new();
+ let mut repository = NodeRepository::new();
+
+ // Create a root container node
+ let mut root_container_node = nf.create_container_node();
+ root_container_node.base.name = "Root Container".to_string();
+ root_container_node.size = Size {
+ width: 1080.0,
+ height: 1080.0,
+ };
+
+ let mut all_shape_ids = Vec::new();
+ let spacing = 100.0;
+ let start_x = 50.0;
+ let base_size = 80.0;
+ let items_per_row = 10;
+
+ // Solid Colors Row
+ for i in 0..items_per_row {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Solid Color {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 100.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(
+ 255 - (i * 25) as u8,
+ 100 + (i * 15) as u8,
+ 50 + (i * 20) as u8,
+ 255,
+ ),
+ opacity: 1.0,
+ });
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Linear Gradient Row
+ for i in 0..items_per_row {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Linear Gradient {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 200.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+
+ // Create a linear gradient that changes angle based on index
+ let angle = (i as f32 * 36.0) * std::f32::consts::PI / 180.0; // 0 to 360 degrees
+ let transform = AffineTransform::new(0.0, 0.0, angle);
+
+ rect.fill = Paint::LinearGradient(LinearGradientPaint {
+ transform,
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 100, 100, 255),
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(100, 100, 255, 255),
+ },
+ ],
+ opacity: 1.0,
+ });
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Radial Gradient Row
+ for i in 0..items_per_row {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Radial Gradient {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 300.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+
+ // Create a radial gradient with varying center positions
+ let center_x = 0.2 + (i as f32 * 0.06); // 0.2 to 0.8
+ let center_y = 0.2 + (i as f32 * 0.06); // 0.2 to 0.8
+ let transform = AffineTransform::new(center_x * base_size, center_y * base_size, 0.0);
+
+ rect.fill = Paint::RadialGradient(RadialGradientPaint {
+ transform,
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 255, 100, 255),
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(100, 255, 100, 255),
+ },
+ ],
+ opacity: 1.0,
+ });
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Stroke Solid Colors Row
+ for i in 0..items_per_row {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Stroke Solid Color {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 400.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+
+ // No fill
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0), // Transparent
+ opacity: 1.0,
+ });
+
+ // Solid color stroke with varying colors
+ rect.stroke = Paint::Solid(SolidPaint {
+ color: Color(
+ 255 - (i * 25) as u8,
+ 100 + (i * 15) as u8,
+ 50 + (i * 20) as u8,
+ 255,
+ ),
+ opacity: 1.0,
+ });
+ rect.stroke_width = 4.0; // Consistent stroke width
+
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Stroke Linear Gradient Row
+ for i in 0..items_per_row {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Stroke Linear Gradient {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 500.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+
+ // No fill
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0), // Transparent
+ opacity: 1.0,
+ });
+
+ // Create a linear gradient that changes angle based on index
+ let angle = (i as f32 * 36.0) * std::f32::consts::PI / 180.0; // 0 to 360 degrees
+ let transform = AffineTransform::new(0.0, 0.0, angle);
+
+ rect.stroke = Paint::LinearGradient(LinearGradientPaint {
+ transform,
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 100, 100, 255),
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(100, 100, 255, 255),
+ },
+ ],
+ opacity: 1.0,
+ });
+ rect.stroke_width = 4.0; // Consistent stroke width
+
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Stroke Radial Gradient Row
+ for i in 0..items_per_row {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Stroke Radial Gradient {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 600.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+
+ // No fill
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0), // Transparent
+ opacity: 1.0,
+ });
+
+ // Create a radial gradient with varying center positions
+ let center_x = 0.2 + (i as f32 * 0.06); // 0.2 to 0.8
+ let center_y = 0.2 + (i as f32 * 0.06); // 0.2 to 0.8
+ let transform = AffineTransform::new(center_x * base_size, center_y * base_size, 0.0);
+
+ rect.stroke = Paint::RadialGradient(RadialGradientPaint {
+ transform,
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 255, 100, 255),
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(100, 255, 100, 255),
+ },
+ ],
+ opacity: 1.0,
+ });
+ rect.stroke_width = 4.0; // Consistent stroke width
+
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Set up the root container
+ root_container_node.children.extend(all_shape_ids);
+ let root_container_id = root_container_node.base.id.clone();
+ repository.insert(Node::Container(root_container_node));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Paints Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_container_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_paints().await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/shapes.rs b/crates/grida-canvas/examples/shapes.rs
new file mode 100644
index 0000000000..ddb55a8e33
--- /dev/null
+++ b/crates/grida-canvas/examples/shapes.rs
@@ -0,0 +1,200 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_shapes() -> Scene {
+ let nf = NodeFactory::new();
+
+ // Create a root container node
+ let mut root_container_node = nf.create_container_node();
+ root_container_node.base.name = "Root Container".to_string();
+ root_container_node.size = Size {
+ width: 1080.0,
+ height: 1080.0,
+ };
+
+ let mut repository = NodeRepository::new();
+
+ let mut all_shape_ids = Vec::new();
+ let spacing = 100.0;
+ let start_x = 50.0;
+ let base_size = 80.0;
+ let items_per_row = 10;
+
+ // Rectangle Row - demonstrating corner radius variations
+ for i in 0..items_per_row {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Rectangle {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 100.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(0.0 + (i as f32 * 8.0)); // 0 to 72
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 255,
+ ), // Fading gray
+ opacity: 1.0,
+ });
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Ellipse Row - demonstrating width/height ratio variations
+ for i in 0..items_per_row {
+ let mut ellipse = nf.create_ellipse_node();
+ ellipse.base.name = format!("Ellipse {}", i + 1);
+ ellipse.transform = AffineTransform::new(start_x + spacing * i as f32, 200.0, 0.0);
+ ellipse.size = Size {
+ width: base_size * (1.0 + (i as f32 * 0.1)), // 1.0x to 1.9x width
+ height: base_size,
+ };
+ ellipse.fill = Paint::Solid(SolidPaint {
+ color: Color(
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 255,
+ ), // Fading gray
+ opacity: 1.0,
+ });
+ all_shape_ids.push(ellipse.base.id.clone());
+ repository.insert(Node::Ellipse(ellipse));
+ }
+
+ // Polygon Row - demonstrating point count variations
+ for i in 0..items_per_row {
+ let point_count = 3 + i; // 3 to 12 points
+ let points = (0..point_count)
+ .map(|j| {
+ let angle = std::f32::consts::PI * 2.0 * (j as f32) / (point_count as f32)
+ - std::f32::consts::FRAC_PI_2;
+ let radius = base_size / 2.0;
+ let x = radius * angle.cos();
+ let y = radius * angle.sin();
+ Point { x, y }
+ })
+ .collect::>();
+
+ let mut polygon = nf.create_polygon_node();
+ polygon.base.name = format!("Polygon {}", i + 1);
+ polygon.transform = AffineTransform::new(start_x + spacing * i as f32, 300.0, 0.0);
+ polygon.points = points;
+ polygon.corner_radius = 16.0;
+ polygon.fill = Paint::Solid(SolidPaint {
+ color: Color(
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 255,
+ ), // Fading gray
+ opacity: 1.0,
+ });
+ all_shape_ids.push(polygon.base.id.clone());
+ repository.insert(Node::Polygon(polygon));
+ }
+
+ // Regular Polygon Row - demonstrating point count variations
+ for i in 0..items_per_row {
+ let mut regular_polygon = nf.create_regular_polygon_node();
+ regular_polygon.base.name = format!("Regular Polygon {}", i + 1);
+ regular_polygon.transform = AffineTransform::new(start_x + spacing * i as f32, 400.0, 0.0);
+ regular_polygon.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ regular_polygon.point_count = 3 + i; // 3 to 12 points
+ regular_polygon.fill = Paint::Solid(SolidPaint {
+ color: Color(
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 255,
+ ), // Fading gray
+ opacity: 1.0,
+ });
+ all_shape_ids.push(regular_polygon.base.id.clone());
+ repository.insert(Node::RegularPolygon(regular_polygon));
+ }
+
+ // Path Row - demonstrating different path patterns
+ let path_data = vec![
+ "M50,0 L61,35 L98,35 L68,57 L79,91 L50,71 L21,91 L32,57 L2,35 L39,35 Z", // 5-point star
+ "M50,0 L100,50 L0,50 Z", // Triangle
+ "M0,0 L100,0 L100,100 L0,100 Z", // Square
+ "M50,0 L100,50 L50,100 L0,50 Z", // Diamond
+ "M0,0 L100,0 L100,100 L0,100 L0,0 M20,20 L80,20 L80,80 L20,80 Z", // Square with hole
+ "M50,0 A50,50 0 0 1 100,50 A50,50 0 0 1 50,100 A50,50 0 0 1 0,50 A50,50 0 0 1 50,0 Z", // Circle
+ "M0,50 L50,0 L100,50 L50,100 Z", // Diamond
+ "M0,0 L100,0 L50,100 Z", // Triangle
+ "M0,0 L100,0 L100,100 L0,100 Z M20,20 L80,20 L80,80 L20,80 Z", // Square with hole
+ "M50,0 A50,50 0 0 1 100,50 A50,50 0 0 1 50,100 A50,50 0 0 1 0,50 A50,50 0 0 1 50,0 Z", // Circle
+ ];
+ for (i, data) in path_data.iter().enumerate() {
+ let mut path = nf.create_path_node();
+ path.base.name = format!("Path {}", i + 1);
+ path.transform = AffineTransform::new(start_x + spacing * i as f32, 500.0, 0.0);
+ path.data = data.to_string();
+ path.fill = Paint::Solid(SolidPaint {
+ color: Color(
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 255,
+ ), // Fading gray
+ opacity: 1.0,
+ });
+ all_shape_ids.push(path.base.id.clone());
+ repository.insert(Node::Path(path));
+ }
+
+ // Star Polygon Row - demonstrating different point counts and inner radius variations
+ for i in 0..items_per_row {
+ let mut star = nf.create_regular_star_polygon_node();
+ star.base.name = format!("Star Polygon {}", i + 1);
+ star.transform = AffineTransform::new(start_x + spacing * i as f32, 600.0, 0.0);
+ star.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ star.point_count = 3 + i; // 3 to 12 points
+ star.inner_radius = 0.7 - (i as f32 * 0.05); // 0.3 to 0.75 inner radius
+ star.fill = Paint::Solid(SolidPaint {
+ color: Color(
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 200 - (i * 20) as u8,
+ 255,
+ ), // Fading gray
+ opacity: 1.0,
+ });
+ all_shape_ids.push(star.base.id.clone());
+ repository.insert(Node::RegularStarPolygon(star));
+ }
+
+ // Set up the root container
+ root_container_node.children.extend(all_shape_ids);
+ let root_container_id = root_container_node.base.id.clone();
+ repository.insert(Node::Container(root_container_node));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Shapes Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_container_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_shapes().await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/srcset.rs b/crates/grida-canvas/examples/srcset.rs
new file mode 100644
index 0000000000..50af34dc2e
--- /dev/null
+++ b/crates/grida-canvas/examples/srcset.rs
@@ -0,0 +1,177 @@
+use skia_safe::canvas::SrcRectConstraint;
+use skia_safe::{Canvas, Color, Image, Paint, Rect, Surface};
+use std::path::Path;
+use std::time::{Duration, Instant};
+
+struct ImageBenchmark {
+ original: Image,
+ scaled_versions: Vec<(f32, Image)>, // (scale_factor, image)
+}
+
+impl ImageBenchmark {
+ fn new(image_path: &str) -> Self {
+ // Load the original image
+ let data = std::fs::read(image_path).expect("Failed to read image file");
+ let original =
+ Image::from_encoded(skia_safe::Data::new_copy(&data)).expect("Failed to decode image");
+
+ // Create scaled versions
+ let mut scaled_versions = Vec::new();
+ let scales = [1.0, 0.5, 0.25, 0.125]; // Added 0.125 scale
+
+ for scale in scales {
+ let width = (original.width() as f32 * scale) as i32;
+ let height = (original.height() as f32 * scale) as i32;
+
+ // Create a surface for the scaled image
+ let mut surface =
+ Surface::new_raster_n32_premul((width, height)).expect("Failed to create surface");
+ let canvas = surface.canvas();
+
+ // Clear the surface
+ canvas.clear(Color::WHITE);
+
+ // Draw the original image scaled down
+ let mut paint = Paint::default();
+ paint.set_anti_alias(true);
+
+ let src_rect = Rect::new(0.0, 0.0, original.width() as f32, original.height() as f32);
+ let dst_rect = Rect::new(0.0, 0.0, width as f32, height as f32);
+ canvas.draw_image_rect(
+ &original,
+ Some((&src_rect, SrcRectConstraint::Fast)),
+ dst_rect,
+ &paint,
+ );
+
+ // Get the scaled image
+ let scaled_image = surface.image_snapshot();
+ scaled_versions.push((scale, scaled_image));
+ }
+
+ Self {
+ original,
+ scaled_versions,
+ }
+ }
+
+ fn benchmark_rendering(
+ &self,
+ iterations: usize,
+ images_per_iteration: usize,
+ ) -> Vec<(f32, Duration, Duration)> {
+ let mut results = Vec::new();
+
+ // Create a surface for rendering
+ let mut surface =
+ Surface::new_raster_n32_premul((800, 600)).expect("Failed to create surface");
+ let canvas = surface.canvas();
+
+ // Calculate grid layout
+ let grid_size = (images_per_iteration as f32).sqrt().ceil() as i32;
+ let image_width = 800.0 / grid_size as f32;
+ let image_height = 600.0 / grid_size as f32;
+
+ // Benchmark each scaled version
+ for (scale, image) in &self.scaled_versions {
+ let mut total_time_fast = Duration::new(0, 0);
+ let mut total_time_strict = Duration::new(0, 0);
+
+ for _ in 0..iterations {
+ // Test with Fast constraint
+ canvas.clear(Color::WHITE);
+ let start = Instant::now();
+ for i in 0..images_per_iteration {
+ let row = (i as i32) / grid_size;
+ let col = (i as i32) % grid_size;
+
+ let x = col as f32 * image_width;
+ let y = row as f32 * image_height;
+
+ let mut paint = Paint::default();
+ paint.set_anti_alias(true);
+
+ let src_rect = Rect::new(0.0, 0.0, image.width() as f32, image.height() as f32);
+ let dst_rect = Rect::new(x, y, x + image_width, y + image_height);
+ canvas.draw_image_rect(
+ image,
+ Some((&src_rect, SrcRectConstraint::Fast)),
+ dst_rect,
+ &paint,
+ );
+ }
+ total_time_fast += start.elapsed();
+
+ // Test with Strict constraint
+ canvas.clear(Color::WHITE);
+ let start = Instant::now();
+ for i in 0..images_per_iteration {
+ let row = (i as i32) / grid_size;
+ let col = (i as i32) % grid_size;
+
+ let x = col as f32 * image_width;
+ let y = row as f32 * image_height;
+
+ let mut paint = Paint::default();
+ paint.set_anti_alias(true);
+
+ let src_rect = Rect::new(0.0, 0.0, image.width() as f32, image.height() as f32);
+ let dst_rect = Rect::new(x, y, x + image_width, y + image_height);
+ canvas.draw_image_rect(
+ image,
+ Some((&src_rect, SrcRectConstraint::Strict)),
+ dst_rect,
+ &paint,
+ );
+ }
+ total_time_strict += start.elapsed();
+ }
+
+ let avg_time_fast = total_time_fast / iterations as u32;
+ let avg_time_strict = total_time_strict / iterations as u32;
+ results.push((*scale, avg_time_fast, avg_time_strict));
+ }
+
+ results
+ }
+}
+
+fn main() {
+ // Check if image exists
+ let image_path = "resources/4k.jpg";
+ if !Path::new(image_path).exists() {
+ println!(
+ "Error: {} not found. Please place a 4K image named '4k.jpg' in the resources directory.",
+ image_path
+ );
+ return;
+ }
+
+ println!("Loading and preparing images...");
+ let benchmark = ImageBenchmark::new(image_path);
+
+ let iterations = 1000;
+ let images_per_iteration = 16; // Render 16 images per iteration in a 4x4 grid
+
+ println!(
+ "\nRunning benchmark ({} iterations, {} images per iteration)...",
+ iterations, images_per_iteration
+ );
+ let results = benchmark.benchmark_rendering(iterations, images_per_iteration);
+
+ println!("\nResults:");
+ println!(
+ "Scale | Fast Quality (ms) | Strict Quality (ms) | Fast/Image (ms) | Strict/Image (ms)"
+ );
+ println!("------|-----------------|-------------------|----------------|------------------");
+ for (scale, fast_time, strict_time) in results {
+ let fast_ms = fast_time.as_secs_f64() * 1000.0;
+ let strict_ms = strict_time.as_secs_f64() * 1000.0;
+ let fast_per_image = fast_ms / images_per_iteration as f64;
+ let strict_per_image = strict_ms / images_per_iteration as f64;
+ println!(
+ "{:.3}x | {:.2} | {:.2} | {:.2} | {:.2}",
+ scale, fast_ms, strict_ms, fast_per_image, strict_per_image
+ );
+ }
+}
diff --git a/crates/grida-canvas/examples/stroke.rs b/crates/grida-canvas/examples/stroke.rs
new file mode 100644
index 0000000000..2e61b9395d
--- /dev/null
+++ b/crates/grida-canvas/examples/stroke.rs
@@ -0,0 +1,397 @@
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+async fn demo_strokes() -> Scene {
+ let nf = NodeFactory::new();
+
+ // Create a root container node
+ let mut root_container_node = nf.create_container_node();
+ root_container_node.base.name = "Root Container".to_string();
+ root_container_node.size = Size {
+ width: 1000.0,
+ height: 1000.0,
+ };
+
+ let mut repository = NodeRepository::new();
+
+ let mut all_shape_ids = Vec::new();
+ let spacing = 120.0;
+ let start_x = 50.0;
+ let base_size = 100.0;
+ let items_per_row = 8;
+
+ // Stroke Alignment Demo Row
+ for i in 0..3 {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Stroke Alignment {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 100.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+
+ // No fill
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0), // Transparent
+ opacity: 1.0,
+ });
+
+ // Solid color stroke
+ rect.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255), // Black stroke
+ opacity: 1.0,
+ });
+ rect.stroke_width = 8.0; // Thick stroke to make alignment visible
+
+ // Set different alignments
+ rect.stroke_align = match i {
+ 0 => StrokeAlign::Inside,
+ 1 => StrokeAlign::Center,
+ 2 => StrokeAlign::Outside,
+ _ => unreachable!(),
+ };
+
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Stroke Width Demo Row
+ for i in 0..items_per_row {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Stroke Width {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 250.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+
+ // No fill
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0), // Transparent
+ opacity: 1.0,
+ });
+
+ // Solid color stroke
+ rect.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255), // Black stroke
+ opacity: 1.0,
+ });
+ rect.stroke_width = (i + 1) as f32 * 2.0; // Increasing stroke width
+ rect.stroke_align = StrokeAlign::Center;
+
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Stroke with Different Shapes Row
+ {
+ // Rectangle
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = "Rectangle Stroke".to_string();
+ rect.transform = AffineTransform::new(start_x, 400.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ rect.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+ rect.stroke_width = 4.0;
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+
+ // Ellipse
+ let mut ellipse = nf.create_ellipse_node();
+ ellipse.base.name = "Ellipse Stroke".to_string();
+ ellipse.transform = AffineTransform::new(start_x + spacing, 400.0, 0.0);
+ ellipse.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ ellipse.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ ellipse.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+ ellipse.stroke_width = 4.0;
+ all_shape_ids.push(ellipse.base.id.clone());
+ repository.insert(Node::Ellipse(ellipse));
+
+ // Regular Polygon (Hexagon)
+ let mut polygon = nf.create_regular_polygon_node();
+ polygon.base.name = "Hexagon Stroke".to_string();
+ polygon.transform = AffineTransform::new(start_x + spacing * 2.0, 400.0, 0.0);
+ polygon.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ polygon.point_count = 6;
+ polygon.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ polygon.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+ polygon.stroke_width = 4.0;
+ all_shape_ids.push(polygon.base.id.clone());
+ repository.insert(Node::RegularPolygon(polygon));
+
+ // Star
+ let mut star = nf.create_regular_star_polygon_node();
+ star.base.name = "Star Stroke".to_string();
+ star.transform = AffineTransform::new(start_x + spacing * 3.0, 400.0, 0.0);
+ star.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ star.point_count = 5;
+ star.inner_radius = 0.4;
+ star.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ star.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+ star.stroke_width = 4.0;
+ all_shape_ids.push(star.base.id.clone());
+ repository.insert(Node::RegularStarPolygon(star));
+ }
+
+ // Stroke with Effects Row
+ for i in 0..3 {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Stroke with Effect {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 550.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+
+ // No fill
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+
+ // Solid color stroke
+ rect.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+ rect.stroke_width = 4.0;
+
+ // Add different effects
+ rect.effect = match i {
+ 0 => Some(FilterEffect::DropShadow(FeDropShadow {
+ dx: 4.0,
+ dy: 4.0,
+ blur: 4.0,
+ color: Color(0, 0, 0, 128),
+ })),
+ 1 => Some(FilterEffect::GaussianBlur(FeGaussianBlur { radius: 2.0 })),
+ 2 => Some(FilterEffect::BackdropBlur(FeBackdropBlur { radius: 4.0 })),
+ _ => unreachable!(),
+ };
+
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Stroke Dash Array Demo Row
+ for i in 0..4 {
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = format!("Stroke Dash Array {}", i + 1);
+ rect.transform = AffineTransform::new(start_x + spacing * i as f32, 700.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+
+ // No fill
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+
+ // Solid color stroke
+ rect.stroke = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 255),
+ opacity: 1.0,
+ });
+ rect.stroke_width = 4.0;
+
+ // Add different dash patterns
+ rect.stroke_dash_array = match i {
+ 0 => Some(vec![5.0, 5.0]), // Basic dashed line
+ 1 => Some(vec![10.0, 5.0]), // Longer dashes
+ 2 => Some(vec![5.0, 5.0, 1.0, 5.0]), // Dash-dot pattern
+ 3 => Some(vec![1.0, 1.0]), // Dotted line
+ _ => unreachable!(),
+ };
+
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Stroke Paint Types Demo Row
+ {
+ // Linear Gradient Stroke
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = "Linear Gradient Stroke".to_string();
+ rect.transform = AffineTransform::new(start_x, 850.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ rect.stroke = Paint::LinearGradient(LinearGradientPaint {
+ transform: AffineTransform::new(0.0, 0.0, 0.0),
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 0, 0, 255), // Red
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(0, 0, 255, 255), // Blue
+ },
+ ],
+ opacity: 1.0,
+ });
+ rect.stroke_width = 8.0;
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+
+ // Radial Gradient Stroke
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = "Radial Gradient Stroke".to_string();
+ rect.transform = AffineTransform::new(start_x + spacing, 850.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ rect.stroke = Paint::RadialGradient(RadialGradientPaint {
+ transform: AffineTransform::new(base_size / 2.0, base_size / 2.0, 0.0),
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(255, 255, 0, 255), // Yellow
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(255, 0, 255, 255), // Magenta
+ },
+ ],
+ opacity: 1.0,
+ });
+ rect.stroke_width = 8.0;
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+
+ // Conic Gradient Stroke
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = "Conic Gradient Stroke".to_string();
+ rect.transform = AffineTransform::new(start_x + spacing * 2.0, 850.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ rect.stroke = Paint::RadialGradient(RadialGradientPaint {
+ transform: AffineTransform::new(base_size / 2.0, base_size / 2.0, 0.0),
+ stops: vec![
+ GradientStop {
+ offset: 0.0,
+ color: Color(0, 255, 0, 255), // Green
+ },
+ GradientStop {
+ offset: 0.5,
+ color: Color(0, 255, 255, 255), // Cyan
+ },
+ GradientStop {
+ offset: 1.0,
+ color: Color(0, 255, 0, 255), // Green
+ },
+ ],
+ opacity: 1.0,
+ });
+ rect.stroke_width = 8.0;
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+
+ // Multi-color Solid Stroke
+ let mut rect = nf.create_rectangle_node();
+ rect.base.name = "Multi-color Stroke".to_string();
+ rect.transform = AffineTransform::new(start_x + spacing * 3.0, 850.0, 0.0);
+ rect.size = Size {
+ width: base_size,
+ height: base_size,
+ };
+ rect.corner_radius = RectangularCornerRadius::all(8.0);
+ rect.fill = Paint::Solid(SolidPaint {
+ color: Color(0, 0, 0, 0),
+ opacity: 1.0,
+ });
+ rect.stroke = Paint::Solid(SolidPaint {
+ color: Color(255, 128, 0, 255), // Orange
+ opacity: 1.0,
+ });
+ rect.stroke_width = 8.0;
+ rect.stroke_dash_array = Some(vec![20.0, 10.0, 5.0, 10.0]); // Complex dash pattern
+ all_shape_ids.push(rect.base.id.clone());
+ repository.insert(Node::Rectangle(rect));
+ }
+
+ // Set up the root container
+ root_container_node.children.extend(all_shape_ids);
+ let root_container_id = root_container_node.base.id.clone();
+ repository.insert(Node::Container(root_container_node));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Strokes Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_container_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_strokes().await;
+ window::run_demo_window(scene).await;
+}
diff --git a/crates/grida-canvas/examples/texts.rs b/crates/grida-canvas/examples/texts.rs
new file mode 100644
index 0000000000..30860d01d2
--- /dev/null
+++ b/crates/grida-canvas/examples/texts.rs
@@ -0,0 +1,182 @@
+use cg::font_loader::FontLoader;
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::window;
+use math2::transform::AffineTransform;
+
+const LOREM: &str = r#"
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sed leo quis orci porta auctor eget nec dui. Nullam egestas tempus sapien quis venenatis. Nullam placerat, elit eu aliquet luctus, risus elit sodales elit, eu iaculis ante lacus nec lacus. Vestibulum eget dolor at orci iaculis malesuada. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque cursus tincidunt accumsan. In hac habitasse platea dictumst. Etiam ultricies laoreet ipsum id pulvinar. Aenean fermentum gravida nisi, et congue lectus interdum et. Cras pellentesque scelerisque quam, ut mollis ligula aliquet ut.
+
+Maecenas convallis nisl non porta consectetur. Nulla scelerisque urna ut massa condimentum hendrerit. Cras eu orci malesuada, ornare est ut, viverra libero. Praesent at turpis ultrices, eleifend leo id, gravida lorem. Aenean eu nunc ac orci aliquam ultricies. Suspendisse mi est, convallis et tincidunt nec, iaculis nec metus. Vestibulum vitae metus nisi. Etiam felis mauris, ullamcorper sed aliquet eu, porttitor eu magna. Vestibulum vel mattis purus, vitae semper tortor. Etiam vestibulum ex id risus viverra vulputate. Aenean euismod lectus tortor, vitae interdum erat blandit sed. Vestibulum accumsan massa vehicula tellus efficitur vehicula. Donec accumsan eget purus sed condimentum. Nunc tempor imperdiet odio a molestie. Phasellus velit nulla, volutpat ac ipsum id, iaculis pretium ipsum.
+
+Cras ac justo iaculis, sollicitudin nisl vel, maximus turpis. Nulla sed nunc elit. Maecenas ultricies auctor mi quis semper. Suspendisse eget rhoncus enim. Morbi tincidunt, urna sed dapibus consequat, ex lorem scelerisque risus, vel auctor libero dui eu diam. Aliquam a rutrum risus. Nunc facilisis, est a rutrum commodo, eros ipsum pulvinar enim, sit amet elementum est dolor quis mi. Nam aliquet, massa eget vestibulum tincidunt, tortor leo dictum arcu, quis eleifend felis ligula in odio. Nullam pharetra mauris ac tortor pharetra ultricies. Aenean in dictum lorem, eu vestibulum libero. Praesent efficitur pretium magna, nec tristique urna condimentum vitae. Aliquam eu nibh quis urna rhoncus porta. Duis lacus leo, tempus ut urna sit amet, dignissim consectetur lorem. Duis luctus scelerisque ultricies. Quisque pharetra feugiat metus in tempor.
+"#;
+
+const LOREM_SHORT: &str = r#"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sed leo quis orci porta auctor eget nec dui. Nullam egestas tempus sapien quis venenatis. Nullam placerat, elit eu aliquet luctus, risus elit sodales elit, eu iaculis ante lacus nec lacus. Vestibulum eget dolor at orci iaculis malesuada. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque cursus tincidunt accumsan. In hac habitasse platea dictumst. Etiam ultricies laoreet ipsum id pulvinar. Aenean fermentum gravida nisi, et congue lectus interdum et. Cras pellentesque scelerisque quam, ut mollis ligula aliquet ut."#;
+
+async fn demo_texts() -> Scene {
+ let nf = NodeFactory::new();
+
+ // Create a single word text span
+ let mut word_text_node = nf.create_text_span_node();
+ word_text_node.base.name = "Word Text".to_string();
+ word_text_node.transform = AffineTransform::new(50.0, 50.0, 0.0);
+ word_text_node.size = Size {
+ width: 400.0,
+ height: 100.0,
+ };
+ word_text_node.text = "Grida Canvas".to_string();
+ word_text_node.text_style = TextStyle {
+ text_decoration: TextDecoration::None,
+ font_family: "Arial".to_string(),
+ font_size: 48.0,
+ italic: false, // TODO: add italic to text style
+ font_weight: FontWeight::new(700), // Bold
+ letter_spacing: None,
+ line_height: None,
+ text_transform: TextTransform::Uppercase,
+ };
+ word_text_node.stroke = Some(Paint::Solid(SolidPaint {
+ color: Color(255, 255, 255, 255),
+ opacity: 1.0,
+ }));
+ word_text_node.stroke_width = Some(1.0);
+ word_text_node.text_align = TextAlign::Left;
+ word_text_node.text_align_vertical = TextAlignVertical::Top;
+
+ // Create a sentence text span
+ let mut sentence_text_node = nf.create_text_span_node();
+ sentence_text_node.base.name = "Sentence Text".to_string();
+ sentence_text_node.transform = AffineTransform::new(50.0, 150.0, 0.0);
+ sentence_text_node.size = Size {
+ width: 500.0,
+ height: 100.0,
+ };
+ sentence_text_node.text =
+ "Grida Canvas Skia Backend provides accurate rendering of Texts and Text layouts"
+ .to_string();
+ sentence_text_node.text_style = TextStyle {
+ text_decoration: TextDecoration::Underline,
+ font_family: "Caveat".to_string(),
+ font_size: 32.0,
+ italic: false, // TODO: add italic to text style
+ font_weight: FontWeight::new(400), // Regular
+ letter_spacing: None,
+ line_height: None,
+ text_transform: TextTransform::None,
+ };
+ sentence_text_node.text_align = TextAlign::Left;
+ sentence_text_node.text_align_vertical = TextAlignVertical::Center;
+
+ // Create a paragraph text span
+ let mut paragraph_text_node = nf.create_text_span_node();
+ paragraph_text_node.base.name = "Paragraph Text".to_string();
+ paragraph_text_node.transform = AffineTransform::new(50.0, 250.0, 0.0);
+ paragraph_text_node.size = Size {
+ width: 800.0,
+ height: 300.0,
+ };
+ paragraph_text_node.text = LOREM.to_string();
+ paragraph_text_node.text_style = TextStyle {
+ text_decoration: TextDecoration::None,
+ font_family: "Arial".to_string(),
+ font_size: 16.0,
+ italic: false, // TODO: add italic to text style
+ font_weight: FontWeight::new(400), // Regular
+ letter_spacing: None,
+ line_height: Some(1.5), // 1.5 line height for better readability
+ text_transform: TextTransform::None,
+ };
+ paragraph_text_node.text_align = TextAlign::Left;
+ paragraph_text_node.text_align_vertical = TextAlignVertical::Top;
+
+ // Create a second paragraph text span with different color
+ let mut second_paragraph_text_node = nf.create_text_span_node();
+ second_paragraph_text_node.base.name = "Second Paragraph Text".to_string();
+ second_paragraph_text_node.transform = AffineTransform::new(50.0, 800.0, 0.0);
+ second_paragraph_text_node.size = Size {
+ width: 800.0,
+ height: 300.0,
+ };
+ second_paragraph_text_node.text = LOREM_SHORT.to_string();
+ second_paragraph_text_node.text_style = TextStyle {
+ text_decoration: TextDecoration::None,
+ font_family: "VT323".to_string(),
+ font_size: 16.0,
+ italic: false, // TODO: add italic to text style
+ font_weight: FontWeight::new(400), // Regular
+ letter_spacing: None,
+ line_height: Some(1.5), // 1.5 line height for better readability
+ text_transform: TextTransform::None,
+ };
+ second_paragraph_text_node.text_align = TextAlign::Left;
+ second_paragraph_text_node.text_align_vertical = TextAlignVertical::Top;
+ second_paragraph_text_node.fill = Paint::Solid(SolidPaint {
+ color: Color(70, 130, 180, 255), // Steel blue color
+ opacity: 1.0,
+ });
+
+ // Create a root container node
+ let mut root_container_node = nf.create_container_node();
+ root_container_node.base.name = "Root Container".to_string();
+ root_container_node.size = Size {
+ width: 1080.0,
+ height: 1080.0,
+ };
+
+ // Create a node repository and add all nodes
+ let mut repository = NodeRepository::new();
+
+ // Collect all the IDs
+ let word_text_id = word_text_node.base.id.clone();
+ let sentence_text_id = sentence_text_node.base.id.clone();
+ let paragraph_text_id = paragraph_text_node.base.id.clone();
+ let second_paragraph_text_id = second_paragraph_text_node.base.id.clone();
+
+ // Add all nodes to the repository
+ repository.insert(Node::TextSpan(word_text_node));
+ repository.insert(Node::TextSpan(sentence_text_node));
+ repository.insert(Node::TextSpan(paragraph_text_node));
+ repository.insert(Node::TextSpan(second_paragraph_text_node));
+
+ // Set up the root container with all IDs
+ root_container_node.children = vec![
+ word_text_id,
+ sentence_text_id,
+ paragraph_text_id,
+ second_paragraph_text_id,
+ ];
+ let root_container_id = root_container_node.base.id.clone();
+ repository.insert(Node::Container(root_container_node));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Text Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_container_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_texts().await;
+ let caveat_font_path = "resources/Caveat-VariableFont_wght.ttf".to_string();
+ let vt323_font_path = "resources/VT323-Regular.ttf".to_string();
+
+ window::run_demo_window_with(scene, move |_renderer, _img_tx, font_tx, proxy| {
+ println!("📝 Loading fonts asynchronously...");
+ let caveat_path = caveat_font_path.clone();
+ let vt323_path = vt323_font_path.clone();
+ tokio::spawn(async move {
+ let mut loader = FontLoader::new_lifecycle(font_tx, proxy);
+ loader.load_font("Caveat", &caveat_path).await;
+ println!("✅ Font loaded: Caveat");
+ loader.load_font("VT323", &vt323_path).await;
+ println!("✅ Font loaded: VT323");
+ });
+ })
+ .await;
+}
diff --git a/crates/grida-canvas/examples/tiling.rs b/crates/grida-canvas/examples/tiling.rs
new file mode 100644
index 0000000000..84ce6eff0f
--- /dev/null
+++ b/crates/grida-canvas/examples/tiling.rs
@@ -0,0 +1,111 @@
+use skia_safe::{Canvas, Color, Image, Paint, Picture, PictureRecorder, Rect, Surface};
+use std::path::Path;
+
+struct TiledScene {
+ picture: Picture,
+ width: f32,
+ height: f32,
+}
+
+impl TiledScene {
+ fn new(width: f32, height: f32) -> Self {
+ // Create a recorder to capture the scene
+ let mut recorder = PictureRecorder::new();
+ let bounds = Rect::new(0.0, 0.0, width, height);
+ let canvas = recorder.begin_recording(bounds, None);
+
+ // Draw 100 rectangles in a grid
+ Self::draw_rectangles(canvas);
+
+ // End recording and create the picture
+ let picture = recorder.finish_recording_as_picture(None).unwrap();
+
+ Self {
+ picture,
+ width,
+ height,
+ }
+ }
+
+ fn draw_rectangles(canvas: &Canvas) {
+ println!("Drawing 100 rectangles");
+ // Draw a grid of rectangles
+ for i in 0..10 {
+ for j in 0..10 {
+ let mut paint = Paint::default();
+ // Create a gradient of colors
+ let r = (i * 25) as u8;
+ let g = (j * 25) as u8;
+ let b = ((i + j) * 12) as u8;
+ paint.set_color(Color::from_argb(255, r, g, b));
+
+ let x = i as f32 * 100.0;
+ let y = j as f32 * 100.0;
+ let rect = Rect::new(x, y, x + 80.0, y + 80.0);
+ canvas.draw_rect(rect, &paint);
+ }
+ }
+ }
+
+ fn render_to_tiles(&self, tile_size: i32) -> Vec {
+ let mut tiles = Vec::new();
+ let cols = (self.width as i32 + tile_size - 1) / tile_size;
+ let rows = (self.height as i32 + tile_size - 1) / tile_size;
+
+ for row in 0..rows {
+ for col in 0..cols {
+ // Create a surface for this tile
+ let mut surface = Surface::new_raster_n32_premul((tile_size, tile_size))
+ .expect("Failed to create surface for tile");
+ let canvas = surface.canvas();
+
+ // Clear the tile
+ canvas.clear(Color::from_argb(255, 255, 255, 255));
+
+ // Calculate the offset for this tile
+ let offset_x = -(col * tile_size) as f32;
+ let offset_y = -(row * tile_size) as f32;
+
+ // Apply the offset
+ canvas.save();
+ canvas.translate((offset_x, offset_y));
+
+ // Draw the picture
+ canvas.draw_picture(&self.picture, None, None);
+
+ canvas.restore();
+
+ // Convert the surface to an image
+ let image = surface.image_snapshot();
+ tiles.push(image);
+ }
+ }
+
+ tiles
+ }
+}
+
+fn save_tiles(tiles: &[Image], output_dir: &str) {
+ let output_path = Path::new(output_dir);
+ std::fs::create_dir_all(output_path).expect("Failed to create output directory");
+
+ for (i, tile) in tiles.iter().enumerate() {
+ let file_path = output_path.join(format!("tile_{:03}.png", i));
+ if let Some(data) = tile.encode_to_data(skia_safe::EncodedImageFormat::PNG) {
+ std::fs::write(file_path, data.as_bytes()).expect("Failed to write tile");
+ }
+ }
+}
+
+fn main() {
+ // Create a scene with 1000x1000 dimensions
+ let scene = TiledScene::new(1000.0, 1000.0);
+
+ // Render the scene into 200x200 tiles
+ let tiles = scene.render_to_tiles(200);
+
+ // Save the tiles to the output directory
+ save_tiles(&tiles, "tiles_output");
+
+ println!("Generated {} tiles in tiles_output directory", tiles.len());
+}
diff --git a/crates/grida-canvas/examples/webfonts.rs b/crates/grida-canvas/examples/webfonts.rs
new file mode 100644
index 0000000000..a25e0883ad
--- /dev/null
+++ b/crates/grida-canvas/examples/webfonts.rs
@@ -0,0 +1,220 @@
+use cg::font_loader::FontLoader;
+use cg::node::factory::NodeFactory;
+use cg::node::repository::NodeRepository;
+use cg::node::schema::*;
+use cg::webfont_helper::{find_font_files_by_family, load_webfonts_metadata};
+use cg::window;
+use futures::future::join_all;
+use math2::transform::AffineTransform;
+
+const PARAGRAPH: &str = r#"
+This demo showcases how multiple static TTF font files—each representing a different weight or style—can be loaded under the same font family name. It verifies that our system can correctly resolve and apply the appropriate font file when rendering text spans with varying font weights and styles. Each text line uses the same font family (“Albert Sans”) but specifies a different combination of weight and italic flag. If the system behaves correctly, the rendered output should match the intended visual style for each variant, demonstrating accurate font resolution and fallback handling within the shared family context.
+"#;
+
+async fn demo_webfonts() -> Scene {
+ let nf = NodeFactory::new();
+
+ // Create a heading with Playfair Display
+ let mut heading_node = nf.create_text_span_node();
+ heading_node.base.name = "Heading".to_string();
+ heading_node.transform = AffineTransform::new(50.0, 50.0, 0.0);
+ heading_node.size = Size {
+ width: 800.0,
+ height: 100.0,
+ };
+ heading_node.text = "Web fonts demo".to_string();
+ heading_node.text_style = TextStyle {
+ text_decoration: TextDecoration::None,
+ font_family: "Playfair Display".to_string(),
+ font_size: 64.0,
+ font_weight: FontWeight::new(700), // Bold
+ letter_spacing: None,
+ italic: false,
+ line_height: None,
+ text_transform: TextTransform::None,
+ };
+ heading_node.text_align = TextAlign::Left;
+ heading_node.text_align_vertical = TextAlignVertical::Top;
+
+ // Create a description paragraph with Playfair Display
+ let mut description_node = nf.create_text_span_node();
+ description_node.base.name = "Description".to_string();
+ description_node.transform = AffineTransform::new(50.0, 120.0, 0.0);
+ description_node.size = Size {
+ width: 800.0,
+ height: 120.0,
+ };
+ description_node.text = PARAGRAPH.to_string();
+ description_node.text_style = TextStyle {
+ text_decoration: TextDecoration::None,
+ font_family: "Playfair Display".to_string(),
+ font_size: 14.0,
+ font_weight: FontWeight::new(400), // Regular
+ letter_spacing: None,
+ italic: false,
+ line_height: Some(1.5), // 1.5 line height for better readability
+ text_transform: TextTransform::None,
+ };
+ description_node.text_align = TextAlign::Left;
+ description_node.text_align_vertical = TextAlignVertical::Top;
+
+ // Create text nodes for Albert Sans variants
+ let mut albert_text_nodes = Vec::new();
+ let variants = [
+ ("Regular", 400, false),
+ ("Thin", 100, false),
+ ("ExtraLight", 200, false),
+ ("Light", 300, false),
+ ("Medium", 500, false),
+ ("SemiBold", 600, false),
+ ("Bold", 700, false),
+ ("ExtraBold", 800, false),
+ ("Black", 900, false),
+ ("ThinItalic", 100, true),
+ ("ExtraLightItalic", 200, true),
+ ("LightItalic", 300, true),
+ ("MediumItalic", 500, true),
+ ("SemiBoldItalic", 600, true),
+ ("BoldItalic", 700, true),
+ ("ExtraBoldItalic", 800, true),
+ ("BlackItalic", 900, true),
+ ];
+
+ for (i, (variant, weight, is_italic)) in variants.iter().enumerate() {
+ let mut text_node = nf.create_text_span_node();
+ text_node.base.name = format!("Albert Sans {}", variant);
+ text_node.transform = AffineTransform::new(50.0, 280.0 + (i as f32 * 40.0), 0.0);
+ text_node.size = Size {
+ width: 800.0,
+ height: 40.0,
+ };
+ text_node.text = format!("AlbertSans {}", variant);
+ text_node.text_style = TextStyle {
+ text_decoration: TextDecoration::None,
+ font_family: "Albert Sans".to_string(),
+ font_size: 24.0,
+ font_weight: FontWeight::new(*weight),
+ letter_spacing: None,
+ italic: *is_italic,
+ line_height: None,
+ text_transform: TextTransform::None,
+ };
+ text_node.text_align = TextAlign::Left;
+ text_node.text_align_vertical = TextAlignVertical::Top;
+ albert_text_nodes.push(text_node);
+ }
+
+ // Create a root container node
+ let mut root_container_node = nf.create_container_node();
+ root_container_node.base.name = "Root Container".to_string();
+ root_container_node.size = Size {
+ width: 1080.0,
+ height: 1080.0,
+ };
+
+ // Create a node repository and add all nodes
+ let mut repository = NodeRepository::new();
+
+ // Collect all the IDs
+ let heading_id = heading_node.base.id.clone();
+ let description_id = description_node.base.id.clone();
+ let albert_text_ids: Vec<_> = albert_text_nodes
+ .iter()
+ .map(|n| n.base.id.clone())
+ .collect();
+
+ // Add all nodes to the repository
+ repository.insert(Node::TextSpan(heading_node));
+ repository.insert(Node::TextSpan(description_node));
+ for text_node in albert_text_nodes {
+ repository.insert(Node::TextSpan(text_node));
+ }
+
+ // Set up the root container with all IDs
+ let mut children = vec![heading_id, description_id];
+ children.extend(albert_text_ids);
+ root_container_node.children = children;
+ let root_container_id = root_container_node.base.id.clone();
+ repository.insert(Node::Container(root_container_node));
+
+ Scene {
+ id: "scene".to_string(),
+ name: "Webfonts Demo".to_string(),
+ transform: AffineTransform::identity(),
+ children: vec![root_container_id],
+ nodes: repository,
+ background_color: Some(Color(250, 250, 250, 255)),
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let scene = demo_webfonts().await;
+
+ // Load webfonts metadata and find matching font files
+ let webfonts_metadata = load_webfonts_metadata()
+ .await
+ .expect("Failed to load webfonts metadata");
+
+ // Get the fonts we need for this demo
+ let required_fonts = vec!["Playfair Display", "Albert Sans"]
+ .into_iter()
+ .map(|s| s.to_string())
+ .collect::>();
+ let font_files = find_font_files_by_family(&webfonts_metadata, &required_fonts);
+
+ println!("\nFound {} matching font files:", font_files.len());
+ for font_file in &font_files {
+ println!("Font: {} ({})", font_file.family, font_file.postscript_name);
+ println!(" Style: {}", font_file.style);
+ println!(" URL: {}", font_file.url);
+ println!();
+ }
+
+ // Clone the scene before passing it to run_demo_window_with
+ let scene_for_window = scene.clone();
+ let font_files_clone = font_files.clone();
+
+ window::run_demo_window_with(
+ scene_for_window,
+ move |_renderer, _img_tx, font_tx, proxy| {
+ println!("📝 Initializing font loader...");
+ // No need to create a FontLoader here
+
+ // Load all fonts in the scene - non-blocking
+ println!("🔄 Starting to load scene fonts in background...");
+ let font_files = font_files_clone.clone();
+ let font_tx = font_tx.clone();
+ let proxy = proxy.clone();
+ tokio::spawn(async move {
+ let font_loading_futures: Vec<_> = font_files
+ .into_iter()
+ .map(|font_file| {
+ let font_tx = font_tx.clone();
+ let proxy = proxy.clone();
+ async move {
+ let family = font_file.family;
+ let style = font_file.style;
+ let url = font_file.url;
+ let postscript_name = font_file.postscript_name;
+ println!("Loading font: {} ({})", family, postscript_name);
+ let mut font_loader = FontLoader::new_lifecycle(font_tx, proxy);
+ font_loader
+ .load_font_with_style(&family, Some(&style), &url)
+ .await;
+ println!("✅ Font loaded: {} ({})", family, postscript_name);
+ }
+ })
+ .collect();
+ join_all(font_loading_futures).await;
+ println!("✅ Scene fonts loading completed in background");
+ println!("\n🔍 Font Repository Information:");
+ println!("================================");
+ println!("All fonts have been loaded and sent to the renderer.");
+ println!("Check the console output above for registration messages.");
+ println!("The renderer's font repository now contains the loaded fonts.");
+ });
+ },
+ )
+ .await;
+}
diff --git a/crates/grida-canvas/examples/winit_raf.rs b/crates/grida-canvas/examples/winit_raf.rs
new file mode 100644
index 0000000000..4e16604e27
--- /dev/null
+++ b/crates/grida-canvas/examples/winit_raf.rs
@@ -0,0 +1,89 @@
+use cg::window::scheduler;
+use rand::Rng;
+use std::time::{Duration, Instant};
+use winit::{
+ application::ApplicationHandler,
+ dpi::LogicalSize,
+ event_loop::EventLoop,
+ window::{Window, WindowAttributes},
+};
+
+// Main application struct holding the window and rendering loop state
+struct App {
+ window: Window,
+ frame_count: u32,
+ start_time: Instant,
+ scheduler: scheduler::FrameScheduler,
+}
+
+impl App {
+ /// Simulates rendering work by sleeping for a few milliseconds
+ fn render(&self) {
+ let mut rng = rand::thread_rng();
+ let render_time = rng.gen_range(1..=5);
+ std::thread::sleep(Duration::from_millis(render_time));
+ }
+}
+
+impl ApplicationHandler for App {
+ fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {}
+
+ fn window_event(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ _window_id: winit::window::WindowId,
+ event: winit::event::WindowEvent,
+ ) {
+ match event {
+ // Exit gracefully when the window is closed
+ winit::event::WindowEvent::CloseRequested => {
+ event_loop.exit();
+ }
+ // Called once per frame when redraw is requested
+ winit::event::WindowEvent::RedrawRequested => {
+ self.render(); // Simulate some frame rendering work
+
+ self.scheduler.sleep_to_maintain_fps(); // Apply pacing (no-op on wasm)
+
+ self.frame_count += 1;
+
+ // Log FPS every second
+ let elapsed = self.start_time.elapsed();
+ if elapsed >= Duration::from_secs(1) {
+ println!("Frames in last second: {}", self.frame_count);
+ self.frame_count = 0;
+ self.start_time = Instant::now();
+ }
+
+ self.window.request_redraw(); // Schedule next frame
+ }
+ _ => {}
+ }
+ }
+}
+
+fn main() {
+ // Set up the winit event loop and window
+ let el = EventLoop::new().expect("Failed to create event loop");
+
+ let window_attributes = WindowAttributes::default()
+ .with_title("Winit RAF Demo")
+ .with_inner_size(LogicalSize::new(800, 600));
+
+ let window = el
+ .create_window(window_attributes)
+ .expect("Failed to create window");
+
+ let now = Instant::now();
+
+ // Initialize application with both a target and max FPS
+ let mut app = App {
+ window,
+ frame_count: 0,
+ start_time: now,
+ scheduler: scheduler::FrameScheduler::new(120).with_max_fps(144),
+ };
+
+ // Start the app's event loop
+ el.run_app(&mut app).unwrap();
+}
diff --git a/crates/grida-canvas/package.json b/crates/grida-canvas/package.json
new file mode 100644
index 0000000000..02b2b3f242
--- /dev/null
+++ b/crates/grida-canvas/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@crates/grida-canvas",
+ "description": "turbo ci rust wrapper",
+ "private": true,
+ "scripts": {
+ "build": "cargo build --release",
+ "test": "cargo test"
+ }
+}
diff --git a/crates/grida-canvas/resources/.gitignore b/crates/grida-canvas/resources/.gitignore
new file mode 100644
index 0000000000..c2c027fec1
--- /dev/null
+++ b/crates/grida-canvas/resources/.gitignore
@@ -0,0 +1 @@
+local
\ No newline at end of file
diff --git a/crates/grida-canvas/resources/4k.jpg b/crates/grida-canvas/resources/4k.jpg
new file mode 100644
index 0000000000..9edcb0a229
Binary files /dev/null and b/crates/grida-canvas/resources/4k.jpg differ
diff --git a/crates/grida-canvas/resources/8k.jpg b/crates/grida-canvas/resources/8k.jpg
new file mode 100644
index 0000000000..c484e95593
Binary files /dev/null and b/crates/grida-canvas/resources/8k.jpg differ
diff --git a/crates/grida-canvas/resources/Bungee-Regular.ttf b/crates/grida-canvas/resources/Bungee-Regular.ttf
new file mode 100644
index 0000000000..e1721aecb5
Binary files /dev/null and b/crates/grida-canvas/resources/Bungee-Regular.ttf differ
diff --git a/crates/grida-canvas/resources/Caveat-VariableFont_wght.ttf b/crates/grida-canvas/resources/Caveat-VariableFont_wght.ttf
new file mode 100644
index 0000000000..5adc65821c
Binary files /dev/null and b/crates/grida-canvas/resources/Caveat-VariableFont_wght.ttf differ
diff --git a/crates/grida-canvas/resources/Fruktur-Regular.ttf b/crates/grida-canvas/resources/Fruktur-Regular.ttf
new file mode 100644
index 0000000000..7dbaae0c2a
Binary files /dev/null and b/crates/grida-canvas/resources/Fruktur-Regular.ttf differ
diff --git a/crates/grida-canvas/resources/VT323-Regular.ttf b/crates/grida-canvas/resources/VT323-Regular.ttf
new file mode 100644
index 0000000000..6aec59930e
Binary files /dev/null and b/crates/grida-canvas/resources/VT323-Regular.ttf differ
diff --git a/crates/grida-canvas/src/cache/README.md b/crates/grida-canvas/src/cache/README.md
new file mode 100644
index 0000000000..481d07eeb9
--- /dev/null
+++ b/crates/grida-canvas/src/cache/README.md
@@ -0,0 +1,75 @@
+# Cache
+
+A high-performance runtime store for managing per-node spatial data in a scene graph.
+
+## Geometry Cache
+
+## Purpose
+
+The Geometry Cache exists to store and maintain computed geometric properties of nodes in a scene, independent from the user-authored structure. It enables fast rendering, culling, and future hit-testing by treating transform and bounds as first-class cached data.
+
+### Responsibilities
+
+- Maintain **relative (local)** and **absolute (world)** transforms.
+- Cache **local** and **world-space bounding boxes**.
+- Propagate **dirty flags** on transform or geometry change.
+- Support optional future features like path-based hit-testing.
+
+### Philosophy
+
+1. **Authoring is relative. Rendering is absolute.**
+
+ - `local_transform` is the source of truth.
+ - `world_transform` is cached and derived.
+
+2. **Flat is fast.**
+
+ - The scene is a tree; the cache is a flat map.
+
+3. **Dirty flags are sacred.**
+
+ - Only update what has changed.
+
+4. **Query is constant-time.**
+
+ - `get_world_bounds(node_id)` is always O(1) after update.
+
+5. **No recursion at runtime.**
+
+ - Updates are done top-down, in pre-sorted order or with explicit parent tracking.
+
+### Core Data Structure
+
+```rust
+struct GeometryEntry {
+ local_transform: Transform2D,
+ world_transform: Transform2D,
+ local_bounds: Rect,
+ world_bounds: Rect,
+ parent: Option,
+ dirty_transform: bool,
+ dirty_bounds: bool,
+ // future: path: Option,
+}
+
+struct GeometryCache {
+ entries: HashMap,
+}
+```
+
+### Operations
+
+- `mark_dirty(node_id)` — recursively mark transform + bounds dirty.
+- `update(node_id)` — resolve world transform + world bounds.
+- `get_world_transform(node_id)` — O(1) after update.
+- `get_world_bounds(node_id)` — O(1) after update.
+
+### Why Not a BVH?
+
+This cache stores per-node spatial data. A BVH is a secondary structure built _from_ this cache — optimized for queries like ray intersections, pointer picking, and region invalidation.
+
+---
+
+**GeometryCache is not just a helper — it is the spatial truth of the engine.**
+
+It is designed to be deterministic, cache-efficient, and ready for future integration with spatial indexing, GPU transform buffers, or collaborative scene systems.
diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs
new file mode 100644
index 0000000000..b27998986e
--- /dev/null
+++ b/crates/grida-canvas/src/cache/geometry.rs
@@ -0,0 +1,505 @@
+use crate::node::repository::NodeRepository;
+use crate::node::schema::{FilterEffect, IntrinsicSizeNode, Node, NodeId, Scene, StrokeAlign};
+use math2::rect;
+use math2::rect::Rectangle;
+use math2::transform::AffineTransform;
+use std::collections::HashMap;
+
+/// Geometry data used for layout, culling, and rendering.
+///
+/// `local_bounds` and `world_bounds` represent the tight geometry bounds of the shape.
+/// `render_bounds` includes visual overflow from effects such as blur, stroke, or shadows,
+/// and is used for visibility culling and picture recording regions.
+///
+/// All bounds are updated during geometry cache construction and reused throughout the pipeline.
+#[derive(Debug, Clone)]
+pub struct GeometryEntry {
+ /// relative transform
+ pub transform: AffineTransform,
+ /// absolute (world) transform
+ pub absolute_transform: AffineTransform,
+ /// relative AABB (after the transform is applied)
+ pub bounding_box: Rectangle,
+ /// absolute (world) AABB (after the transform is applied)
+ pub absolute_bounding_box: Rectangle,
+ /// Expanded bounds that include visual effects like blur, shadow, stroke, etc.
+ /// Used for render-time culling and picture recording.
+ pub absolute_render_bounds: Rectangle,
+ pub parent: Option,
+ pub dirty_transform: bool,
+ pub dirty_bounds: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct GeometryCache {
+ entries: HashMap,
+}
+
+impl GeometryCache {
+ pub fn new() -> Self {
+ Self {
+ entries: HashMap::new(),
+ }
+ }
+
+ pub fn from_scene(scene: &Scene) -> Self {
+ let mut cache = Self::new();
+ let root_world = scene.transform;
+ for child in &scene.children {
+ Self::build_recursive(child, &scene.nodes, &root_world, None, &mut cache);
+ }
+ cache
+ }
+
+ fn build_recursive(
+ id: &NodeId,
+ repo: &NodeRepository,
+ parent_world: &AffineTransform,
+ parent_id: Option,
+ cache: &mut GeometryCache,
+ ) -> Rectangle {
+ let node = repo.get(id).expect("node not found");
+
+ match node {
+ Node::Group(n) => {
+ let world_transform = parent_world.compose(&n.transform);
+ let mut union_bounds: Option = None;
+ let mut union_render_bounds: Option = None;
+ for child_id in &n.children {
+ let child_bounds = Self::build_recursive(
+ child_id,
+ repo,
+ &world_transform,
+ Some(id.clone()),
+ cache,
+ );
+ union_bounds = match union_bounds {
+ Some(b) => Some(rect::union(&[b, child_bounds])),
+ None => Some(child_bounds),
+ };
+ if let Some(rb) = cache.get_render_bounds(child_id) {
+ union_render_bounds = match union_render_bounds {
+ Some(b) => Some(rect::union(&[b, rb])),
+ None => Some(rb),
+ };
+ }
+ }
+
+ let world_bounds = union_bounds.unwrap_or_else(|| Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: 0.0,
+ height: 0.0,
+ });
+
+ let local_bounds = if let Some(inv) = world_transform.inverse() {
+ transform_rect(&world_bounds, &inv)
+ } else {
+ Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: 0.0,
+ height: 0.0,
+ }
+ };
+
+ let render_bounds = union_render_bounds.unwrap_or(world_bounds);
+
+ let entry = GeometryEntry {
+ transform: n.transform,
+ absolute_transform: world_transform,
+ bounding_box: local_bounds,
+ absolute_bounding_box: world_bounds,
+ absolute_render_bounds: render_bounds,
+ parent: parent_id.clone(),
+ dirty_transform: false,
+ dirty_bounds: false,
+ };
+
+ cache.entries.insert(id.clone(), entry.clone());
+ entry.absolute_bounding_box
+ }
+ Node::BooleanOperation(n) => {
+ let world_transform = parent_world.compose(&n.transform);
+ let mut union_bounds: Option = None;
+ for child_id in &n.children {
+ let child_bounds = Self::build_recursive(
+ child_id,
+ repo,
+ &world_transform,
+ Some(id.clone()),
+ cache,
+ );
+ union_bounds = match union_bounds {
+ Some(b) => Some(rect::union(&[b, child_bounds])),
+ None => Some(child_bounds),
+ };
+ }
+
+ let world_bounds = union_bounds.unwrap_or_else(|| Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: 0.0,
+ height: 0.0,
+ });
+
+ let local_bounds = if let Some(inv) = world_transform.inverse() {
+ transform_rect(&world_bounds, &inv)
+ } else {
+ Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: 0.0,
+ height: 0.0,
+ }
+ };
+
+ let render_bounds = compute_render_bounds_from_style(
+ world_bounds,
+ if n.stroke.is_some() {
+ n.stroke_width
+ } else {
+ 0.0
+ },
+ n.stroke_align,
+ n.effect.as_ref(),
+ );
+
+ let entry = GeometryEntry {
+ transform: n.transform,
+ absolute_transform: world_transform,
+ bounding_box: local_bounds,
+ absolute_bounding_box: world_bounds,
+ absolute_render_bounds: render_bounds,
+ parent: parent_id.clone(),
+ dirty_transform: false,
+ dirty_bounds: false,
+ };
+
+ cache.entries.insert(id.clone(), entry.clone());
+ entry.absolute_bounding_box
+ }
+ Node::Container(n) => {
+ let local_transform = n.transform;
+ let world_transform = parent_world.compose(&local_transform);
+ let local_bounds = n.rect();
+ let world_bounds = transform_rect(&local_bounds, &world_transform);
+ let mut union_world_bounds = world_bounds;
+ let render_bounds = compute_render_bounds_from_style(
+ world_bounds,
+ if n.stroke.is_some() {
+ n.stroke_width
+ } else {
+ 0.0
+ },
+ n.stroke_align,
+ n.effect.as_ref(),
+ );
+
+ for child_id in &n.children {
+ let child_bounds = Self::build_recursive(
+ child_id,
+ repo,
+ &world_transform,
+ Some(id.clone()),
+ cache,
+ );
+ union_world_bounds = rect::union(&[union_world_bounds, child_bounds]);
+ }
+
+ let entry = GeometryEntry {
+ transform: local_transform,
+ absolute_transform: world_transform,
+ bounding_box: local_bounds,
+ absolute_bounding_box: world_bounds,
+ absolute_render_bounds: render_bounds,
+ parent: parent_id.clone(),
+ dirty_transform: false,
+ dirty_bounds: false,
+ };
+ cache.entries.insert(id.clone(), entry.clone());
+
+ union_world_bounds
+ }
+ _ => {
+ let intrinsic_node = Box::new(match node {
+ Node::Path(n) => IntrinsicSizeNode::Path(n.clone()),
+ Node::Rectangle(n) => IntrinsicSizeNode::Rectangle(n.clone()),
+ Node::Ellipse(n) => IntrinsicSizeNode::Ellipse(n.clone()),
+ Node::Polygon(n) => IntrinsicSizeNode::Polygon(n.clone()),
+ Node::RegularPolygon(n) => IntrinsicSizeNode::RegularPolygon(n.clone()),
+ Node::RegularStarPolygon(n) => IntrinsicSizeNode::RegularStarPolygon(n.clone()),
+ Node::Line(n) => IntrinsicSizeNode::Line(n.clone()),
+ Node::TextSpan(n) => IntrinsicSizeNode::TextSpan(n.clone()),
+ Node::Image(n) => IntrinsicSizeNode::Image(n.clone()),
+ Node::Container(n) => IntrinsicSizeNode::Container(n.clone()),
+ Node::Error(n) => IntrinsicSizeNode::Error(n.clone()),
+ Node::Group(_) | Node::BooleanOperation(_) => panic!("Unsupported node type"),
+ });
+ let intrinsic = intrinsic_node.as_ref();
+
+ let (local_transform, local_bounds) = node_geometry(intrinsic);
+ let world_transform = parent_world.compose(&local_transform);
+ let world_bounds = transform_rect(&local_bounds, &world_transform);
+ let render_bounds = compute_render_bounds(node, world_bounds);
+
+ let entry = GeometryEntry {
+ transform: local_transform,
+ absolute_transform: world_transform,
+ bounding_box: local_bounds,
+ absolute_bounding_box: world_bounds,
+ absolute_render_bounds: render_bounds,
+ parent: parent_id.clone(),
+ dirty_transform: false,
+ dirty_bounds: false,
+ };
+
+ cache.entries.insert(id.clone(), entry.clone());
+ entry.absolute_bounding_box
+ }
+ }
+ }
+
+ pub fn get_world_transform(&self, id: &NodeId) -> Option {
+ self.entries.get(id).map(|e| e.absolute_transform)
+ }
+
+ pub fn get_world_bounds(&self, id: &NodeId) -> Option {
+ self.entries.get(id).map(|e| e.absolute_bounding_box)
+ }
+
+ /// Return expanded render bounds for a node if available.
+ pub fn get_render_bounds(&self, id: &NodeId) -> Option {
+ self.entries.get(id).map(|e| e.absolute_render_bounds)
+ }
+
+ /// Return the parent NodeId for a given node if available.
+ pub fn get_parent(&self, id: &NodeId) -> Option {
+ self.entries.get(id).and_then(|e| e.parent.clone())
+ }
+
+ pub fn len(&self) -> usize {
+ self.entries.len()
+ }
+
+ pub fn has(&self, id: &NodeId) -> bool {
+ self.entries.contains_key(id)
+ }
+
+ /// filter by node id and its entry data
+ pub fn filter(&self, filter: impl Fn(&NodeId, &GeometryEntry) -> bool) -> Self {
+ Self {
+ entries: self
+ .entries
+ .iter()
+ .filter(|(id, entry)| filter(id, entry))
+ .map(|(id, entry)| (id.clone(), entry.clone()))
+ .collect(),
+ }
+ }
+}
+
+fn node_geometry(node: &IntrinsicSizeNode) -> (AffineTransform, Rectangle) {
+ match node {
+ IntrinsicSizeNode::Error(n) => (n.transform, n.rect()),
+ IntrinsicSizeNode::Container(n) => (n.transform, n.rect()),
+ IntrinsicSizeNode::Rectangle(n) => (n.transform, n.rect()),
+ IntrinsicSizeNode::Ellipse(n) => (n.transform, n.rect()),
+ IntrinsicSizeNode::Polygon(n) => (n.transform, polygon_bounds(&n.points)),
+ IntrinsicSizeNode::RegularPolygon(n) => (n.transform, n.rect()),
+ IntrinsicSizeNode::RegularStarPolygon(n) => (n.transform, n.rect()),
+ IntrinsicSizeNode::Line(n) => (
+ n.transform,
+ Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: n.size.width,
+ height: 0.0,
+ },
+ ),
+ IntrinsicSizeNode::TextSpan(n) => (
+ n.transform,
+ Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: n.size.width,
+ height: n.size.height,
+ },
+ ),
+ IntrinsicSizeNode::Path(n) => (n.transform, path_bounds(&n.data)),
+ IntrinsicSizeNode::Image(n) => (n.transform, n.rect()),
+ }
+}
+
+fn transform_rect(rect: &Rectangle, t: &AffineTransform) -> Rectangle {
+ rect::transform(*rect, t)
+}
+
+fn polygon_bounds(points: &[crate::node::schema::Point]) -> Rectangle {
+ let mut min_x = f32::INFINITY;
+ let mut min_y = f32::INFINITY;
+ let mut max_x = f32::NEG_INFINITY;
+ let mut max_y = f32::NEG_INFINITY;
+ for p in points {
+ min_x = min_x.min(p.x);
+ min_y = min_y.min(p.y);
+ max_x = max_x.max(p.x);
+ max_y = max_y.max(p.y);
+ }
+ if points.is_empty() {
+ Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: 0.0,
+ height: 0.0,
+ }
+ } else {
+ Rectangle {
+ x: min_x,
+ y: min_y,
+ width: max_x - min_x,
+ height: max_y - min_y,
+ }
+ }
+}
+
+fn path_bounds(data: &str) -> Rectangle {
+ if let Some(path) = skia_safe::path::Path::from_svg(data) {
+ let b = path.compute_tight_bounds();
+ Rectangle {
+ x: b.left(),
+ y: b.top(),
+ width: b.width(),
+ height: b.height(),
+ }
+ } else {
+ Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: 0.0,
+ height: 0.0,
+ }
+ }
+}
+
+fn inflate_rect(rect: Rectangle, delta: f32) -> Rectangle {
+ if delta <= 0.0 {
+ return rect;
+ }
+ Rectangle {
+ x: rect.x - delta,
+ y: rect.y - delta,
+ width: rect.width + 2.0 * delta,
+ height: rect.height + 2.0 * delta,
+ }
+}
+
+fn stroke_outset(align: StrokeAlign, width: f32) -> f32 {
+ match align {
+ StrokeAlign::Inside => 0.0,
+ StrokeAlign::Center => width / 2.0,
+ StrokeAlign::Outside => width,
+ }
+}
+
+fn compute_render_bounds_from_style(
+ world_bounds: Rectangle,
+ stroke_width: f32,
+ stroke_align: StrokeAlign,
+ effect: Option<&FilterEffect>,
+) -> Rectangle {
+ let mut bounds = inflate_rect(world_bounds, stroke_outset(stroke_align, stroke_width));
+
+ if let Some(effect) = effect {
+ match effect {
+ FilterEffect::GaussianBlur(blur) => {
+ bounds = inflate_rect(bounds, blur.radius);
+ }
+ FilterEffect::BackdropBlur(blur) => {
+ bounds = inflate_rect(bounds, blur.radius);
+ }
+ FilterEffect::DropShadow(shadow) => {
+ let shadow_rect = inflate_rect(
+ Rectangle {
+ x: world_bounds.x + shadow.dx,
+ y: world_bounds.y + shadow.dy,
+ width: world_bounds.width,
+ height: world_bounds.height,
+ },
+ shadow.blur,
+ );
+ bounds = rect::union(&[bounds, shadow_rect]);
+ }
+ }
+ }
+
+ bounds
+}
+
+fn compute_render_bounds(node: &Node, world_bounds: Rectangle) -> Rectangle {
+ match node {
+ Node::Rectangle(n) => compute_render_bounds_from_style(
+ world_bounds,
+ n.stroke_width,
+ n.stroke_align,
+ n.effect.as_ref(),
+ ),
+ Node::Ellipse(n) => compute_render_bounds_from_style(
+ world_bounds,
+ n.stroke_width,
+ n.stroke_align,
+ n.effect.as_ref(),
+ ),
+ Node::Polygon(n) => compute_render_bounds_from_style(
+ world_bounds,
+ n.stroke_width,
+ n.stroke_align,
+ n.effect.as_ref(),
+ ),
+ Node::RegularPolygon(n) => compute_render_bounds_from_style(
+ world_bounds,
+ n.stroke_width,
+ n.stroke_align,
+ n.effect.as_ref(),
+ ),
+ Node::RegularStarPolygon(n) => compute_render_bounds_from_style(
+ world_bounds,
+ n.stroke_width,
+ n.stroke_align,
+ n.effect.as_ref(),
+ ),
+ Node::Path(n) => compute_render_bounds_from_style(
+ world_bounds,
+ n.stroke_width,
+ n.stroke_align,
+ n.effect.as_ref(),
+ ),
+ Node::Image(n) => compute_render_bounds_from_style(
+ world_bounds,
+ n.stroke_width,
+ n.stroke_align,
+ n.effect.as_ref(),
+ ),
+ Node::Line(n) => {
+ compute_render_bounds_from_style(world_bounds, n.stroke_width, n.stroke_align, None)
+ }
+ Node::TextSpan(n) => compute_render_bounds_from_style(
+ world_bounds,
+ n.stroke_width.unwrap_or(0.0),
+ n.stroke_align,
+ None,
+ ),
+ Node::Container(n) => compute_render_bounds_from_style(
+ world_bounds,
+ if n.stroke.is_some() {
+ n.stroke_width
+ } else {
+ 0.0
+ },
+ n.stroke_align,
+ n.effect.as_ref(),
+ ),
+ Node::Error(_) => world_bounds,
+ Node::Group(_) | Node::BooleanOperation(_) => world_bounds,
+ }
+}
diff --git a/crates/grida-canvas/src/cache/mod.rs b/crates/grida-canvas/src/cache/mod.rs
new file mode 100644
index 0000000000..b89ab90649
--- /dev/null
+++ b/crates/grida-canvas/src/cache/mod.rs
@@ -0,0 +1,4 @@
+pub mod geometry;
+pub mod picture;
+pub mod scene;
+pub mod tile;
diff --git a/crates/grida-canvas/src/cache/picture.rs b/crates/grida-canvas/src/cache/picture.rs
new file mode 100644
index 0000000000..cc2b293f6a
--- /dev/null
+++ b/crates/grida-canvas/src/cache/picture.rs
@@ -0,0 +1,63 @@
+use crate::node::schema::NodeId;
+use skia_safe::Picture;
+use std::collections::HashMap;
+
+/// Configuration for how the scene should be cached.
+///
+/// Currently only the `depth` parameter is used:
+/// - `None` caches the entire scene as a single picture.
+/// - `Some(depth)` caches up to `depth` levels of nodes separately.
+#[derive(Debug, Clone)]
+pub struct PictureCacheStrategy {
+ pub depth: Option,
+}
+
+impl Default for PictureCacheStrategy {
+ fn default() -> Self {
+ Self { depth: None }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct PictureCache {
+ strategy: PictureCacheStrategy,
+ node_pictures: HashMap,
+}
+
+impl PictureCache {
+ pub fn new() -> Self {
+ Self {
+ strategy: PictureCacheStrategy::default(),
+ node_pictures: HashMap::new(),
+ }
+ }
+
+ pub fn strategy(&self) -> &PictureCacheStrategy {
+ &self.strategy
+ }
+
+ pub fn set_strategy(&mut self, strategy: PictureCacheStrategy) {
+ self.strategy = strategy;
+ self.invalidate();
+ }
+
+ pub fn get_node_picture(&self, id: &NodeId) -> Option<&Picture> {
+ self.node_pictures.get(id)
+ }
+
+ pub fn set_node_picture(&mut self, id: NodeId, picture: Picture) {
+ self.node_pictures.insert(id, picture);
+ }
+
+ pub fn len(&self) -> usize {
+ self.node_pictures.len()
+ }
+
+ pub fn depth(&self) -> Option {
+ self.strategy.depth
+ }
+
+ pub fn invalidate(&mut self) {
+ self.node_pictures.clear();
+ }
+}
diff --git a/crates/grida-canvas/src/cache/scene.rs b/crates/grida-canvas/src/cache/scene.rs
new file mode 100644
index 0000000000..71ca70beac
--- /dev/null
+++ b/crates/grida-canvas/src/cache/scene.rs
@@ -0,0 +1,166 @@
+use crate::node::schema::{NodeId, Scene};
+use crate::runtime::camera::Camera2D;
+use crate::{
+ cache::{
+ geometry::GeometryCache,
+ picture::{PictureCache, PictureCacheStrategy},
+ tile::ImageTileCache,
+ },
+ painter::layer::{Layer, LayerList},
+};
+use math2::{rect::Rectangle, vector2::Vector2};
+use rstar::{AABB, RTree, RTreeObject};
+use skia_safe::{Picture, Surface};
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct IndexedLayer {
+ pub index: usize,
+ pub bounds: AABB<[f32; 2]>,
+}
+
+impl RTreeObject for IndexedLayer {
+ type Envelope = AABB<[f32; 2]>;
+
+ fn envelope(&self) -> Self::Envelope {
+ self.bounds
+ }
+}
+
+/// A unified cache storing geometry information and recorded pictures for a scene.
+#[derive(Debug, Clone)]
+pub struct SceneCache {
+ pub layers: LayerList,
+ pub geometry: GeometryCache,
+ pub picture: PictureCache,
+ pub tile: ImageTileCache,
+ pub layer_index: RTree,
+}
+
+impl SceneCache {
+ /// Create a new empty cache with the given picture cache strategy.
+ pub fn new() -> Self {
+ Self {
+ layers: LayerList::default(),
+ geometry: GeometryCache::new(),
+ picture: PictureCache::new(),
+ tile: ImageTileCache::new(),
+ layer_index: RTree::new(),
+ }
+ }
+
+ /// Rebuild the geometry cache from the provided scene.
+ pub fn update_geometry(&mut self, scene: &Scene) {
+ self.geometry = GeometryCache::from_scene(scene);
+ }
+
+ pub fn update_layers(&mut self, scene: &Scene) {
+ self.layers = LayerList::from_scene(scene, &self.geometry);
+ self.layers.layers.sort_by_key(|l| l.z_index());
+ self.layer_index = RTree::new();
+ for (i, layer) in self.layers.layers.iter().enumerate() {
+ if let Some(rb) = self.geometry.get_render_bounds(&layer.id()) {
+ let bounds = AABB::from_corners([rb.x, rb.y], [rb.x + rb.width, rb.y + rb.height]);
+ self.layer_index.insert(IndexedLayer { index: i, bounds });
+ }
+ }
+ }
+
+ /// Access the geometry cache.
+ pub fn geometry(&self) -> &GeometryCache {
+ &self.geometry
+ }
+
+ /// Mutable access to the geometry cache.
+ pub fn geometry_mut(&mut self) -> &mut GeometryCache {
+ &mut self.geometry
+ }
+
+ /// Access the picture cache.
+ pub fn picture(&self) -> &PictureCache {
+ &self.picture
+ }
+
+ /// Mutable access to the picture cache.
+ pub fn picture_mut(&mut self) -> &mut PictureCache {
+ &mut self.picture
+ }
+
+ /// Retrieve the current picture cache strategy.
+ pub fn picture_strategy(&self) -> &PictureCacheStrategy {
+ self.picture.strategy()
+ }
+
+ /// Invalidate all cached data.
+ pub fn invalidate(&mut self) {
+ self.picture.invalidate();
+ }
+
+ /// Return a picture for a specific node if cached.
+ pub fn get_node_picture(&self, id: &NodeId) -> Option<&Picture> {
+ self.picture.get_node_picture(id)
+ }
+
+ /// Store a picture for a node.
+ pub fn set_node_picture(&mut self, id: NodeId, picture: Picture) {
+ self.picture.set_node_picture(id, picture);
+ }
+
+ /// Query painter layer indices whose bounds intersect with the given rectangle.
+ /// This includes layers that are:
+ /// - Fully contained within the rectangle
+ /// - Partially overlapping with the rectangle
+ /// - Touching the rectangle's edges
+ pub fn intersects(&self, rect: Rectangle) -> Vec {
+ let env = AABB::from_corners(
+ [rect.x, rect.y],
+ [rect.x + rect.width, rect.y + rect.height],
+ );
+ self.layer_index
+ .locate_in_envelope_intersecting(&env)
+ .map(|il| il.index)
+ .collect()
+ }
+
+ /// Query painter layer indices whose bounds are fully contained within the given rectangle.
+ /// This only includes layers that are completely inside the rectangle, not touching its edges.
+ pub fn contains(&self, rect: &Rectangle) -> Vec {
+ // Get layers that are fully contained
+ let env = AABB::from_corners(
+ [rect.x, rect.y],
+ [rect.x + rect.width, rect.y + rect.height],
+ );
+ self.layer_index
+ .locate_in_envelope(&env)
+ .map(|il| il.index)
+ .collect()
+ }
+
+ /// Query painter layer indices whose bounds contain the given point.
+ pub fn intersects_point(&self, point: Vector2) -> Vec {
+ let env = AABB::from_point([point[0], point[1]]);
+ self.layer_index
+ .locate_in_envelope_intersecting(&env)
+ .map(|il| il.index)
+ .collect()
+ }
+
+ /// Update raster tile cache using the given camera and surface.
+ pub fn update_tiles(
+ &mut self,
+ camera: &Camera2D,
+ surface: &mut Surface,
+ width: f32,
+ height: f32,
+ ) {
+ let index = &self.layer_index;
+ let intersects = |rect: Rectangle| {
+ let env = AABB::from_corners(
+ [rect.x, rect.y],
+ [rect.x + rect.width, rect.y + rect.height],
+ );
+ index.locate_in_envelope_intersecting(&env).next().is_some()
+ };
+ self.tile
+ .update_tiles(camera, width, height, surface, intersects);
+ }
+}
diff --git a/crates/grida-canvas/src/cache/tile.rs b/crates/grida-canvas/src/cache/tile.rs
new file mode 100644
index 0000000000..543970578a
--- /dev/null
+++ b/crates/grida-canvas/src/cache/tile.rs
@@ -0,0 +1,438 @@
+use crate::runtime::camera::Camera2D;
+use math2::rect::{self, Rectangle};
+use skia_safe::{IRect, Image, Surface};
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::time::{Duration, Instant};
+
+/// Debounce duration after zooming before capturing tiles
+const CACHE_DEBOUNCE_BY_ZOOM: Duration = Duration::from_millis(500);
+
+/// (x, y, width, height) in canvas space
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub struct TileRectKey(pub i32, pub i32, pub u32, pub u32);
+
+impl TileRectKey {
+ pub fn to_rect(&self) -> Rectangle {
+ Rectangle {
+ x: self.0 as f32,
+ y: self.1 as f32,
+ width: self.2 as f32,
+ height: self.3 as f32,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct TileAtZoom {
+ pub image: Rc,
+ pub zoom: f32,
+}
+
+/// Information about a tile including whether it should be blurred
+#[derive(Debug, Clone)]
+pub struct RegionTileInfo {
+ pub key: TileRectKey,
+ /// When true, the tile should be blurred as it was captured at a lower zoom level
+ /// (lower resolution) than the current view
+ pub blur: bool,
+ /// The blur radius to use for this tile (adaptive by zoom difference)
+ pub blur_radius: f32,
+ /// The zoom level at which this tile was snapshotted
+ pub zoom: f32,
+}
+
+/// A collection of tiles for a specific region with blur information and sorting.
+/// This encapsulates the logic for retrieving tiles from the cache and calculating
+/// blur parameters based on zoom differences.
+#[derive(Debug, Clone)]
+pub struct RegionTiles {
+ pub tiles: Vec,
+ pub tile_rects: Vec,
+}
+
+impl RegionTiles {
+ /// Create a new RegionTiles instance with tiles filtered from the cache
+ /// for the given bounds and current zoom level.
+ pub fn new(cache: &ImageTileCache, bounds: &Rectangle, current_zoom: f32) -> Self {
+ let mut tiles: Vec = Vec::new();
+ let mut tile_rects: Vec = Vec::new();
+
+ const BLUR_SCALE: f32 = 2.0;
+ const MAX_BLUR_RADIUS: f32 = 16.0;
+
+ // Filter tiles that intersect with the current viewport bounds
+ for key in cache.filter(bounds) {
+ let tile_at_zoom = cache.get_tile(key);
+ let (should_blur, blur_radius, tile_zoom) = if let Some(tile) = tile_at_zoom {
+ let zoom_diff = current_zoom / tile.zoom;
+ if zoom_diff > 1.0 + f32::EPSILON {
+ let blur_radius = ((zoom_diff - 1.0) * BLUR_SCALE).clamp(0.0, MAX_BLUR_RADIUS);
+ (true, blur_radius, tile.zoom)
+ } else {
+ (false, 0.0, tile.zoom)
+ }
+ } else {
+ (false, 0.0, 0.0)
+ };
+
+ tiles.push(RegionTileInfo {
+ key: *key,
+ blur: should_blur,
+ blur_radius,
+ zoom: tile_zoom,
+ });
+ tile_rects.push(key.to_rect());
+ }
+
+ // Sort tiles by zoom difference from current zoom (closest to current zoom last)
+ // This ensures highest quality tiles are drawn on top
+ tiles.sort_by(|a, b| {
+ let diff_a = (current_zoom - a.zoom).abs();
+ let diff_b = (current_zoom - b.zoom).abs();
+ diff_b
+ .partial_cmp(&diff_a)
+ .unwrap_or(std::cmp::Ordering::Equal)
+ });
+
+ Self { tiles, tile_rects }
+ }
+
+ /// Get the tile information for rendering
+ pub fn tiles(&self) -> &[RegionTileInfo] {
+ &self.tiles
+ }
+
+ /// Get the tile rectangles for region calculation
+ pub fn tile_rects(&self) -> &[Rectangle] {
+ &self.tile_rects
+ }
+
+ /// Get the number of tiles in this region
+ pub fn len(&self) -> usize {
+ self.tiles.len()
+ }
+
+ /// Check if this region has any tiles
+ pub fn is_empty(&self) -> bool {
+ self.tiles.is_empty()
+ }
+}
+
+/// Simple raster tile cache used by the renderer.
+///
+/// This cache maintains two types of tiles:
+/// 1. Regular tiles at various zoom levels for current viewport coverage
+/// 2. Lowest zoom tiles as explicit fallbacks for zoom out optimization
+///
+/// Why we save lowest zoom tiles explicitly:
+/// 1. Zoom out optimization: When zooming out, we need tiles at lower zoom levels.
+/// Without explicit lowest zoom caching, we'd have no way to optimize zoom out
+/// as we'd need to re-render everything at the new zoom level.
+/// 2. Memory efficiency: Lowest zoom means fewer tiles to cover the same area,
+/// keeping memory usage minimal while providing essential fallback coverage.
+/// 3. Progressive quality: Allows smooth transitions between zoom levels by
+/// providing immediate fallback tiles while higher quality tiles load.
+#[derive(Debug, Clone)]
+pub struct ImageTileCache {
+ tile_size: u16,
+ tiles: HashMap,
+ /// The lowest zoom level resolved, even if at least 1 tile exists.
+ /// This is used to track the most zoomed-out level we've cached,
+ /// ensuring we always have fallback tiles for zoom out operations.
+ lowest_zoom: Option,
+ /// Keys to tiles that are at the lowest zoom level.
+ /// These tiles are protected from being cleared during zoom changes
+ /// and serve as essential fallbacks for zoom out optimization.
+ /// Since lowest zoom means fewer tiles to cover the same area,
+ /// keeping these in memory is memory-efficient while providing
+ /// critical coverage for smooth zoom out operations.
+ lowest_zoom_indices: std::collections::HashSet,
+ prev_zoom: Option,
+ zoom_changed_at: Option,
+ /// Caching is enabled while the camera zoom is at or below this level.
+ /// When zooming in beyond this value the cache is cleared and disabled
+ /// as the picture based rendering is fast enough.
+ pub max_zoom_for_cache: f32,
+ /// Flag indicating that a full repaint is required to refresh tiles
+ /// after the cache was cleared due to a zoom change.
+ needs_full_repaint: bool,
+}
+
+impl Default for ImageTileCache {
+ fn default() -> Self {
+ Self {
+ tile_size: 512,
+ tiles: HashMap::new(),
+ lowest_zoom: None,
+ lowest_zoom_indices: std::collections::HashSet::new(),
+ prev_zoom: None,
+ zoom_changed_at: None,
+ max_zoom_for_cache: 2.0,
+ needs_full_repaint: false,
+ }
+ }
+}
+
+impl ImageTileCache {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn from_size(tile_size: u16) -> Self {
+ Self {
+ tile_size,
+ tiles: HashMap::new(),
+ lowest_zoom: None,
+ lowest_zoom_indices: std::collections::HashSet::new(),
+ prev_zoom: None,
+ zoom_changed_at: None,
+ max_zoom_for_cache: 2.0,
+ needs_full_repaint: false,
+ }
+ }
+
+ pub fn with_max_zoom(mut self, max_zoom: f32) -> Self {
+ self.max_zoom_for_cache = max_zoom;
+ self
+ }
+
+ /// Access currently cached raster tiles.
+ pub fn tiles(&self) -> &HashMap {
+ &self.tiles
+ }
+
+ /// Get the lowest zoom level resolved.
+ pub fn lowest_zoom(&self) -> Option {
+ self.lowest_zoom
+ }
+
+ /// Get the keys of tiles at the lowest zoom level.
+ pub fn lowest_zoom_indices(&self) -> &std::collections::HashSet {
+ &self.lowest_zoom_indices
+ }
+
+ /// Get a tile from regular storage only.
+ /// Lowest zoom tiles are maintained separately for future use.
+ pub fn get_tile(&self, key: &TileRectKey) -> Option<&TileAtZoom> {
+ self.tiles.get(key)
+ }
+
+ /// Get all tiles that intersect with the given bounds (regular tiles only).
+ /// Returns tiles sorted by zoom level (lowest first) so lower resolution tiles
+ /// are drawn below higher resolution tiles.
+ pub fn filter(&self, bounds: &Rectangle) -> Vec<&TileRectKey> {
+ let mut keys: Vec<&TileRectKey> = self
+ .tiles
+ .iter()
+ .filter_map(|(key, _)| {
+ if rect::intersects(&key.to_rect(), bounds) {
+ Some(key)
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ // Sort by zoom level in ascending order (lowest zoom first)
+ keys.sort_by(|a, b| {
+ let zoom_a = self
+ .tiles
+ .get(*a)
+ .map(|tile| tile.zoom)
+ .unwrap_or(f32::INFINITY);
+ let zoom_b = self
+ .tiles
+ .get(*b)
+ .map(|tile| tile.zoom)
+ .unwrap_or(f32::INFINITY);
+ zoom_a
+ .partial_cmp(&zoom_b)
+ .unwrap_or(std::cmp::Ordering::Equal)
+ });
+
+ keys
+ }
+
+ /// Remove all cached tiles (but preserve lowest zoom tiles).
+ ///
+ /// Lowest zoom tiles are preserved because:
+ /// 1. They provide essential fallback coverage for zoom out operations
+ /// 2. They represent minimal memory usage while covering large areas
+ /// 3. Without them, zoom out would require expensive re-rendering
+ pub fn clear(&mut self) {
+ // Only clear tiles that are not in lowest_zoom_indices
+ self.tiles
+ .retain(|key, _| self.lowest_zoom_indices.contains(key));
+ self.needs_full_repaint = true;
+ }
+
+ /// Remove all tiles including lowest zoom tiles.
+ pub fn clear_all(&mut self) {
+ self.tiles.clear();
+ self.lowest_zoom_indices.clear();
+ self.lowest_zoom = None;
+ self.needs_full_repaint = true;
+ }
+
+ /// Returns true if the cache should repaint all tiles due to a zoom change
+ /// that has settled for the debounce duration.
+ pub fn should_repaint_all(&self) -> bool {
+ self.zoom_changed_at
+ .map(|t| t.elapsed() >= CACHE_DEBOUNCE_BY_ZOOM)
+ .unwrap_or(false)
+ }
+
+ /// Returns true if a full repaint is required to refresh tiles
+ pub fn needs_full_repaint(&self) -> bool {
+ self.needs_full_repaint
+ }
+
+ /// Mark that the pending full repaint request has been handled
+ pub fn reset_full_repaint(&mut self) {
+ self.needs_full_repaint = false;
+ }
+
+ /// Whether tiles should be cached based on zoom change debounce.
+ pub fn should_cache_tiles(&self) -> bool {
+ self.zoom_changed_at
+ .map(|t| t.elapsed() >= CACHE_DEBOUNCE_BY_ZOOM)
+ .unwrap_or(true)
+ }
+
+ /// Notify the cache about a camera zoom change.
+ pub fn update_zoom(&mut self, zoom: f32) {
+ if self
+ .prev_zoom
+ .map_or(true, |z| (z - zoom).abs() > f32::EPSILON)
+ {
+ if zoom > self.max_zoom_for_cache {
+ // Disable caching when sufficiently zoomed in - picture mode
+ // is fast enough at this scale.
+ self.clear();
+ self.zoom_changed_at = None;
+ } else {
+ // Mark tiles as outdated so they are refreshed after debounce.
+ // Note: lowest_zoom_indices are preserved
+ self.zoom_changed_at = Some(Instant::now());
+ }
+ self.prev_zoom = Some(zoom);
+ }
+ }
+
+ /// Capture visible region tiles from the provided surface.
+ pub fn update_tiles(
+ &mut self,
+ camera: &Camera2D,
+ width: f32,
+ height: f32,
+ surface: &mut Surface,
+ mut intersects: F,
+ ) where
+ F: FnMut(Rectangle) -> bool,
+ {
+ let zoom = camera.get_zoom();
+ if zoom > self.max_zoom_for_cache {
+ // Caching disabled when zoomed in beyond the threshold
+ self.clear();
+ self.zoom_changed_at = None;
+ return;
+ }
+
+ let world_size = self.tile_size as f32 / zoom;
+ let rect = camera.rect();
+
+ let start_col = (rect.x / world_size).floor() as i32;
+ let end_col = ((rect.x + rect.width) / world_size).ceil() as i32;
+ let start_row = (rect.y / world_size).floor() as i32;
+ let end_row = ((rect.y + rect.height) / world_size).ceil() as i32;
+
+ for col in start_col..end_col {
+ for row in start_row..end_row {
+ let world_rect = Rectangle {
+ x: col as f32 * world_size,
+ y: row as f32 * world_size,
+ width: world_size,
+ height: world_size,
+ };
+
+ if !intersects(world_rect) {
+ continue;
+ }
+
+ let screen_rect = rect::transform(world_rect, &camera.view_matrix());
+
+ if screen_rect.x >= 0.0
+ && screen_rect.y >= 0.0
+ && screen_rect.x + screen_rect.width <= width
+ && screen_rect.y + screen_rect.height <= height
+ {
+ let key = TileRectKey(
+ world_rect.x.round() as i32,
+ world_rect.y.round() as i32,
+ world_rect.width.round() as u32,
+ world_rect.height.round() as u32,
+ );
+
+ // Check if we should update lowest zoom tracking
+ let should_update_lowest_zoom = if let Some(current_lowest) = self.lowest_zoom {
+ if zoom < current_lowest {
+ // New lower zoom level found, clear previous lowest zoom indices
+ self.lowest_zoom_indices.clear();
+ true
+ } else if (zoom - current_lowest).abs() < f32::EPSILON {
+ // Same zoom level, add to lowest zoom indices
+ true
+ } else {
+ // Higher zoom level, don't update lowest zoom tracking
+ false
+ }
+ } else {
+ // No lowest zoom set yet, this becomes the lowest
+ true
+ };
+
+ if should_update_lowest_zoom {
+ self.lowest_zoom = Some(zoom);
+ self.lowest_zoom_indices.insert(key);
+ }
+
+ // Always update regular tiles if they don't exist or if we have a higher zoom (better quality)
+ let should_update_tile = if let Some(existing_tile) = self.tiles.get(&key) {
+ // Update if the new tile has higher zoom (better quality)
+ zoom > existing_tile.zoom
+ } else {
+ // No existing tile, so add it
+ true
+ };
+
+ if should_update_tile {
+ if let Some(image) = surface.image_snapshot_with_bounds(IRect::from_xywh(
+ screen_rect.x as i32,
+ screen_rect.y as i32,
+ self.tile_size as i32,
+ self.tile_size as i32,
+ )) {
+ self.tiles.insert(
+ key,
+ TileAtZoom {
+ image: Rc::new(image),
+ zoom,
+ },
+ );
+ }
+ }
+ }
+ }
+ }
+ self.zoom_changed_at = None;
+ self.needs_full_repaint = false;
+ }
+
+ /// Get tiles for a specific region with blur information and sorting.
+ /// This encapsulates the logic for filtering tiles, calculating blur parameters,
+ /// and sorting by quality for optimal rendering.
+ pub fn get_region_tiles(&self, bounds: &Rectangle, current_zoom: f32) -> RegionTiles {
+ RegionTiles::new(self, bounds, current_zoom)
+ }
+}
diff --git a/crates/grida-canvas/src/font_loader.rs b/crates/grida-canvas/src/font_loader.rs
new file mode 100644
index 0000000000..53c5415a59
--- /dev/null
+++ b/crates/grida-canvas/src/font_loader.rs
@@ -0,0 +1,167 @@
+use std::collections::HashMap;
+
+use async_trait::async_trait;
+
+use crate::resource_loader::ResourceLoader;
+
+#[cfg(not(target_arch = "wasm32"))]
+use reqwest;
+#[cfg(not(target_arch = "wasm32"))]
+use tokio::sync::mpsc;
+#[cfg(not(target_arch = "wasm32"))]
+use winit::event_loop::EventLoopProxy;
+
+/// Represents a font loading mode
+#[cfg(not(target_arch = "wasm32"))]
+pub enum FontLoadingMode {
+ /// Simple mode - direct loading without lifecycle management
+ Simple,
+ /// Lifecycle mode - full lifecycle management with async loading
+ Lifecycle {
+ tx: mpsc::UnboundedSender,
+ proxy: EventLoopProxy<()>,
+ },
+}
+
+#[cfg(target_arch = "wasm32")]
+pub enum FontLoadingMode {
+ /// Simple mode - direct loading without lifecycle management
+ Simple,
+}
+
+/// Message type for font loading
+#[derive(Debug, Clone)]
+pub struct FontMessage {
+ pub family: String,
+ pub style: Option,
+ pub data: Vec,
+}
+
+/// Manages font loading and caching
+pub struct FontLoader {
+ mode: FontLoadingMode,
+ cache: HashMap>,
+}
+
+impl FontLoader {
+ /// Create a new FontLoader with the specified mode
+ pub fn new(mode: FontLoadingMode) -> Self {
+ Self {
+ mode,
+ cache: HashMap::new(),
+ }
+ }
+
+ /// Create a simple font loader without lifecycle management
+ pub fn new_simple() -> Self {
+ Self::new(FontLoadingMode::Simple)
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ /// Create a lifecycle-based font loader
+ pub fn new_lifecycle(
+ tx: mpsc::UnboundedSender,
+ proxy: EventLoopProxy<()>,
+ ) -> Self {
+ Self::new(FontLoadingMode::Lifecycle { tx, proxy })
+ }
+
+ /// Load a font from a URL or file path
+ pub async fn load_font(&mut self, family: &str, src: &str) -> Option> {
+ self.load_font_with_style(family, None, src).await
+ }
+
+ /// Load a font from a URL or file path with style information
+ pub async fn load_font_with_style(
+ &mut self,
+ family: &str,
+ style: Option<&str>,
+ src: &str,
+ ) -> Option> {
+ // Check cache first
+ let cache_key = if let Some(style) = style {
+ format!("{}:{}", family, style)
+ } else {
+ family.to_string()
+ };
+
+ if let Some(data) = self.cache.get(&cache_key) {
+ return Some(data.clone());
+ }
+
+ // Load the font
+ let data = match self.fetch_font_data(src).await {
+ Ok(data) => data,
+ Err(e) => {
+ eprintln!("Failed to load font {}: {}", src, e);
+ return None;
+ }
+ };
+
+ // Cache the data
+ self.cache.insert(cache_key, data.clone());
+
+ // If in lifecycle mode, send the font data through the channel
+ #[cfg(not(target_arch = "wasm32"))]
+ if let FontLoadingMode::Lifecycle { tx, proxy } = &self.mode {
+ let _ = tx.send(FontMessage {
+ family: family.to_string(),
+ style: style.map(|s| s.to_string()),
+ data: data.clone(),
+ });
+ let _ = proxy.send_event(());
+ }
+
+ Some(data)
+ }
+
+ /// Fetch font data from URL or file
+ async fn fetch_font_data(&self, path: &str) -> Result, Box> {
+ #[cfg(not(target_arch = "wasm32"))]
+ if path.starts_with("http") {
+ let response = reqwest::get(path).await?;
+ Ok(response.bytes().await?.to_vec())
+ } else {
+ Ok(std::fs::read(path)?)
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ Err("Font loading not supported in wasm".into())
+ }
+
+ /// Clear the font cache
+ pub fn clear_cache(&mut self) {
+ self.cache.clear();
+ }
+
+ /// Remove a specific font from the cache
+ pub fn remove_from_cache(&mut self, family: &str) {
+ self.cache.remove(family);
+ }
+}
+
+#[async_trait]
+impl ResourceLoader for FontLoader {
+ type Output = Vec;
+
+ async fn load(&mut self, key: &str, src: &str) -> Option {
+ self.load_font(key, src).await
+ }
+
+ async fn unload(&mut self, key: &str) {
+ self.remove_from_cache(key);
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub async fn load_font(path: &str) -> Result, Box> {
+ let response = reqwest::get(path).await?;
+ let bytes = response.bytes().await?;
+ Ok(bytes.to_vec())
+}
+
+#[cfg(target_arch = "wasm32")]
+pub async fn load_font(path: &str) -> Result, Box> {
+ // Stub for wasm
+ Err("Font loading not supported in wasm".into())
+}
diff --git a/crates/grida-canvas/src/hit_test.rs b/crates/grida-canvas/src/hit_test.rs
new file mode 100644
index 0000000000..706a14db26
--- /dev/null
+++ b/crates/grida-canvas/src/hit_test.rs
@@ -0,0 +1,80 @@
+use crate::cache::scene::SceneCache;
+use crate::node::schema::NodeId;
+use crate::painter::layer::Layer;
+use math2::rect;
+use math2::vector2::Vector2;
+
+/// Hit testing utilities for [`SceneCache`].
+///
+/// This module implements a simple geometry based hit tester. It queries
+/// [`GeometryCache`] bounds stored inside a [`SceneCache`] and returns the node
+/// identifiers that intersect a screen point.
+///
+/// Hit testing happens in a few steps:
+/// 1. Filter nodes whose render bounds contain the point
+/// 2. Sort the filtered nodes by z-index (which reflects tree order)
+/// 3. Return the first match (path level checks TBD)
+///
+/// The sorted order mirrors DOM hit testing behaviour where the deepest node is
+/// evaluated first. Step three is left as a TODO until more reliable path
+/// testing is implemented.
+#[derive(Debug)]
+pub struct HitTester<'a> {
+ cache: &'a SceneCache,
+}
+
+impl<'a> HitTester<'a> {
+ /// Create a new [`HitTester`] backed by the given scene cache.
+ pub fn new(cache: &'a SceneCache) -> Self {
+ Self { cache }
+ }
+
+ /// Returns the top-most node containing the point, if any.
+ ///
+ /// Layers are checked from deepest to shallowest, so the first match mimics
+ /// DOM hit testing semantics. This stops as soon as a match is found,
+ /// making it faster when only one result is needed.
+ pub fn hit_first(&self, point: Vector2) -> Option {
+ let mut indices = self.cache.intersects_point(point);
+ indices.sort();
+ for idx in indices.into_iter().rev() {
+ let layer = &self.cache.layers.layers[idx];
+ if let Some(bounds) = self.cache.geometry.get_render_bounds(layer.id()) {
+ if rect::contains_point(&bounds, point) {
+ // TODO: perform precise path hit testing
+ return Some(layer.id().clone());
+ }
+ }
+ }
+ None
+ }
+
+ /// Returns all nodes containing the point ordered from top to bottom.
+ ///
+ /// The returned vector is sorted from deepest to shallowest, mirroring how
+ /// events bubble in typical DOM systems.
+ pub fn hits(&self, point: Vector2) -> Vec {
+ let mut indices = self.cache.intersects_point(point);
+ indices.sort();
+ let mut out = Vec::with_capacity(indices.len());
+ for idx in indices.into_iter().rev() {
+ let layer = &self.cache.layers.layers[idx];
+ if let Some(bounds) = self.cache.geometry.get_render_bounds(layer.id()) {
+ if rect::contains_point(&bounds, point) {
+ out.push(layer.id().clone());
+ }
+ }
+ }
+ out
+ }
+
+ /// Returns `true` if the specified node contains the point within its
+ /// render bounds.
+ pub fn contains(&self, id: &NodeId, point: Vector2) -> bool {
+ self.cache
+ .geometry
+ .get_render_bounds(id)
+ .map(|b| rect::contains_point(&b, point))
+ .unwrap_or(false)
+ }
+}
diff --git a/crates/grida-canvas/src/image_loader.rs b/crates/grida-canvas/src/image_loader.rs
new file mode 100644
index 0000000000..69c1b0b9b1
--- /dev/null
+++ b/crates/grida-canvas/src/image_loader.rs
@@ -0,0 +1,179 @@
+use crate::node::schema::*;
+use std::collections::HashMap;
+
+use async_trait::async_trait;
+
+use crate::resource_loader::ResourceLoader;
+
+#[cfg(not(target_arch = "wasm32"))]
+use reqwest;
+#[cfg(not(target_arch = "wasm32"))]
+use tokio::sync::mpsc;
+#[cfg(not(target_arch = "wasm32"))]
+use winit::event_loop::EventLoopProxy;
+
+/// Represents an image loading mode
+#[cfg(not(target_arch = "wasm32"))]
+pub enum ImageLoadingMode {
+ /// Simple mode - direct loading without lifecycle management
+ Simple,
+ /// Lifecycle mode - full lifecycle management with async loading
+ Lifecycle {
+ tx: mpsc::UnboundedSender,
+ proxy: EventLoopProxy<()>,
+ },
+}
+
+#[cfg(target_arch = "wasm32")]
+pub enum ImageLoadingMode {
+ /// Simple mode - direct loading without lifecycle management
+ Simple,
+}
+
+/// Message type for image loading
+#[derive(Debug, Clone)]
+pub struct ImageMessage {
+ pub src: String,
+ pub data: Vec,
+}
+
+/// Manages image loading and caching
+pub struct ImageLoader {
+ mode: ImageLoadingMode,
+ cache: HashMap>,
+}
+
+impl ImageLoader {
+ /// Create a new ImageLoader with the specified mode
+ pub fn new(mode: ImageLoadingMode) -> Self {
+ Self {
+ mode,
+ cache: HashMap::new(),
+ }
+ }
+
+ /// Create a simple image loader without lifecycle management
+ pub fn new_simple() -> Self {
+ Self::new(ImageLoadingMode::Simple)
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ /// Create a lifecycle-based image loader
+ pub fn new_lifecycle(
+ tx: mpsc::UnboundedSender,
+ proxy: EventLoopProxy<()>,
+ ) -> Self {
+ Self::new(ImageLoadingMode::Lifecycle { tx, proxy })
+ }
+
+ /// Load an image from a URL or file path
+ pub async fn load_image(&mut self, src: &str) -> Option> {
+ // Check cache first
+ if let Some(data) = self.cache.get(src) {
+ return Some(data.clone());
+ }
+
+ // Load the image
+ let data = match self.fetch_image_data(src).await {
+ Ok(data) => data,
+ Err(_e) => {
+ // eprintln!("Failed to load image {}: {}", src, e);
+ return None;
+ }
+ };
+
+ // Cache the data
+ self.cache.insert(src.to_string(), data.clone());
+
+ // If in lifecycle mode, send the image data through the channel
+ #[cfg(not(target_arch = "wasm32"))]
+ if let ImageLoadingMode::Lifecycle { tx, proxy } = &self.mode {
+ let _ = tx.send(ImageMessage {
+ src: src.to_string(),
+ data: data.clone(),
+ });
+ let _ = proxy.send_event(());
+ }
+
+ Some(data)
+ }
+
+ /// Fetch image data from URL or file
+ async fn fetch_image_data(&self, path: &str) -> Result, Box> {
+ #[cfg(not(target_arch = "wasm32"))]
+ if path.starts_with("http") {
+ let response = reqwest::get(path).await?;
+ Ok(response.bytes().await?.to_vec())
+ } else {
+ Ok(std::fs::read(path)?)
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ Err("Image loading not supported in wasm".into())
+ }
+
+ /// Clear the image cache
+ pub fn clear_cache(&mut self) {
+ self.cache.clear();
+ }
+
+ /// Remove a specific image from the cache
+ pub fn remove_from_cache(&mut self, src: &str) {
+ self.cache.remove(src);
+ }
+}
+
+#[async_trait]
+impl ResourceLoader for ImageLoader {
+ type Output = Vec;
+
+ async fn load(&mut self, key: &str, src: &str) -> Option {
+ // For images, the key and src are generally the same
+ let path = if src.is_empty() { key } else { src };
+ self.load_image(path).await
+ }
+
+ async fn unload(&mut self, key: &str) {
+ self.remove_from_cache(key);
+ }
+}
+
+/// Helper function to extract image URLs from a scene
+pub fn extract_image_urls(scene: &Scene) -> Vec {
+ scene
+ .nodes
+ .iter()
+ .filter_map(|(_, n)| match n {
+ Node::Rectangle(rect) => match (&rect.fill, &rect.stroke) {
+ (Paint::Image(img), _) => Some(img._ref.clone()),
+ (_, Paint::Image(img)) => Some(img._ref.clone()),
+ _ => None,
+ },
+ _ => None,
+ })
+ .collect()
+}
+
+/// Helper function to load all images in a scene
+pub async fn load_scene_images(loader: &mut L, scene: &Scene)
+where
+ L: ResourceLoader