From b891d51eefd03a31d466de36cdf7aba7ccec1d5c Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 13:07:05 +0900 Subject: [PATCH 001/262] init demo --- .../(dev)/canvas/experimental/skia/page.tsx | 172 ++++++++++++++++++ editor/next.config.ts | 12 +- editor/package.json | 1 + pnpm-lock.yaml | 15 ++ 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 editor/app/(dev)/canvas/experimental/skia/page.tsx diff --git a/editor/app/(dev)/canvas/experimental/skia/page.tsx b/editor/app/(dev)/canvas/experimental/skia/page.tsx new file mode 100644 index 0000000000..e9b51aed35 --- /dev/null +++ b/editor/app/(dev)/canvas/experimental/skia/page.tsx @@ -0,0 +1,172 @@ +"use client"; +import * as React from "react"; +import CanvasKitInit, { type CanvasKit } from "canvaskit-wasm"; + +class CanvasKitRenderer { + private kit: CanvasKit | null = null; + private surface: any = null; + private canvas: HTMLCanvasElement | null = null; + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + this.__init(); + } + + private __init() { + CanvasKitInit({ + locateFile: (file) => + "https://unpkg.com/canvaskit-wasm@latest/bin/" + file, + }).then((CanvasKit) => { + this.kit = CanvasKit; + this.surface = this.kit.MakeWebGLCanvasSurface(this.canvas!); + this.draw(); + }); + } + + private draw() { + if (!this.kit || !this.surface) return; + + const canvas = this.surface.getCanvas(); + const paint = new this.kit.Paint(); + const textPaint = new this.kit.Paint(); + const font = new this.kit.Font(null, 20); + + // Clear canvas + canvas.clear(this.kit.Color(255, 255, 255, 1.0)); + + // Helper function to draw shapes with labels + const drawShapeWithLabel = ( + x: number, + y: number, + drawFn: () => void, + label: string + ) => { + // Draw shape + drawFn(); + + // Draw label + textPaint.setColor(this.kit!.Color(0, 0, 0, 1.0)); + canvas.drawText(label, x, y + 100, textPaint, font); + }; + + // Rectangle + drawShapeWithLabel( + 100, + 50, + () => { + paint.setColor(this.kit!.Color(255, 0, 0, 1.0)); + paint.setStyle(this.kit!.PaintStyle.Fill); + canvas.drawRect(this.kit!.LTRBRect(100, 50, 200, 100), paint); + }, + "Rectangle" + ); + + // Circle + drawShapeWithLabel( + 300, + 50, + () => { + paint.setColor(this.kit!.Color(0, 255, 0, 1.0)); + paint.setStyle(this.kit!.PaintStyle.Fill); + canvas.drawCircle(350, 75, 25, paint); + }, + "Circle" + ); + + // Ellipse + drawShapeWithLabel( + 500, + 50, + () => { + paint.setColor(this.kit!.Color(0, 0, 255, 1.0)); + paint.setStyle(this.kit!.PaintStyle.Fill); + canvas.drawOval(this.kit!.LTRBRect(500, 50, 600, 100), paint); + }, + "Ellipse" + ); + + // Triangle (Polygon) + drawShapeWithLabel( + 100, + 200, + () => { + paint.setColor(this.kit!.Color(255, 165, 0, 1.0)); + paint.setStyle(this.kit!.PaintStyle.Fill); + const path = new this.kit!.Path(); + path.moveTo(150, 200); + path.lineTo(100, 250); + path.lineTo(200, 250); + path.close(); + canvas.drawPath(path, paint); + }, + "Triangle" + ); + + // Line + drawShapeWithLabel( + 300, + 200, + () => { + paint.setColor(this.kit!.Color(128, 0, 128, 1.0)); + paint.setStyle(this.kit!.PaintStyle.Stroke); + paint.setStrokeWidth(3); + canvas.drawLine(300, 225, 400, 225, paint); + }, + "Line" + ); + + // Text + drawShapeWithLabel( + 500, + 200, + () => { + paint.setColor(this.kit!.Color(0, 128, 128, 1.0)); + canvas.drawText("Hello", 500, 225, paint, font); + }, + "Text" + ); + + // SVG Path + drawShapeWithLabel( + 100, + 350, + () => { + paint.setColor(this.kit!.Color(255, 192, 203, 1.0)); + paint.setStyle(this.kit!.PaintStyle.Fill); + const path = new this.kit!.Path(); + // Draw a heart shape + path.moveTo(150, 350); + path.cubicTo(150, 350, 100, 300, 100, 350); + path.cubicTo(100, 400, 150, 450, 150, 450); + path.cubicTo(150, 450, 200, 400, 200, 350); + path.cubicTo(200, 300, 150, 350, 150, 350); + canvas.drawPath(path, paint); + }, + "SVG Path" + ); + + this.surface.flush(); + } +} + +export default function SkiaCanvasKitExperimentalPage() { + const canvasRef = React.useRef(null); + const rendererRef = React.useRef(null); + + React.useEffect(() => { + if (canvasRef.current && !rendererRef.current) { + rendererRef.current = new CanvasKitRenderer(canvasRef.current); + } + }, []); + + return ( +
+ +
+ ); +} diff --git a/editor/next.config.ts b/editor/next.config.ts index 2f4d521969..33ee68816d 100644 --- a/editor/next.config.ts +++ b/editor/next.config.ts @@ -196,7 +196,17 @@ const nextConfig: NextConfig = { // #endregion }, }, - webpack: (config) => { + webpack: (config, { isServer }) => { + // #region canvaskit-wasm (canvaskit-wasm `requires` fs and path) + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + path: false, + }; + } + // #endregion + // #region handlebars https://github.com/handlebars-lang/handlebars.js/issues/1174#issuecomment-229918935 config.resolve.alias.handlebars = "handlebars/dist/handlebars.min.js"; // #endregion diff --git a/editor/package.json b/editor/package.json index 04010438cf..bb5bc3100e 100644 --- a/editor/package.json +++ b/editor/package.json @@ -125,6 +125,7 @@ "ajv": "^8.13.0", "axios": "1.6.7", "canvas-confetti": "^1.9.3", + "canvaskit-wasm": "^0.40.0", "change-case": "^5.4.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bce6590ab6..dcd63cd4da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -597,6 +597,9 @@ importers: canvas-confetti: specifier: ^1.9.3 version: 1.9.3 + canvaskit-wasm: + specifier: ^0.40.0 + version: 0.40.0 change-case: specifier: ^5.4.3 version: 5.4.4 @@ -6154,6 +6157,9 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@webgpu/types@0.1.21': + resolution: {integrity: sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==} + '@webgpu/types@0.1.60': resolution: {integrity: sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==} @@ -6702,6 +6708,9 @@ packages: resolution: {integrity: sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==} engines: {node: ^18.12.0 || >= 20.9.0} + canvaskit-wasm@0.40.0: + resolution: {integrity: sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -20081,6 +20090,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@webgpu/types@0.1.21': {} + '@webgpu/types@0.1.60': {} '@webgpu/types@0.1.61': {} @@ -20799,6 +20810,10 @@ snapshots: prebuild-install: 7.1.3 optional: true + canvaskit-wasm@0.40.0: + dependencies: + '@webgpu/types': 0.1.21 + ccount@2.0.1: {} chalk@3.0.0: From 67a6f32bc9451fbf8a5cca91bb14e6209a8d2a98 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 13:29:22 +0900 Subject: [PATCH 002/262] rectangle demo --- .../(dev)/canvas/experimental/skia/page.tsx | 267 ++++++++++-------- 1 file changed, 152 insertions(+), 115 deletions(-) diff --git a/editor/app/(dev)/canvas/experimental/skia/page.tsx b/editor/app/(dev)/canvas/experimental/skia/page.tsx index e9b51aed35..b4aef74b59 100644 --- a/editor/app/(dev)/canvas/experimental/skia/page.tsx +++ b/editor/app/(dev)/canvas/experimental/skia/page.tsx @@ -1,12 +1,67 @@ "use client"; import * as React from "react"; -import CanvasKitInit, { type CanvasKit } from "canvaskit-wasm"; +import CanvasKitInit, { + type Color, + type CanvasKit, + type Surface, + Paint, +} from "canvaskit-wasm"; +import type grida from "@grida/schema"; +import cg from "@grida/cg"; + +const rectNode: grida.program.nodes.RectangleNode = { + type: "rectangle", + id: "1", + name: "Rectangle", + active: true, + locked: false, + position: "absolute", + left: 100, + top: 50, + width: 200, + height: 100, + fill: { type: "solid", color: { r: 255, g: 0, b: 0, a: 1 } }, + stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, + strokeWidth: 4, + cornerRadius: 12, + opacity: 0.8, + rotation: 15, + zIndex: 0, + strokeCap: "butt", + effects: [], +}; class CanvasKitRenderer { private kit: CanvasKit | null = null; - private surface: any = null; + private get $kit(): CanvasKit { + return this.kit!; + } + private surface: Surface | null = null; private canvas: HTMLCanvasElement | null = null; + private _fillPaint: Paint | null = null; + private _strokePaint: Paint | null = null; + private _textPaint: Paint | null = null; + + private get $fillPaint(): Paint { + if (!this._fillPaint) { + this._fillPaint = new this.$kit.Paint(); + } + return this._fillPaint; + } + private get $strokePaint(): Paint { + if (!this._strokePaint) { + this._strokePaint = new this.$kit.Paint(); + } + return this._strokePaint; + } + private get $textPaint(): Paint { + if (!this._textPaint) { + this._textPaint = new this.$kit.Paint(); + } + return this._textPaint; + } + constructor(canvas: HTMLCanvasElement) { this.canvas = canvas; this.__init(); @@ -19,133 +74,115 @@ class CanvasKitRenderer { }).then((CanvasKit) => { this.kit = CanvasKit; this.surface = this.kit.MakeWebGLCanvasSurface(this.canvas!); - this.draw(); + this._fillPaint = new this.$kit.Paint(); + this._strokePaint = new this.$kit.Paint(); + this._strokePaint.setStyle(this.$kit.PaintStyle.Stroke); + this.drawDemo(); }); } - private draw() { + private drawDemo() { if (!this.kit || !this.surface) return; - const canvas = this.surface.getCanvas(); - const paint = new this.kit.Paint(); - const textPaint = new this.kit.Paint(); - const font = new this.kit.Font(null, 20); - - // Clear canvas - canvas.clear(this.kit.Color(255, 255, 255, 1.0)); - - // Helper function to draw shapes with labels - const drawShapeWithLabel = ( - x: number, - y: number, - drawFn: () => void, - label: string - ) => { - // Draw shape - drawFn(); - - // Draw label - textPaint.setColor(this.kit!.Color(0, 0, 0, 1.0)); - canvas.drawText(label, x, y + 100, textPaint, font); - }; - - // Rectangle - drawShapeWithLabel( - 100, - 50, - () => { - paint.setColor(this.kit!.Color(255, 0, 0, 1.0)); - paint.setStyle(this.kit!.PaintStyle.Fill); - canvas.drawRect(this.kit!.LTRBRect(100, 50, 200, 100), paint); - }, - "Rectangle" - ); - // Circle - drawShapeWithLabel( - 300, - 50, - () => { - paint.setColor(this.kit!.Color(0, 255, 0, 1.0)); - paint.setStyle(this.kit!.PaintStyle.Fill); - canvas.drawCircle(350, 75, 25, paint); - }, - "Circle" - ); + // Clear white + canvas.clear(this.kit.Color(255, 255, 255, 1)); - // Ellipse - drawShapeWithLabel( - 500, - 50, - () => { - paint.setColor(this.kit!.Color(0, 0, 255, 1.0)); - paint.setStyle(this.kit!.PaintStyle.Fill); - canvas.drawOval(this.kit!.LTRBRect(500, 50, 600, 100), paint); - }, - "Ellipse" - ); + // Example RectangleNode data - // Triangle (Polygon) - drawShapeWithLabel( - 100, - 200, - () => { - paint.setColor(this.kit!.Color(255, 165, 0, 1.0)); - paint.setStyle(this.kit!.PaintStyle.Fill); - const path = new this.kit!.Path(); - path.moveTo(150, 200); - path.lineTo(100, 250); - path.lineTo(200, 250); - path.close(); - canvas.drawPath(path, paint); - }, - "Triangle" - ); + this.$draw(rectNode); + this.surface.flush(); + } - // Line - drawShapeWithLabel( - 300, - 200, - () => { - paint.setColor(this.kit!.Color(128, 0, 128, 1.0)); - paint.setStyle(this.kit!.PaintStyle.Stroke); - paint.setStrokeWidth(3); - canvas.drawLine(300, 225, 400, 225, paint); - }, - "Line" - ); + private $draw(node: grida.program.nodes.Node) { + switch (node.type) { + case "rectangle": + this.renderRectangle(node as grida.program.nodes.RectangleNode); + break; + default: + throw new Error("Unsupported node type"); + } + } + + private $fill(p: cg.Paint | null): Paint { + this.$fillPaint.setAntiAlias(true); + + if (!p) { + this.$fillPaint.setStyle(this.$kit.PaintStyle.Fill); + this.$fillPaint.setColor(this.$kit.Color(0, 0, 0, 0)); + return this.$fillPaint; + } + + switch (p.type) { + case "solid": + this.$fillPaint.setStyle(this.$kit.PaintStyle.Fill); + this.$fillPaint.setColor( + this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a) + ); + return this.$fillPaint; + default: + throw new Error("Unsupported fill"); + } + } + + private $stroke(p: cg.Paint | null, width: number): Paint { + this.$strokePaint.setAntiAlias(true); + + if (!p) { + this.$strokePaint.setStyle(this.$kit.PaintStyle.Stroke); + this.$strokePaint.setStrokeWidth(width); + this.$strokePaint.setColor(this.$kit.Color(0, 0, 0, 0)); + return this.$strokePaint; + } + switch (p.type) { + case "solid": + this.$strokePaint.setStyle(this.$kit.PaintStyle.Stroke); + this.$strokePaint.setStrokeWidth(width); + this.$strokePaint.setColor( + this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a) + ); + return this.$strokePaint; + default: + throw new Error("Unsupported stroke"); + } + } - // Text - drawShapeWithLabel( - 500, - 200, - () => { - paint.setColor(this.kit!.Color(0, 128, 128, 1.0)); - canvas.drawText("Hello", 500, 225, paint, font); - }, - "Text" + private renderRectangle(node: grida.program.nodes.RectangleNode) { + if (!this.kit) return; + if (!this.surface) return; + const { left: x = 0, top: y = 0, width, height } = node; + const paint = new this.kit.Paint(); + const innerRect = this.kit.LTRBRect(x, y, x + width, y + height); + const rrect = this.kit.RRectXY( + innerRect, + typeof node.cornerRadius === "number" ? node.cornerRadius : 0, + typeof node.cornerRadius === "number" ? node.cornerRadius : 0 ); - // SVG Path - drawShapeWithLabel( - 100, - 350, - () => { - paint.setColor(this.kit!.Color(255, 192, 203, 1.0)); - paint.setStyle(this.kit!.PaintStyle.Fill); - const path = new this.kit!.Path(); - // Draw a heart shape - path.moveTo(150, 350); - path.cubicTo(150, 350, 100, 300, 100, 350); - path.cubicTo(100, 400, 150, 450, 150, 450); - path.cubicTo(150, 450, 200, 400, 200, 350); - path.cubicTo(200, 300, 150, 350, 150, 350); - canvas.drawPath(path, paint); - }, - "SVG Path" + const canvas = this.surface.getCanvas(); + // Apply rotation & opacity via save() / restore() + canvas.restore(); + canvas.save(); + + // move pivot to rect center, rotate, then restore pivot + if (node.rotation) { + const cx = x + width / 2; + const cy = y + height / 2; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + // Fill + canvas.drawRRect(rrect, this.$fill(node.fill ?? null)); + + // Stroke (if provided) + canvas.drawRRect( + rrect, + this.$stroke(node.stroke ?? null, node.strokeWidth) ); - this.surface.flush(); + canvas.restore(); } } From 8333768efa0d4cc7f23b215e91e5997d45d4acd1 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 13:52:58 +0900 Subject: [PATCH 003/262] text demo --- .../(dev)/canvas/experimental/skia/page.tsx | 207 +++++++++++++++++- 1 file changed, 206 insertions(+), 1 deletion(-) diff --git a/editor/app/(dev)/canvas/experimental/skia/page.tsx b/editor/app/(dev)/canvas/experimental/skia/page.tsx index b4aef74b59..7cce0de947 100644 --- a/editor/app/(dev)/canvas/experimental/skia/page.tsx +++ b/editor/app/(dev)/canvas/experimental/skia/page.tsx @@ -9,6 +9,47 @@ import CanvasKitInit, { import type grida from "@grida/schema"; import cg from "@grida/cg"; +const textNode: grida.program.nodes.TextNode = { + type: "text", + id: "1", + name: "Text", + active: true, + locked: false, + style: {}, + fontFamily: "Arial", + opacity: 1, + rotation: 0, + zIndex: 0, + position: "absolute", + width: 200, + height: 100, + textAlign: "left", + textAlignVertical: "top", + textDecoration: "none", + fontSize: 16, + fontWeight: 100, + text: "Hello, world!", +}; + +const lineNode: grida.program.nodes.LineNode = { + type: "line", + id: "1", + name: "Line", + active: true, + locked: false, + height: 0, + top: 50, + left: 100, + position: "absolute", + stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, + strokeWidth: 1, + strokeCap: "butt", + width: 200, + opacity: 1, + zIndex: 0, + rotation: 0, +}; + const rectNode: grida.program.nodes.RectangleNode = { type: "rectangle", id: "1", @@ -31,6 +72,27 @@ const rectNode: grida.program.nodes.RectangleNode = { effects: [], }; +const ellipseNode: grida.program.nodes.EllipseNode = { + type: "ellipse", + id: "1", + name: "Ellipse", + active: true, + locked: false, + position: "absolute", + left: 100, + top: 200, + width: 100, + height: 200, + fill: { type: "solid", color: { r: 0, g: 0, b: 255, a: 1 } }, + stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, + strokeWidth: 4, + opacity: 0.8, + rotation: 15, + zIndex: 0, + strokeCap: "butt", + effects: [], +}; + class CanvasKitRenderer { private kit: CanvasKit | null = null; private get $kit(): CanvasKit { @@ -62,6 +124,8 @@ class CanvasKitRenderer { return this._textPaint; } + private __roboto_data: ArrayBuffer | null = null; + constructor(canvas: HTMLCanvasElement) { this.canvas = canvas; this.__init(); @@ -77,7 +141,17 @@ class CanvasKitRenderer { this._fillPaint = new this.$kit.Paint(); this._strokePaint = new this.$kit.Paint(); this._strokePaint.setStyle(this.$kit.PaintStyle.Stroke); - this.drawDemo(); + const loadFont = fetch( + "https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf" + ).then((response) => response.arrayBuffer()); + + loadFont + .then((roboto) => { + this.__roboto_data = roboto; + }) + .finally(() => { + this.drawDemo(); + }); }); } @@ -91,6 +165,9 @@ class CanvasKitRenderer { // Example RectangleNode data this.$draw(rectNode); + this.$draw(lineNode); + this.$draw(ellipseNode); + this.$draw(textNode); this.surface.flush(); } @@ -99,6 +176,15 @@ class CanvasKitRenderer { case "rectangle": this.renderRectangle(node as grida.program.nodes.RectangleNode); break; + case "line": + this.renderLine(node as grida.program.nodes.LineNode); + break; + case "ellipse": + this.renderEllipse(node as grida.program.nodes.EllipseNode); + break; + case "text": + this.renderText(node as grida.program.nodes.TextNode); + break; default: throw new Error("Unsupported node type"); } @@ -184,6 +270,125 @@ class CanvasKitRenderer { canvas.restore(); } + + private renderLine(node: grida.program.nodes.LineNode) { + if (!this.kit) return; + if (!this.surface) return; + const { left: x = 0, top: y = 0, width } = node; + const canvas = this.surface.getCanvas(); + + // Apply rotation & opacity via save() / restore() + canvas.restore(); + canvas.save(); + + // move pivot to line center, rotate, then restore pivot + if (node.rotation) { + const cx = x + width / 2; + const cy = y; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + // Draw the line + canvas.drawLine( + x, + y, + x + width, + y, + this.$stroke(node.stroke ?? null, node.strokeWidth) + ); + + canvas.restore(); + } + + private renderEllipse(node: grida.program.nodes.EllipseNode) { + if (!this.kit) return; + if (!this.surface) return; + const { left: x = 0, top: y = 0, width, height } = node; + const canvas = this.surface.getCanvas(); + + // Apply rotation & opacity via save() / restore() + canvas.restore(); + canvas.save(); + + // move pivot to ellipse center, rotate, then restore pivot + if (node.rotation) { + const cx = x + width / 2; + const cy = y + height / 2; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + // Create an oval path + const oval = new this.kit.Path(); + oval.addOval(this.kit.LTRBRect(x, y, x + width, y + height)); + + // Fill + canvas.drawPath(oval, this.$fill(node.fill ?? null)); + + // Stroke (if provided) + canvas.drawPath(oval, this.$stroke(node.stroke ?? null, node.strokeWidth)); + + // Clean up + oval.delete(); + + canvas.restore(); + } + + private renderText(node: grida.program.nodes.TextNode) { + if (!this.kit) return; + if (!this.surface) return; + const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; + const canvas = this.surface.getCanvas(); + + // Apply rotation & opacity via save() / restore() + canvas.restore(); + canvas.save(); + + // move pivot to text center, rotate, then restore pivot + if (node.rotation) { + const cx = x + (width as number) / 2; + const cy = y + (height as number) / 2; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + const fontMgr = this.kit.FontMgr.FromData(this.__roboto_data!); + const paraStyle = new this.kit.ParagraphStyle({ + textStyle: { + color: this.kit.BLACK, + fontFamilies: ["Roboto"], + fontSize: 28, + }, + textAlign: this.kit.TextAlign.Left, + }); + const text = String(node.text || ""); + const builder = this.kit.ParagraphBuilder.Make(paraStyle, fontMgr!); + builder.addText(text); + const paragraph = builder.build(); + + // Calculate text position based on alignment + let textX = x; + let textY = y; + + if (node.textAlign === "center") { + textX = x + (width as number) / 2; + } else if (node.textAlign === "right") { + textX = x + (width as number); + } + + if (node.textAlignVertical === "center") { + textY = y + (height as number) / 2; + } else if (node.textAlignVertical === "bottom") { + textY = y + (height as number); + } + + canvas.drawParagraph(paragraph, 10, 10); + canvas.restore(); + } } export default function SkiaCanvasKitExperimentalPage() { From 49c9bb52aa9c9d2474fd1e2d3affdaa23b812c5d Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 14:30:03 +0900 Subject: [PATCH 004/262] pkg --- .../(dev)/canvas/experimental/skia/page.tsx | 398 +----------- editor/package.json | 2 +- packages/grida-canvas-skia/index.ts | 591 ++++++++++++++++++ packages/grida-canvas-skia/package.json | 14 + pnpm-lock.yaml | 211 +++---- 5 files changed, 683 insertions(+), 533 deletions(-) create mode 100644 packages/grida-canvas-skia/index.ts create mode 100644 packages/grida-canvas-skia/package.json diff --git a/editor/app/(dev)/canvas/experimental/skia/page.tsx b/editor/app/(dev)/canvas/experimental/skia/page.tsx index 7cce0de947..780e11b972 100644 --- a/editor/app/(dev)/canvas/experimental/skia/page.tsx +++ b/editor/app/(dev)/canvas/experimental/skia/page.tsx @@ -1,395 +1,6 @@ "use client"; import * as React from "react"; -import CanvasKitInit, { - type Color, - type CanvasKit, - type Surface, - Paint, -} from "canvaskit-wasm"; -import type grida from "@grida/schema"; -import cg from "@grida/cg"; - -const textNode: grida.program.nodes.TextNode = { - type: "text", - id: "1", - name: "Text", - active: true, - locked: false, - style: {}, - fontFamily: "Arial", - opacity: 1, - rotation: 0, - zIndex: 0, - position: "absolute", - width: 200, - height: 100, - textAlign: "left", - textAlignVertical: "top", - textDecoration: "none", - fontSize: 16, - fontWeight: 100, - text: "Hello, world!", -}; - -const lineNode: grida.program.nodes.LineNode = { - type: "line", - id: "1", - name: "Line", - active: true, - locked: false, - height: 0, - top: 50, - left: 100, - position: "absolute", - stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, - strokeWidth: 1, - strokeCap: "butt", - width: 200, - opacity: 1, - zIndex: 0, - rotation: 0, -}; - -const rectNode: grida.program.nodes.RectangleNode = { - type: "rectangle", - id: "1", - name: "Rectangle", - active: true, - locked: false, - position: "absolute", - left: 100, - top: 50, - width: 200, - height: 100, - fill: { type: "solid", color: { r: 255, g: 0, b: 0, a: 1 } }, - stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, - strokeWidth: 4, - cornerRadius: 12, - opacity: 0.8, - rotation: 15, - zIndex: 0, - strokeCap: "butt", - effects: [], -}; - -const ellipseNode: grida.program.nodes.EllipseNode = { - type: "ellipse", - id: "1", - name: "Ellipse", - active: true, - locked: false, - position: "absolute", - left: 100, - top: 200, - width: 100, - height: 200, - fill: { type: "solid", color: { r: 0, g: 0, b: 255, a: 1 } }, - stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, - strokeWidth: 4, - opacity: 0.8, - rotation: 15, - zIndex: 0, - strokeCap: "butt", - effects: [], -}; - -class CanvasKitRenderer { - private kit: CanvasKit | null = null; - private get $kit(): CanvasKit { - return this.kit!; - } - private surface: Surface | null = null; - private canvas: HTMLCanvasElement | null = null; - - private _fillPaint: Paint | null = null; - private _strokePaint: Paint | null = null; - private _textPaint: Paint | null = null; - - private get $fillPaint(): Paint { - if (!this._fillPaint) { - this._fillPaint = new this.$kit.Paint(); - } - return this._fillPaint; - } - private get $strokePaint(): Paint { - if (!this._strokePaint) { - this._strokePaint = new this.$kit.Paint(); - } - return this._strokePaint; - } - private get $textPaint(): Paint { - if (!this._textPaint) { - this._textPaint = new this.$kit.Paint(); - } - return this._textPaint; - } - - private __roboto_data: ArrayBuffer | null = null; - - constructor(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this.__init(); - } - - private __init() { - CanvasKitInit({ - locateFile: (file) => - "https://unpkg.com/canvaskit-wasm@latest/bin/" + file, - }).then((CanvasKit) => { - this.kit = CanvasKit; - this.surface = this.kit.MakeWebGLCanvasSurface(this.canvas!); - this._fillPaint = new this.$kit.Paint(); - this._strokePaint = new this.$kit.Paint(); - this._strokePaint.setStyle(this.$kit.PaintStyle.Stroke); - const loadFont = fetch( - "https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf" - ).then((response) => response.arrayBuffer()); - - loadFont - .then((roboto) => { - this.__roboto_data = roboto; - }) - .finally(() => { - this.drawDemo(); - }); - }); - } - - private drawDemo() { - if (!this.kit || !this.surface) return; - const canvas = this.surface.getCanvas(); - - // Clear white - canvas.clear(this.kit.Color(255, 255, 255, 1)); - - // Example RectangleNode data - - this.$draw(rectNode); - this.$draw(lineNode); - this.$draw(ellipseNode); - this.$draw(textNode); - this.surface.flush(); - } - - private $draw(node: grida.program.nodes.Node) { - switch (node.type) { - case "rectangle": - this.renderRectangle(node as grida.program.nodes.RectangleNode); - break; - case "line": - this.renderLine(node as grida.program.nodes.LineNode); - break; - case "ellipse": - this.renderEllipse(node as grida.program.nodes.EllipseNode); - break; - case "text": - this.renderText(node as grida.program.nodes.TextNode); - break; - default: - throw new Error("Unsupported node type"); - } - } - - private $fill(p: cg.Paint | null): Paint { - this.$fillPaint.setAntiAlias(true); - - if (!p) { - this.$fillPaint.setStyle(this.$kit.PaintStyle.Fill); - this.$fillPaint.setColor(this.$kit.Color(0, 0, 0, 0)); - return this.$fillPaint; - } - - switch (p.type) { - case "solid": - this.$fillPaint.setStyle(this.$kit.PaintStyle.Fill); - this.$fillPaint.setColor( - this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a) - ); - return this.$fillPaint; - default: - throw new Error("Unsupported fill"); - } - } - - private $stroke(p: cg.Paint | null, width: number): Paint { - this.$strokePaint.setAntiAlias(true); - - if (!p) { - this.$strokePaint.setStyle(this.$kit.PaintStyle.Stroke); - this.$strokePaint.setStrokeWidth(width); - this.$strokePaint.setColor(this.$kit.Color(0, 0, 0, 0)); - return this.$strokePaint; - } - switch (p.type) { - case "solid": - this.$strokePaint.setStyle(this.$kit.PaintStyle.Stroke); - this.$strokePaint.setStrokeWidth(width); - this.$strokePaint.setColor( - this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a) - ); - return this.$strokePaint; - default: - throw new Error("Unsupported stroke"); - } - } - - private renderRectangle(node: grida.program.nodes.RectangleNode) { - if (!this.kit) return; - if (!this.surface) return; - const { left: x = 0, top: y = 0, width, height } = node; - const paint = new this.kit.Paint(); - const innerRect = this.kit.LTRBRect(x, y, x + width, y + height); - const rrect = this.kit.RRectXY( - innerRect, - typeof node.cornerRadius === "number" ? node.cornerRadius : 0, - typeof node.cornerRadius === "number" ? node.cornerRadius : 0 - ); - - const canvas = this.surface.getCanvas(); - // Apply rotation & opacity via save() / restore() - canvas.restore(); - canvas.save(); - - // move pivot to rect center, rotate, then restore pivot - if (node.rotation) { - const cx = x + width / 2; - const cy = y + height / 2; - canvas.translate(cx, cy); - canvas.rotate(node.rotation, cx, cy); - canvas.translate(-cx, -cy); - } - - // Fill - canvas.drawRRect(rrect, this.$fill(node.fill ?? null)); - - // Stroke (if provided) - canvas.drawRRect( - rrect, - this.$stroke(node.stroke ?? null, node.strokeWidth) - ); - - canvas.restore(); - } - - private renderLine(node: grida.program.nodes.LineNode) { - if (!this.kit) return; - if (!this.surface) return; - const { left: x = 0, top: y = 0, width } = node; - const canvas = this.surface.getCanvas(); - - // Apply rotation & opacity via save() / restore() - canvas.restore(); - canvas.save(); - - // move pivot to line center, rotate, then restore pivot - if (node.rotation) { - const cx = x + width / 2; - const cy = y; - canvas.translate(cx, cy); - canvas.rotate(node.rotation, cx, cy); - canvas.translate(-cx, -cy); - } - - // Draw the line - canvas.drawLine( - x, - y, - x + width, - y, - this.$stroke(node.stroke ?? null, node.strokeWidth) - ); - - canvas.restore(); - } - - private renderEllipse(node: grida.program.nodes.EllipseNode) { - if (!this.kit) return; - if (!this.surface) return; - const { left: x = 0, top: y = 0, width, height } = node; - const canvas = this.surface.getCanvas(); - - // Apply rotation & opacity via save() / restore() - canvas.restore(); - canvas.save(); - - // move pivot to ellipse center, rotate, then restore pivot - if (node.rotation) { - const cx = x + width / 2; - const cy = y + height / 2; - canvas.translate(cx, cy); - canvas.rotate(node.rotation, cx, cy); - canvas.translate(-cx, -cy); - } - - // Create an oval path - const oval = new this.kit.Path(); - oval.addOval(this.kit.LTRBRect(x, y, x + width, y + height)); - - // Fill - canvas.drawPath(oval, this.$fill(node.fill ?? null)); - - // Stroke (if provided) - canvas.drawPath(oval, this.$stroke(node.stroke ?? null, node.strokeWidth)); - - // Clean up - oval.delete(); - - canvas.restore(); - } - - private renderText(node: grida.program.nodes.TextNode) { - if (!this.kit) return; - if (!this.surface) return; - const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; - const canvas = this.surface.getCanvas(); - - // Apply rotation & opacity via save() / restore() - canvas.restore(); - canvas.save(); - - // move pivot to text center, rotate, then restore pivot - if (node.rotation) { - const cx = x + (width as number) / 2; - const cy = y + (height as number) / 2; - canvas.translate(cx, cy); - canvas.rotate(node.rotation, cx, cy); - canvas.translate(-cx, -cy); - } - - const fontMgr = this.kit.FontMgr.FromData(this.__roboto_data!); - const paraStyle = new this.kit.ParagraphStyle({ - textStyle: { - color: this.kit.BLACK, - fontFamilies: ["Roboto"], - fontSize: 28, - }, - textAlign: this.kit.TextAlign.Left, - }); - const text = String(node.text || ""); - const builder = this.kit.ParagraphBuilder.Make(paraStyle, fontMgr!); - builder.addText(text); - const paragraph = builder.build(); - - // Calculate text position based on alignment - let textX = x; - let textY = y; - - if (node.textAlign === "center") { - textX = x + (width as number) / 2; - } else if (node.textAlign === "right") { - textX = x + (width as number); - } - - if (node.textAlignVertical === "center") { - textY = y + (height as number) / 2; - } else if (node.textAlignVertical === "bottom") { - textY = y + (height as number); - } - - canvas.drawParagraph(paragraph, 10, 10); - canvas.restore(); - } -} +import { CanvasKitRenderer } from "@grida/skia"; export default function SkiaCanvasKitExperimentalPage() { const canvasRef = React.useRef(null); @@ -402,7 +13,12 @@ export default function SkiaCanvasKitExperimentalPage() { }, []); return ( -
+
+
+

+ Grida Canvas SKIA BACKEND +

+
+ "https://unpkg.com/canvaskit-wasm@latest/bin/" + file, + }).then((CanvasKit) => { + this.kit = CanvasKit; + this.surface = this.kit.MakeWebGLCanvasSurface(this.canvas!); + this._fillPaint = new this.$kit.Paint(); + this._strokePaint = new this.$kit.Paint(); + this._strokePaint.setStyle(this.$kit.PaintStyle.Stroke); + const loadFont = fetch( + "https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf" + ).then((response) => response.arrayBuffer()); + + loadFont + .then((roboto) => { + this.__roboto_data = roboto; + }) + .finally(() => { + this.drawDemo(); + }); + }); + } + + private drawDemo() { + if (!this.kit || !this.surface) return; + const canvas = this.surface.getCanvas(); + + // Clear white + canvas.clear(this.kit.Color(255, 255, 255, 1)); + + // Draw nodes + this.$draw(rectNode); + this.$draw(lineNode); + this.$draw(ellipseNode); + this.$draw(textNode); + this.$draw(imageNode); + + // Don't flush here since image rendering is async + // FIXME: add this after making image rendering async + // this.surface.flush(); + } + + private $draw(node: grida.program.nodes.Node) { + switch (node.type) { + case "rectangle": + this.drawRectangleNode(node as grida.program.nodes.RectangleNode); + break; + case "line": + this.drawLineNode(node as grida.program.nodes.LineNode); + break; + case "ellipse": + this.drawEllipseNode(node as grida.program.nodes.EllipseNode); + break; + case "text": + this.drawTextNode(node as grida.program.nodes.TextNode); + break; + case "image": + this.drawImageNode(node as grida.program.nodes.ImageNode); + break; + default: + throw new Error("Unsupported node type"); + } + } + + private $fill(p: cg.Paint | null): Paint { + this.$fillPaint.setAntiAlias(true); + + if (!p) { + this.$fillPaint.setStyle(this.$kit.PaintStyle.Fill); + this.$fillPaint.setColor(this.$kit.Color(0, 0, 0, 0)); + return this.$fillPaint; + } + + switch (p.type) { + case "solid": + this.$fillPaint.setStyle(this.$kit.PaintStyle.Fill); + this.$fillPaint.setColor( + this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a) + ); + return this.$fillPaint; + default: + throw new Error("Unsupported fill"); + } + } + + private $stroke(p: cg.Paint | null, width: number): Paint { + this.$strokePaint.setAntiAlias(true); + + if (!p) { + this.$strokePaint.setStyle(this.$kit.PaintStyle.Stroke); + this.$strokePaint.setStrokeWidth(width); + this.$strokePaint.setColor(this.$kit.Color(0, 0, 0, 0)); + return this.$strokePaint; + } + switch (p.type) { + case "solid": + this.$strokePaint.setStyle(this.$kit.PaintStyle.Stroke); + this.$strokePaint.setStrokeWidth(width); + this.$strokePaint.setColor( + this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a) + ); + return this.$strokePaint; + default: + throw new Error("Unsupported stroke"); + } + } + + private drawRectangleNode(node: grida.program.nodes.RectangleNode) { + if (!this.kit) return; + if (!this.surface) return; + const { left: x = 0, top: y = 0, width, height } = node; + const paint = new this.kit.Paint(); + const innerRect = this.kit.LTRBRect(x, y, x + width, y + height); + const rrect = this.kit.RRectXY( + innerRect, + typeof node.cornerRadius === "number" ? node.cornerRadius : 0, + typeof node.cornerRadius === "number" ? node.cornerRadius : 0 + ); + + const canvas = this.surface.getCanvas(); + // Apply rotation & opacity via save() / restore() + canvas.restore(); + canvas.save(); + + // move pivot to rect center, rotate, then restore pivot + if (node.rotation) { + const cx = x + width / 2; + const cy = y + height / 2; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + // Fill + canvas.drawRRect(rrect, this.$fill(node.fill ?? null)); + + // Stroke (if provided) + canvas.drawRRect( + rrect, + this.$stroke(node.stroke ?? null, node.strokeWidth) + ); + + canvas.restore(); + } + + private drawLineNode(node: grida.program.nodes.LineNode) { + if (!this.kit) return; + if (!this.surface) return; + const { left: x = 0, top: y = 0, width } = node; + const canvas = this.surface.getCanvas(); + + // Apply rotation & opacity via save() / restore() + canvas.restore(); + canvas.save(); + + // move pivot to line center, rotate, then restore pivot + if (node.rotation) { + const cx = x + width / 2; + const cy = y; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + // Draw the line + canvas.drawLine( + x, + y, + x + width, + y, + this.$stroke(node.stroke ?? null, node.strokeWidth) + ); + + canvas.restore(); + } + + private drawEllipseNode(node: grida.program.nodes.EllipseNode) { + if (!this.kit) return; + if (!this.surface) return; + const { left: x = 0, top: y = 0, width, height } = node; + const canvas = this.surface.getCanvas(); + + // Apply rotation & opacity via save() / restore() + canvas.restore(); + canvas.save(); + + // move pivot to ellipse center, rotate, then restore pivot + if (node.rotation) { + const cx = x + width / 2; + const cy = y + height / 2; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + // Create an oval path + const oval = new this.kit.Path(); + oval.addOval(this.kit.LTRBRect(x, y, x + width, y + height)); + + // Fill + canvas.drawPath(oval, this.$fill(node.fill ?? null)); + + // Stroke (if provided) + canvas.drawPath(oval, this.$stroke(node.stroke ?? null, node.strokeWidth)); + + // Clean up + oval.delete(); + + canvas.restore(); + } + + private drawTextNode(node: grida.program.nodes.TextNode) { + if (!this.kit) return; + if (!this.surface) return; + const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; + const canvas = this.surface.getCanvas(); + + // Apply rotation & opacity via save() / restore() + canvas.restore(); + canvas.save(); + + // move pivot to text center, rotate, then restore pivot + if (node.rotation) { + const cx = x + (width as number) / 2; + const cy = y + (height as number) / 2; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + const fontMgr = this.kit.FontMgr.FromData(this.__roboto_data!); + const paraStyle = new this.kit.ParagraphStyle({ + textStyle: { + color: this.kit.BLACK, + fontFamilies: ["Roboto"], + fontSize: 28, + }, + textAlign: this.kit.TextAlign.Left, + }); + const text = String(node.text || ""); + const builder = this.kit.ParagraphBuilder.Make(paraStyle, fontMgr!); + builder.addText(text); + const paragraph = builder.build(); + + // Calculate text position based on alignment + let textX = x; + let textY = y; + + if (node.textAlign === "center") { + textX = x + (width as number) / 2; + } else if (node.textAlign === "right") { + textX = x + (width as number); + } + + if (node.textAlignVertical === "center") { + textY = y + (height as number) / 2; + } else if (node.textAlignVertical === "bottom") { + textY = y + (height as number); + } + + canvas.drawParagraph(paragraph, 10, 10); + canvas.restore(); + } + + private __image_cache: Record = {}; + + private async loadImage(src: string): Promise { + if (!this.__image_cache) this.__image_cache = {}; + if (this.__image_cache[src]) return this.__image_cache[src]; + + return new Promise((resolve, reject) => { + fetch(src) + .then((res) => { + if (!res.ok) throw new Error(`Failed to fetch image: ${src}`); + return res.arrayBuffer(); + }) + .then((buffer) => { + const uint8 = new Uint8Array(buffer); + this.__image_cache[src] = uint8; + resolve(uint8); + }) + .catch(reject); + }); + } + + private drawImageNode(node: grida.program.nodes.ImageNode) { + if (!this.kit) return; + if (!this.surface) return; + const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; + const canvas = this.surface.getCanvas(); + + // Apply rotation & opacity via save() / restore() + canvas.restore(); + canvas.save(); + + // move pivot to image center, rotate, then restore pivot + if (node.rotation) { + const cx = x + (width as number) / 2; + const cy = y + (height as number) / 2; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + // Load and draw image + this.loadImage(String(node.src || "")) + .then((img) => { + // Create SkImage from ImageData + const image = this.kit!.MakeImageFromEncoded(img)!; + if (!image) { + reportError("Failed to create SkImage from encoded data"); + return; + } + + // Get image dimensions + const imgWidth = image.width(); + const imgHeight = image.height(); + + // Create destination rect (where to draw) + const dstRect = this.kit!.LTRBRect( + x, + y, + x + (width as number), + y + (height as number) + ); + + // Create source rect (what part of image to draw) + const srcRect = this.kit!.LTRBRect(0, 0, imgWidth, imgHeight); + + // Calculate source and destination rectangles based on fit + let finalSrcRect = srcRect; + let finalDstRect = dstRect; + + switch (node.fit) { + case "contain": { + // Calculate aspect ratios + const imgAspect = imgWidth / imgHeight; + const dstAspect = (width as number) / (height as number); + + if (imgAspect > dstAspect) { + // Image is wider than destination + const newHeight = (width as number) / imgAspect; + const yOffset = ((height as number) - newHeight) / 2; + finalDstRect = this.kit!.LTRBRect( + x, + y + yOffset, + x + (width as number), + y + yOffset + newHeight + ); + } else { + // Image is taller than destination + const newWidth = (height as number) * imgAspect; + const xOffset = ((width as number) - newWidth) / 2; + finalDstRect = this.kit!.LTRBRect( + x + xOffset, + y, + x + xOffset + newWidth, + y + (height as number) + ); + } + break; + } + case "cover": { + // Calculate aspect ratios + const imgAspect = imgWidth / imgHeight; + const dstAspect = (width as number) / (height as number); + + if (imgAspect > dstAspect) { + // Image is wider than destination + const newWidth = (height as number) * imgAspect; + const xOffset = (newWidth - (width as number)) / 2; + finalSrcRect = this.kit!.LTRBRect( + xOffset, + 0, + xOffset + (width as number), + imgHeight + ); + } else { + // Image is taller than destination + const newHeight = (width as number) / imgAspect; + const yOffset = (newHeight - (height as number)) / 2; + finalSrcRect = this.kit!.LTRBRect( + 0, + yOffset, + imgWidth, + yOffset + (height as number) + ); + } + break; + } + case "none": { + // Use original image dimensions + finalDstRect = this.kit!.LTRBRect( + x, + y, + x + imgWidth, + y + imgHeight + ); + break; + } + } + + // Draw image with corner radius if specified + if (typeof node.cornerRadius === "number") { + const rrect = this.kit!.RRectXY( + finalDstRect, + node.cornerRadius, + node.cornerRadius + ); + canvas.drawImageRect( + image, + finalSrcRect, + rrect, + this.$fillPaint, + true + ); + } else { + canvas.drawImageRect( + image, + finalSrcRect, + finalDstRect, + this.$fillPaint, + true + ); + } + + // Clean up + image.delete(); + + // Flush the surface after drawing + this.surface!.flush(); + }) + .catch((error) => { + reportError("Failed to load image: " + error); + }); + + canvas.restore(); + } +} diff --git a/packages/grida-canvas-skia/package.json b/packages/grida-canvas-skia/package.json new file mode 100644 index 0000000000..18fc9c8f96 --- /dev/null +++ b/packages/grida-canvas-skia/package.json @@ -0,0 +1,14 @@ +{ + "name": "@grida/skia", + "description": "Grida Canvas Renderer Skia Backend", + "private": true, + "dependencies": { + "@grida/cg": "workspace:*", + "@grida/cmath": "workspace:*", + "@grida/vn": "workspace:*", + "canvaskit-wasm": "^0.40.0" + }, + "devDependencies": { + "@grida/schema": "workspace:*" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcd63cd4da..b43d29d42f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 3.5.3 ts-jest: specifier: ^29.3.2 - version: 29.3.4(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.4)(jest@29.7.0(@types/node@22.15.28)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.28)(typescript@5.8.3)))(typescript@5.8.3) + version: 29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@22.15.28)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.28)(typescript@5.8.3)))(typescript@5.8.3) tsup: specifier: ^8.4.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.4)(typescript@5.8.3)(yaml@2.7.0) @@ -49,7 +49,7 @@ importers: dependencies: '@next/third-parties': specifier: 15.3.2 - version: 15.3.2(next@15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 15.3.2(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) '@react-three/drei': specifier: ^10.0.7 version: 10.1.2(@react-three/fiber@9.1.2(@types/react@19.1.3)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.170.0))(@types/react@19.1.3)(@types/three@0.170.0)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.170.0) @@ -64,7 +64,7 @@ importers: version: 12.15.0(@emotion/is-prop-valid@1.3.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: 15.3.2 - version: 15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -211,7 +211,7 @@ importers: version: 0.511.0(react@19.0.0) next: specifier: 15.3.2 - version: 15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) pdfjs-dist: specifier: 4.8.69 version: 4.8.69 @@ -342,6 +342,9 @@ importers: '@grida/schema': specifier: workspace:* version: link:../packages/grida-canvas-schema + '@grida/skia': + specifier: workspace:* + version: link:../packages/grida-canvas-skia '@grida/tokens': specifier: workspace:* version: link:../packages/grida-tokens @@ -597,9 +600,6 @@ importers: canvas-confetti: specifier: ^1.9.3 version: 1.9.3 - canvaskit-wasm: - specifier: ^0.40.0 - version: 0.40.0 change-case: specifier: ^5.4.3 version: 5.4.4 @@ -1146,6 +1146,25 @@ importers: specifier: 3.1.0 version: 3.1.0 + packages/grida-canvas-skia: + dependencies: + '@grida/cg': + specifier: workspace:* + version: link:../grida-canvas-cg + '@grida/cmath': + specifier: workspace:* + version: link:../grida-cmath + '@grida/vn': + specifier: workspace:* + version: link:../grida-canvas-vn + canvaskit-wasm: + specifier: ^0.40.0 + version: 0.40.0 + devDependencies: + '@grida/schema': + specifier: workspace:* + version: link:../grida-canvas-schema + packages/grida-canvas-tailwind: {} packages/grida-canvas-transparency-grid: @@ -13933,45 +13952,21 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -13987,34 +13982,16 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -14025,89 +14002,41 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.26.5 - optional: true - '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -16794,6 +16723,12 @@ snapshots: '@next/swc-win32-x64-msvc@15.3.2': optional: true + '@next/third-parties@15.3.2(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + dependencies: + next: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + third-party-capital: 1.0.20 + '@next/third-parties@15.3.2(next@15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': dependencies: next: 15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -20432,20 +20367,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-jest@29.7.0(@babel/core@7.27.4): - dependencies: - '@babel/core': 7.27.4 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.27.4) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - optional: true - babel-loader@9.2.1(@babel/core@7.27.1)(webpack@5.98.0(esbuild@0.25.4)): dependencies: '@babel/core': 7.27.1 @@ -20555,39 +20476,12 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.1) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.1) - babel-preset-current-node-syntax@1.1.0(@babel/core@7.27.4): - dependencies: - '@babel/core': 7.27.4 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.27.4) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.27.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.27.4) - '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.27.4) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.27.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.27.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.27.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.4) - optional: true - babel-preset-jest@29.6.3(@babel/core@7.27.1): dependencies: '@babel/core': 7.27.1 babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.1) - babel-preset-jest@29.6.3(@babel/core@7.27.4): - dependencies: - '@babel/core': 7.27.4 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.4) - optional: true - bail@2.0.2: {} balanced-match@0.4.2: {} @@ -25147,6 +25041,33 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@next/env': 15.3.2 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001717 + postcss: 8.4.31 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + styled-jsx: 5.1.6(@babel/core@7.27.1)(babel-plugin-macros@3.1.0)(react@19.0.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.3.2 + '@next/swc-darwin-x64': 15.3.2 + '@next/swc-linux-arm64-gnu': 15.3.2 + '@next/swc-linux-arm64-musl': 15.3.2 + '@next/swc-linux-x64-gnu': 15.3.2 + '@next/swc-linux-x64-musl': 15.3.2 + '@next/swc-win32-arm64-msvc': 15.3.2 + '@next/swc-win32-x64-msvc': 15.3.2 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.52.0 + sharp: 0.34.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.3.2 @@ -27694,6 +27615,14 @@ snapshots: dependencies: inline-style-parser: 0.2.4 + styled-jsx@5.1.6(@babel/core@7.27.1)(babel-plugin-macros@3.1.0)(react@19.0.0): + dependencies: + client-only: 0.0.1 + react: 19.0.0 + optionalDependencies: + '@babel/core': 7.27.1 + babel-plugin-macros: 3.1.0 + styled-jsx@5.1.6(@babel/core@7.27.4)(babel-plugin-macros@3.1.0)(react@19.0.0): dependencies: client-only: 0.0.1 @@ -28019,7 +27948,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.3.4(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.4)(jest@29.7.0(@types/node@22.15.28)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.28)(typescript@5.8.3)))(typescript@5.8.3): + ts-jest@29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@22.15.28)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.28)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -28034,10 +27963,10 @@ snapshots: typescript: 5.8.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.1 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.27.4) + babel-jest: 29.7.0(@babel/core@7.27.1) esbuild: 0.25.4 ts-node@10.9.2(@types/node@22.15.28)(typescript@5.8.3): From bbd424575f739706d4f33b5826c303459cbbe2ba Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 14:59:09 +0900 Subject: [PATCH 005/262] staged rendering --- .../(dev)/canvas/experimental/skia/page.tsx | 109 ++++- packages/grida-canvas-skia/index.ts | 406 +++++++----------- 2 files changed, 256 insertions(+), 259 deletions(-) diff --git a/editor/app/(dev)/canvas/experimental/skia/page.tsx b/editor/app/(dev)/canvas/experimental/skia/page.tsx index 780e11b972..d7d293cff5 100644 --- a/editor/app/(dev)/canvas/experimental/skia/page.tsx +++ b/editor/app/(dev)/canvas/experimental/skia/page.tsx @@ -1,6 +1,111 @@ "use client"; import * as React from "react"; import { CanvasKitRenderer } from "@grida/skia"; +import type grida from "@grida/schema"; + +const imageNode: grida.program.nodes.ImageNode = { + type: "image", + id: "1", + name: "Image", + active: true, + locked: false, + style: {}, + opacity: 1, + rotation: 0, + zIndex: 0, + position: "absolute", + left: 300, + top: 300, + width: 100, + height: 100, + fit: "contain", + src: "/images/abstract-placeholder.jpg", + cornerRadius: 0, +}; + +const textNode: grida.program.nodes.TextNode = { + type: "text", + id: "1", + name: "Text", + active: true, + locked: false, + style: {}, + fontFamily: "Arial", + opacity: 1, + rotation: 0, + zIndex: 0, + position: "absolute", + width: 200, + height: 100, + textAlign: "left", + textAlignVertical: "top", + textDecoration: "none", + fontSize: 16, + fontWeight: 100, + text: "Hello, world!", +}; + +const lineNode: grida.program.nodes.LineNode = { + type: "line", + id: "1", + name: "Line", + active: true, + locked: false, + height: 0, + top: 50, + left: 100, + position: "absolute", + stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, + strokeWidth: 1, + strokeCap: "butt", + width: 200, + opacity: 1, + zIndex: 0, + rotation: 0, +}; + +const rectNode: grida.program.nodes.RectangleNode = { + type: "rectangle", + id: "1", + name: "Rectangle", + active: true, + locked: false, + position: "absolute", + left: 100, + top: 50, + width: 200, + height: 100, + fill: { type: "solid", color: { r: 255, g: 0, b: 0, a: 1 } }, + stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, + strokeWidth: 4, + cornerRadius: 12, + opacity: 0.8, + rotation: 15, + zIndex: 0, + strokeCap: "butt", + effects: [], +}; + +const ellipseNode: grida.program.nodes.EllipseNode = { + type: "ellipse", + id: "1", + name: "Ellipse", + active: true, + locked: false, + position: "absolute", + left: 100, + top: 200, + width: 100, + height: 200, + fill: { type: "solid", color: { r: 0, g: 0, b: 255, a: 1 } }, + stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, + strokeWidth: 4, + opacity: 0.8, + rotation: 15, + zIndex: 0, + strokeCap: "butt", + effects: [], +}; export default function SkiaCanvasKitExperimentalPage() { const canvasRef = React.useRef(null); @@ -8,7 +113,9 @@ export default function SkiaCanvasKitExperimentalPage() { React.useEffect(() => { if (canvasRef.current && !rendererRef.current) { - rendererRef.current = new CanvasKitRenderer(canvasRef.current); + const renderer = new CanvasKitRenderer(canvasRef.current); + rendererRef.current = renderer; + renderer.setNodes([imageNode, textNode, lineNode, rectNode, ellipseNode]); } }, []); diff --git a/packages/grida-canvas-skia/index.ts b/packages/grida-canvas-skia/index.ts index 66d7d2af49..9c87343710 100644 --- a/packages/grida-canvas-skia/index.ts +++ b/packages/grida-canvas-skia/index.ts @@ -7,110 +7,6 @@ import CanvasKitInit, { import type grida from "@grida/schema"; import cg from "@grida/cg"; -const imageNode: grida.program.nodes.ImageNode = { - type: "image", - id: "1", - name: "Image", - active: true, - locked: false, - style: {}, - opacity: 1, - rotation: 0, - zIndex: 0, - position: "absolute", - left: 300, - top: 300, - width: 100, - height: 100, - fit: "contain", - src: "/images/abstract-placeholder.jpg", - cornerRadius: 0, -}; - -const textNode: grida.program.nodes.TextNode = { - type: "text", - id: "1", - name: "Text", - active: true, - locked: false, - style: {}, - fontFamily: "Arial", - opacity: 1, - rotation: 0, - zIndex: 0, - position: "absolute", - width: 200, - height: 100, - textAlign: "left", - textAlignVertical: "top", - textDecoration: "none", - fontSize: 16, - fontWeight: 100, - text: "Hello, world!", -}; - -const lineNode: grida.program.nodes.LineNode = { - type: "line", - id: "1", - name: "Line", - active: true, - locked: false, - height: 0, - top: 50, - left: 100, - position: "absolute", - stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, - strokeWidth: 1, - strokeCap: "butt", - width: 200, - opacity: 1, - zIndex: 0, - rotation: 0, -}; - -const rectNode: grida.program.nodes.RectangleNode = { - type: "rectangle", - id: "1", - name: "Rectangle", - active: true, - locked: false, - position: "absolute", - left: 100, - top: 50, - width: 200, - height: 100, - fill: { type: "solid", color: { r: 255, g: 0, b: 0, a: 1 } }, - stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, - strokeWidth: 4, - cornerRadius: 12, - opacity: 0.8, - rotation: 15, - zIndex: 0, - strokeCap: "butt", - effects: [], -}; - -const ellipseNode: grida.program.nodes.EllipseNode = { - type: "ellipse", - id: "1", - name: "Ellipse", - active: true, - locked: false, - position: "absolute", - left: 100, - top: 200, - width: 100, - height: 200, - fill: { type: "solid", color: { r: 0, g: 0, b: 255, a: 1 } }, - stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, - strokeWidth: 4, - opacity: 0.8, - rotation: 15, - zIndex: 0, - strokeCap: "butt", - effects: [], -}; - export class CanvasKitRenderer { private kit: CanvasKit | null = null; private get $kit(): CanvasKit { @@ -119,6 +15,15 @@ export class CanvasKitRenderer { private surface: Surface | null = null; private canvas: HTMLCanvasElement | null = null; + private nodes: grida.program.nodes.Node[] = []; + + private _imageMap: Record< + string, + ReturnType | null + > = {}; + private _imageLoading: Record | undefined> = {}; + private _renderScheduled = false; + private _fillPaint: Paint | null = null; private _strokePaint: Paint | null = null; private _textPaint: Paint | null = null; @@ -168,28 +73,33 @@ export class CanvasKitRenderer { this.__roboto_data = roboto; }) .finally(() => { - this.drawDemo(); + this.requestRender(); }); }); } - private drawDemo() { + public setNodes(nodes: grida.program.nodes.Node[]) { + this.nodes = nodes; + this.requestRender(); + } + + private requestRender() { + if (this._renderScheduled) return; + this._renderScheduled = true; + requestAnimationFrame(() => { + this._renderScheduled = false; + this.render(); + }); + } + + private render() { if (!this.kit || !this.surface) return; const canvas = this.surface.getCanvas(); - - // Clear white canvas.clear(this.kit.Color(255, 255, 255, 1)); - - // Draw nodes - this.$draw(rectNode); - this.$draw(lineNode); - this.$draw(ellipseNode); - this.$draw(textNode); - this.$draw(imageNode); - - // Don't flush here since image rendering is async - // FIXME: add this after making image rendering async - // this.surface.flush(); + for (const node of this.nodes) { + this.$draw(node); + } + this.surface.flush(); } private $draw(node: grida.program.nodes.Node) { @@ -414,25 +324,28 @@ export class CanvasKitRenderer { canvas.restore(); } - private __image_cache: Record = {}; + private async loadImage(src: string): Promise { + if (this._imageMap[src]) return; + if (this._imageLoading[src]) return this._imageLoading[src]; - private async loadImage(src: string): Promise { - if (!this.__image_cache) this.__image_cache = {}; - if (this.__image_cache[src]) return this.__image_cache[src]; + this._imageLoading[src] = fetch(src) + .then((res) => { + if (!res.ok) throw new Error(`Failed to fetch image: ${src}`); + return res.arrayBuffer(); + }) + .then((buffer) => { + const uint8 = new Uint8Array(buffer); + const img = this.$kit.MakeImageFromEncoded(uint8); + if (img) { + this._imageMap[src] = img; + } + }) + .finally(() => { + delete this._imageLoading[src]; + this.requestRender(); + }); - return new Promise((resolve, reject) => { - fetch(src) - .then((res) => { - if (!res.ok) throw new Error(`Failed to fetch image: ${src}`); - return res.arrayBuffer(); - }) - .then((buffer) => { - const uint8 = new Uint8Array(buffer); - this.__image_cache[src] = uint8; - resolve(uint8); - }) - .catch(reject); - }); + return this._imageLoading[src]; } private drawImageNode(node: grida.program.nodes.ImageNode) { @@ -454,138 +367,115 @@ export class CanvasKitRenderer { canvas.translate(-cx, -cy); } - // Load and draw image - this.loadImage(String(node.src || "")) - .then((img) => { - // Create SkImage from ImageData - const image = this.kit!.MakeImageFromEncoded(img)!; - if (!image) { - reportError("Failed to create SkImage from encoded data"); - return; - } + const src = String(node.src || ""); + const image = this._imageMap[src]; + if (!image) { + this.loadImage(src); + canvas.restore(); + return; + } - // Get image dimensions - const imgWidth = image.width(); - const imgHeight = image.height(); + const imgWidth = image.width(); + const imgHeight = image.height(); - // Create destination rect (where to draw) - const dstRect = this.kit!.LTRBRect( - x, - y, - x + (width as number), - y + (height as number) - ); - - // Create source rect (what part of image to draw) - const srcRect = this.kit!.LTRBRect(0, 0, imgWidth, imgHeight); - - // Calculate source and destination rectangles based on fit - let finalSrcRect = srcRect; - let finalDstRect = dstRect; - - switch (node.fit) { - case "contain": { - // Calculate aspect ratios - const imgAspect = imgWidth / imgHeight; - const dstAspect = (width as number) / (height as number); - - if (imgAspect > dstAspect) { - // Image is wider than destination - const newHeight = (width as number) / imgAspect; - const yOffset = ((height as number) - newHeight) / 2; - finalDstRect = this.kit!.LTRBRect( - x, - y + yOffset, - x + (width as number), - y + yOffset + newHeight - ); - } else { - // Image is taller than destination - const newWidth = (height as number) * imgAspect; - const xOffset = ((width as number) - newWidth) / 2; - finalDstRect = this.kit!.LTRBRect( - x + xOffset, - y, - x + xOffset + newWidth, - y + (height as number) - ); - } - break; - } - case "cover": { - // Calculate aspect ratios - const imgAspect = imgWidth / imgHeight; - const dstAspect = (width as number) / (height as number); - - if (imgAspect > dstAspect) { - // Image is wider than destination - const newWidth = (height as number) * imgAspect; - const xOffset = (newWidth - (width as number)) / 2; - finalSrcRect = this.kit!.LTRBRect( - xOffset, - 0, - xOffset + (width as number), - imgHeight - ); - } else { - // Image is taller than destination - const newHeight = (width as number) / imgAspect; - const yOffset = (newHeight - (height as number)) / 2; - finalSrcRect = this.kit!.LTRBRect( - 0, - yOffset, - imgWidth, - yOffset + (height as number) - ); - } - break; - } - case "none": { - // Use original image dimensions - finalDstRect = this.kit!.LTRBRect( - x, - y, - x + imgWidth, - y + imgHeight - ); - break; - } - } + // Create destination rect (where to draw) + const dstRect = this.kit!.LTRBRect( + x, + y, + x + (width as number), + y + (height as number) + ); - // Draw image with corner radius if specified - if (typeof node.cornerRadius === "number") { - const rrect = this.kit!.RRectXY( - finalDstRect, - node.cornerRadius, - node.cornerRadius + // Create source rect (what part of image to draw) + const srcRect = this.kit!.LTRBRect(0, 0, imgWidth, imgHeight); + + // Calculate source and destination rectangles based on fit + let finalSrcRect = srcRect; + let finalDstRect = dstRect; + + switch (node.fit) { + case "contain": { + // Calculate aspect ratios + const imgAspect = imgWidth / imgHeight; + const dstAspect = (width as number) / (height as number); + + if (imgAspect > dstAspect) { + // Image is wider than destination + const newHeight = (width as number) / imgAspect; + const yOffset = ((height as number) - newHeight) / 2; + finalDstRect = this.kit!.LTRBRect( + x, + y + yOffset, + x + (width as number), + y + yOffset + newHeight ); - canvas.drawImageRect( - image, - finalSrcRect, - rrect, - this.$fillPaint, - true + } else { + // Image is taller than destination + const newWidth = (height as number) * imgAspect; + const xOffset = ((width as number) - newWidth) / 2; + finalDstRect = this.kit!.LTRBRect( + x + xOffset, + y, + x + xOffset + newWidth, + y + (height as number) + ); + } + break; + } + case "cover": { + // Calculate aspect ratios + const imgAspect = imgWidth / imgHeight; + const dstAspect = (width as number) / (height as number); + + if (imgAspect > dstAspect) { + // Image is wider than destination + const newWidth = (height as number) * imgAspect; + const xOffset = (newWidth - (width as number)) / 2; + finalSrcRect = this.kit!.LTRBRect( + xOffset, + 0, + xOffset + (width as number), + imgHeight ); } else { - canvas.drawImageRect( - image, - finalSrcRect, - finalDstRect, - this.$fillPaint, - true + // Image is taller than destination + const newHeight = (width as number) / imgAspect; + const yOffset = (newHeight - (height as number)) / 2; + finalSrcRect = this.kit!.LTRBRect( + 0, + yOffset, + imgWidth, + yOffset + (height as number) ); } + break; + } + case "none": { + // Use original image dimensions + finalDstRect = this.kit!.LTRBRect(x, y, x + imgWidth, y + imgHeight); + break; + } + } - // Clean up - image.delete(); - - // Flush the surface after drawing - this.surface!.flush(); - }) - .catch((error) => { - reportError("Failed to load image: " + error); - }); + // Draw image with corner radius if specified + if (typeof node.cornerRadius === "number") { + const rrect = this.kit!.RRectXY( + finalDstRect, + node.cornerRadius, + node.cornerRadius + ); + canvas.drawImageRect(image, finalSrcRect, rrect, this.$fillPaint, true); + } else { + canvas.drawImageRect( + image, + finalSrcRect, + finalDstRect, + this.$fillPaint, + true + ); + } + this.surface.flush(); canvas.restore(); } } From e7c6b8f4d24dba5cbc50ed5dcdb6dfbe272a6ad3 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 15:12:41 +0900 Subject: [PATCH 006/262] chore --- .../examples/canvas/instagram-post-01.grida | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/editor/public/examples/canvas/instagram-post-01.grida b/editor/public/examples/canvas/instagram-post-01.grida index 2b099a2388..08c82b5ef3 100644 --- a/editor/public/examples/canvas/instagram-post-01.grida +++ b/editor/public/examples/canvas/instagram-post-01.grida @@ -65,7 +65,7 @@ "active": true, "locked": false, "rotation": 0, - "opacity": 0.699999988079071, + "opacity": 0.7, "zIndex": 0, "type": "text", "text": "# our favorite question", @@ -145,7 +145,7 @@ "textAlign": "left", "textAlignVertical": "top", "textDecoration": "none", - "lineHeight": 1.0909091186523439, + "lineHeight": 1.01, "letterSpacing": 0, "fontSize": 44, "fontFamily": "Inter", @@ -157,7 +157,7 @@ "active": true, "locked": false, "rotation": 0, - "opacity": 0.699999988079071, + "opacity": 0.7, "zIndex": 0, "type": "text", "text": "1 of 10", @@ -179,7 +179,7 @@ "textAlign": "left", "textAlignVertical": "top", "textDecoration": "none", - "lineHeight": 1.4285714721679688, + "lineHeight": 1.43, "letterSpacing": 0, "fontSize": 14, "fontFamily": "Inter", @@ -191,7 +191,7 @@ "active": true, "locked": false, "rotation": 0, - "opacity": 0.699999988079071, + "opacity": 0.7, "zIndex": 0, "type": "text", "text": "getting started", @@ -213,7 +213,7 @@ "textAlign": "left", "textAlignVertical": "top", "textDecoration": "none", - "lineHeight": 1.4285714721679688, + "lineHeight": 1.43, "letterSpacing": 1.5, "fontSize": 14, "fontFamily": "Inter", @@ -247,7 +247,7 @@ "textAlign": "right", "textAlignVertical": "top", "textDecoration": "none", - "lineHeight": 1.4285714721679688, + "lineHeight": 1.43, "letterSpacing": 1.5, "fontSize": 14, "fontFamily": "Inter", @@ -311,10 +311,10 @@ "zIndex": 0, "type": "vector", "position": "absolute", - "left": 1.6666641235351562, - "top": 1.6666641235351562, - "width": 16.666667938232422, - "height": 16.666667938232422, + "left": 1.67, + "top": 1.67, + "width": 16.67, + "height": 16.67, "fill": { "type": "solid", "color": { From ddf819d3700117703fd5947089314b95e9d9fdb2 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 16:38:16 +0900 Subject: [PATCH 007/262] add benchmarks --- .../canvas/experimental/benchmark/2d/page.tsx | 21 + .../experimental/benchmark/skia-gl/page.tsx | 21 + .../experimental/benchmark/skia-gpu/page.tsx | 23 + .../experimental/benchmark/webgl/page.tsx | 21 + .../benchmarks/rectangles.ts | 625 ++++++++++++++++++ 5 files changed, 711 insertions(+) create mode 100644 editor/app/(dev)/canvas/experimental/benchmark/2d/page.tsx create mode 100644 editor/app/(dev)/canvas/experimental/benchmark/skia-gl/page.tsx create mode 100644 editor/app/(dev)/canvas/experimental/benchmark/skia-gpu/page.tsx create mode 100644 editor/app/(dev)/canvas/experimental/benchmark/webgl/page.tsx create mode 100644 packages/grida-canvas-skia/benchmarks/rectangles.ts diff --git a/editor/app/(dev)/canvas/experimental/benchmark/2d/page.tsx b/editor/app/(dev)/canvas/experimental/benchmark/2d/page.tsx new file mode 100644 index 0000000000..cac92e4cf3 --- /dev/null +++ b/editor/app/(dev)/canvas/experimental/benchmark/2d/page.tsx @@ -0,0 +1,21 @@ +"use client"; +import * as React from "react"; +import { BenchmarkCanvas2DRectangles } from "@grida/skia/benchmarks/rectangles"; + +export default function Canvas2DBenchmark() { + const canvasRef = React.useRef(null); + const rendererRef = React.useRef(null); + + React.useEffect(() => { + if (canvasRef.current && !rendererRef.current) { + rendererRef.current = new BenchmarkCanvas2DRectangles(canvasRef.current); + } + return () => rendererRef.current?.dispose(); + }, []); + + return ( +
+ +
+ ); +} diff --git a/editor/app/(dev)/canvas/experimental/benchmark/skia-gl/page.tsx b/editor/app/(dev)/canvas/experimental/benchmark/skia-gl/page.tsx new file mode 100644 index 0000000000..b1784ffc8e --- /dev/null +++ b/editor/app/(dev)/canvas/experimental/benchmark/skia-gl/page.tsx @@ -0,0 +1,21 @@ +"use client"; +import * as React from "react"; +import { BenchmarkSkiaGLRectangles } from "@grida/skia/benchmarks/rectangles"; + +export default function CanvasKitBenchmark() { + const canvasRef = React.useRef(null); + const rendererRef = React.useRef(null); + + React.useEffect(() => { + if (canvasRef.current && !rendererRef.current) { + rendererRef.current = new BenchmarkSkiaGLRectangles(canvasRef.current); + } + return () => rendererRef.current?.dispose(); + }, []); + + return ( +
+ +
+ ); +} diff --git a/editor/app/(dev)/canvas/experimental/benchmark/skia-gpu/page.tsx b/editor/app/(dev)/canvas/experimental/benchmark/skia-gpu/page.tsx new file mode 100644 index 0000000000..557c1ed84f --- /dev/null +++ b/editor/app/(dev)/canvas/experimental/benchmark/skia-gpu/page.tsx @@ -0,0 +1,23 @@ +"use client"; +import * as React from "react"; +import { BenchmarkSkiaWebGPURectangles } from "@grida/skia/benchmarks/rectangles"; + +export default function CanvasKitBenchmark() { + const canvasRef = React.useRef(null); + const rendererRef = React.useRef(null); + + React.useEffect(() => { + if (canvasRef.current && !rendererRef.current) { + rendererRef.current = new BenchmarkSkiaWebGPURectangles( + canvasRef.current + ); + } + return () => rendererRef.current?.dispose(); + }, []); + + return ( +
+ +
+ ); +} diff --git a/editor/app/(dev)/canvas/experimental/benchmark/webgl/page.tsx b/editor/app/(dev)/canvas/experimental/benchmark/webgl/page.tsx new file mode 100644 index 0000000000..5ae9ed16cb --- /dev/null +++ b/editor/app/(dev)/canvas/experimental/benchmark/webgl/page.tsx @@ -0,0 +1,21 @@ +"use client"; +import * as React from "react"; +import { BenchmarkWebGLRectangles } from "@grida/skia/benchmarks/rectangles"; + +export default function WebGLBenchmark() { + const canvasRef = React.useRef(null); + const rendererRef = React.useRef(null); + + React.useEffect(() => { + if (canvasRef.current && !rendererRef.current) { + rendererRef.current = new BenchmarkWebGLRectangles(canvasRef.current); + } + return () => rendererRef.current?.dispose(); + }, []); + + return ( +
+ +
+ ); +} diff --git a/packages/grida-canvas-skia/benchmarks/rectangles.ts b/packages/grida-canvas-skia/benchmarks/rectangles.ts new file mode 100644 index 0000000000..278ae057cc --- /dev/null +++ b/packages/grida-canvas-skia/benchmarks/rectangles.ts @@ -0,0 +1,625 @@ +import CanvasKitInit, { CanvasKit, Paint, Surface } from "canvaskit-wasm"; + +const __n = 10000; + +interface RotatingRect { + x: number; + y: number; + w: number; + h: number; + angle: number; // in degrees + speed: number; // degrees per frame + color: [number, number, number, number]; +} + +export class BenchmarkSkiaGLRectangles { + private kit!: CanvasKit; + private surface!: Surface; + private paint!: Paint; + private strokePaint!: Paint; + private rafId = 0; + + private lastFpsUpdate = performance.now(); + private frameCount = 0; + private fps = 0; + + private rects: RotatingRect[] = []; + + constructor( + private canvas: HTMLCanvasElement, + private count: number = __n + ) { + this.init(); + } + + private async init() { + this.kit = await CanvasKitInit({ + locateFile: (file) => + `https://unpkg.com/canvaskit-wasm@latest/bin/${file}`, + }); + + this.surface = this.kit.MakeWebGLCanvasSurface(this.canvas)!; + + this.paint = new this.kit.Paint(); + this.paint.setAntiAlias(true); + this.paint.setStyle(this.kit.PaintStyle.Fill); + + this.strokePaint = new this.kit.Paint(); + this.strokePaint.setAntiAlias(true); + this.strokePaint.setStyle(this.kit.PaintStyle.Stroke); + this.strokePaint.setColor(this.kit.Color(0, 0, 0, 0)); // transparent stroke + this.strokePaint.setStrokeWidth(1); + + this.rects = this.generateRects(); + this.start(); + } + + private generateRects(): RotatingRect[] { + return Array.from({ length: this.count }, () => ({ + x: Math.random() * 800, + y: Math.random() * 600, + w: 20 + Math.random() * 30, + h: 20 + Math.random() * 30, + angle: Math.random() * 360, + speed: 0.5 + Math.random() * 2, + color: [ + Math.random() * 255, + Math.random() * 255, + Math.random() * 255, + 1.0, + ] as [number, number, number, number], + })); + } + + private start() { + const canvas = this.surface.getCanvas(); + + const loop = () => { + const frameStart = performance.now(); + + canvas.clear(this.kit.Color(255, 255, 255, 1)); + + for (const r of this.rects) { + const cx = r.x + r.w / 2; + const cy = r.y + r.h / 2; + + canvas.save(); + canvas.rotate((r.angle * Math.PI) / 180, cx, cy); + + this.paint.setColor(this.kit.Color(...r.color)); + const rect = this.kit.LTRBRect(r.x, r.y, r.x + r.w, r.y + r.h); + canvas.drawRect(rect, this.paint); + + // 🚨 NO-OP overhead stroke (adds extra WASM call) (testing) + canvas.drawRect(rect, this.strokePaint); + + canvas.restore(); + + r.angle = (r.angle + r.speed) % 360; + } + + this.surface.flush(); + + const frameEnd = performance.now(); + const frameTime = frameEnd - frameStart; + + this.frameCount++; + const now = performance.now(); + if (now - this.lastFpsUpdate >= 1000) { + this.fps = this.frameCount; + console.clear(); + console.table({ + FPS: this.fps, + "Frame Time (ms)": frameTime.toFixed(2), + Rectangles: this.count, + }); + this.frameCount = 0; + this.lastFpsUpdate = now; + } + + this.rafId = requestAnimationFrame(loop); + }; + + loop(); + } + + dispose() { + cancelAnimationFrame(this.rafId); + this.paint?.delete?.(); + this.strokePaint?.delete?.(); + this.surface?.delete?.(); + } +} + +// export class BenchmarkSkiaGLRectangles { +// private kit!: CanvasKit; +// private surface!: Surface; +// private paint!: Paint; +// private rafId = 0; + +// private lastFpsUpdate = performance.now(); +// private frameCount = 0; +// private fps = 0; + +// private rects: RotatingRect[] = []; + +// constructor( +// private canvas: HTMLCanvasElement, +// private count: number = __n +// ) { +// this.init(); +// } + +// private async init() { +// this.kit = await CanvasKitInit({ +// locateFile: (file) => +// `https://unpkg.com/canvaskit-wasm@latest/bin/${file}`, +// }); + +// this.surface = this.kit.MakeWebGLCanvasSurface(this.canvas)!; +// this.paint = new this.kit.Paint(); +// this.paint.setAntiAlias(true); +// this.paint.setStyle(this.kit.PaintStyle.Fill); + +// this.rects = this.generateRects(); +// this.start(); +// } + +// private generateRects(): RotatingRect[] { +// return Array.from({ length: this.count }, () => ({ +// x: Math.random() * 800, +// y: Math.random() * 600, +// w: 20 + Math.random() * 30, +// h: 20 + Math.random() * 30, +// angle: Math.random() * 360, +// speed: 0.5 + Math.random() * 2, // degrees/frame +// color: [ +// Math.random() * 255, +// Math.random() * 255, +// Math.random() * 255, +// 1.0, +// ] as [number, number, number, number], +// })); +// } + +// private start() { +// const canvas = this.surface.getCanvas(); + +// const loop = () => { +// const frameStart = performance.now(); + +// canvas.clear(this.kit.Color(255, 255, 255, 1)); + +// for (const r of this.rects) { +// const cx = r.x + r.w / 2; +// const cy = r.y + r.h / 2; + +// canvas.save(); +// canvas.rotate((r.angle * Math.PI) / 180, cx, cy); + +// this.paint.setColor(this.kit.Color(...r.color)); +// canvas.drawRect( +// this.kit.LTRBRect(r.x, r.y, r.x + r.w, r.y + r.h), +// this.paint +// ); + +// canvas.restore(); + +// // update rotation +// r.angle = (r.angle + r.speed) % 360; +// } + +// this.surface.flush(); + +// const frameEnd = performance.now(); +// const frameTime = frameEnd - frameStart; + +// this.frameCount++; +// const now = performance.now(); +// if (now - this.lastFpsUpdate >= 1000) { +// this.fps = this.frameCount; +// console.clear(); +// console.table({ +// FPS: this.fps, +// "Frame Time (ms)": frameTime.toFixed(2), +// Rectangles: this.count, +// }); +// this.frameCount = 0; +// this.lastFpsUpdate = now; +// } + +// this.rafId = requestAnimationFrame(loop); +// }; + +// loop(); +// } + +// dispose() { +// cancelAnimationFrame(this.rafId); +// this.paint?.delete?.(); +// this.surface?.delete?.(); +// } +// } + +export class BenchmarkSkiaWebGPURectangles { + private kit!: CanvasKit; + private surface!: Surface; + private paint!: Paint; + private rafId = 0; + + private lastFpsUpdate = performance.now(); + private frameCount = 0; + private fps = 0; + + private rects: RotatingRect[] = []; + + constructor( + private canvas: HTMLCanvasElement, + private count: number = __n + ) { + this.init(); + } + + private async init() { + this.kit = await CanvasKitInit({ + locateFile: (file) => + `https://unpkg.com/canvaskit-wasm@latest/bin/${file}`, + }); + + // 1. Get WebGPU context + const context = this.canvas.getContext("webgpu") as GPUCanvasContext; + + // 2. Request device + adapter + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter?.requestDevice()!; + + // 3. Configure context + context.configure({ + device, + format: navigator.gpu.getPreferredCanvasFormat(), + alphaMode: "premultiplied", + }); + + const surface = this.kit.MakeGPUCanvasSurface( + device as any, + this.kit.ColorSpace.SRGB, + this.canvas.width, + this.canvas.height + ); + if (!surface) throw new Error("WebGPU surface creation failed"); + + this.surface = surface; + this.paint = new this.kit.Paint(); + this.paint.setAntiAlias(true); + this.paint.setStyle(this.kit.PaintStyle.Fill); + + this.rects = this.generateRects(); + this.start(); + } + + private generateRects(): RotatingRect[] { + return Array.from({ length: this.count }, () => ({ + x: Math.random() * 800, + y: Math.random() * 600, + w: 20 + Math.random() * 30, + h: 20 + Math.random() * 30, + angle: Math.random() * 360, + speed: 0.5 + Math.random() * 2, + color: [ + Math.random() * 255, + Math.random() * 255, + Math.random() * 255, + 1.0, + ] as [number, number, number, number], + })); + } + + private start() { + const canvas = this.surface.getCanvas(); + + const loop = () => { + const frameStart = performance.now(); + + canvas.clear(this.kit.Color(255, 255, 255, 1)); + + for (const r of this.rects) { + const cx = r.x + r.w / 2; + const cy = r.y + r.h / 2; + + canvas.save(); + canvas.rotate((r.angle * Math.PI) / 180, cx, cy); + + this.paint.setColor(this.kit.Color(...r.color)); + canvas.drawRect( + this.kit.LTRBRect(r.x, r.y, r.x + r.w, r.y + r.h), + this.paint + ); + + canvas.restore(); + + r.angle = (r.angle + r.speed) % 360; + } + + this.surface.flush(); + + const frameEnd = performance.now(); + const frameTime = frameEnd - frameStart; + + this.frameCount++; + const now = performance.now(); + if (now - this.lastFpsUpdate >= 1000) { + this.fps = this.frameCount; + console.clear(); + console.table({ + FPS: this.fps, + "Frame Time (ms)": frameTime.toFixed(2), + Rectangles: this.count, + }); + this.frameCount = 0; + this.lastFpsUpdate = now; + } + + this.rafId = requestAnimationFrame(loop); + }; + + loop(); + } + + dispose() { + cancelAnimationFrame(this.rafId); + this.paint?.delete?.(); + this.surface?.delete?.(); + } +} + +export class BenchmarkCanvas2DRectangles { + private ctx!: CanvasRenderingContext2D; + private rafId = 0; + + private lastFpsUpdate = performance.now(); + private frameCount = 0; + private fps = 0; + + private rects: RotatingRect[] = []; + + constructor( + private canvas: HTMLCanvasElement, + private count: number = __n + ) { + this.ctx = this.canvas.getContext("2d")!; + this.rects = this.generateRects(); + this.start(); + } + private generateRects(): RotatingRect[] { + return Array.from( + { length: this.count }, + (): RotatingRect => ({ + x: Math.random() * 800, + y: Math.random() * 600, + w: 20 + Math.random() * 30, + h: 20 + Math.random() * 30, + angle: Math.random() * 360, + speed: 0.5 + Math.random() * 2, + color: [ + Math.floor(Math.random() * 255), + Math.floor(Math.random() * 255), + Math.floor(Math.random() * 255), + 1, + ], + }) + ); + } + + private start() { + const loop = () => { + const start = performance.now(); + + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + for (const r of this.rects) { + const cx = r.x + r.w / 2; + const cy = r.y + r.h / 2; + + this.ctx.save(); + this.ctx.translate(cx, cy); + this.ctx.rotate((r.angle * Math.PI) / 180); + this.ctx.translate(-cx, -cy); + + this.ctx.fillStyle = `rgba(${r.color[0]}, ${r.color[1]}, ${r.color[2]}, ${r.color[3]})`; + this.ctx.fillRect(r.x, r.y, r.w, r.h); + + this.ctx.restore(); + + r.angle = (r.angle + r.speed) % 360; + } + + const end = performance.now(); + const frameTime = end - start; + + this.frameCount++; + const now = performance.now(); + if (now - this.lastFpsUpdate >= 1000) { + this.fps = this.frameCount; + console.clear(); + console.table({ + FPS: this.fps, + "Frame Time (ms)": frameTime.toFixed(2), + Rectangles: this.rects.length, + }); + this.frameCount = 0; + this.lastFpsUpdate = now; + } + + this.rafId = requestAnimationFrame(loop); + }; + loop(); + } + + dispose() { + cancelAnimationFrame(this.rafId); + } +} + +export class BenchmarkWebGLRectangles { + private gl: WebGLRenderingContext; + private program!: WebGLProgram; + private positionLocation!: number; + private colorLocation!: WebGLUniformLocation; + private matrixLocation!: WebGLUniformLocation; + + private buffer!: WebGLBuffer; + private rafId = 0; + + private lastFpsUpdate = performance.now(); + private frameCount = 0; + private fps = 0; + + private rects: RotatingRect[] = []; + + constructor( + private canvas: HTMLCanvasElement, + private count: number = __n + ) { + this.gl = this.canvas.getContext("webgl")!; + this.rects = this.generateRects(); + this.initGL(); + this.start(); + } + + private generateRects(): RotatingRect[] { + return Array.from({ length: this.count }, () => ({ + x: Math.random() * 800, + y: Math.random() * 600, + w: 20 + Math.random() * 30, + h: 20 + Math.random() * 30, + angle: Math.random() * 360, + speed: 0.5 + Math.random() * 2, + color: [Math.random(), Math.random(), Math.random(), 1.0] as [ + number, + number, + number, + number, + ], + })); + } + + private initGL() { + const gl = this.gl; + + const vert = ` + attribute vec2 a_position; + uniform mat3 u_matrix; + void main() { + vec3 pos = u_matrix * vec3(a_position, 1.0); + gl_Position = vec4(pos.xy, 0, 1); + } + `; + + const frag = ` + precision mediump float; + uniform vec4 u_color; + void main() { + gl_FragColor = u_color; + } + `; + + const vs = gl.createShader(gl.VERTEX_SHADER)!; + gl.shaderSource(vs, vert); + gl.compileShader(vs); + + const fs = gl.createShader(gl.FRAGMENT_SHADER)!; + gl.shaderSource(fs, frag); + gl.compileShader(fs); + + this.program = gl.createProgram()!; + gl.attachShader(this.program, vs); + gl.attachShader(this.program, fs); + gl.linkProgram(this.program); + gl.useProgram(this.program); + + this.positionLocation = gl.getAttribLocation(this.program, "a_position"); + this.colorLocation = gl.getUniformLocation(this.program, "u_color")!; + this.matrixLocation = gl.getUniformLocation(this.program, "u_matrix")!; + + // Rect vertex buffer (2 triangles for 1 unit rect) + this.buffer = gl.createBuffer()!; + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]), + gl.STATIC_DRAW + ); + } + + private start() { + const gl = this.gl; + const loop = () => { + const start = performance.now(); + gl.viewport(0, 0, this.canvas.width, this.canvas.height); + gl.clearColor(1, 1, 1, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + gl.useProgram(this.program); + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.enableVertexAttribArray(this.positionLocation); + gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0); + + for (const r of this.rects) { + const cx = r.x + r.w / 2; + const cy = r.y + r.h / 2; + const rad = (r.angle * Math.PI) / 180; + + const cos = Math.cos(rad); + const sin = Math.sin(rad); + + const sx = r.w; + const sy = r.h; + + // 3x3 transform matrix (translation + rotation + scale) + const tx = r.x; + const ty = r.y; + const mat = [ + (cos * sx) / 400, + (sin * sy) / 300, + 0, + (-sin * sx) / 400, + (cos * sy) / 300, + 0, + (tx + r.w / 2 - 400) / 400, + (ty + r.h / 2 - 300) / 300, + 1, + ]; + + gl.uniformMatrix3fv(this.matrixLocation, false, new Float32Array(mat)); + gl.uniform4fv(this.colorLocation, r.color); + gl.drawArrays(gl.TRIANGLES, 0, 6); + + r.angle = (r.angle + r.speed) % 360; + } + + const end = performance.now(); + const frameTime = end - start; + + this.frameCount++; + const now = performance.now(); + if (now - this.lastFpsUpdate >= 1000) { + this.fps = this.frameCount; + console.clear(); + console.table({ + FPS: this.fps, + "Frame Time (ms)": frameTime.toFixed(2), + Rectangles: this.rects.length, + }); + this.frameCount = 0; + this.lastFpsUpdate = now; + } + + this.rafId = requestAnimationFrame(loop); + }; + loop(); + } + + dispose() { + cancelAnimationFrame(this.rafId); + } +} From 99c066e45b07952a4d041c80c5bb55d8805e0198 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 17:32:09 +0900 Subject: [PATCH 008/262] skia playground --- .../(dev)/canvas/experimental/skia/page.tsx | 215 +++++++++-------- editor/grida-canvas-react/use-editor.tsx | 25 +- .../grida-canvas-react/viewport/hotkeys.tsx | 6 + packages/grida-canvas-skia/index.ts | 216 ++++++++++-------- 4 files changed, 251 insertions(+), 211 deletions(-) diff --git a/editor/app/(dev)/canvas/experimental/skia/page.tsx b/editor/app/(dev)/canvas/experimental/skia/page.tsx index d7d293cff5..254edf6641 100644 --- a/editor/app/(dev)/canvas/experimental/skia/page.tsx +++ b/editor/app/(dev)/canvas/experimental/skia/page.tsx @@ -1,137 +1,132 @@ "use client"; import * as React from "react"; import { CanvasKitRenderer } from "@grida/skia"; -import type grida from "@grida/schema"; +import { + AutoInitialFitTransformer, + EditorSurface, + StandaloneDocumentEditor, + StandaloneSceneBackground, + StandaloneSceneContent, + useEditor, + ViewportRoot, +} from "@/grida-canvas-react"; +import { WindowCurrentEditorProvider } from "@/grida-canvas-react/devtools/global-api-host"; +import { Hotkeys } from "@/grida-canvas-react/viewport/hotkeys"; -const imageNode: grida.program.nodes.ImageNode = { - type: "image", - id: "1", - name: "Image", - active: true, - locked: false, - style: {}, - opacity: 1, - rotation: 0, - zIndex: 0, - position: "absolute", - left: 300, - top: 300, - width: 100, - height: 100, - fit: "contain", - src: "/images/abstract-placeholder.jpg", - cornerRadius: 0, -}; +// const imageNode: grida.program.nodes.ImageNode = { +// type: "image", +// id: "1", +// name: "Image", +// active: true, +// locked: false, +// style: {}, +// opacity: 1, +// rotation: 0, +// zIndex: 0, +// position: "absolute", +// left: 300, +// top: 300, +// width: 100, +// height: 100, +// fit: "contain", +// src: "/images/abstract-placeholder.jpg", +// cornerRadius: 0, +// }; -const textNode: grida.program.nodes.TextNode = { - type: "text", - id: "1", - name: "Text", - active: true, - locked: false, - style: {}, - fontFamily: "Arial", - opacity: 1, - rotation: 0, - zIndex: 0, - position: "absolute", - width: 200, - height: 100, - textAlign: "left", - textAlignVertical: "top", - textDecoration: "none", - fontSize: 16, - fontWeight: 100, - text: "Hello, world!", -}; +// const textNode: grida.program.nodes.TextNode = { +// type: "text", +// id: "1", +// name: "Text", +// active: true, +// locked: false, +// style: {}, +// fontFamily: "Arial", +// opacity: 1, +// rotation: 0, +// zIndex: 0, +// position: "absolute", +// width: 200, +// height: 100, +// textAlign: "left", +// textAlignVertical: "top", +// textDecoration: "none", +// fontSize: 16, +// fontWeight: 100, +// text: "Hello, world!", +// }; -const lineNode: grida.program.nodes.LineNode = { - type: "line", - id: "1", - name: "Line", - active: true, - locked: false, - height: 0, - top: 50, - left: 100, - position: "absolute", - stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, - strokeWidth: 1, - strokeCap: "butt", - width: 200, - opacity: 1, - zIndex: 0, - rotation: 0, -}; - -const rectNode: grida.program.nodes.RectangleNode = { - type: "rectangle", - id: "1", - name: "Rectangle", - active: true, - locked: false, - position: "absolute", - left: 100, - top: 50, - width: 200, - height: 100, - fill: { type: "solid", color: { r: 255, g: 0, b: 0, a: 1 } }, - stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, - strokeWidth: 4, - cornerRadius: 12, - opacity: 0.8, - rotation: 15, - zIndex: 0, - strokeCap: "butt", - effects: [], -}; - -const ellipseNode: grida.program.nodes.EllipseNode = { - type: "ellipse", - id: "1", - name: "Ellipse", - active: true, - locked: false, - position: "absolute", - left: 100, - top: 200, - width: 100, - height: 200, - fill: { type: "solid", color: { r: 0, g: 0, b: 255, a: 1 } }, - stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, - strokeWidth: 4, - opacity: 0.8, - rotation: 15, - zIndex: 0, - strokeCap: "butt", - effects: [], -}; +// const lineNode: grida.program.nodes.LineNode = { +// type: "line", +// id: "1", +// name: "Line", +// active: true, +// locked: false, +// height: 0, +// top: 50, +// left: 100, +// position: "absolute", +// stroke: { type: "solid", color: { r: 0, g: 0, b: 0, a: 1 } }, +// strokeWidth: 1, +// strokeCap: "butt", +// width: 200, +// opacity: 1, +// zIndex: 0, +// rotation: 0, +// }; export default function SkiaCanvasKitExperimentalPage() { const canvasRef = React.useRef(null); const rendererRef = React.useRef(null); + const editor = useEditor(); React.useEffect(() => { if (canvasRef.current && !rendererRef.current) { const renderer = new CanvasKitRenderer(canvasRef.current); rendererRef.current = renderer; - renderer.setNodes([imageNode, textNode, lineNode, rectNode, ellipseNode]); + + editor.subscribeWithSelector( + (state) => state.document.nodes, + (editor, selected) => { + rendererRef.current?.setDocument( + selected, + editor.state.document.scenes["main"].children[0] + ); + } + ); } }, []); return ( -
+

Grida Canvas SKIA BACKEND

- +
+ + +
); } diff --git a/editor/grida-canvas-react/use-editor.tsx b/editor/grida-canvas-react/use-editor.tsx index aee00a242c..dc03346629 100644 --- a/editor/grida-canvas-react/use-editor.tsx +++ b/editor/grida-canvas-react/use-editor.tsx @@ -6,8 +6,29 @@ import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/w import type { editor } from "@/grida-canvas"; import deepEqual from "fast-deep-equal/es6/react.js"; -export function useEditor(init: editor.state.IEditorStateInit) { - const [_editor] = React.useState(new Editor(init)); +export function useEditor(init?: editor.state.IEditorStateInit) { + const [_editor] = React.useState( + new Editor( + init ?? { + debug: false, + document: { + nodes: {}, + entry_scene_id: "main", + scenes: { + main: { + type: "scene", + id: "main", + name: "main", + children: [], + guides: [], + constraints: { children: "multiple" }, + }, + }, + }, + editable: true, + } + ) + ); const editor = useSyncExternalStore( _editor.subscribe.bind(_editor), diff --git a/editor/grida-canvas-react/viewport/hotkeys.tsx b/editor/grida-canvas-react/viewport/hotkeys.tsx index d36616b9b2..4b2994564c 100644 --- a/editor/grida-canvas-react/viewport/hotkeys.tsx +++ b/editor/grida-canvas-react/viewport/hotkeys.tsx @@ -927,3 +927,9 @@ export function useEditorHotKeys() { toast.error("[eject component] is not implemented yet"); }); } + +export function Hotkeys() { + useEditorHotKeys(); + + return null; +} diff --git a/packages/grida-canvas-skia/index.ts b/packages/grida-canvas-skia/index.ts index 9c87343710..e2694ee3f9 100644 --- a/packages/grida-canvas-skia/index.ts +++ b/packages/grida-canvas-skia/index.ts @@ -1,5 +1,4 @@ import CanvasKitInit, { - type Color, type CanvasKit, type Surface, Paint, @@ -15,7 +14,8 @@ export class CanvasKitRenderer { private surface: Surface | null = null; private canvas: HTMLCanvasElement | null = null; - private nodes: grida.program.nodes.Node[] = []; + private nodeMap: Record = {}; + private rootId: string | null = null; private _imageMap: Record< string, @@ -78,8 +78,12 @@ export class CanvasKitRenderer { }); } - public setNodes(nodes: grida.program.nodes.Node[]) { - this.nodes = nodes; + public setDocument( + nodes: Record, + rootId: string + ) { + this.nodeMap = nodes; + this.rootId = rootId; this.requestRender(); } @@ -95,10 +99,15 @@ export class CanvasKitRenderer { private render() { if (!this.kit || !this.surface) return; const canvas = this.surface.getCanvas(); + canvas.save(); canvas.clear(this.kit.Color(255, 255, 255, 1)); - for (const node of this.nodes) { - this.$draw(node); + if (this.rootId) { + const root = this.nodeMap[this.rootId]; + if (root) { + this.$draw(root); + } } + canvas.restore(); this.surface.flush(); } @@ -119,8 +128,12 @@ export class CanvasKitRenderer { case "image": this.drawImageNode(node as grida.program.nodes.ImageNode); break; + case "container": + this.drawContainerNode(node as grida.program.nodes.ContainerNode); + break; default: - throw new Error("Unsupported node type"); + reportError(`Unsupported node type: ${node.type}`); + // throw new Error("Unsupported node type"); } } @@ -168,11 +181,9 @@ export class CanvasKitRenderer { } private drawRectangleNode(node: grida.program.nodes.RectangleNode) { - if (!this.kit) return; - if (!this.surface) return; + if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width, height } = node; - const paint = new this.kit.Paint(); - const innerRect = this.kit.LTRBRect(x, y, x + width, y + height); + const innerRect = this.kit.LTRBRect(0, 0, width, height); const rrect = this.kit.RRectXY( innerRect, typeof node.cornerRadius === "number" ? node.cornerRadius : 0, @@ -180,14 +191,13 @@ export class CanvasKitRenderer { ); const canvas = this.surface.getCanvas(); - // Apply rotation & opacity via save() / restore() - canvas.restore(); canvas.save(); + canvas.translate(x, y); - // move pivot to rect center, rotate, then restore pivot + // move pivot to rect center for rotation if (node.rotation) { - const cx = x + width / 2; - const cy = y + height / 2; + const cx = width / 2; + const cy = height / 2; canvas.translate(cx, cy); canvas.rotate(node.rotation, cx, cy); canvas.translate(-cx, -cy); @@ -206,30 +216,26 @@ export class CanvasKitRenderer { } private drawLineNode(node: grida.program.nodes.LineNode) { - if (!this.kit) return; - if (!this.surface) return; + if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width } = node; const canvas = this.surface.getCanvas(); - // Apply rotation & opacity via save() / restore() - canvas.restore(); canvas.save(); + canvas.translate(x, y); - // move pivot to line center, rotate, then restore pivot if (node.rotation) { - const cx = x + width / 2; - const cy = y; + const cx = width / 2; + const cy = 0; canvas.translate(cx, cy); canvas.rotate(node.rotation, cx, cy); canvas.translate(-cx, -cy); } - // Draw the line canvas.drawLine( - x, - y, - x + width, - y, + 0, + 0, + width, + 0, this.$stroke(node.stroke ?? null, node.strokeWidth) ); @@ -237,27 +243,23 @@ export class CanvasKitRenderer { } private drawEllipseNode(node: grida.program.nodes.EllipseNode) { - if (!this.kit) return; - if (!this.surface) return; + if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width, height } = node; const canvas = this.surface.getCanvas(); - // Apply rotation & opacity via save() / restore() - canvas.restore(); canvas.save(); + canvas.translate(x, y); - // move pivot to ellipse center, rotate, then restore pivot if (node.rotation) { - const cx = x + width / 2; - const cy = y + height / 2; + const cx = width / 2; + const cy = height / 2; canvas.translate(cx, cy); canvas.rotate(node.rotation, cx, cy); canvas.translate(-cx, -cy); } - // Create an oval path const oval = new this.kit.Path(); - oval.addOval(this.kit.LTRBRect(x, y, x + width, y + height)); + oval.addOval(this.kit.LTRBRect(0, 0, width, height)); // Fill canvas.drawPath(oval, this.$fill(node.fill ?? null)); @@ -272,19 +274,18 @@ export class CanvasKitRenderer { } private drawTextNode(node: grida.program.nodes.TextNode) { - if (!this.kit) return; - if (!this.surface) return; + if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; + const w = Number(width); + const h = Number(height); const canvas = this.surface.getCanvas(); - // Apply rotation & opacity via save() / restore() - canvas.restore(); canvas.save(); + canvas.translate(x, y); - // move pivot to text center, rotate, then restore pivot if (node.rotation) { - const cx = x + (width as number) / 2; - const cy = y + (height as number) / 2; + const cx = w / 2; + const cy = h / 2; canvas.translate(cx, cy); canvas.rotate(node.rotation, cx, cy); canvas.translate(-cx, -cy); @@ -305,22 +306,22 @@ export class CanvasKitRenderer { const paragraph = builder.build(); // Calculate text position based on alignment - let textX = x; - let textY = y; + let textX = 0; + let textY = 0; if (node.textAlign === "center") { - textX = x + (width as number) / 2; + textX = w / 2; } else if (node.textAlign === "right") { - textX = x + (width as number); + textX = w; } if (node.textAlignVertical === "center") { - textY = y + (height as number) / 2; + textY = h / 2; } else if (node.textAlignVertical === "bottom") { - textY = y + (height as number); + textY = h; } - canvas.drawParagraph(paragraph, 10, 10); + canvas.drawParagraph(paragraph, textX, textY); canvas.restore(); } @@ -349,19 +350,18 @@ export class CanvasKitRenderer { } private drawImageNode(node: grida.program.nodes.ImageNode) { - if (!this.kit) return; - if (!this.surface) return; + if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; + const w = Number(width); + const h = Number(height); const canvas = this.surface.getCanvas(); - // Apply rotation & opacity via save() / restore() - canvas.restore(); canvas.save(); + canvas.translate(x, y); - // move pivot to image center, rotate, then restore pivot if (node.rotation) { - const cx = x + (width as number) / 2; - const cy = y + (height as number) / 2; + const cx = w / 2; + const cy = h / 2; canvas.translate(cx, cy); canvas.rotate(node.rotation, cx, cy); canvas.translate(-cx, -cy); @@ -379,12 +379,7 @@ export class CanvasKitRenderer { const imgHeight = image.height(); // Create destination rect (where to draw) - const dstRect = this.kit!.LTRBRect( - x, - y, - x + (width as number), - y + (height as number) - ); + const dstRect = this.kit!.LTRBRect(0, 0, w, h); // Create source rect (what part of image to draw) const srcRect = this.kit!.LTRBRect(0, 0, imgWidth, imgHeight); @@ -397,62 +392,42 @@ export class CanvasKitRenderer { case "contain": { // Calculate aspect ratios const imgAspect = imgWidth / imgHeight; - const dstAspect = (width as number) / (height as number); + const dstAspect = w / h; if (imgAspect > dstAspect) { // Image is wider than destination - const newHeight = (width as number) / imgAspect; - const yOffset = ((height as number) - newHeight) / 2; - finalDstRect = this.kit!.LTRBRect( - x, - y + yOffset, - x + (width as number), - y + yOffset + newHeight - ); + const newHeight = w / imgAspect; + const yOffset = (h - newHeight) / 2; + finalDstRect = this.kit!.LTRBRect(0, yOffset, w, yOffset + newHeight); } else { // Image is taller than destination - const newWidth = (height as number) * imgAspect; - const xOffset = ((width as number) - newWidth) / 2; - finalDstRect = this.kit!.LTRBRect( - x + xOffset, - y, - x + xOffset + newWidth, - y + (height as number) - ); + const newWidth = h * imgAspect; + const xOffset = (w - newWidth) / 2; + finalDstRect = this.kit!.LTRBRect(xOffset, 0, xOffset + newWidth, h); } break; } case "cover": { // Calculate aspect ratios const imgAspect = imgWidth / imgHeight; - const dstAspect = (width as number) / (height as number); + const dstAspect = w / h; if (imgAspect > dstAspect) { // Image is wider than destination - const newWidth = (height as number) * imgAspect; - const xOffset = (newWidth - (width as number)) / 2; - finalSrcRect = this.kit!.LTRBRect( - xOffset, - 0, - xOffset + (width as number), - imgHeight - ); + const newWidth = h * imgAspect; + const xOffset = (newWidth - w) / 2; + finalSrcRect = this.kit!.LTRBRect(xOffset, 0, xOffset + w, imgHeight); } else { // Image is taller than destination - const newHeight = (width as number) / imgAspect; - const yOffset = (newHeight - (height as number)) / 2; - finalSrcRect = this.kit!.LTRBRect( - 0, - yOffset, - imgWidth, - yOffset + (height as number) - ); + const newHeight = w / imgAspect; + const yOffset = (newHeight - h) / 2; + finalSrcRect = this.kit!.LTRBRect(0, yOffset, imgWidth, yOffset + h); } break; } case "none": { // Use original image dimensions - finalDstRect = this.kit!.LTRBRect(x, y, x + imgWidth, y + imgHeight); + finalDstRect = this.kit!.LTRBRect(0, 0, imgWidth, imgHeight); break; } } @@ -478,4 +453,47 @@ export class CanvasKitRenderer { this.surface.flush(); canvas.restore(); } + + private drawContainerNode(node: grida.program.nodes.ContainerNode) { + if (!this.kit || !this.surface) return; + const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; + const w = Number(width); + const h = Number(height); + const canvas = this.surface.getCanvas(); + + canvas.save(); + canvas.translate(x, y); + + if (node.rotation) { + const cx = w / 2; + const cy = h / 2; + canvas.translate(cx, cy); + canvas.rotate(node.rotation, cx, cy); + canvas.translate(-cx, -cy); + } + + const rect = this.kit.LTRBRect(0, 0, w, h); + const rrect = this.kit.RRectXY( + rect, + typeof node.cornerRadius === "number" ? node.cornerRadius : 0, + typeof node.cornerRadius === "number" ? node.cornerRadius : 0 + ); + + if (node.fill) { + canvas.drawRRect(rrect, this.$fill(node.fill as cg.Paint)); + } + + if (node.style?.overflow === "clip") { + canvas.clipRRect(rrect, this.$kit.ClipOp.Intersect, true); + } + + for (const childId of node.children || []) { + const child = this.nodeMap[childId]; + if (child) { + this.$draw(child); + } + } + + canvas.restore(); + } } From ea9be38d566871d58b83b2e08b6629da96092f9b Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 17:36:42 +0900 Subject: [PATCH 009/262] opacity --- packages/grida-canvas-skia/index.ts | 94 +++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/packages/grida-canvas-skia/index.ts b/packages/grida-canvas-skia/index.ts index e2694ee3f9..decdd2330c 100644 --- a/packages/grida-canvas-skia/index.ts +++ b/packages/grida-canvas-skia/index.ts @@ -111,33 +111,47 @@ export class CanvasKitRenderer { this.surface.flush(); } - private $draw(node: grida.program.nodes.Node) { + private $draw(node: grida.program.nodes.Node, parentOpacity: number = 1) { + const currentOpacity = + ("opacity" in node ? node.opacity : 1) * parentOpacity; + switch (node.type) { case "rectangle": - this.drawRectangleNode(node as grida.program.nodes.RectangleNode); + this.drawRectangleNode( + node as grida.program.nodes.RectangleNode, + currentOpacity + ); break; case "line": - this.drawLineNode(node as grida.program.nodes.LineNode); + this.drawLineNode(node as grida.program.nodes.LineNode, currentOpacity); break; case "ellipse": - this.drawEllipseNode(node as grida.program.nodes.EllipseNode); + this.drawEllipseNode( + node as grida.program.nodes.EllipseNode, + currentOpacity + ); break; case "text": - this.drawTextNode(node as grida.program.nodes.TextNode); + this.drawTextNode(node as grida.program.nodes.TextNode, currentOpacity); break; case "image": - this.drawImageNode(node as grida.program.nodes.ImageNode); + this.drawImageNode( + node as grida.program.nodes.ImageNode, + currentOpacity + ); break; case "container": - this.drawContainerNode(node as grida.program.nodes.ContainerNode); + this.drawContainerNode( + node as grida.program.nodes.ContainerNode, + currentOpacity + ); break; default: reportError(`Unsupported node type: ${node.type}`); - // throw new Error("Unsupported node type"); } } - private $fill(p: cg.Paint | null): Paint { + private $fill(p: cg.Paint | null, opacity: number = 1): Paint { this.$fillPaint.setAntiAlias(true); if (!p) { @@ -150,7 +164,7 @@ export class CanvasKitRenderer { case "solid": this.$fillPaint.setStyle(this.$kit.PaintStyle.Fill); this.$fillPaint.setColor( - this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a) + this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a * opacity) ); return this.$fillPaint; default: @@ -158,7 +172,11 @@ export class CanvasKitRenderer { } } - private $stroke(p: cg.Paint | null, width: number): Paint { + private $stroke( + p: cg.Paint | null, + width: number, + opacity: number = 1 + ): Paint { this.$strokePaint.setAntiAlias(true); if (!p) { @@ -172,7 +190,7 @@ export class CanvasKitRenderer { this.$strokePaint.setStyle(this.$kit.PaintStyle.Stroke); this.$strokePaint.setStrokeWidth(width); this.$strokePaint.setColor( - this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a) + this.$kit.Color(p.color.r, p.color.g, p.color.b, p.color.a * opacity) ); return this.$strokePaint; default: @@ -180,7 +198,10 @@ export class CanvasKitRenderer { } } - private drawRectangleNode(node: grida.program.nodes.RectangleNode) { + private drawRectangleNode( + node: grida.program.nodes.RectangleNode, + opacity: number = 1 + ) { if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width, height } = node; const innerRect = this.kit.LTRBRect(0, 0, width, height); @@ -204,18 +225,21 @@ export class CanvasKitRenderer { } // Fill - canvas.drawRRect(rrect, this.$fill(node.fill ?? null)); + canvas.drawRRect(rrect, this.$fill(node.fill ?? null, opacity)); // Stroke (if provided) canvas.drawRRect( rrect, - this.$stroke(node.stroke ?? null, node.strokeWidth) + this.$stroke(node.stroke ?? null, node.strokeWidth, opacity) ); canvas.restore(); } - private drawLineNode(node: grida.program.nodes.LineNode) { + private drawLineNode( + node: grida.program.nodes.LineNode, + opacity: number = 1 + ) { if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width } = node; const canvas = this.surface.getCanvas(); @@ -236,13 +260,16 @@ export class CanvasKitRenderer { 0, width, 0, - this.$stroke(node.stroke ?? null, node.strokeWidth) + this.$stroke(node.stroke ?? null, node.strokeWidth, opacity) ); canvas.restore(); } - private drawEllipseNode(node: grida.program.nodes.EllipseNode) { + private drawEllipseNode( + node: grida.program.nodes.EllipseNode, + opacity: number = 1 + ) { if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width, height } = node; const canvas = this.surface.getCanvas(); @@ -262,10 +289,13 @@ export class CanvasKitRenderer { oval.addOval(this.kit.LTRBRect(0, 0, width, height)); // Fill - canvas.drawPath(oval, this.$fill(node.fill ?? null)); + canvas.drawPath(oval, this.$fill(node.fill ?? null, opacity)); // Stroke (if provided) - canvas.drawPath(oval, this.$stroke(node.stroke ?? null, node.strokeWidth)); + canvas.drawPath( + oval, + this.$stroke(node.stroke ?? null, node.strokeWidth, opacity) + ); // Clean up oval.delete(); @@ -273,7 +303,10 @@ export class CanvasKitRenderer { canvas.restore(); } - private drawTextNode(node: grida.program.nodes.TextNode) { + private drawTextNode( + node: grida.program.nodes.TextNode, + opacity: number = 1 + ) { if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; const w = Number(width); @@ -294,7 +327,7 @@ export class CanvasKitRenderer { const fontMgr = this.kit.FontMgr.FromData(this.__roboto_data!); const paraStyle = new this.kit.ParagraphStyle({ textStyle: { - color: this.kit.BLACK, + color: this.kit.Color(0, 0, 0, opacity), fontFamilies: ["Roboto"], fontSize: 28, }, @@ -349,7 +382,10 @@ export class CanvasKitRenderer { return this._imageLoading[src]; } - private drawImageNode(node: grida.program.nodes.ImageNode) { + private drawImageNode( + node: grida.program.nodes.ImageNode, + opacity: number = 1 + ) { if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; const w = Number(width); @@ -432,6 +468,9 @@ export class CanvasKitRenderer { } } + // Set opacity for image + this.$fillPaint.setAlphaf(opacity); + // Draw image with corner radius if specified if (typeof node.cornerRadius === "number") { const rrect = this.kit!.RRectXY( @@ -454,7 +493,10 @@ export class CanvasKitRenderer { canvas.restore(); } - private drawContainerNode(node: grida.program.nodes.ContainerNode) { + private drawContainerNode( + node: grida.program.nodes.ContainerNode, + opacity: number = 1 + ) { if (!this.kit || !this.surface) return; const { left: x = 0, top: y = 0, width = 0, height = 0 } = node; const w = Number(width); @@ -480,7 +522,7 @@ export class CanvasKitRenderer { ); if (node.fill) { - canvas.drawRRect(rrect, this.$fill(node.fill as cg.Paint)); + canvas.drawRRect(rrect, this.$fill(node.fill as cg.Paint, opacity)); } if (node.style?.overflow === "clip") { @@ -490,7 +532,7 @@ export class CanvasKitRenderer { for (const childId of node.children || []) { const child = this.nodeMap[childId]; if (child) { - this.$draw(child); + this.$draw(child, opacity); } } From 12def8317c740f023f6dd0a10388902bacaf0e7d Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Jun 2025 18:09:40 +0900 Subject: [PATCH 010/262] text --- packages/grida-canvas-skia/index.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/grida-canvas-skia/index.ts b/packages/grida-canvas-skia/index.ts index decdd2330c..3f0dd5b520 100644 --- a/packages/grida-canvas-skia/index.ts +++ b/packages/grida-canvas-skia/index.ts @@ -329,10 +329,12 @@ export class CanvasKitRenderer { textStyle: { color: this.kit.Color(0, 0, 0, opacity), fontFamilies: ["Roboto"], - fontSize: 28, + fontSize: node.fontSize || 16, }, textAlign: this.kit.TextAlign.Left, + textDirection: this.kit.TextDirection.LTR, }); + const text = String(node.text || ""); const builder = this.kit.ParagraphBuilder.Make(paraStyle, fontMgr!); builder.addText(text); @@ -342,19 +344,26 @@ export class CanvasKitRenderer { let textX = 0; let textY = 0; + // Handle horizontal alignment if (node.textAlign === "center") { textX = w / 2; + paragraph.layout(w); } else if (node.textAlign === "right") { textX = w; + paragraph.layout(w); + } else { + paragraph.layout(w); } + // Handle vertical alignment if (node.textAlignVertical === "center") { - textY = h / 2; + textY = (h - paragraph.getHeight()) / 2; } else if (node.textAlignVertical === "bottom") { - textY = h; + textY = h - paragraph.getHeight(); } - canvas.drawParagraph(paragraph, textX, textY); + // Draw text with a slight offset to ensure visibility + canvas.drawParagraph(paragraph, textX, textY + 1); canvas.restore(); } From d0ae80a4aec83df8b7c0b2927c507e43ba925e3d Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 13:59:43 +0900 Subject: [PATCH 011/262] skia wasm example --- grida/skia-safe-wasm-example/.gitignore | 104 +++ grida/skia-safe-wasm-example/Cargo.lock | 682 ++++++++++++++++++++ grida/skia-safe-wasm-example/Cargo.toml | 13 + grida/skia-safe-wasm-example/README.md | 41 ++ grida/skia-safe-wasm-example/src/main.rs | 135 ++++ grida/skia-safe-wasm-example/web/index.html | 12 + grida/skia-safe-wasm-example/web/main.js | 53 ++ 7 files changed, 1040 insertions(+) create mode 100644 grida/skia-safe-wasm-example/.gitignore create mode 100644 grida/skia-safe-wasm-example/Cargo.lock create mode 100644 grida/skia-safe-wasm-example/Cargo.toml create mode 100644 grida/skia-safe-wasm-example/README.md create mode 100644 grida/skia-safe-wasm-example/src/main.rs create mode 100644 grida/skia-safe-wasm-example/web/index.html create mode 100644 grida/skia-safe-wasm-example/web/main.js diff --git a/grida/skia-safe-wasm-example/.gitignore b/grida/skia-safe-wasm-example/.gitignore new file mode 100644 index 0000000000..6419b784a5 --- /dev/null +++ b/grida/skia-safe-wasm-example/.gitignore @@ -0,0 +1,104 @@ +# This file should only ignore things that are generated during a `x.py` build, +# generated by common IDEs, and optional files controlled by the user that +# affect the build (such as bootstrap.toml). +# In particular, things like `mir_dump` should not be listed here; they are only +# created during manual debugging and many people like to clean up instead of +# having git ignore such leftovers. You can use `.git/info/exclude` to +# configure your local ignore list. + +## File system +.DS_Store +desktop.ini + +## Editor +*.swp +*.swo +Session.vim +.cproject +.idea +*.iml +.vscode +.project +.vim/ +.helix/ +.zed/ +.favorites.json +.settings/ +.vs/ +.dir-locals.el + +## Tool +.valgrindrc +.cargo +# Included because it is part of the test case +!/tests/run-make/thumb-none-qemu/example/.cargo + +## Configuration +/bootstrap.toml +/config.toml +/Makefile +config.mk +config.stamp +no_llvm_build + +## Build +/dl/ +/doc/ +/inst/ +/llvm/ +/mingw-build/ +/build +/build-rust-analyzer/ +/dist/ +/unicode-downloads +/target +/library/target +/src/bootstrap/target +/src/ci/citool/target +/src/tools/x/target +# Created by `x vendor` +/vendor +# Created by default with `src/ci/docker/run.sh` +/obj/ +# Created by nix dev shell / .envrc +src/tools/nix-dev-shell/flake.lock + +## ICE reports +rustc-ice-*.txt + +## Temporary files +*~ +\#* +\#*\# +.#* + +## Tags +tags +tags.* +TAGS +TAGS.* + +## Python +__pycache__/ +*.py[cod] +*$py.class + +## Node +node_modules +package-lock.json +package.json +/src/doc/rustc-dev-guide/mermaid.min.js + +## Rustdoc GUI tests +tests/rustdoc-gui/src/**.lock + +## direnv +/.envrc +/.direnv/ + +## nix +/flake.nix +flake.lock +/default.nix + +# Before adding new lines, see the comment at the top. \ No newline at end of file diff --git a/grida/skia-safe-wasm-example/Cargo.lock b/grida/skia-safe-wasm-example/Cargo.lock new file mode 100644 index 0000000000..74454915f0 --- /dev/null +++ b/grida/skia-safe-wasm-example/Cargo.lock @@ -0,0 +1,682 @@ +# 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 = "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 = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[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 = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[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", + "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 = "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 = "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 = "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 = "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 = "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 = "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 = "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-example" +version = "0.1.0" +dependencies = [ + "gl", + "skia-safe", +] + +[[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", +] + +[[package]] +name = "xml-rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" diff --git a/grida/skia-safe-wasm-example/Cargo.toml b/grida/skia-safe-wasm-example/Cargo.toml new file mode 100644 index 0000000000..c29a596b0b --- /dev/null +++ b/grida/skia-safe-wasm-example/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "wasm-example" +version = "0.1.0" +edition = "2021" + +[workspace] + +[features] +default = ["skia-safe/gl"] + +[dependencies] +skia-safe = "0.86.0" +gl = "0.14.0" diff --git a/grida/skia-safe-wasm-example/README.md b/grida/skia-safe-wasm-example/README.md new file mode 100644 index 0000000000..5eeef2dfb0 --- /dev/null +++ b/grida/skia-safe-wasm-example/README.md @@ -0,0 +1,41 @@ + + +# WebAssembly Sample + +## Build and Run the Example + +To build this sample you will need to install Emscripten. By default the build script looks for the current installed [asdf](http://asdf-vm.com/) version of `emsdk`. If Emscripten is installed by other means on your system, you can customize its location by setting the `EMSDK` environment variable. + +Then build the example: + +```shell +make build +``` + +Start a web server (requires Python 3): + +```shell +make serve +``` + +Then open http://localhost:8000/web/ in your browser. + +## Notes + +This sample uses the `wasm32-unknown-emscripten` target, because plain WASM [does not support linking to C/C++ libraries](https://github.com/rustwasm/team/issues/291#issuecomment-645482430) (yet). + +For this reason there is a bit of ceremony involved both for building and for running the Rust code. + +The build requires to set several environment variables: + +- `EMSDK` -- required by the rust-skia build script to retrieve Emscripten's include files. + +- `EMCC_CFLAGS` -- used to customize the Emscripten build, specifically: +- `-s ERROR_ON_UNDEFINED_SYMBOLS=0` -- required for Emscripten > 2.0.9, which stopped providing a stub for the `__gxx_personality_v0` C++ function. + + - `-s MAX_WEBGL_VERSION=2` -- enable Emscripten WebGL (1 & 2) support. + + - `-s MODULARIZE=1` -- make Emscripten output module-ish JS. This does not output proper ES6 modules, but without it the init relies on global variables and modules loading order. + - `-s EXPORT_NAME=createRustSkiaModule` -- customize Emscripten's load function. + +- `-s EXPORTED_RUNTIME_METHODS=GL` -- give access to Emscripten's GL group of functions, required to bind Emscripten to the WebGL context. diff --git a/grida/skia-safe-wasm-example/src/main.rs b/grida/skia-safe-wasm-example/src/main.rs new file mode 100644 index 0000000000..94982460f1 --- /dev/null +++ b/grida/skia-safe-wasm-example/src/main.rs @@ -0,0 +1,135 @@ +use std::boxed::Box; + +use skia_safe::{ + gpu::{self, gl::FramebufferInfo, DirectContext}, + Color, Paint, PaintStyle, Surface, +}; + +extern "C" { + pub fn emscripten_GetProcAddress( + name: *const ::std::os::raw::c_char, + ) -> *const ::std::os::raw::c_void; +} + +struct GpuState { + context: DirectContext, + framebuffer_info: FramebufferInfo, +} + +/// This struct holds the state of the Rust application between JS calls. +/// +/// It is created by [init] and passed to the other exported functions. Note that rust-skia data +/// structures are not thread safe, so a state must not be shared between different Web Workers. +pub struct State { + gpu_state: GpuState, + surface: Surface, +} + +impl State { + fn new(gpu_state: GpuState, surface: Surface) -> Self { + State { gpu_state, surface } + } + + fn set_surface(&mut self, surface: Surface) { + self.surface = surface; + } +} + +/// Load GL functions pointers from JavaScript so we can call OpenGL functions from Rust. +/// +/// This only needs to be done once. +fn init_gl() { + unsafe { + gl::load_with(|addr| { + let addr = std::ffi::CString::new(addr).unwrap(); + emscripten_GetProcAddress(addr.into_raw() as *const _) as *const _ + }); + } +} + +/// Create the GPU state from the JavaScript WebGL context. +/// +/// This needs to be done once per WebGL context. +fn create_gpu_state() -> GpuState { + let interface = skia_safe::gpu::gl::Interface::new_native().unwrap(); + let context = skia_safe::gpu::direct_contexts::make_gl(interface, None).unwrap(); + let framebuffer_info = { + let mut fboid: gl::types::GLint = 0; + unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) }; + + FramebufferInfo { + fboid: fboid.try_into().unwrap(), + format: skia_safe::gpu::gl::Format::RGBA8.into(), + protected: skia_safe::gpu::Protected::No, + } + }; + + GpuState { + context, + framebuffer_info, + } +} + +/// Create the Skia surface that will be used for rendering. +fn create_surface(gpu_state: &mut GpuState, width: i32, height: i32) -> Surface { + let backend_render_target = + gpu::backend_render_targets::make_gl((width, height), 1, 8, gpu_state.framebuffer_info); + + gpu::surfaces::wrap_backend_render_target( + &mut gpu_state.context, + &backend_render_target, + skia_safe::gpu::SurfaceOrigin::BottomLeft, + skia_safe::ColorType::RGBA8888, + None, + None, + ) + .unwrap() +} + +fn render_circle(surface: &mut Surface, x: f32, y: f32, radius: f32) { + let mut paint = Paint::default(); + paint.set_style(PaintStyle::Fill); + paint.set_color(Color::BLACK); + paint.set_anti_alias(true); + surface.canvas().draw_circle((x, y), radius, &paint); +} + +/// Initialize the renderer. +/// +/// This is called from JS after the WebGL context has been created. +#[no_mangle] +pub extern "C" fn init(width: i32, height: i32) -> Box { + let mut gpu_state = create_gpu_state(); + let surface = create_surface(&mut gpu_state, width, height); + let state = State::new(gpu_state, surface); + Box::new(state) +} + +/// Resize the Skia surface +/// +/// This is called from JS when the window is resized. +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn resize_surface(state: *mut State, width: i32, height: i32) { + let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); + let surface = create_surface(&mut state.gpu_state, width, height); + state.set_surface(surface); +} + +/// Draw a black circle at the specified coordinates. +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn draw_circle(state: *mut State, x: i32, y: i32) { + let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); + //state.surface.canvas().clear(Color::WHITE); + render_circle(&mut state.surface, x as f32, y as f32, 50.); + state + .gpu_state + .context + .flush_and_submit_surface(&mut state.surface, None); +} + +/// The main function is called by emscripten when the WASM object is created. +fn main() { + init_gl(); +} diff --git a/grida/skia-safe-wasm-example/web/index.html b/grida/skia-safe-wasm-example/web/index.html new file mode 100644 index 0000000000..7e689efd88 --- /dev/null +++ b/grida/skia-safe-wasm-example/web/index.html @@ -0,0 +1,12 @@ + + + + + rust-skia Wasm sample + + + + + + + diff --git a/grida/skia-safe-wasm-example/web/main.js b/grida/skia-safe-wasm-example/web/main.js new file mode 100644 index 0000000000..2411fb2064 --- /dev/null +++ b/grida/skia-safe-wasm-example/web/main.js @@ -0,0 +1,53 @@ +/** + * Make a canvas element fit to the display window. + */ +function resizeCanvasToDisplaySize(canvas) { + const width = canvas.clientWidth | 1; + const height = canvas.clientHeight | 1; + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + return true; + } + return false; +} + +// This loads and initialize our WASM module +createRustSkiaModule().then((RustSkia) => { + // Create the WebGL context + let context; + const canvas = document.querySelector("#glcanvas"); + context = canvas.getContext("webgl2", { + antialias: true, + depth: true, + stencil: true, + alpha: true, + }); + + // Register the context with emscripten + handle = RustSkia.GL.registerContext(context, { majorVersion: 2 }); + RustSkia.GL.makeContextCurrent(handle); + + // Fit the canvas to the viewport + resizeCanvasToDisplaySize(canvas); + + // Initialize Skia + const state = RustSkia._init(canvas.width, canvas.height); + + // Draw a circle that follows the mouse pointer + window.addEventListener("mousemove", (event) => { + const canvasPos = canvas.getBoundingClientRect(); + RustSkia._draw_circle( + state, + event.clientX - canvasPos.x, + event.clientY - canvasPos.y + ); + }); + + // Make canvas size stick to the window size + window.addEventListener("resize", () => { + if (resizeCanvasToDisplaySize(canvas)) { + RustSkia._resize_surface(state, canvas.width, canvas.height); + } + }); +}); From 4e52975db3ba757ec995f3b474e683da2be8eff6 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 15:04:29 +0900 Subject: [PATCH 012/262] cg test --- .gitignore | 10 +- Cargo.lock | 784 ++++++++++++++++++ Cargo.toml | 4 + .../cg}/.gitignore | 0 crates/cg/Cargo.lock | 784 ++++++++++++++++++ crates/cg/Cargo.toml | 25 + crates/cg/output.png | Bin 0 -> 3183 bytes crates/cg/src/bin.rs | 72 ++ crates/cg/src/lib.rs | 121 +++ crates/cg/src/schema.rs | 74 ++ crates/skia-safe-wasm-example/.gitignore | 104 +++ .../skia-safe-wasm-example/Cargo.lock | 0 .../skia-safe-wasm-example/Cargo.toml | 0 .../skia-safe-wasm-example/README.md | 0 .../skia-safe-wasm-example/src/main.rs | 0 .../skia-safe-wasm-example/web/index.html | 0 .../skia-safe-wasm-example/web/main.js | 0 17 files changed, 1972 insertions(+), 6 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml rename {grida/skia-safe-wasm-example => crates/cg}/.gitignore (100%) create mode 100644 crates/cg/Cargo.lock create mode 100644 crates/cg/Cargo.toml create mode 100644 crates/cg/output.png create mode 100644 crates/cg/src/bin.rs create mode 100644 crates/cg/src/lib.rs create mode 100644 crates/cg/src/schema.rs create mode 100644 crates/skia-safe-wasm-example/.gitignore rename {grida => crates}/skia-safe-wasm-example/Cargo.lock (100%) rename {grida => crates}/skia-safe-wasm-example/Cargo.toml (100%) rename {grida => crates}/skia-safe-wasm-example/README.md (100%) rename {grida => crates}/skia-safe-wasm-example/src/main.rs (100%) rename {grida => crates}/skia-safe-wasm-example/web/index.html (100%) rename {grida => crates}/skia-safe-wasm-example/web/main.js (100%) 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/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..04ad23b8dd --- /dev/null +++ b/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/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..ec38312726 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "crates/cg", +] \ No newline at end of file diff --git a/grida/skia-safe-wasm-example/.gitignore b/crates/cg/.gitignore similarity index 100% rename from grida/skia-safe-wasm-example/.gitignore rename to crates/cg/.gitignore diff --git a/crates/cg/Cargo.lock b/crates/cg/Cargo.lock new file mode 100644 index 0000000000..04ad23b8dd --- /dev/null +++ b/crates/cg/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/cg/Cargo.toml b/crates/cg/Cargo.toml new file mode 100644 index 0000000000..d523abf8fa --- /dev/null +++ b/crates/cg/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cg" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "bin" +path = "src/bin.rs" + +[dependencies] +wasm-bindgen = "0.2.100" +skia-safe = "0.86.0" +console_error_panic_hook = "0.1.7" +wee_alloc = { version = "0.4.5", optional = true } + +[features] +default = ["wee_alloc"] + +[target.wasm32-unknown-emscripten] +rustflags = [ + "-s ERROR_ON_UNDEFINED_SYMBOLS=0" +] \ No newline at end of file diff --git a/crates/cg/output.png b/crates/cg/output.png new file mode 100644 index 0000000000000000000000000000000000000000..a56334227c12a9a0465264d3fb18f9f42ee7ee2f GIT binary patch literal 3183 zcmeHIZA?>F7=CYC=q(6sDb|7@{QyK{v#i=Re0iz0h*UrZi)}x&=Rmi$a zKtTa85`qQLL(W0x6!@9uM1>U8lLi zYwbtB+g~LPY9)oXP3o{I%Ir9?he!*F*(Z`y`>&YX9FNZQ_6*t!cv_8L?N+j^e#UV|i(Mxk-Sk!U z)6oxDBGdz-WYR46Sqhb4h_^*y-9>scwe_E91xCXO7A1=t@wgT3n(p`IgkvFZ>@6zC zgEffx4^Bi2m?oY8Y=tMKsg*OvSJg~}^}qM~o-0P~e3vjY)+jMIi_@hPg!Mm(f>_Ms zps>`U+4X!!S41!mF(IuqcAIK3;X(A*u{R0PNtM#ptj^Q;A{zG>Q_E(()oNZsQvAGL zF!bhgw+oLc{bvnX>6bDt{ck?qr>DQ$*taXT15-$vXi~4;F{1tQ0WI zaf34s9lFhfRf8ul!ErYK zL;i3{p?j@}v&lUd!lf0377PD$|FApr+Ye&-FgyIbgP(Fu=Zxe%cGclH3zJB wGvnnNW++bp4$+QbT`o4++?Z3~|EWOVE6+!&>(1&_=UUeW6IUneTC}w3Z||xh`v3p{ literal 0 HcmV?d00001 diff --git a/crates/cg/src/bin.rs b/crates/cg/src/bin.rs new file mode 100644 index 0000000000..af031563fb --- /dev/null +++ b/crates/cg/src/bin.rs @@ -0,0 +1,72 @@ +use cg::schema::{BaseNode, Color, EllipseNode, RectNode, Size, Transform}; +use cg::{draw_ellipse_node, draw_rect_node, free, init}; + +fn main() { + let width = 800; + let height = 600; + + // Initialize the surface using the library function + let surface_ptr = init(width, height); + + // Create a test rectangle node + let rect_node = RectNode { + base: BaseNode { + id: "test_rect".to_string(), + name: "Test Rectangle".to_string(), + active: true, + }, + transform: Transform { + x: 200.0, + y: 100.0, + z: 0, + rotation: 0.0, + opacity: 1.0, + }, + size: Size { + width: 200.0, + height: 150.0, + }, + corner_radius: 10.0, + fill: Color(255, 0, 0, 255), // Red color + }; + + // Create a test ellipse node + let ellipse_node = EllipseNode { + base: BaseNode { + id: "test_ellipse".to_string(), + name: "Test Ellipse".to_string(), + active: true, + }, + transform: Transform { + x: 500.0, + y: 300.0, + z: 0, + rotation: 45.0, // Rotated 45 degrees + opacity: 1.0, + }, + size: Size { + width: 150.0, + height: 100.0, + }, + fill: Color(0, 0, 255, 255), // Blue color + }; + + // Draw the rectangle using our schema + draw_rect_node(surface_ptr, &rect_node); + + // Draw the ellipse using our schema + draw_ellipse_node(surface_ptr, &ellipse_node); + + // Get the surface from the pointer to save the image + let surface = unsafe { &mut *surface_ptr }; + let image = surface.image_snapshot(); + image + .encode_to_data(skia_safe::EncodedImageFormat::PNG) + .and_then(|data| std::fs::write("output.png", data.as_bytes()).ok()) + .expect("Failed to save PNG"); + + // Free the surface + free(surface_ptr); + + println!("Saved output.png"); +} diff --git a/crates/cg/src/lib.rs b/crates/cg/src/lib.rs new file mode 100644 index 0000000000..26e547f4c3 --- /dev/null +++ b/crates/cg/src/lib.rs @@ -0,0 +1,121 @@ +pub mod schema; +use crate::schema::{Color as SchemaColor, EllipseNode, RectNode}; +use console_error_panic_hook::set_once as init_panic_hook; +use skia_safe::{Color, Paint, Rect, Surface, surfaces}; + +pub fn init(width: i32, height: i32) -> *mut Surface { + init_panic_hook(); + let surface = surfaces::raster_n32_premul((width, height)).unwrap(); + Box::into_raw(Box::new(surface)) +} + +pub fn draw_rect( + ptr: *mut Surface, + x: f32, + y: f32, + w: f32, + h: f32, + r: f32, + g: f32, + b: f32, + a: f32, +) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let color = Color::from_argb( + (a * 255.0) as u8, + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + ); + + let mut paint = Paint::default(); + paint.set_color(color); + + canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); +} + +pub fn draw_rect_node(ptr: *mut Surface, node: &RectNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let mut paint = Paint::default(); + let SchemaColor(r, g, b, a) = node.fill; + paint.set_color(Color::from_argb(a, r, g, b)); + + // Apply transform + canvas.save(); + canvas.translate((node.transform.x, node.transform.y)); + canvas.rotate(node.transform.rotation, None); + + // Draw rectangle with corner radius + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + if node.corner_radius > 0.0 { + canvas.draw_round_rect(rect, node.corner_radius, node.corner_radius, &paint); + } else { + canvas.draw_rect(rect, &paint); + } + + canvas.restore(); +} + +pub fn draw_ellipse( + ptr: *mut Surface, + x: f32, + y: f32, + rx: f32, + ry: f32, + r: f32, + g: f32, + b: f32, + a: f32, +) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let color = Color::from_argb( + (a * 255.0) as u8, + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + ); + + let mut paint = Paint::default(); + paint.set_color(color); + + canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); +} + +pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let mut paint = Paint::default(); + let SchemaColor(r, g, b, a) = node.fill; + paint.set_color(Color::from_argb(a, r, g, b)); + + // Apply transform + canvas.save(); + canvas.translate((node.transform.x, node.transform.y)); + canvas.rotate(node.transform.rotation, None); + + // Draw ellipse + let rect = Rect::from_xywh( + -node.size.width / 2.0, + -node.size.height / 2.0, + node.size.width, + node.size.height, + ); + canvas.draw_oval(rect, &paint); + + canvas.restore(); +} + +pub fn flush(_ptr: *mut Surface) { + // No flush needed for raster surfaces +} + +pub fn free(ptr: *mut Surface) { + unsafe { Box::from_raw(ptr) }; +} diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs new file mode 100644 index 0000000000..3edcf00f63 --- /dev/null +++ b/crates/cg/src/schema.rs @@ -0,0 +1,74 @@ +use std::collections::HashMap; + +pub type NodeId = String; + +#[derive(Debug, Clone)] +pub enum Node { + Container(ContainerNode), + Rectangle(RectNode), + Ellipse(EllipseNode), + Text(TextNode), +} + +#[derive(Debug, Clone)] +pub struct BaseNode { + pub id: NodeId, + pub name: String, + pub active: bool, +} + +#[derive(Debug, Clone)] +pub struct Transform { + pub x: f32, + pub y: f32, + pub z: i32, + pub rotation: f32, + pub opacity: f32, +} + +#[derive(Debug, Clone)] +pub struct Size { + pub width: f32, + pub height: f32, +} + +#[derive(Debug, Clone)] +pub struct ContainerNode { + pub base: BaseNode, + pub transform: Transform, + pub size: Size, + pub children: Vec, +} + +#[derive(Debug, Clone)] +pub struct RectNode { + pub base: BaseNode, + pub transform: Transform, + pub size: Size, + pub corner_radius: f32, + pub fill: Color, +} + +#[derive(Debug, Clone)] +pub struct EllipseNode { + pub base: BaseNode, + pub transform: Transform, + pub size: Size, + pub fill: Color, +} + +#[derive(Debug, Clone)] +pub struct TextNode { + pub base: BaseNode, + pub transform: Transform, + pub size: Size, + pub content: String, + pub font_size: f32, + pub fill: Color, +} + +#[derive(Debug, Clone, Copy)] +pub struct Color(pub u8, pub u8, pub u8, pub u8); + +// Example doc tree container +pub type NodeMap = HashMap; diff --git a/crates/skia-safe-wasm-example/.gitignore b/crates/skia-safe-wasm-example/.gitignore new file mode 100644 index 0000000000..6419b784a5 --- /dev/null +++ b/crates/skia-safe-wasm-example/.gitignore @@ -0,0 +1,104 @@ +# This file should only ignore things that are generated during a `x.py` build, +# generated by common IDEs, and optional files controlled by the user that +# affect the build (such as bootstrap.toml). +# In particular, things like `mir_dump` should not be listed here; they are only +# created during manual debugging and many people like to clean up instead of +# having git ignore such leftovers. You can use `.git/info/exclude` to +# configure your local ignore list. + +## File system +.DS_Store +desktop.ini + +## Editor +*.swp +*.swo +Session.vim +.cproject +.idea +*.iml +.vscode +.project +.vim/ +.helix/ +.zed/ +.favorites.json +.settings/ +.vs/ +.dir-locals.el + +## Tool +.valgrindrc +.cargo +# Included because it is part of the test case +!/tests/run-make/thumb-none-qemu/example/.cargo + +## Configuration +/bootstrap.toml +/config.toml +/Makefile +config.mk +config.stamp +no_llvm_build + +## Build +/dl/ +/doc/ +/inst/ +/llvm/ +/mingw-build/ +/build +/build-rust-analyzer/ +/dist/ +/unicode-downloads +/target +/library/target +/src/bootstrap/target +/src/ci/citool/target +/src/tools/x/target +# Created by `x vendor` +/vendor +# Created by default with `src/ci/docker/run.sh` +/obj/ +# Created by nix dev shell / .envrc +src/tools/nix-dev-shell/flake.lock + +## ICE reports +rustc-ice-*.txt + +## Temporary files +*~ +\#* +\#*\# +.#* + +## Tags +tags +tags.* +TAGS +TAGS.* + +## Python +__pycache__/ +*.py[cod] +*$py.class + +## Node +node_modules +package-lock.json +package.json +/src/doc/rustc-dev-guide/mermaid.min.js + +## Rustdoc GUI tests +tests/rustdoc-gui/src/**.lock + +## direnv +/.envrc +/.direnv/ + +## nix +/flake.nix +flake.lock +/default.nix + +# Before adding new lines, see the comment at the top. \ No newline at end of file diff --git a/grida/skia-safe-wasm-example/Cargo.lock b/crates/skia-safe-wasm-example/Cargo.lock similarity index 100% rename from grida/skia-safe-wasm-example/Cargo.lock rename to crates/skia-safe-wasm-example/Cargo.lock diff --git a/grida/skia-safe-wasm-example/Cargo.toml b/crates/skia-safe-wasm-example/Cargo.toml similarity index 100% rename from grida/skia-safe-wasm-example/Cargo.toml rename to crates/skia-safe-wasm-example/Cargo.toml diff --git a/grida/skia-safe-wasm-example/README.md b/crates/skia-safe-wasm-example/README.md similarity index 100% rename from grida/skia-safe-wasm-example/README.md rename to crates/skia-safe-wasm-example/README.md diff --git a/grida/skia-safe-wasm-example/src/main.rs b/crates/skia-safe-wasm-example/src/main.rs similarity index 100% rename from grida/skia-safe-wasm-example/src/main.rs rename to crates/skia-safe-wasm-example/src/main.rs diff --git a/grida/skia-safe-wasm-example/web/index.html b/crates/skia-safe-wasm-example/web/index.html similarity index 100% rename from grida/skia-safe-wasm-example/web/index.html rename to crates/skia-safe-wasm-example/web/index.html diff --git a/grida/skia-safe-wasm-example/web/main.js b/crates/skia-safe-wasm-example/web/main.js similarity index 100% rename from grida/skia-safe-wasm-example/web/main.js rename to crates/skia-safe-wasm-example/web/main.js From d2d44e4b146c2b49fd827f381197ea5ec67f3339 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 16:04:32 +0900 Subject: [PATCH 013/262] add transform --- crates/cg/src/bin.rs | 30 ++++----- crates/cg/src/draw.rs | 131 +++++++++++++++++++++++++++++++++++++ crates/cg/src/lib.rs | 122 +--------------------------------- crates/cg/src/schema.rs | 42 +++++++----- crates/cg/src/transform.rs | 94 ++++++++++++++++++++++++++ 5 files changed, 267 insertions(+), 152 deletions(-) create mode 100644 crates/cg/src/draw.rs create mode 100644 crates/cg/src/transform.rs diff --git a/crates/cg/src/bin.rs b/crates/cg/src/bin.rs index af031563fb..741bb79e98 100644 --- a/crates/cg/src/bin.rs +++ b/crates/cg/src/bin.rs @@ -1,5 +1,6 @@ -use cg::schema::{BaseNode, Color, EllipseNode, RectNode, Size, Transform}; -use cg::{draw_ellipse_node, draw_rect_node, free, init}; +use cg::draw::{draw_ellipse_node, draw_rect_node, free, init}; +use cg::schema::{BaseNode, Color, EllipseNode, RectNode, RectangularCornerRadius, Size}; +use cg::transform::AffineTransform; fn main() { let width = 800; @@ -15,18 +16,18 @@ fn main() { name: "Test Rectangle".to_string(), active: true, }, - transform: Transform { - x: 200.0, - y: 100.0, - z: 0, - rotation: 0.0, - opacity: 1.0, - }, + opacity: 0.3, + transform: AffineTransform::new(200.0, 100.0, 15.0), size: Size { width: 200.0, height: 150.0, }, - corner_radius: 10.0, + corner_radius: RectangularCornerRadius { + tl: 0.0, + tr: 25.0, + bl: 50.0, + br: 100.0, + }, fill: Color(255, 0, 0, 255), // Red color }; @@ -37,13 +38,8 @@ fn main() { name: "Test Ellipse".to_string(), active: true, }, - transform: Transform { - x: 500.0, - y: 300.0, - z: 0, - rotation: 45.0, // Rotated 45 degrees - opacity: 1.0, - }, + opacity: 1.0, + transform: AffineTransform::new(500.0, 300.0, 45.0), // Rotated 45 degrees size: Size { width: 150.0, height: 100.0, diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs new file mode 100644 index 0000000000..ca3d573b8f --- /dev/null +++ b/crates/cg/src/draw.rs @@ -0,0 +1,131 @@ +use crate::schema::{Color as SchemaColor, EllipseNode, RectNode, RectangularCornerRadius}; +use console_error_panic_hook::set_once as init_panic_hook; +use skia_safe::{Color, Paint, Point, RRect, Rect, Surface, surfaces}; + +pub fn init(width: i32, height: i32) -> *mut Surface { + init_panic_hook(); + let surface = surfaces::raster_n32_premul((width, height)).unwrap(); + Box::into_raw(Box::new(surface)) +} + +pub fn draw_rect( + ptr: *mut Surface, + x: f32, + y: f32, + w: f32, + h: f32, + r: f32, + g: f32, + b: f32, + a: f32, +) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let color = Color::from_argb( + (a * 255.0) as u8, + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + ); + + let mut paint = Paint::default(); + paint.set_color(color); + + canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); +} + +pub fn draw_rect_node(ptr: *mut Surface, node: &RectNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let mut paint = Paint::default(); + let SchemaColor(r, g, b, a) = node.fill; + let final_alpha = (a as f32 * node.opacity) as u8; + paint.set_color(Color::from_argb(final_alpha, r, g, b)); + + canvas.save(); + let [[a, c, tx], [b, d, ty]] = node.transform.matrix; + let matrix = [a, b, c, d, tx, ty]; + canvas.concat(&skia_safe::Matrix::from_affine(&matrix)); + + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; + + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + canvas.draw_rrect(rrect, &paint); + } else { + canvas.draw_rect(rect, &paint); + } + + canvas.restore(); +} + +pub fn draw_ellipse( + ptr: *mut Surface, + x: f32, + y: f32, + rx: f32, + ry: f32, + r: f32, + g: f32, + b: f32, + a: f32, +) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let color = Color::from_argb( + (a * 255.0) as u8, + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + ); + + let mut paint = Paint::default(); + paint.set_color(color); + + canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); +} + +pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let mut paint = Paint::default(); + let SchemaColor(r, g, b, a) = node.fill; + let final_alpha = (a as f32 * node.opacity) as u8; + paint.set_color(Color::from_argb(final_alpha, r, g, b)); + + canvas.save(); + let [[a, c, tx], [b, d, ty]] = node.transform.matrix; + let matrix = [a, b, c, d, tx, ty]; + canvas.concat(&skia_safe::Matrix::from_affine(&matrix)); + + let rect = Rect::from_xywh( + -node.size.width / 2.0, + -node.size.height / 2.0, + node.size.width, + node.size.height, + ); + canvas.draw_oval(rect, &paint); + + canvas.restore(); +} + +pub fn flush(_ptr: *mut Surface) { + // No flush needed for raster surfaces +} + +pub fn free(ptr: *mut Surface) { + unsafe { Box::from_raw(ptr) }; +} diff --git a/crates/cg/src/lib.rs b/crates/cg/src/lib.rs index 26e547f4c3..3d73c91255 100644 --- a/crates/cg/src/lib.rs +++ b/crates/cg/src/lib.rs @@ -1,121 +1,3 @@ +pub mod draw; pub mod schema; -use crate::schema::{Color as SchemaColor, EllipseNode, RectNode}; -use console_error_panic_hook::set_once as init_panic_hook; -use skia_safe::{Color, Paint, Rect, Surface, surfaces}; - -pub fn init(width: i32, height: i32) -> *mut Surface { - init_panic_hook(); - let surface = surfaces::raster_n32_premul((width, height)).unwrap(); - Box::into_raw(Box::new(surface)) -} - -pub fn draw_rect( - ptr: *mut Surface, - x: f32, - y: f32, - w: f32, - h: f32, - r: f32, - g: f32, - b: f32, - a: f32, -) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - - let color = Color::from_argb( - (a * 255.0) as u8, - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - ); - - let mut paint = Paint::default(); - paint.set_color(color); - - canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); -} - -pub fn draw_rect_node(ptr: *mut Surface, node: &RectNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - - let mut paint = Paint::default(); - let SchemaColor(r, g, b, a) = node.fill; - paint.set_color(Color::from_argb(a, r, g, b)); - - // Apply transform - canvas.save(); - canvas.translate((node.transform.x, node.transform.y)); - canvas.rotate(node.transform.rotation, None); - - // Draw rectangle with corner radius - let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - if node.corner_radius > 0.0 { - canvas.draw_round_rect(rect, node.corner_radius, node.corner_radius, &paint); - } else { - canvas.draw_rect(rect, &paint); - } - - canvas.restore(); -} - -pub fn draw_ellipse( - ptr: *mut Surface, - x: f32, - y: f32, - rx: f32, - ry: f32, - r: f32, - g: f32, - b: f32, - a: f32, -) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - - let color = Color::from_argb( - (a * 255.0) as u8, - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - ); - - let mut paint = Paint::default(); - paint.set_color(color); - - canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); -} - -pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - - let mut paint = Paint::default(); - let SchemaColor(r, g, b, a) = node.fill; - paint.set_color(Color::from_argb(a, r, g, b)); - - // Apply transform - canvas.save(); - canvas.translate((node.transform.x, node.transform.y)); - canvas.rotate(node.transform.rotation, None); - - // Draw ellipse - let rect = Rect::from_xywh( - -node.size.width / 2.0, - -node.size.height / 2.0, - node.size.width, - node.size.height, - ); - canvas.draw_oval(rect, &paint); - - canvas.restore(); -} - -pub fn flush(_ptr: *mut Surface) { - // No flush needed for raster surfaces -} - -pub fn free(ptr: *mut Surface) { - unsafe { Box::from_raw(ptr) }; -} +pub mod transform; diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 3edcf00f63..fb768edd9d 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -1,3 +1,4 @@ +use crate::transform::AffineTransform; use std::collections::HashMap; pub type NodeId = String; @@ -17,15 +18,6 @@ pub struct BaseNode { pub active: bool, } -#[derive(Debug, Clone)] -pub struct Transform { - pub x: f32, - pub y: f32, - pub z: i32, - pub rotation: f32, - pub opacity: f32, -} - #[derive(Debug, Clone)] pub struct Size { pub width: f32, @@ -35,36 +27,56 @@ pub struct Size { #[derive(Debug, Clone)] pub struct ContainerNode { pub base: BaseNode, - pub transform: Transform, + pub transform: AffineTransform, pub size: Size, pub children: Vec, + pub opacity: f32, +} + +#[derive(Debug, Clone, Copy)] +pub struct RectangularCornerRadius { + pub tl: f32, + pub tr: f32, + pub bl: f32, + pub br: f32, +} + +pub struct LineNode { + pub base: BaseNode, + pub transform: AffineTransform, + pub size: Size, + pub fill: Color, + pub opacity: f32, } #[derive(Debug, Clone)] pub struct RectNode { pub base: BaseNode, - pub transform: Transform, + pub transform: AffineTransform, pub size: Size, - pub corner_radius: f32, + pub corner_radius: RectangularCornerRadius, pub fill: Color, + pub opacity: f32, } #[derive(Debug, Clone)] pub struct EllipseNode { pub base: BaseNode, - pub transform: Transform, + pub transform: AffineTransform, pub size: Size, pub fill: Color, + pub opacity: f32, } #[derive(Debug, Clone)] pub struct TextNode { pub base: BaseNode, - pub transform: Transform, + pub transform: AffineTransform, pub size: Size, - pub content: String, + pub text: String, pub font_size: f32, pub fill: Color, + pub opacity: f32, } #[derive(Debug, Clone, Copy)] diff --git a/crates/cg/src/transform.rs b/crates/cg/src/transform.rs new file mode 100644 index 0000000000..e2cafbbecd --- /dev/null +++ b/crates/cg/src/transform.rs @@ -0,0 +1,94 @@ +/// Represents a 2D affine transformation matrix. +/// +/// The matrix is a 2x3 transformation: +/// [ [a, c, tx], +/// [b, d, ty] ] +/// +/// It supports translation and rotation, and can be composed or inverted. +#[derive(Debug, Clone, Copy)] +pub struct AffineTransform { + /// The 2x3 transformation matrix: [ [a, c, tx], [b, d, ty] ] + pub matrix: [[f32; 3]; 2], +} + +impl AffineTransform { + /// Returns the identity transform. + /// + /// This is equivalent to no transformation. + pub fn identity() -> Self { + Self { + matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + } + } + + /// Creates a translation transform by (tx, ty). + pub fn translate(tx: f32, ty: f32) -> Self { + Self { + matrix: [[1.0, 0.0, tx], [0.0, 1.0, ty]], + } + } + + /// Creates a rotation transform in degrees, counter-clockwise. + pub fn rotate(degrees: f32) -> Self { + let rad = degrees.to_radians(); + let (sin, cos) = rad.sin_cos(); + + Self { + matrix: [[cos, -sin, 0.0], [sin, cos, 0.0]], + } + } + + /// Creates a combined transform of translation followed by rotation. + pub fn new(tx: f32, ty: f32, rotation: f32) -> Self { + Self::translate(tx, ty).compose(&Self::rotate(rotation)) + } + + /// Composes this transform with another. + /// + /// This is equivalent to applying `other` after `self`. + pub fn compose(&self, other: &Self) -> Self { + let a = self.matrix; + let b = other.matrix; + + Self { + matrix: [ + [ + a[0][0] * b[0][0] + a[0][1] * b[1][0], + a[0][0] * b[0][1] + a[0][1] * b[1][1], + a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2], + ], + [ + a[1][0] * b[0][0] + a[1][1] * b[1][0], + a[1][0] * b[0][1] + a[1][1] * b[1][1], + a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2], + ], + ], + } + } + + /// Returns the inverse of this affine transform, if it exists. + /// + /// Returns `None` if the matrix is singular (i.e. non-invertible). + pub fn inverse(&self) -> Option { + let [[a, c, tx], [b, d, ty]] = self.matrix; + + let det = a * d - b * c; + if det.abs() < std::f32::EPSILON { + return None; + } + + let inv_det = 1.0 / det; + + let a_inv = d * inv_det; + let b_inv = -b * inv_det; + let c_inv = -c * inv_det; + let d_inv = a * inv_det; + + let tx_inv = -(a_inv * tx + c_inv * ty); + let ty_inv = -(b_inv * tx + d_inv * ty); + + Some(Self { + matrix: [[a_inv, c_inv, tx_inv], [b_inv, d_inv, ty_inv]], + }) + } +} From dee789b5598068fff8d6420c0e04b621109a321c Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 16:18:31 +0900 Subject: [PATCH 014/262] line node --- crates/cg/src/bin.rs | 23 +++++++++++++++++++++-- crates/cg/src/draw.rs | 28 +++++++++++++++++++++++++++- crates/cg/src/schema.rs | 11 +++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/crates/cg/src/bin.rs b/crates/cg/src/bin.rs index 741bb79e98..35994e8e6c 100644 --- a/crates/cg/src/bin.rs +++ b/crates/cg/src/bin.rs @@ -1,5 +1,5 @@ -use cg::draw::{draw_ellipse_node, draw_rect_node, free, init}; -use cg::schema::{BaseNode, Color, EllipseNode, RectNode, RectangularCornerRadius, Size}; +use cg::draw::{draw_ellipse_node, draw_line_node, draw_rect_node, free, init}; +use cg::schema::{BaseNode, Color, EllipseNode, LineNode, RectNode, RectangularCornerRadius, Size}; use cg::transform::AffineTransform; fn main() { @@ -47,12 +47,31 @@ fn main() { fill: Color(0, 0, 255, 255), // Blue color }; + // Create a test line node + let line_node = LineNode { + base: BaseNode { + id: "test_line".to_string(), + name: "Test Line".to_string(), + active: true, + }, + opacity: 0.8, + transform: AffineTransform::new(100.0, 400.0, 30.0), + size: Size { + width: 200.0, + height: 100.0, + }, + fill: Color(0, 255, 0, 255), // Green color + }; + // Draw the rectangle using our schema draw_rect_node(surface_ptr, &rect_node); // Draw the ellipse using our schema draw_ellipse_node(surface_ptr, &ellipse_node); + // Draw the line using our schema + draw_line_node(surface_ptr, &line_node); + // Get the surface from the pointer to save the image let surface = unsafe { &mut *surface_ptr }; let image = surface.image_snapshot(); diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index ca3d573b8f..adfcd88bd0 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,4 +1,6 @@ -use crate::schema::{Color as SchemaColor, EllipseNode, RectNode, RectangularCornerRadius}; +use crate::schema::{ + Color as SchemaColor, EllipseNode, LineNode, RectNode, RectangularCornerRadius, +}; use console_error_panic_hook::set_once as init_panic_hook; use skia_safe::{Color, Paint, Point, RRect, Rect, Surface, surfaces}; @@ -122,6 +124,30 @@ pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { canvas.restore(); } +pub fn draw_line_node(ptr: *mut Surface, node: &LineNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let mut paint = Paint::default(); + let SchemaColor(r, g, b, a) = node.fill; + let final_alpha = (a as f32 * node.opacity) as u8; + paint.set_color(Color::from_argb(final_alpha, r, g, b)); + + canvas.save(); + let [[a, c, tx], [b, d, ty]] = node.transform.matrix; + let matrix = [a, b, c, d, tx, ty]; + canvas.concat(&skia_safe::Matrix::from_affine(&matrix)); + + // Draw a line from (0,0) to (width,height) + canvas.draw_line( + Point::new(0.0, 0.0), + Point::new(node.size.width, node.size.height), + &paint, + ); + + canvas.restore(); +} + pub fn flush(_ptr: *mut Surface) { // No flush needed for raster surfaces } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index fb768edd9d..8853cc4ae5 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -41,6 +41,7 @@ pub struct RectangularCornerRadius { pub br: f32, } +#[derive(Debug, Clone)] pub struct LineNode { pub base: BaseNode, pub transform: AffineTransform, @@ -59,6 +60,16 @@ pub struct RectNode { pub opacity: f32, } +#[derive(Debug, Clone)] +pub struct ImageNode { + pub base: BaseNode, + pub transform: AffineTransform, + pub size: Size, + pub corner_radius: RectangularCornerRadius, + pub src: String, + pub opacity: f32, +} + #[derive(Debug, Clone)] pub struct EllipseNode { pub base: BaseNode, From d953187b3fd447c07df3ef37f4bdb672b237ebeb Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 16:39:18 +0900 Subject: [PATCH 015/262] gradient paints --- crates/cg/src/bin.rs | 51 ++++++++++++++--- crates/cg/src/draw.rs | 123 +++++++++++++++++++++++++++------------- crates/cg/src/schema.rs | 75 +++++++++++++++++------- 3 files changed, 181 insertions(+), 68 deletions(-) diff --git a/crates/cg/src/bin.rs b/crates/cg/src/bin.rs index 35994e8e6c..ac46d6b8f6 100644 --- a/crates/cg/src/bin.rs +++ b/crates/cg/src/bin.rs @@ -1,5 +1,8 @@ use cg::draw::{draw_ellipse_node, draw_line_node, draw_rect_node, free, init}; -use cg::schema::{BaseNode, Color, EllipseNode, LineNode, RectNode, RectangularCornerRadius, Size}; +use cg::schema::{ + BaseNode, Color, EllipseNode, GradientStop, LineNode, LinearGradientPaint, Paint, + RadialGradientPaint, RectNode, RectangularCornerRadius, Size, SolidPaint, +}; use cg::transform::AffineTransform; fn main() { @@ -9,14 +12,14 @@ fn main() { // Initialize the surface using the library function let surface_ptr = init(width, height); - // Create a test rectangle node + // Create a test rectangle node with linear gradient let rect_node = RectNode { base: BaseNode { id: "test_rect".to_string(), name: "Test Rectangle".to_string(), active: true, }, - opacity: 0.3, + opacity: 1.0, transform: AffineTransform::new(200.0, 100.0, 15.0), size: Size { width: 200.0, @@ -28,10 +31,23 @@ fn main() { bl: 50.0, br: 100.0, }, - fill: Color(255, 0, 0, 255), // Red color + fill: Paint::LinearGradient(LinearGradientPaint { + id: "gradient1".to_string(), + transform: AffineTransform::identity(), + stops: vec![ + GradientStop { + offset: 0.0, + color: Color(255, 0, 0, 255), // Red + }, + GradientStop { + offset: 1.0, + color: Color(0, 0, 255, 255), // Blue + }, + ], + }), }; - // Create a test ellipse node + // Create a test ellipse node with radial gradient let ellipse_node = EllipseNode { base: BaseNode { id: "test_ellipse".to_string(), @@ -44,10 +60,27 @@ fn main() { width: 150.0, height: 100.0, }, - fill: Color(0, 0, 255, 255), // Blue color + fill: Paint::RadialGradient(RadialGradientPaint { + id: "gradient2".to_string(), + 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 + }, + ], + }), }; - // Create a test line node + // Create a test line node with solid color let line_node = LineNode { base: BaseNode { id: "test_line".to_string(), @@ -60,7 +93,9 @@ fn main() { width: 200.0, height: 100.0, }, - fill: Color(0, 255, 0, 255), // Green color + fill: Paint::Solid(SolidPaint { + color: Color(0, 255, 0, 255), // Green color + }), }; // Draw the rectangle using our schema diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index adfcd88bd0..e9a5cfb85d 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,8 +1,9 @@ use crate::schema::{ - Color as SchemaColor, EllipseNode, LineNode, RectNode, RectangularCornerRadius, + Color as SchemaColor, EllipseNode, GradientStop, LineNode, LinearGradientPaint, Paint, + RadialGradientPaint, RectNode, RectangularCornerRadius, SolidPaint, }; use console_error_panic_hook::set_once as init_panic_hook; -use skia_safe::{Color, Paint, Point, RRect, Rect, Surface, surfaces}; +use skia_safe::{Color, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, surfaces}; pub fn init(width: i32, height: i32) -> *mut Surface { init_panic_hook(); @@ -10,6 +11,67 @@ pub fn init(width: i32, height: i32) -> *mut Surface { Box::into_raw(Box::new(surface)) } +fn sk_matrix(m: [[f32; 3]; 2]) -> skia_safe::Matrix { + let [[a, c, tx], [b, d, ty]] = m; + skia_safe::Matrix::from_affine(&[a, b, c, d, tx, ty]) +} + +fn build_gradient_stops(stops: &[GradientStop], opacity: f32) -> (Vec, Vec) { + let mut colors = Vec::with_capacity(stops.len()); + let mut positions = Vec::with_capacity(stops.len()); + + for stop in stops { + let SchemaColor(r, g, b, a) = stop.color; + let alpha = (a as f32 * opacity).round().clamp(0.0, 255.0) as u8; + colors.push(Color::from_argb(alpha, r, g, b)); + positions.push(stop.offset); + } + + (colors, positions) +} + +fn create_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> SkiaPaint { + let mut skia_paint = SkiaPaint::default(); + let (width, height) = size; + match paint { + Paint::Solid(solid) => { + let SchemaColor(r, g, b, a) = solid.color; + let final_alpha = (a as f32 * opacity) as u8; + skia_paint.set_color(Color::from_argb(final_alpha, r, g, b)); + } + Paint::LinearGradient(gradient) => { + let (colors, positions) = build_gradient_stops(&gradient.stops, opacity); + let shader = Shader::linear_gradient( + (Point::new(0.0, 0.0), Point::new(width, 0.0)), + &colors[..], + Some(&positions[..]), + skia_safe::TileMode::Clamp, + None, + Some(&sk_matrix(gradient.transform.matrix)), + ) + .unwrap(); + skia_paint.set_shader(shader); + } + Paint::RadialGradient(gradient) => { + let (colors, positions) = build_gradient_stops(&gradient.stops, opacity); + let center = Point::new(width / 2.0, height / 2.0); + let radius = width.min(height) / 2.0; + let shader = Shader::radial_gradient( + center, + radius, + &colors[..], + Some(&positions[..]), + skia_safe::TileMode::Clamp, + None, + Some(&sk_matrix(gradient.transform.matrix)), + ) + .unwrap(); + skia_paint.set_shader(shader); + } + } + skia_paint +} + pub fn draw_rect( ptr: *mut Surface, x: f32, @@ -31,7 +93,7 @@ pub fn draw_rect( (b * 255.0) as u8, ); - let mut paint = Paint::default(); + let mut paint = SkiaPaint::default(); paint.set_color(color); canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); @@ -40,20 +102,15 @@ pub fn draw_rect( pub fn draw_rect_node(ptr: *mut Surface, node: &RectNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); - - let mut paint = Paint::default(); - let SchemaColor(r, g, b, a) = node.fill; - let final_alpha = (a as f32 * node.opacity) as u8; - paint.set_color(Color::from_argb(final_alpha, r, g, b)); - + let paint = create_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); canvas.save(); - let [[a, c, tx], [b, d, ty]] = node.transform.matrix; - let matrix = [a, b, c, d, tx, ty]; - canvas.concat(&skia_safe::Matrix::from_affine(&matrix)); - + canvas.concat(&sk_matrix(node.transform.matrix)); let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { let rrect = RRect::new_rect_radii( rect, @@ -68,7 +125,6 @@ pub fn draw_rect_node(ptr: *mut Surface, node: &RectNode) { } else { canvas.draw_rect(rect, &paint); } - canvas.restore(); } @@ -93,7 +149,7 @@ pub fn draw_ellipse( (b * 255.0) as u8, ); - let mut paint = Paint::default(); + let mut paint = SkiaPaint::default(); paint.set_color(color); canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); @@ -102,17 +158,13 @@ pub fn draw_ellipse( pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); - - let mut paint = Paint::default(); - let SchemaColor(r, g, b, a) = node.fill; - let final_alpha = (a as f32 * node.opacity) as u8; - paint.set_color(Color::from_argb(final_alpha, r, g, b)); - + let paint = create_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); canvas.save(); - let [[a, c, tx], [b, d, ty]] = node.transform.matrix; - let matrix = [a, b, c, d, tx, ty]; - canvas.concat(&skia_safe::Matrix::from_affine(&matrix)); - + canvas.concat(&sk_matrix(node.transform.matrix)); let rect = Rect::from_xywh( -node.size.width / 2.0, -node.size.height / 2.0, @@ -120,31 +172,24 @@ pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { node.size.height, ); canvas.draw_oval(rect, &paint); - canvas.restore(); } pub fn draw_line_node(ptr: *mut Surface, node: &LineNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); - - let mut paint = Paint::default(); - let SchemaColor(r, g, b, a) = node.fill; - let final_alpha = (a as f32 * node.opacity) as u8; - paint.set_color(Color::from_argb(final_alpha, r, g, b)); - + let paint = create_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); canvas.save(); - let [[a, c, tx], [b, d, ty]] = node.transform.matrix; - let matrix = [a, b, c, d, tx, ty]; - canvas.concat(&skia_safe::Matrix::from_affine(&matrix)); - - // Draw a line from (0,0) to (width,height) + canvas.concat(&sk_matrix(node.transform.matrix)); canvas.draw_line( Point::new(0.0, 0.0), Point::new(node.size.width, node.size.height), &paint, ); - canvas.restore(); } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 8853cc4ae5..2694572ea3 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -3,6 +3,56 @@ use std::collections::HashMap; pub type NodeId = String; +#[derive(Debug, Clone, Copy)] +pub struct Color(pub u8, pub u8, pub u8, pub u8); + +#[derive(Debug, Clone, Copy)] +pub struct GradientStop { + /// 0.0 = start, 1.0 = end + pub offset: f32, + pub color: Color, +} + +#[derive(Debug, Clone)] +pub enum Paint { + Solid(SolidPaint), + LinearGradient(LinearGradientPaint), + RadialGradient(RadialGradientPaint), +} + +#[derive(Debug, Clone)] +pub struct SolidPaint { + pub color: Color, +} + +#[derive(Debug, Clone)] +pub struct LinearGradientPaint { + pub id: String, + pub transform: super::transform::AffineTransform, + pub stops: Vec, +} + +#[derive(Debug, Clone)] +pub struct RadialGradientPaint { + pub id: String, + pub transform: super::transform::AffineTransform, + pub stops: Vec, +} + +#[derive(Debug, Clone)] +pub struct Size { + pub width: f32, + pub height: f32, +} + +#[derive(Debug, Clone, Copy)] +pub struct RectangularCornerRadius { + pub tl: f32, + pub tr: f32, + pub bl: f32, + pub br: f32, +} + #[derive(Debug, Clone)] pub enum Node { Container(ContainerNode), @@ -18,12 +68,6 @@ pub struct BaseNode { pub active: bool, } -#[derive(Debug, Clone)] -pub struct Size { - pub width: f32, - pub height: f32, -} - #[derive(Debug, Clone)] pub struct ContainerNode { pub base: BaseNode, @@ -33,20 +77,12 @@ pub struct ContainerNode { pub opacity: f32, } -#[derive(Debug, Clone, Copy)] -pub struct RectangularCornerRadius { - pub tl: f32, - pub tr: f32, - pub bl: f32, - pub br: f32, -} - #[derive(Debug, Clone)] pub struct LineNode { pub base: BaseNode, pub transform: AffineTransform, pub size: Size, - pub fill: Color, + pub fill: Paint, pub opacity: f32, } @@ -56,7 +92,7 @@ pub struct RectNode { pub transform: AffineTransform, pub size: Size, pub corner_radius: RectangularCornerRadius, - pub fill: Color, + pub fill: Paint, pub opacity: f32, } @@ -75,7 +111,7 @@ pub struct EllipseNode { pub base: BaseNode, pub transform: AffineTransform, pub size: Size, - pub fill: Color, + pub fill: Paint, pub opacity: f32, } @@ -86,12 +122,9 @@ pub struct TextNode { pub size: Size, pub text: String, pub font_size: f32, - pub fill: Color, + pub fill: Paint, pub opacity: f32, } -#[derive(Debug, Clone, Copy)] -pub struct Color(pub u8, pub u8, pub u8, pub u8); - // Example doc tree container pub type NodeMap = HashMap; From 08e57dbc42197b00c6f16f5e30ead93b592a0834 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 16:59:25 +0900 Subject: [PATCH 016/262] stroke_width --- crates/cg/src/bin.rs | 19 +-- crates/cg/src/draw.rs | 293 ++++++++++++++++++++-------------------- crates/cg/src/schema.rs | 5 +- 3 files changed, 161 insertions(+), 156 deletions(-) diff --git a/crates/cg/src/bin.rs b/crates/cg/src/bin.rs index ac46d6b8f6..029290a7a7 100644 --- a/crates/cg/src/bin.rs +++ b/crates/cg/src/bin.rs @@ -1,4 +1,4 @@ -use cg::draw::{draw_ellipse_node, draw_line_node, draw_rect_node, free, init}; +use cg::draw::Renderer; use cg::schema::{ BaseNode, Color, EllipseNode, GradientStop, LineNode, LinearGradientPaint, Paint, RadialGradientPaint, RectNode, RectangularCornerRadius, Size, SolidPaint, @@ -9,8 +9,8 @@ fn main() { let width = 800; let height = 600; - // Initialize the surface using the library function - let surface_ptr = init(width, height); + // Initialize the surface using the Renderer + let surface_ptr = Renderer::init(width, height); // Create a test rectangle node with linear gradient let rect_node = RectNode { @@ -91,21 +91,22 @@ fn main() { transform: AffineTransform::new(100.0, 400.0, 30.0), size: Size { width: 200.0, - height: 100.0, + height: 0.0, // ignored }, - fill: Paint::Solid(SolidPaint { + stroke: Paint::Solid(SolidPaint { color: Color(0, 255, 0, 255), // Green color }), + stroke_width: 4.0, }; // Draw the rectangle using our schema - draw_rect_node(surface_ptr, &rect_node); + Renderer::draw_rect_node(surface_ptr, &rect_node); // Draw the ellipse using our schema - draw_ellipse_node(surface_ptr, &ellipse_node); + Renderer::draw_ellipse_node(surface_ptr, &ellipse_node); // Draw the line using our schema - draw_line_node(surface_ptr, &line_node); + Renderer::draw_line_node(surface_ptr, &line_node); // Get the surface from the pointer to save the image let surface = unsafe { &mut *surface_ptr }; @@ -116,7 +117,7 @@ fn main() { .expect("Failed to save PNG"); // Free the surface - free(surface_ptr); + Renderer::free(surface_ptr); println!("Saved output.png"); } diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index e9a5cfb85d..a3508a2a4b 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -5,33 +5,151 @@ use crate::schema::{ use console_error_panic_hook::set_once as init_panic_hook; use skia_safe::{Color, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, surfaces}; -pub fn init(width: i32, height: i32) -> *mut Surface { - init_panic_hook(); - let surface = surfaces::raster_n32_premul((width, height)).unwrap(); - Box::into_raw(Box::new(surface)) -} +pub struct Renderer; -fn sk_matrix(m: [[f32; 3]; 2]) -> skia_safe::Matrix { - let [[a, c, tx], [b, d, ty]] = m; - skia_safe::Matrix::from_affine(&[a, b, c, d, tx, ty]) -} +impl Renderer { + pub fn init(width: i32, height: i32) -> *mut Surface { + init_panic_hook(); + let surface = surfaces::raster_n32_premul((width, height)).unwrap(); + Box::into_raw(Box::new(surface)) + } -fn build_gradient_stops(stops: &[GradientStop], opacity: f32) -> (Vec, Vec) { - let mut colors = Vec::with_capacity(stops.len()); - let mut positions = Vec::with_capacity(stops.len()); + pub fn draw_rect( + ptr: *mut Surface, + x: f32, + y: f32, + w: f32, + h: f32, + r: f32, + g: f32, + b: f32, + a: f32, + ) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let color = Color::from_argb( + (a * 255.0) as u8, + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + ); - for stop in stops { - let SchemaColor(r, g, b, a) = stop.color; - let alpha = (a as f32 * opacity).round().clamp(0.0, 255.0) as u8; - colors.push(Color::from_argb(alpha, r, g, b)); - positions.push(stop.offset); + let mut paint = SkiaPaint::default(); + paint.set_color(color); + + canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); } - (colors, positions) + pub fn draw_rect_node(ptr: *mut Surface, node: &RectNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + let paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + canvas.draw_rrect(rrect, &paint); + } else { + canvas.draw_rect(rect, &paint); + } + canvas.restore(); + } + + pub fn draw_ellipse( + ptr: *mut Surface, + x: f32, + y: f32, + rx: f32, + ry: f32, + r: f32, + g: f32, + b: f32, + a: f32, + ) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + let color = Color::from_argb( + (a * 255.0) as u8, + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + ); + + let mut paint = SkiaPaint::default(); + paint.set_color(color); + + canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); + } + + pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + let paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + let rect = Rect::from_xywh( + -node.size.width / 2.0, + -node.size.height / 2.0, + node.size.width, + node.size.height, + ); + canvas.draw_oval(rect, &paint); + canvas.restore(); + } + + pub fn draw_line_node(ptr: *mut Surface, node: &LineNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + let mut paint = sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); + paint.set_stroke(true); + paint.set_stroke_width(node.stroke_width); + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + canvas.draw_line( + Point::new(0.0, 0.0), + Point::new(node.size.width, 0.0), + &paint, + ); + canvas.restore(); + } + + pub fn flush(_ptr: *mut Surface) { + // No flush needed for raster surfaces + } + + pub fn free(ptr: *mut Surface) { + unsafe { Box::from_raw(ptr) }; + } } -fn create_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> SkiaPaint { +fn sk_matrix(m: [[f32; 3]; 2]) -> skia_safe::Matrix { + let [[a, c, tx], [b, d, ty]] = m; + skia_safe::Matrix::from_affine(&[a, b, c, d, tx, ty]) +} + +fn sk_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> SkiaPaint { let mut skia_paint = SkiaPaint::default(); + skia_paint.set_anti_alias(true); let (width, height) = size; match paint { Paint::Solid(solid) => { @@ -40,7 +158,7 @@ fn create_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> SkiaPaint { skia_paint.set_color(Color::from_argb(final_alpha, r, g, b)); } Paint::LinearGradient(gradient) => { - let (colors, positions) = build_gradient_stops(&gradient.stops, opacity); + let (colors, positions) = cg_build_gradient_stops(&gradient.stops, opacity); let shader = Shader::linear_gradient( (Point::new(0.0, 0.0), Point::new(width, 0.0)), &colors[..], @@ -53,7 +171,7 @@ fn create_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> SkiaPaint { skia_paint.set_shader(shader); } Paint::RadialGradient(gradient) => { - let (colors, positions) = build_gradient_stops(&gradient.stops, opacity); + let (colors, positions) = cg_build_gradient_stops(&gradient.stops, opacity); let center = Point::new(width / 2.0, height / 2.0); let radius = width.min(height) / 2.0; let shader = Shader::radial_gradient( @@ -72,131 +190,16 @@ fn create_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> SkiaPaint { skia_paint } -pub fn draw_rect( - ptr: *mut Surface, - x: f32, - y: f32, - w: f32, - h: f32, - r: f32, - g: f32, - b: f32, - a: f32, -) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - - let color = Color::from_argb( - (a * 255.0) as u8, - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - ); - - let mut paint = SkiaPaint::default(); - paint.set_color(color); - - canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); -} +fn cg_build_gradient_stops(stops: &[GradientStop], opacity: f32) -> (Vec, Vec) { + let mut colors = Vec::with_capacity(stops.len()); + let mut positions = Vec::with_capacity(stops.len()); -pub fn draw_rect_node(ptr: *mut Surface, node: &RectNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - let paint = create_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left - ], - ); - canvas.draw_rrect(rrect, &paint); - } else { - canvas.draw_rect(rect, &paint); + for stop in stops { + let SchemaColor(r, g, b, a) = stop.color; + let alpha = (a as f32 * opacity).round().clamp(0.0, 255.0) as u8; + colors.push(Color::from_argb(alpha, r, g, b)); + positions.push(stop.offset); } - canvas.restore(); -} - -pub fn draw_ellipse( - ptr: *mut Surface, - x: f32, - y: f32, - rx: f32, - ry: f32, - r: f32, - g: f32, - b: f32, - a: f32, -) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - - let color = Color::from_argb( - (a * 255.0) as u8, - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - ); - - let mut paint = SkiaPaint::default(); - paint.set_color(color); - - canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); -} -pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - let paint = create_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - let rect = Rect::from_xywh( - -node.size.width / 2.0, - -node.size.height / 2.0, - node.size.width, - node.size.height, - ); - canvas.draw_oval(rect, &paint); - canvas.restore(); -} - -pub fn draw_line_node(ptr: *mut Surface, node: &LineNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - let paint = create_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - canvas.draw_line( - Point::new(0.0, 0.0), - Point::new(node.size.width, node.size.height), - &paint, - ); - canvas.restore(); -} - -pub fn flush(_ptr: *mut Surface) { - // No flush needed for raster surfaces -} - -pub fn free(ptr: *mut Surface) { - unsafe { Box::from_raw(ptr) }; + (colors, positions) } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 2694572ea3..79b517322e 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -81,8 +81,9 @@ pub struct ContainerNode { pub struct LineNode { pub base: BaseNode, pub transform: AffineTransform, - pub size: Size, - pub fill: Paint, + pub size: Size, // height is always 0 (ignored) + pub stroke: Paint, + pub stroke_width: f32, pub opacity: f32, } From 4681af46638a534a37ccadb688db8f9169851c06 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 17:19:21 +0900 Subject: [PATCH 017/262] ellipse stroke --- crates/cg/src/bin.rs | 6 +++- crates/cg/src/draw.rs | 20 +++++++++--- crates/cg/src/schema.rs | 68 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/crates/cg/src/bin.rs b/crates/cg/src/bin.rs index 029290a7a7..3015809afc 100644 --- a/crates/cg/src/bin.rs +++ b/crates/cg/src/bin.rs @@ -47,7 +47,7 @@ fn main() { }), }; - // Create a test ellipse node with radial gradient + // Create a test ellipse node with radial gradient and a visible stroke let ellipse_node = EllipseNode { base: BaseNode { id: "test_ellipse".to_string(), @@ -78,6 +78,10 @@ fn main() { }, ], }), + stroke: Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), // Black stroke + }), + stroke_width: 6.0, }; // Create a test line node with solid color diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index a3508a2a4b..203875b660 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -100,20 +100,32 @@ impl Renderer { pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); - let paint = sk_paint( + let fill_paint = sk_paint( &node.fill, node.opacity, (node.size.width, node.size.height), ); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); let rect = Rect::from_xywh( -node.size.width / 2.0, -node.size.height / 2.0, node.size.width, node.size.height, ); - canvas.draw_oval(rect, &paint); + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + // Draw fill + canvas.draw_oval(rect, &fill_paint); + // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint( + &node.stroke, + node.opacity, + (node.size.width, node.size.height), + ); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + canvas.draw_oval(rect, &stroke_paint); + } canvas.restore(); } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 79b517322e..dd249bae22 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -58,6 +58,8 @@ pub enum Node { Container(ContainerNode), Rectangle(RectNode), Ellipse(EllipseNode), + Polygon(PolygonNode), + RegularPolygon(RegularPolygonNode), Text(TextNode), } @@ -113,6 +115,72 @@ pub struct EllipseNode { pub transform: AffineTransform, pub size: Size, pub fill: Paint, + pub stroke: Paint, + pub stroke_width: f32, + pub opacity: f32, +} + +/// A polygon shape defined by a list of absolute 2D points, following the SVG `` model. +/// +/// ## Characteristics +/// - Always **closed**: The shape is implicitly closed by connecting the last point back to the first. +/// - For **open shapes**, use a different type such as [`PathNode`] or a potential `PolylineNode`. +/// +/// ## Reference +/// Mirrors the behavior of the SVG `` element: +/// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon +#[derive(Debug, Clone)] +pub struct PolygonNode { + /// Common base metadata and identity. + pub base: BaseNode, + + /// 2D affine transform matrix applied to the shape. + pub transform: AffineTransform, + + /// The list of absolute coordinates (x, y) defining the polygon vertices. + pub points: Vec<(f32, f32)>, + + /// The paint used to fill the interior of the polygon. + pub fill: Paint, + + /// The stroke paint used to outline the polygon. + pub stroke: Paint, + + /// The stroke width used to outline the polygon. + pub stroke_width: f32, + + /// Opacity applied to the polygon shape (`0.0` - transparent, `1.0` - opaque). + pub opacity: f32, +} + +/// A node representing a regular polygon (triangle, square, pentagon, etc.) +/// that fits inside a bounding box defined by `size`, optionally transformed. +/// +/// The polygon is defined by `point_count` (number of sides), and is centered +/// within the box, with even and odd point counts having slightly different +/// initial orientations: +/// - Odd `point_count` (e.g. triangle) aligns the top point to the vertical center top. +/// - Even `point_count` aligns the top edge flat. +/// +/// The actual rendering is derived, not stored. Rotation should be applied via `transform`. +#[derive(Debug, Clone)] +pub struct RegularPolygonNode { + /// Core identity + metadata + pub base: BaseNode, + + /// Affine transform applied to this node + pub transform: AffineTransform, + + /// Bounding box size the polygon is fit into + pub size: Size, + + /// Number of equally spaced points (>= 3) + pub point_count: usize, + + /// Fill paint (solid or gradient) + pub fill: Paint, + + /// Overall node opacity (0.0–1.0) pub opacity: f32, } From 119b0879a39b432cee83d06bb0bfe135556e9ed9 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 17:23:05 +0900 Subject: [PATCH 018/262] polygon node --- crates/cg/src/bin.rs | 31 +++++++++++++++++++++++++++++++ crates/cg/src/draw.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/crates/cg/src/bin.rs b/crates/cg/src/bin.rs index 3015809afc..d2472cae7b 100644 --- a/crates/cg/src/bin.rs +++ b/crates/cg/src/bin.rs @@ -103,6 +103,34 @@ fn main() { stroke_width: 4.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 = 60.0; + let x = 600.0 + radius * angle.cos(); + let y = 150.0 + radius * angle.sin(); + (x, y) + }) + .collect::>(); + let polygon_node = cg::schema::PolygonNode { + base: BaseNode { + id: "test_polygon".to_string(), + name: "Test Polygon".to_string(), + active: true, + }, + transform: AffineTransform::identity(), + points: pentagon_points, + fill: Paint::Solid(SolidPaint { + color: Color(255, 200, 0, 255), // Orange fill + }), + stroke: Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), // Black stroke + }), + stroke_width: 5.0, + opacity: 1.0, + }; + // Draw the rectangle using our schema Renderer::draw_rect_node(surface_ptr, &rect_node); @@ -112,6 +140,9 @@ fn main() { // Draw the line using our schema Renderer::draw_line_node(surface_ptr, &line_node); + // Draw the polygon using our schema + Renderer::draw_polygon_node(surface_ptr, &polygon_node); + // Get the surface from the pointer to save the image let surface = unsafe { &mut *surface_ptr }; let image = surface.image_snapshot(); diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 203875b660..58e63cebdb 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -145,6 +145,37 @@ impl Renderer { canvas.restore(); } + pub fn draw_polygon_node(ptr: *mut Surface, node: &crate::schema::PolygonNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + if node.points.len() < 3 { + // Not enough points to form a polygon + return; + } + let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + let mut path = skia_safe::Path::new(); + let mut points_iter = node.points.iter(); + if let Some(&(x0, y0)) = points_iter.next() { + path.move_to((x0, y0)); + for &(x, y) in points_iter { + path.line_to((x, y)); + } + path.close(); + } + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + // Draw fill + canvas.draw_path(&path, &fill_paint); + // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + canvas.draw_path(&path, &stroke_paint); + } + canvas.restore(); + } + pub fn flush(_ptr: *mut Surface) { // No flush needed for raster surfaces } From 1e6db0db7dbd12a683adfca9e6abecd496ac817b Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 17:30:06 +0900 Subject: [PATCH 019/262] regular polygon --- crates/cg/src/bin.rs | 26 +++++++++++++++++++++ crates/cg/src/draw.rs | 50 ++++++++++++++++++++++++++++++++++++++++- crates/cg/src/schema.rs | 6 +++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/crates/cg/src/bin.rs b/crates/cg/src/bin.rs index d2472cae7b..e49cc3e3a2 100644 --- a/crates/cg/src/bin.rs +++ b/crates/cg/src/bin.rs @@ -131,6 +131,29 @@ fn main() { opacity: 1.0, }; + // Create a test regular polygon node (hexagon) + let regular_polygon_node = cg::schema::RegularPolygonNode { + base: BaseNode { + id: "test_regular_polygon".to_string(), + name: "Test Regular Polygon".to_string(), + active: true, + }, + transform: AffineTransform::new(600.0, 350.0, 0.0), + size: Size { + width: 120.0, + height: 120.0, + }, + point_count: 6, // hexagon + fill: Paint::Solid(SolidPaint { + color: Color(0, 200, 255, 255), // Cyan fill + }), + stroke: Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), // Black stroke + }), + stroke_width: 4.0, + opacity: 1.0, + }; + // Draw the rectangle using our schema Renderer::draw_rect_node(surface_ptr, &rect_node); @@ -143,6 +166,9 @@ fn main() { // Draw the polygon using our schema Renderer::draw_polygon_node(surface_ptr, &polygon_node); + // Draw the regular polygon using our schema + Renderer::draw_regular_polygon_node(surface_ptr, ®ular_polygon_node); + // Get the surface from the pointer to save the image let surface = unsafe { &mut *surface_ptr }; let image = surface.image_snapshot(); diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 58e63cebdb..18047e99a3 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,9 +1,10 @@ use crate::schema::{ Color as SchemaColor, EllipseNode, GradientStop, LineNode, LinearGradientPaint, Paint, - RadialGradientPaint, RectNode, RectangularCornerRadius, SolidPaint, + PolygonNode, RadialGradientPaint, RectNode, RectangularCornerRadius, RegularPolygonNode, }; use console_error_panic_hook::set_once as init_panic_hook; use skia_safe::{Color, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, surfaces}; +use std::f32::consts::PI; pub struct Renderer; @@ -176,6 +177,11 @@ impl Renderer { canvas.restore(); } + pub fn draw_regular_polygon_node(ptr: *mut Surface, node: &RegularPolygonNode) { + let poly = cg_regular_to_polygon(node); + Self::draw_polygon_node(ptr, &poly); + } + pub fn flush(_ptr: *mut Surface) { // No flush needed for raster surfaces } @@ -246,3 +252,45 @@ fn cg_build_gradient_stops(stops: &[GradientStop], opacity: f32) -> (Vec, (colors, positions) } + +pub fn cg_regular_to_polygon(node: &RegularPolygonNode) -> PolygonNode { + let RegularPolygonNode { + base, + transform, + size, + point_count, + fill, + stroke, + stroke_width, + opacity, + } = node; + + let cx = size.width / 2.0; + let cy = size.height / 2.0; + let r = cx.min(cy); // fit within bounding box + + let angle_offset = if point_count % 2 == 0 { + PI / *point_count as f32 + } else { + -PI / 2.0 + }; + + let points: Vec<(f32, f32)> = (0..*point_count) + .map(|i| { + let angle = (i as f32 / *point_count as f32) * 2.0 * PI + angle_offset; + let x = cx + r * angle.cos(); + let y = cy + r * angle.sin(); + (x, y) + }) + .collect(); + + PolygonNode { + base: base.clone(), + transform: *transform, + points, + fill: fill.clone(), + stroke: stroke.clone(), + stroke_width: *stroke_width, + opacity: *opacity, + } +} diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index dd249bae22..0ba0e39845 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -180,6 +180,12 @@ pub struct RegularPolygonNode { /// Fill paint (solid or gradient) pub fill: Paint, + /// The stroke paint used to outline the polygon. + pub stroke: Paint, + + /// The stroke width used to outline the polygon. + pub stroke_width: f32, + /// Overall node opacity (0.0–1.0) pub opacity: f32, } From aa7a806d59921667e86b563c016e0be5e3c30400 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 17:44:10 +0900 Subject: [PATCH 020/262] mv demo --- crates/cg/Cargo.toml | 4 ++-- crates/cg/src/{bin.rs => demo.rs} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename crates/cg/src/{bin.rs => demo.rs} (100%) diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index d523abf8fa..5b4c11941c 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -7,8 +7,8 @@ edition = "2024" crate-type = ["cdylib", "rlib"] [[bin]] -name = "bin" -path = "src/bin.rs" +name = "demo" +path = "src/demo.rs" [dependencies] wasm-bindgen = "0.2.100" diff --git a/crates/cg/src/bin.rs b/crates/cg/src/demo.rs similarity index 100% rename from crates/cg/src/bin.rs rename to crates/cg/src/demo.rs From 5e707f542134cccd1eef76d098df1c728fff5f02 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 18:24:03 +0900 Subject: [PATCH 021/262] text span --- crates/cg/src/demo.rs | 37 ++++++++++++- crates/cg/src/draw.rs | 76 ++++++++++++++++++++++++++- crates/cg/src/schema.rs | 113 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 3 deletions(-) diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index e49cc3e3a2..6e9486873d 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -1,7 +1,8 @@ use cg::draw::Renderer; use cg::schema::{ - BaseNode, Color, EllipseNode, GradientStop, LineNode, LinearGradientPaint, Paint, - RadialGradientPaint, RectNode, RectangularCornerRadius, Size, SolidPaint, + BaseNode, Color, EllipseNode, FontWeight, GradientStop, LineNode, LinearGradientPaint, Paint, + RadialGradientPaint, RectNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, + TextAlignVertical, TextDecoration, TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; @@ -154,6 +155,35 @@ fn main() { opacity: 1.0, }; + // Create a test text span node + let text_span_node = TextSpanNode { + base: BaseNode { + id: "test_text".to_string(), + name: "Test Text".to_string(), + active: true, + }, + transform: AffineTransform::new(100.0, 100.0, 0.0), + size: Size { + width: 300.0, + height: 100.0, + }, + text: "Grida Canvas SKIA Bindings Backend".to_string(), + text_style: TextStyle { + text_decoration: TextDecoration::None, + font_family: None, + font_size: 24.0, + font_weight: FontWeight::W400, + letter_spacing: None, + line_height: None, + }, + text_align: TextAlign::Center, + text_align_vertical: TextAlignVertical::Center, + fill: Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), // Black text + }), + opacity: 1.0, + }; + // Draw the rectangle using our schema Renderer::draw_rect_node(surface_ptr, &rect_node); @@ -169,6 +199,9 @@ fn main() { // Draw the regular polygon using our schema Renderer::draw_regular_polygon_node(surface_ptr, ®ular_polygon_node); + // Draw the text span node + Renderer::draw_text_span_node(surface_ptr, &text_span_node); + // Get the surface from the pointer to save the image let surface = unsafe { &mut *surface_ptr }; let image = surface.image_snapshot(); diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 18047e99a3..02f1c7c6e9 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,11 +1,38 @@ use crate::schema::{ Color as SchemaColor, EllipseNode, GradientStop, LineNode, LinearGradientPaint, Paint, PolygonNode, RadialGradientPaint, RectNode, RectangularCornerRadius, RegularPolygonNode, + TextAlign, TextAlignVertical, TextSpanNode, }; use console_error_panic_hook::set_once as init_panic_hook; -use skia_safe::{Color, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, surfaces}; +use skia_safe::{ + Color, Font, FontMgr, FontStyle, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, + TextBlob, Typeface, surfaces, +}; +use std::cell::RefCell; use std::f32::consts::PI; +thread_local! { + static DEFAULT_TYPEFACE: RefCell> = RefCell::new(None); +} + +fn default_typeface() -> Typeface { + DEFAULT_TYPEFACE.with(|typeface| { + let mut typeface = typeface.borrow_mut(); + if typeface.is_none() { + let font_mgr = FontMgr::new(); + *typeface = Some( + font_mgr + .legacy_make_typeface(None, FontStyle::default()) + .unwrap(), + ); + } + typeface + .as_ref() + .expect("Failed to initialize default typeface") + .clone() + }) +} + pub struct Renderer; impl Renderer { @@ -182,6 +209,53 @@ impl Renderer { Self::draw_polygon_node(ptr, &poly); } + pub fn draw_text_span_node(ptr: *mut Surface, node: &TextSpanNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + // Create font with the specified size + let font = Font::from_typeface(default_typeface(), node.text_style.font_size); + + // Create text blob + let blob = TextBlob::from_str(&node.text, &font).unwrap(); + + // Create paint with the fill color + let mut paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + + // Calculate text position based on alignment + let (x, y) = match (node.text_align, node.text_align_vertical) { + (TextAlign::Left, TextAlignVertical::Top) => (0.0, node.text_style.font_size), + (TextAlign::Left, TextAlignVertical::Center) => (0.0, node.size.height / 2.0), + (TextAlign::Left, TextAlignVertical::Bottom) => (0.0, node.size.height), + (TextAlign::Center, TextAlignVertical::Top) => { + (node.size.width / 2.0, node.text_style.font_size) + } + (TextAlign::Center, TextAlignVertical::Center) => { + (node.size.width / 2.0, node.size.height / 2.0) + } + (TextAlign::Center, TextAlignVertical::Bottom) => { + (node.size.width / 2.0, node.size.height) + } + (TextAlign::Right, TextAlignVertical::Top) => { + (node.size.width, node.text_style.font_size) + } + (TextAlign::Right, TextAlignVertical::Center) => { + (node.size.width, node.size.height / 2.0) + } + (TextAlign::Right, TextAlignVertical::Bottom) => (node.size.width, node.size.height), + (TextAlign::Justify, _) => (0.0, node.text_style.font_size), // Justify not supported yet + }; + + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + canvas.draw_text_blob(&blob, (x, y), &paint); + canvas.restore(); + } + pub fn flush(_ptr: *mut Surface) { // No flush needed for raster surfaces } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 0ba0e39845..fdb290e7e4 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -6,6 +6,87 @@ pub type NodeId = String; #[derive(Debug, Clone, Copy)] pub struct Color(pub u8, pub u8, pub u8, pub u8); +/// Supported text decoration modes. +/// +/// Only `Underline` and `None` are supported in the current version. +/// +/// - [Flutter](https://api.flutter.dev/flutter/dart-ui/TextDecoration-class.html) +/// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration) +#[derive(Debug, Clone, Copy)] +pub enum TextDecoration { + None, + Underline, +} + +/// Supported horizontal text alignment. +/// +/// Does not include `Start` or `End`, as they are not supported currently. +/// +/// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/text-align) +/// - [Flutter](https://api.flutter.dev/flutter/dart-ui/TextAlign.html) +#[derive(Debug, Clone, Copy)] +pub enum TextAlign { + Left, + Right, + Center, + Justify, +} + +/// Supported vertical alignment values for text. +/// +/// In CSS, this maps to `align-content`. +/// +/// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content) +/// - [Konva](https://konvajs.org/api/Konva.Text.html#verticalAlign) +#[derive(Debug, Clone, Copy)] +pub enum TextAlignVertical { + Top, + Center, + Bottom, +} + +/// Supported font weights, mapped from CSS/OpenType numeric values. +/// +/// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight) +/// - [Flutter](https://api.flutter.dev/flutter/dart-ui/FontWeight-class.html) +/// - [OpenType spec](https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass) +#[derive(Debug, Clone, Copy)] +pub enum FontWeight { + W100, + W200, + W300, + W400, + W500, + W600, + W700, + W800, + W900, +} + +/// A set of style properties that can be applied to a text or text span. +#[derive(Debug, Clone)] +pub struct TextStyle { + /// Text decoration (e.g. underline or none). + pub text_decoration: TextDecoration, + + /// Optional font family name (e.g. "Roboto"). + pub font_family: Option, + + /// Font size in logical pixels. + pub font_size: f32, + + /// Font weight (100–900). + pub font_weight: FontWeight, + + /// Additional spacing between characters, in logical pixels. + /// Default is `0.0`. + pub letter_spacing: Option, + + /// Line height (deprecated). + #[deprecated(note = "Line height is not currently supported or recommended.")] + pub line_height: Option, +} + #[derive(Debug, Clone, Copy)] pub struct GradientStop { /// 0.0 = start, 1.0 = end @@ -190,6 +271,38 @@ pub struct RegularPolygonNode { pub opacity: f32, } +/// A node representing a plain text block (non-rich). +/// For multi-style content, see `RichTextNode` (not implemented yet). +#[derive(Debug, Clone)] +pub struct TextSpanNode { + /// Metadata and identity. + pub base: BaseNode, + + /// Transform applied to the text container. + pub transform: AffineTransform, + + /// Layout bounds (used for wrapping and alignment). + pub size: Size, + + /// Text content (plain UTF-8). + pub text: String, + + /// Font & fill appearance. + pub text_style: TextStyle, + + /// Horizontal alignment. + pub text_align: TextAlign, + + /// Vertical alignment. + pub text_align_vertical: TextAlignVertical, + + /// Fill paint (solid or gradient) + pub fill: Paint, + + /// Overall node opacity. + pub opacity: f32, +} + #[derive(Debug, Clone)] pub struct TextNode { pub base: BaseNode, From 3063116815819975bac40088a0afbafc1981aa3b Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 18:26:27 +0900 Subject: [PATCH 022/262] text stroke --- crates/cg/src/demo.rs | 6 +++++- crates/cg/src/draw.rs | 26 ++++++++++++++++++-------- crates/cg/src/schema.rs | 6 ++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index 6e9486873d..3bb4f8a8e4 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -179,8 +179,12 @@ fn main() { text_align: TextAlign::Center, text_align_vertical: TextAlignVertical::Center, fill: Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), // Black text + color: Color(255, 255, 255, 255), // White text }), + stroke: Some(Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), // Black stroke + })), + stroke_width: Some(4.0), opacity: 1.0, }; diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 02f1c7c6e9..7e55dacd5d 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -219,13 +219,6 @@ impl Renderer { // Create text blob let blob = TextBlob::from_str(&node.text, &font).unwrap(); - // Create paint with the fill color - let mut paint = sk_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - // Calculate text position based on alignment let (x, y) = match (node.text_align, node.text_align_vertical) { (TextAlign::Left, TextAlignVertical::Top) => (0.0, node.text_style.font_size), @@ -252,7 +245,24 @@ impl Renderer { canvas.save(); canvas.concat(&sk_matrix(node.transform.matrix)); - canvas.draw_text_blob(&blob, (x, y), &paint); + + // Draw stroke if specified + if let (Some(stroke), Some(stroke_width)) = (&node.stroke, node.stroke_width) { + let mut stroke_paint = + sk_paint(stroke, node.opacity, (node.size.width, node.size.height)); + stroke_paint.set_style(skia_safe::paint::Style::Stroke); + stroke_paint.set_stroke_width(stroke_width); + canvas.draw_text_blob(&blob, (x, y), &stroke_paint); + } + + // Draw fill + let fill_paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + canvas.draw_text_blob(&blob, (x, y), &fill_paint); + canvas.restore(); } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index fdb290e7e4..3a6c9ace16 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -299,6 +299,12 @@ pub struct TextSpanNode { /// Fill paint (solid or gradient) pub fill: Paint, + /// Stroke paint (solid or gradient) + pub stroke: Option, + + /// Stroke width + pub stroke_width: Option, + /// Overall node opacity. pub opacity: f32, } From 1501f052c5e4aa7321904630449e555ec2c2b794 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 19:08:34 +0900 Subject: [PATCH 023/262] FeDropShadow --- crates/cg/src/demo.rs | 109 ++++++++++++++++++++-------------------- crates/cg/src/draw.rs | 66 ++++++++++++++++++++++-- crates/cg/src/schema.rs | 50 +++++++++++++++++- 3 files changed, 165 insertions(+), 60 deletions(-) diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index 3bb4f8a8e4..571ac14700 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -1,7 +1,9 @@ use cg::draw::Renderer; +use cg::schema::FeDropShadow; +use cg::schema::FilterEffect; use cg::schema::{ BaseNode, Color, EllipseNode, FontWeight, GradientStop, LineNode, LinearGradientPaint, Paint, - RadialGradientPaint, RectNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, + RadialGradientPaint, RectangleNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, TextAlignVertical, TextDecoration, TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; @@ -14,38 +16,37 @@ fn main() { let surface_ptr = Renderer::init(width, height); // Create a test rectangle node with linear gradient - let rect_node = RectNode { + let rect_node = RectangleNode { base: BaseNode { id: "test_rect".to_string(), name: "Test Rectangle".to_string(), active: true, }, opacity: 1.0, - transform: AffineTransform::new(200.0, 100.0, 15.0), + transform: AffineTransform::new(50.0, 50.0, 45.0), size: Size { width: 200.0, - height: 150.0, + height: 100.0, }, corner_radius: RectangularCornerRadius { - tl: 0.0, - tr: 25.0, - bl: 50.0, - br: 100.0, + tl: 10.0, + tr: 10.0, + bl: 10.0, + br: 10.0, }, - fill: Paint::LinearGradient(LinearGradientPaint { - id: "gradient1".to_string(), - transform: AffineTransform::identity(), - stops: vec![ - GradientStop { - offset: 0.0, - color: Color(255, 0, 0, 255), // Red - }, - GradientStop { - offset: 1.0, - color: Color(0, 0, 255, 255), // Blue - }, - ], + fill: Paint::Solid(SolidPaint { + color: Color(255, 0, 0, 255), // Red fill + }), + stroke: Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), // Black stroke }), + stroke_width: 2.0, + effect: Some(FilterEffect::DropShadow(FeDropShadow { + dx: 4.0, + dy: 4.0, + blur: 8.0, + color: Color(0, 0, 0, 77), // Semi-transparent black (0.3 * 255 ≈ 77) + })), }; // Create a test ellipse node with radial gradient and a visible stroke @@ -56,10 +57,10 @@ fn main() { active: true, }, opacity: 1.0, - transform: AffineTransform::new(500.0, 300.0, 45.0), // Rotated 45 degrees + transform: AffineTransform::new(300.0, 50.0, 45.0), // Rotated 45 degrees size: Size { - width: 150.0, - height: 100.0, + width: 200.0, + height: 200.0, }, fill: Paint::RadialGradient(RadialGradientPaint { id: "gradient2".to_string(), @@ -85,31 +86,12 @@ fn main() { stroke_width: 6.0, }; - // Create a test line node with solid color - let line_node = LineNode { - base: BaseNode { - id: "test_line".to_string(), - name: "Test Line".to_string(), - active: true, - }, - opacity: 0.8, - transform: AffineTransform::new(100.0, 400.0, 30.0), - size: Size { - width: 200.0, - height: 0.0, // ignored - }, - stroke: Paint::Solid(SolidPaint { - color: Color(0, 255, 0, 255), // Green color - }), - stroke_width: 4.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 = 60.0; - let x = 600.0 + radius * angle.cos(); + let radius = 100.0; + let x = 550.0 + radius * angle.cos(); let y = 150.0 + radius * angle.sin(); (x, y) }) @@ -139,10 +121,10 @@ fn main() { name: "Test Regular Polygon".to_string(), active: true, }, - transform: AffineTransform::new(600.0, 350.0, 0.0), + transform: AffineTransform::new(300.0, 300.0, 0.0), size: Size { - width: 120.0, - height: 120.0, + width: 200.0, + height: 200.0, }, point_count: 6, // hexagon fill: Paint::Solid(SolidPaint { @@ -162,16 +144,16 @@ fn main() { name: "Test Text".to_string(), active: true, }, - transform: AffineTransform::new(100.0, 100.0, 0.0), + transform: AffineTransform::identity(), size: Size { width: 300.0, - height: 100.0, + height: 200.0, }, text: "Grida Canvas SKIA Bindings Backend".to_string(), text_style: TextStyle { text_decoration: TextDecoration::None, font_family: None, - font_size: 24.0, + font_size: 32.0, font_weight: FontWeight::W400, letter_spacing: None, line_height: None, @@ -188,15 +170,31 @@ fn main() { opacity: 1.0, }; + // Create a test line node with solid color + let line_node = LineNode { + base: BaseNode { + id: "test_line".to_string(), + name: "Test Line".to_string(), + active: true, + }, + opacity: 0.8, + transform: AffineTransform::new(0.0, height as f32 - 50.0, 0.0), + size: Size { + width: width as f32, + height: 0.0, // ignored + }, + stroke: Paint::Solid(SolidPaint { + color: Color(0, 255, 0, 255), // Green color + }), + stroke_width: 4.0, + }; + // Draw the rectangle using our schema Renderer::draw_rect_node(surface_ptr, &rect_node); // Draw the ellipse using our schema Renderer::draw_ellipse_node(surface_ptr, &ellipse_node); - // Draw the line using our schema - Renderer::draw_line_node(surface_ptr, &line_node); - // Draw the polygon using our schema Renderer::draw_polygon_node(surface_ptr, &polygon_node); @@ -206,6 +204,9 @@ fn main() { // Draw the text span node Renderer::draw_text_span_node(surface_ptr, &text_span_node); + // Draw the line using our schema + Renderer::draw_line_node(surface_ptr, &line_node); + // Get the surface from the pointer to save the image let surface = unsafe { &mut *surface_ptr }; let image = surface.image_snapshot(); diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 7e55dacd5d..ad401a9d58 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,7 +1,7 @@ use crate::schema::{ - Color as SchemaColor, EllipseNode, GradientStop, LineNode, LinearGradientPaint, Paint, - PolygonNode, RadialGradientPaint, RectNode, RectangularCornerRadius, RegularPolygonNode, - TextAlign, TextAlignVertical, TextSpanNode, + Color as SchemaColor, EllipseNode, FilterEffect, GradientStop, LineNode, LinearGradientPaint, + Paint, PolygonNode, RadialGradientPaint, RectangleNode, RectangularCornerRadius, + RegularPolygonNode, TextAlign, TextAlignVertical, TextSpanNode, }; use console_error_panic_hook::set_once as init_panic_hook; use skia_safe::{ @@ -69,7 +69,7 @@ impl Renderer { canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); } - pub fn draw_rect_node(ptr: *mut Surface, node: &RectNode) { + pub fn draw_rect_node(ptr: *mut Surface, node: &RectangleNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); let paint = sk_paint( @@ -81,6 +81,42 @@ impl Renderer { canvas.concat(&sk_matrix(node.transform.matrix)); let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; + // Draw drop shadow effect if present + if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { + use skia_safe::{MaskFilter, Paint as SkiaPaint}; + let mut shadow_paint = SkiaPaint::default(); + let crate::schema::Color(r, g, b, a) = shadow.color; + shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); + shadow_paint.set_anti_alias(true); + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + skia_safe::BlurStyle::Normal, + shadow.blur, + None, + )); + } + let offset_x = shadow.dx; + let offset_y = shadow.dy; + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + let mut shadow_rrect = rrect; + shadow_rrect.offset((offset_x, offset_y)); + canvas.draw_rrect(shadow_rrect, &shadow_paint); + } else { + let mut shadow_rect = rect; + shadow_rect.offset((offset_x, offset_y)); + canvas.draw_rect(shadow_rect, &shadow_paint); + } + } + // Draw fill and stroke as before if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { let rrect = RRect::new_rect_radii( rect, @@ -92,8 +128,30 @@ impl Renderer { ], ); canvas.draw_rrect(rrect, &paint); + // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint( + &node.stroke, + node.opacity, + (node.size.width, node.size.height), + ); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + canvas.draw_rrect(rrect, &stroke_paint); + } } else { canvas.draw_rect(rect, &paint); + // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint( + &node.stroke, + node.opacity, + (node.size.width, node.size.height), + ); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + canvas.draw_rect(rect, &stroke_paint); + } } canvas.restore(); } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 3a6c9ace16..2aaa595621 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -6,6 +6,43 @@ pub type NodeId = String; #[derive(Debug, Clone, Copy)] pub struct Color(pub u8, pub u8, pub u8, pub u8); +/// Represents filter effects inspired by SVG `` primitives. +/// +/// See also: +/// - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDropShadow +/// - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feGaussianBlur +#[derive(Debug, Clone)] +pub enum FilterEffect { + /// Drop shadow filter: offset + blur + color + DropShadow(FeDropShadow), + + /// Gaussian blur filter: blur only + GaussianBlur(FeGaussianBlur), +} + +/// A drop shadow filter effect (``) +#[derive(Debug, Clone, Copy)] +pub struct FeDropShadow { + /// Horizontal shadow offset in px + pub dx: f32, + + /// Vertical shadow offset in px + pub dy: f32, + + /// Blur radius (`stdDeviation` in SVG) + pub blur: f32, + + /// Shadow color (includes alpha) + pub color: Color, +} + +/// A standalone blur filter effect (``) +#[derive(Debug, Clone, Copy)] +pub struct FeGaussianBlur { + /// Blur radius (`stdDeviation` in SVG) + pub radius: f32, +} + /// Supported text decoration modes. /// /// Only `Underline` and `None` are supported in the current version. @@ -134,10 +171,12 @@ pub struct RectangularCornerRadius { pub br: f32, } +// region: Node Definitions + #[derive(Debug, Clone)] pub enum Node { Container(ContainerNode), - Rectangle(RectNode), + Rectangle(RectangleNode), Ellipse(EllipseNode), Polygon(PolygonNode), RegularPolygon(RegularPolygonNode), @@ -171,16 +210,20 @@ pub struct LineNode { } #[derive(Debug, Clone)] -pub struct RectNode { +pub struct RectangleNode { pub base: BaseNode, pub transform: AffineTransform, pub size: Size, pub corner_radius: RectangularCornerRadius, pub fill: Paint, + pub stroke: Paint, + pub stroke_width: f32, pub opacity: f32, + pub effect: Option, } #[derive(Debug, Clone)] +#[deprecated(note = "Not implemented yet")] pub struct ImageNode { pub base: BaseNode, pub transform: AffineTransform, @@ -310,6 +353,7 @@ pub struct TextSpanNode { } #[derive(Debug, Clone)] +#[deprecated(note = "Not implemented yet")] pub struct TextNode { pub base: BaseNode, pub transform: AffineTransform, @@ -320,5 +364,7 @@ pub struct TextNode { pub opacity: f32, } +// endregion + // Example doc tree container pub type NodeMap = HashMap; From 59ee077436b01352329619dded1c1e2057b2050b Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 19:12:55 +0900 Subject: [PATCH 024/262] blend mode --- crates/cg/src/demo.rs | 12 +++++-- crates/cg/src/draw.rs | 21 ++++++++++-- crates/cg/src/schema.rs | 72 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index 571ac14700..5e2433da7e 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -2,9 +2,9 @@ use cg::draw::Renderer; use cg::schema::FeDropShadow; use cg::schema::FilterEffect; use cg::schema::{ - BaseNode, Color, EllipseNode, FontWeight, GradientStop, LineNode, LinearGradientPaint, Paint, - RadialGradientPaint, RectangleNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, - TextAlignVertical, TextDecoration, TextSpanNode, TextStyle, + BaseNode, BlendMode, Color, EllipseNode, FontWeight, GradientStop, LineNode, + LinearGradientPaint, Paint, RadialGradientPaint, RectangleNode, RectangularCornerRadius, Size, + SolidPaint, TextAlign, TextAlignVertical, TextDecoration, TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; @@ -21,6 +21,7 @@ fn main() { id: "test_rect".to_string(), name: "Test Rectangle".to_string(), active: true, + blend_mode: BlendMode::Normal, }, opacity: 1.0, transform: AffineTransform::new(50.0, 50.0, 45.0), @@ -55,6 +56,7 @@ fn main() { id: "test_ellipse".to_string(), name: "Test Ellipse".to_string(), active: true, + blend_mode: BlendMode::Multiply, // Example of using a different blend mode }, opacity: 1.0, transform: AffineTransform::new(300.0, 50.0, 45.0), // Rotated 45 degrees @@ -101,6 +103,7 @@ fn main() { id: "test_polygon".to_string(), name: "Test Polygon".to_string(), active: true, + blend_mode: BlendMode::Screen, // Example of using Screen blend mode }, transform: AffineTransform::identity(), points: pentagon_points, @@ -120,6 +123,7 @@ fn main() { id: "test_regular_polygon".to_string(), name: "Test Regular Polygon".to_string(), active: true, + blend_mode: BlendMode::Overlay, // Example of using Overlay blend mode }, transform: AffineTransform::new(300.0, 300.0, 0.0), size: Size { @@ -143,6 +147,7 @@ fn main() { id: "test_text".to_string(), name: "Test Text".to_string(), active: true, + blend_mode: BlendMode::Normal, }, transform: AffineTransform::identity(), size: Size { @@ -176,6 +181,7 @@ fn main() { id: "test_line".to_string(), name: "Test Line".to_string(), active: true, + blend_mode: BlendMode::Normal, }, opacity: 0.8, transform: AffineTransform::new(0.0, height as f32 - 50.0, 0.0), diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index ad401a9d58..74df5e2898 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -127,7 +127,9 @@ impl Renderer { Point::new(bl, bl), // bottom-left ], ); - canvas.draw_rrect(rrect, &paint); + let mut fill_paint = paint.clone(); + fill_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_rrect(rrect, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { let mut stroke_paint = sk_paint( @@ -137,10 +139,13 @@ impl Renderer { ); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); canvas.draw_rrect(rrect, &stroke_paint); } } else { - canvas.draw_rect(rect, &paint); + let mut fill_paint = paint.clone(); + fill_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_rect(rect, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { let mut stroke_paint = sk_paint( @@ -150,6 +155,7 @@ impl Renderer { ); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); canvas.draw_rect(rect, &stroke_paint); } } @@ -200,6 +206,8 @@ impl Renderer { canvas.save(); canvas.concat(&sk_matrix(node.transform.matrix)); // Draw fill + let mut fill_paint = fill_paint.clone(); + fill_paint.set_blend_mode(node.base.blend_mode.into()); canvas.draw_oval(rect, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { @@ -210,6 +218,7 @@ impl Renderer { ); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); canvas.draw_oval(rect, &stroke_paint); } canvas.restore(); @@ -221,6 +230,7 @@ impl Renderer { let mut paint = sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); paint.set_stroke(true); paint.set_stroke_width(node.stroke_width); + paint.set_blend_mode(node.base.blend_mode.into()); canvas.save(); canvas.concat(&sk_matrix(node.transform.matrix)); canvas.draw_line( @@ -251,12 +261,15 @@ impl Renderer { canvas.save(); canvas.concat(&sk_matrix(node.transform.matrix)); // Draw fill + let mut fill_paint = fill_paint.clone(); + fill_paint.set_blend_mode(node.base.blend_mode.into()); canvas.draw_path(&path, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); canvas.draw_path(&path, &stroke_paint); } canvas.restore(); @@ -310,15 +323,17 @@ impl Renderer { sk_paint(stroke, node.opacity, (node.size.width, node.size.height)); stroke_paint.set_style(skia_safe::paint::Style::Stroke); stroke_paint.set_stroke_width(stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); canvas.draw_text_blob(&blob, (x, y), &stroke_paint); } // Draw fill - let fill_paint = sk_paint( + let mut fill_paint = sk_paint( &node.fill, node.opacity, (node.size.width, node.size.height), ); + fill_paint.set_blend_mode(node.base.blend_mode.into()); canvas.draw_text_blob(&blob, (x, y), &fill_paint); canvas.restore(); diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 2aaa595621..d59f480293 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -43,6 +43,77 @@ pub struct FeGaussianBlur { pub radius: f32, } +/// Blend modes for compositing layers, compatible with Skia and SVG/CSS. +/// +/// - SVG: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/mix-blend-mode +/// - Skia: https://skia.org/docs/user/api/SkBlendMode_Reference/ +/// - Figma: https://help.figma.com/hc/en-us/articles/360039956994 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BlendMode { + // Skia: kSrcOver, CSS: normal + Normal, + + // Skia: kMultiply + Multiply, + // Skia: kScreen + Screen, + // Skia: kOverlay + Overlay, + // Skia: kDarken + Darken, + // Skia: kLighten + Lighten, + // Skia: kColorDodge + ColorDodge, + // Skia: kColorBurn + ColorBurn, + // Skia: kHardLight + HardLight, + // Skia: kSoftLight + SoftLight, + // Skia: kDifference + Difference, + // Skia: kExclusion + Exclusion, + // Skia: kHue + Hue, + // Skia: kSaturation + Saturation, + // Skia: kColor + Color, + // Skia: kLuminosity + Luminosity, + + /// Like `Normal`, but means no blending at all (pass-through). + /// This is Figma-specific, and typically treated the same as `Normal`. + PassThrough, +} + +impl From for skia_safe::BlendMode { + fn from(mode: BlendMode) -> Self { + use skia_safe::BlendMode::*; + match mode { + BlendMode::Normal => SrcOver, + BlendMode::Multiply => Multiply, + BlendMode::Screen => Screen, + BlendMode::Overlay => Overlay, + BlendMode::Darken => Darken, + BlendMode::Lighten => Lighten, + BlendMode::ColorDodge => ColorDodge, + BlendMode::ColorBurn => ColorBurn, + BlendMode::HardLight => HardLight, + BlendMode::SoftLight => SoftLight, + BlendMode::Difference => Difference, + BlendMode::Exclusion => Exclusion, + BlendMode::Hue => Hue, + BlendMode::Saturation => Saturation, + BlendMode::Color => Color, + BlendMode::Luminosity => Luminosity, + BlendMode::PassThrough => SrcOver, // fallback + } + } +} + /// Supported text decoration modes. /// /// Only `Underline` and `None` are supported in the current version. @@ -188,6 +259,7 @@ pub struct BaseNode { pub id: NodeId, pub name: String, pub active: bool, + pub blend_mode: BlendMode, } #[derive(Debug, Clone)] From 273db4aef2d241a005de177fc63d4a41c0ddaf64 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 19:50:08 +0900 Subject: [PATCH 025/262] group node (partial) --- crates/cg/src/demo.rs | 71 ++++++++++++++++++++++++++++++----------- crates/cg/src/draw.rs | 54 +++++++++++++++++++++++++++++-- crates/cg/src/schema.rs | 24 ++++++++++++-- 3 files changed, 126 insertions(+), 23 deletions(-) diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index 5e2433da7e..3be6f5a5a2 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -2,9 +2,10 @@ use cg::draw::Renderer; use cg::schema::FeDropShadow; use cg::schema::FilterEffect; use cg::schema::{ - BaseNode, BlendMode, Color, EllipseNode, FontWeight, GradientStop, LineNode, - LinearGradientPaint, Paint, RadialGradientPaint, RectangleNode, RectangularCornerRadius, Size, - SolidPaint, TextAlign, TextAlignVertical, TextDecoration, TextSpanNode, TextStyle, + BaseNode, BlendMode, Color, ContainerNode, EllipseNode, FontWeight, GradientStop, GroupNode, + LineNode, LinearGradientPaint, Node, NodeMap, Paint, RadialGradientPaint, RectangleNode, + RectangularCornerRadius, Size, SolidPaint, TextAlign, TextAlignVertical, TextDecoration, + TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; @@ -138,7 +139,7 @@ fn main() { color: Color(0, 0, 0, 255), // Black stroke }), stroke_width: 4.0, - opacity: 1.0, + opacity: 0.5, }; // Create a test text span node @@ -195,23 +196,57 @@ fn main() { stroke_width: 4.0, }; - // Draw the rectangle using our schema - Renderer::draw_rect_node(surface_ptr, &rect_node); - - // Draw the ellipse using our schema - Renderer::draw_ellipse_node(surface_ptr, &ellipse_node); - - // Draw the polygon using our schema - Renderer::draw_polygon_node(surface_ptr, &polygon_node); + // Create a group node for the shapes (rectangle, ellipse, polygon) + let shapes_group_node = GroupNode { + base: BaseNode { + id: "shapes_group".to_string(), + name: "Shapes Group".to_string(), + active: true, + blend_mode: BlendMode::Normal, + }, + transform: AffineTransform::new(0.0, 0.0, -15.0), + children: vec![ + "test_rect".to_string(), + "test_ellipse".to_string(), + "test_polygon".to_string(), + "test_regular_polygon".to_string(), + ], + opacity: 0.8, + }; - // Draw the regular polygon using our schema - Renderer::draw_regular_polygon_node(surface_ptr, ®ular_polygon_node); + // Create a root group node containing the shapes group, text, and line + let root_group_node = GroupNode { + base: BaseNode { + id: "root_group".to_string(), + name: "Root Group".to_string(), + active: true, + blend_mode: BlendMode::Normal, + }, + transform: AffineTransform::identity(), + children: vec![ + "shapes_group".to_string(), + "test_text".to_string(), + "test_line".to_string(), + ], + opacity: 1.0, + }; - // Draw the text span node - Renderer::draw_text_span_node(surface_ptr, &text_span_node); + // Create a node map and add all nodes + let mut nodemap = NodeMap::new(); + nodemap.insert("test_rect".to_string(), Node::Rectangle(rect_node)); + nodemap.insert("test_ellipse".to_string(), Node::Ellipse(ellipse_node)); + nodemap.insert("test_polygon".to_string(), Node::Polygon(polygon_node)); + nodemap.insert( + "test_regular_polygon".to_string(), + Node::RegularPolygon(regular_polygon_node), + ); + nodemap.insert("shapes_group".to_string(), Node::Group(shapes_group_node)); + nodemap.insert("test_text".to_string(), Node::TextSpan(text_span_node)); + nodemap.insert("test_line".to_string(), Node::Line(line_node)); + nodemap.insert("root_group".to_string(), Node::Group(root_group_node)); - // Draw the line using our schema - Renderer::draw_line_node(surface_ptr, &line_node); + // Render the root group node and its children + Renderer::render_node(surface_ptr, &"root_group".to_string(), &nodemap); // Get the surface from the pointer to save the image let surface = unsafe { &mut *surface_ptr }; diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 74df5e2898..a54e712e69 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,7 +1,8 @@ use crate::schema::{ - Color as SchemaColor, EllipseNode, FilterEffect, GradientStop, LineNode, LinearGradientPaint, - Paint, PolygonNode, RadialGradientPaint, RectangleNode, RectangularCornerRadius, - RegularPolygonNode, TextAlign, TextAlignVertical, TextSpanNode, + Color as SchemaColor, EllipseNode, FilterEffect, GradientStop, GroupNode, LineNode, + LinearGradientPaint, Node, NodeId, NodeMap, Paint, PolygonNode, RadialGradientPaint, + RectangleNode, RectangularCornerRadius, RegularPolygonNode, TextAlign, TextAlignVertical, + TextSpanNode, }; use console_error_panic_hook::set_once as init_panic_hook; use skia_safe::{ @@ -346,6 +347,53 @@ impl Renderer { pub fn free(ptr: *mut Surface) { unsafe { Box::from_raw(ptr) }; } + + pub fn render_node(ptr: *mut Surface, id: &NodeId, nodemap: &NodeMap) { + let node = match nodemap.get(id) { + Some(node) => node, + None => return, // Skip if node not found + }; + + match node { + Node::Group(node) => Self::draw_group_node(ptr, node, nodemap), + Node::Rectangle(node) => Self::draw_rect_node(ptr, node), + Node::Ellipse(node) => Self::draw_ellipse_node(ptr, node), + Node::Polygon(node) => Self::draw_polygon_node(ptr, node), + Node::RegularPolygon(node) => Self::draw_regular_polygon_node(ptr, node), + Node::TextSpan(node) => Self::draw_text_span_node(ptr, node), + Node::Line(node) => Self::draw_line_node(ptr, node), + _ => {} + } + } + + pub fn draw_group_node(ptr: *mut Surface, node: &GroupNode, nodemap: &NodeMap) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + // Save canvas state for transform + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + + let needs_opacity_layer = node.opacity < 1.0; + + if needs_opacity_layer { + // Start new layer with opacity + canvas.save_layer_alpha(None, (node.opacity * 255.0) as u32); + } + + // Recursively render children + for child_id in &node.children { + Renderer::render_node(ptr, child_id, nodemap); + } + + if needs_opacity_layer { + // End opacity layer + canvas.restore(); + } + + // Restore transform + canvas.restore(); + } } fn sk_matrix(m: [[f32; 3]; 2]) -> skia_safe::Matrix { diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index d59f480293..6482c05c60 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -242,16 +242,27 @@ pub struct RectangularCornerRadius { pub br: f32, } +// region: Scene +#[derive(Debug, Clone)] +pub struct SceneNode { + pub base: BaseNode, + pub transform: AffineTransform, + pub children: Vec, +} + +// endregion + // region: Node Definitions #[derive(Debug, Clone)] pub enum Node { - Container(ContainerNode), + Group(GroupNode), Rectangle(RectangleNode), Ellipse(EllipseNode), Polygon(PolygonNode), RegularPolygon(RegularPolygonNode), - Text(TextNode), + Line(LineNode), + TextSpan(TextSpanNode), } #[derive(Debug, Clone)] @@ -262,6 +273,15 @@ pub struct BaseNode { pub blend_mode: BlendMode, } +#[derive(Debug, Clone)] +#[deprecated(note = "Partially implemented")] +pub struct GroupNode { + pub base: BaseNode, + pub transform: AffineTransform, + pub children: Vec, + pub opacity: f32, +} + #[derive(Debug, Clone)] pub struct ContainerNode { pub base: BaseNode, From 0df53d2e4e1febc04f99a253fcdfa8a1974ab364 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 20:30:44 +0900 Subject: [PATCH 026/262] doc --- crates/cg/AGENTS.md | 3 +++ crates/cg/README.md | 1 + 2 files changed, 4 insertions(+) create mode 100644 crates/cg/AGENTS.md create mode 100644 crates/cg/README.md diff --git a/crates/cg/AGENTS.md b/crates/cg/AGENTS.md new file mode 100644 index 0000000000..f45322b93e --- /dev/null +++ b/crates/cg/AGENTS.md @@ -0,0 +1,3 @@ +Resources: + +- `skia-safe` docs: https://rust-skia.github.io/doc/skia_safe/ diff --git a/crates/cg/README.md b/crates/cg/README.md new file mode 100644 index 0000000000..bff1d0d120 --- /dev/null +++ b/crates/cg/README.md @@ -0,0 +1 @@ +# `cg` Grida Rendering Backend From 47ec4016cffea1a6c9456a46ef2b50b25265b230 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 20:56:50 +0900 Subject: [PATCH 027/262] image node --- Cargo.lock | 1187 ++++++++++++++++++++++++++++++++++++++- crates/cg/Cargo.toml | 4 +- crates/cg/src/demo.rs | 74 ++- crates/cg/src/draw.rs | 199 ++++--- crates/cg/src/schema.rs | 58 +- 5 files changed, 1430 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04ad23b8dd..c41f445f02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[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" @@ -17,6 +26,39 @@ dependencies = [ "memchr", ] +[[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 1.0.0", + "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" @@ -49,6 +91,12 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cc" version = "1.2.25" @@ -84,7 +132,9 @@ name = "cg" version = "0.1.0" dependencies = [ "console_error_panic_hook", + "reqwest", "skia-safe", + "tokio", "wasm-bindgen", "wee_alloc", ] @@ -110,6 +160,22 @@ dependencies = [ "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 = "crc32fast" version = "1.4.2" @@ -119,12 +185,32 @@ dependencies = [ "cfg-if 1.0.0", ] +[[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 = "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 1.0.0", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -138,9 +224,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys", + "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 = "filetime" version = "0.2.25" @@ -150,7 +242,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "libredox", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -163,12 +255,129 @@ dependencies = [ "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", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[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-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if 1.0.0", + "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 1.0.0", + "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 = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[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", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.3" @@ -181,6 +390,231 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[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 = "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 = "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 = "2.9.0" @@ -191,6 +625,22 @@ dependencies = [ "hashbrown", ] +[[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 = "itertools" version = "0.13.0" @@ -206,6 +656,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[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 = "lazy_static" version = "1.5.0" @@ -245,6 +705,12 @@ 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" @@ -263,6 +729,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" +[[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" @@ -278,6 +750,34 @@ 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 = "nom" version = "7.1.3" @@ -288,12 +788,98 @@ dependencies = [ "minimal-lexical", ] +[[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 = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "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 = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[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 = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "prettyplease" version = "0.2.33" @@ -322,6 +908,12 @@ 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 = "redox_syscall" version = "0.5.12" @@ -360,6 +952,68 @@ 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 1.0.0", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[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" @@ -376,7 +1030,40 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "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]] @@ -391,6 +1078,38 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[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 = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "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" @@ -432,6 +1151,18 @@ 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 = "shlex" version = "1.3.0" @@ -466,6 +1197,43 @@ dependencies = [ "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 = "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 = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.101" @@ -477,6 +1245,47 @@ dependencies = [ "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", + "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" @@ -488,6 +1297,89 @@ dependencies = [ "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", + "windows-sys 0.59.0", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[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" @@ -529,12 +1421,135 @@ 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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2" +dependencies = [ + "bitflags", + "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 = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[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 = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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" @@ -561,6 +1576,19 @@ dependencies = [ "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 1.0.0", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -593,6 +1621,16 @@ dependencies = [ "unicode-ident", ] +[[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 = "wee_alloc" version = "0.4.5" @@ -627,6 +1665,50 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[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", + "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-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" @@ -773,6 +1855,21 @@ 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", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "xattr" version = "1.5.0" @@ -782,3 +1879,87 @@ dependencies = [ "libc", "rustix", ] + +[[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 = "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/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index 5b4c11941c..824d7572e1 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -15,6 +15,8 @@ wasm-bindgen = "0.2.100" skia-safe = "0.86.0" console_error_panic_hook = "0.1.7" wee_alloc = { version = "0.4.5", optional = true } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +reqwest = "0.12.19" [features] default = ["wee_alloc"] @@ -22,4 +24,4 @@ default = ["wee_alloc"] [target.wasm32-unknown-emscripten] rustflags = [ "-s ERROR_ON_UNDEFINED_SYMBOLS=0" -] \ No newline at end of file +] diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index 3be6f5a5a2..c3544ad43a 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -2,20 +2,67 @@ use cg::draw::Renderer; use cg::schema::FeDropShadow; use cg::schema::FilterEffect; use cg::schema::{ - BaseNode, BlendMode, Color, ContainerNode, EllipseNode, FontWeight, GradientStop, GroupNode, - LineNode, LinearGradientPaint, Node, NodeMap, Paint, RadialGradientPaint, RectangleNode, - RectangularCornerRadius, Size, SolidPaint, TextAlign, TextAlignVertical, TextDecoration, - TextSpanNode, TextStyle, + BaseNode, BlendMode, Color, EllipseNode, FontWeight, GradientStop, GroupNode, ImageNode, + LineNode, LinearGradientPaint, Node, NodeMap, Paint, PolygonNode, RadialGradientPaint, + RectangleNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, TextAlignVertical, + TextDecoration, TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; +use reqwest; +use skia_safe::Image; -fn main() { +#[tokio::main] +async fn main() { let width = 800; let height = 600; - // Initialize the surface using the Renderer + // Initialize the renderer with image cache + let mut renderer = Renderer::new(); let surface_ptr = Renderer::init(width, height); + let demo_image_id = "demo_image"; + let demo_image_url = "https://grida.co/images/abstract-placeholder.jpg".to_string(); + + // Preload the image + if let Ok(response) = reqwest::get(&demo_image_url).await { + if let Ok(bytes) = response.bytes().await { + if let Some(image) = Image::from_encoded(skia_safe::Data::new_copy(&bytes)) { + renderer.add_image(demo_image_id.to_string(), image); + } + } + } + + // Create a test image node with URL + let image_node = ImageNode { + base: BaseNode { + id: "test_image".to_string(), + name: "Test Image".to_string(), + active: true, + blend_mode: BlendMode::Normal, + }, + transform: AffineTransform::new(50.0, 50.0, 0.0), + size: Size { + width: 200.0, + height: 200.0, + }, + corner_radius: RectangularCornerRadius::all(20.0), + fill: Paint::Solid(SolidPaint { + color: Color(255, 255, 255, 255), + }), + stroke: Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), + }), + stroke_width: 2.0, + effect: Some(FilterEffect::DropShadow(FeDropShadow { + dx: 4.0, + dy: 4.0, + blur: 8.0, + color: Color(0, 0, 0, 77), + })), + opacity: 1.0, + _ref: demo_image_id.to_string(), + }; + // Create a test rectangle node with linear gradient let rect_node = RectangleNode { base: BaseNode { @@ -25,17 +72,12 @@ fn main() { blend_mode: BlendMode::Normal, }, opacity: 1.0, - transform: AffineTransform::new(50.0, 50.0, 45.0), + transform: AffineTransform::new(50.0, 300.0, 45.0), size: Size { width: 200.0, height: 100.0, }, - corner_radius: RectangularCornerRadius { - tl: 10.0, - tr: 10.0, - bl: 10.0, - br: 10.0, - }, + corner_radius: RectangularCornerRadius::all(10.0), fill: Paint::Solid(SolidPaint { color: Color(255, 0, 0, 255), // Red fill }), @@ -60,7 +102,7 @@ fn main() { blend_mode: BlendMode::Multiply, // Example of using a different blend mode }, opacity: 1.0, - transform: AffineTransform::new(300.0, 50.0, 45.0), // Rotated 45 degrees + transform: AffineTransform::new(300.0, 300.0, 45.0), // Rotated 45 degrees size: Size { width: 200.0, height: 200.0, @@ -227,6 +269,7 @@ fn main() { "shapes_group".to_string(), "test_text".to_string(), "test_line".to_string(), + "test_image".to_string(), ], opacity: 1.0, }; @@ -243,10 +286,11 @@ fn main() { nodemap.insert("shapes_group".to_string(), Node::Group(shapes_group_node)); nodemap.insert("test_text".to_string(), Node::TextSpan(text_span_node)); nodemap.insert("test_line".to_string(), Node::Line(line_node)); + nodemap.insert("test_image".to_string(), Node::Image(image_node)); nodemap.insert("root_group".to_string(), Node::Group(root_group_node)); // Render the root group node and its children - Renderer::render_node(surface_ptr, &"root_group".to_string(), &nodemap); + renderer.render_node(surface_ptr, &"root_group".to_string(), &nodemap); // Get the surface from the pointer to save the image let surface = unsafe { &mut *surface_ptr }; diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index a54e712e69..65e38ef4b2 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,16 +1,15 @@ use crate::schema::{ - Color as SchemaColor, EllipseNode, FilterEffect, GradientStop, GroupNode, LineNode, - LinearGradientPaint, Node, NodeId, NodeMap, Paint, PolygonNode, RadialGradientPaint, - RectangleNode, RectangularCornerRadius, RegularPolygonNode, TextAlign, TextAlignVertical, - TextSpanNode, + Color as SchemaColor, EllipseNode, FilterEffect, GradientStop, GroupNode, ImageNode, LineNode, + Node, NodeId, NodeMap, Paint, PolygonNode, RectangleNode, RectangularCornerRadius, + RegularPolygonNode, TextAlign, TextAlignVertical, TextSpanNode, }; use console_error_panic_hook::set_once as init_panic_hook; use skia_safe::{ - Color, Font, FontMgr, FontStyle, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, - TextBlob, Typeface, surfaces, + Color, Font, FontMgr, FontStyle, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, + Shader, Surface, TextBlob, Typeface, surfaces, }; use std::cell::RefCell; -use std::f32::consts::PI; +use std::collections::HashMap; thread_local! { static DEFAULT_TYPEFACE: RefCell> = RefCell::new(None); @@ -34,9 +33,21 @@ fn default_typeface() -> Typeface { }) } -pub struct Renderer; +pub struct Renderer { + image_cache: HashMap, +} impl Renderer { + pub fn new() -> Self { + Self { + image_cache: HashMap::new(), + } + } + + pub fn add_image(&mut self, src: String, image: Image) { + self.image_cache.insert(src, image); + } + pub fn init(width: i32, height: i32) -> *mut Surface { init_panic_hook(); let surface = surfaces::raster_n32_premul((width, height)).unwrap(); @@ -44,6 +55,7 @@ impl Renderer { } pub fn draw_rect( + &self, ptr: *mut Surface, x: f32, y: f32, @@ -70,7 +82,7 @@ impl Renderer { canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); } - pub fn draw_rect_node(ptr: *mut Surface, node: &RectangleNode) { + pub fn draw_rect_node(&self, ptr: *mut Surface, node: &RectangleNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); let paint = sk_paint( @@ -84,9 +96,8 @@ impl Renderer { let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; // Draw drop shadow effect if present if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { - use skia_safe::{MaskFilter, Paint as SkiaPaint}; let mut shadow_paint = SkiaPaint::default(); - let crate::schema::Color(r, g, b, a) = shadow.color; + let SchemaColor(r, g, b, a) = shadow.color; shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); shadow_paint.set_anti_alias(true); if shadow.blur > 0.0 { @@ -164,6 +175,7 @@ impl Renderer { } pub fn draw_ellipse( + &self, ptr: *mut Surface, x: f32, y: f32, @@ -190,7 +202,7 @@ impl Renderer { canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); } - pub fn draw_ellipse_node(ptr: *mut Surface, node: &EllipseNode) { + pub fn draw_ellipse_node(&self, ptr: *mut Surface, node: &EllipseNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); let fill_paint = sk_paint( @@ -225,7 +237,7 @@ impl Renderer { canvas.restore(); } - pub fn draw_line_node(ptr: *mut Surface, node: &LineNode) { + pub fn draw_line_node(&self, ptr: *mut Surface, node: &LineNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); let mut paint = sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); @@ -242,7 +254,7 @@ impl Renderer { canvas.restore(); } - pub fn draw_polygon_node(ptr: *mut Surface, node: &crate::schema::PolygonNode) { + pub fn draw_polygon_node(&self, ptr: *mut Surface, node: &PolygonNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); if node.points.len() < 3 { @@ -276,12 +288,12 @@ impl Renderer { canvas.restore(); } - pub fn draw_regular_polygon_node(ptr: *mut Surface, node: &RegularPolygonNode) { - let poly = cg_regular_to_polygon(node); - Self::draw_polygon_node(ptr, &poly); + pub fn draw_regular_polygon_node(&self, ptr: *mut Surface, node: &RegularPolygonNode) { + let poly = node.to_polygon(); + self.draw_polygon_node(ptr, &poly); } - pub fn draw_text_span_node(ptr: *mut Surface, node: &TextSpanNode) { + pub fn draw_text_span_node(&self, ptr: *mut Surface, node: &TextSpanNode) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); @@ -340,6 +352,94 @@ impl Renderer { canvas.restore(); } + pub fn draw_image_node(&self, ptr: *mut Surface, node: &ImageNode) { + let surface = unsafe { &mut *ptr }; + let canvas = surface.canvas(); + + if let Some(image) = self.image_cache.get(&node._ref) { + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + + // Draw drop shadow effect if present + if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { + let mut shadow_paint = SkiaPaint::default(); + let SchemaColor(r, g, b, a) = shadow.color; + shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); + shadow_paint.set_anti_alias(true); + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + skia_safe::BlurStyle::Normal, + shadow.blur, + None, + )); + } + let offset_x = shadow.dx; + let offset_y = shadow.dy; + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let mut shadow_rect = rect; + shadow_rect.offset((offset_x, offset_y)); + canvas.draw_image_rect(image, None, shadow_rect, &shadow_paint); + } + + // Draw the image + let mut paint = SkiaPaint::default(); + paint.set_anti_alias(true); + paint.set_blend_mode(node.base.blend_mode.into()); + paint.set_alpha((node.opacity * 255.0) as u8); + + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; + + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + // For rounded rectangles, we need to use a clip path + canvas.save(); + canvas.clip_rrect(rrect, None, true); + canvas.draw_image_rect(image, None, rect, &paint); + canvas.restore(); + } else { + canvas.draw_image_rect(image, None, rect, &paint); + } + + // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint( + &node.stroke, + node.opacity, + (node.size.width, node.size.height), + ); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); + + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + canvas.draw_rrect(rrect, &stroke_paint); + } else { + canvas.draw_rect(rect, &stroke_paint); + } + } + + canvas.restore(); + } + } + pub fn flush(_ptr: *mut Surface) { // No flush needed for raster surfaces } @@ -348,25 +448,26 @@ impl Renderer { unsafe { Box::from_raw(ptr) }; } - pub fn render_node(ptr: *mut Surface, id: &NodeId, nodemap: &NodeMap) { + pub fn render_node(&self, ptr: *mut Surface, id: &NodeId, nodemap: &NodeMap) { let node = match nodemap.get(id) { Some(node) => node, None => return, // Skip if node not found }; match node { - Node::Group(node) => Self::draw_group_node(ptr, node, nodemap), - Node::Rectangle(node) => Self::draw_rect_node(ptr, node), - Node::Ellipse(node) => Self::draw_ellipse_node(ptr, node), - Node::Polygon(node) => Self::draw_polygon_node(ptr, node), - Node::RegularPolygon(node) => Self::draw_regular_polygon_node(ptr, node), - Node::TextSpan(node) => Self::draw_text_span_node(ptr, node), - Node::Line(node) => Self::draw_line_node(ptr, node), + Node::Group(node) => self.draw_group_node(ptr, node, nodemap), + Node::Rectangle(node) => self.draw_rect_node(ptr, node), + Node::Ellipse(node) => self.draw_ellipse_node(ptr, node), + Node::Polygon(node) => self.draw_polygon_node(ptr, node), + Node::RegularPolygon(node) => self.draw_regular_polygon_node(ptr, node), + Node::TextSpan(node) => self.draw_text_span_node(ptr, node), + Node::Line(node) => self.draw_line_node(ptr, node), + Node::Image(node) => self.draw_image_node(ptr, node), _ => {} } } - pub fn draw_group_node(ptr: *mut Surface, node: &GroupNode, nodemap: &NodeMap) { + pub fn draw_group_node(&self, ptr: *mut Surface, node: &GroupNode, nodemap: &NodeMap) { let surface = unsafe { &mut *ptr }; let canvas = surface.canvas(); @@ -383,7 +484,7 @@ impl Renderer { // Recursively render children for child_id in &node.children { - Renderer::render_node(ptr, child_id, nodemap); + self.render_node(ptr, child_id, nodemap); } if needs_opacity_layer { @@ -457,45 +558,3 @@ fn cg_build_gradient_stops(stops: &[GradientStop], opacity: f32) -> (Vec, (colors, positions) } - -pub fn cg_regular_to_polygon(node: &RegularPolygonNode) -> PolygonNode { - let RegularPolygonNode { - base, - transform, - size, - point_count, - fill, - stroke, - stroke_width, - opacity, - } = node; - - let cx = size.width / 2.0; - let cy = size.height / 2.0; - let r = cx.min(cy); // fit within bounding box - - let angle_offset = if point_count % 2 == 0 { - PI / *point_count as f32 - } else { - -PI / 2.0 - }; - - let points: Vec<(f32, f32)> = (0..*point_count) - .map(|i| { - let angle = (i as f32 / *point_count as f32) * 2.0 * PI + angle_offset; - let x = cx + r * angle.cos(); - let y = cy + r * angle.sin(); - (x, y) - }) - .collect(); - - PolygonNode { - base: base.clone(), - transform: *transform, - points, - fill: fill.clone(), - stroke: stroke.clone(), - stroke_width: *stroke_width, - opacity: *opacity, - } -} diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 6482c05c60..553c9b3e4f 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -1,5 +1,6 @@ use crate::transform::AffineTransform; use std::collections::HashMap; +use std::f32::consts::PI; pub type NodeId = String; @@ -242,6 +243,21 @@ pub struct RectangularCornerRadius { pub br: f32, } +impl RectangularCornerRadius { + pub fn zero() -> Self { + Self::all(0.0) + } + + pub fn all(value: f32) -> Self { + Self { + tl: value, + tr: value, + bl: value, + br: value, + } + } +} + // region: Scene #[derive(Debug, Clone)] pub struct SceneNode { @@ -263,6 +279,7 @@ pub enum Node { RegularPolygon(RegularPolygonNode), Line(LineNode), TextSpan(TextSpanNode), + Image(ImageNode), } #[derive(Debug, Clone)] @@ -274,7 +291,6 @@ pub struct BaseNode { } #[derive(Debug, Clone)] -#[deprecated(note = "Partially implemented")] pub struct GroupNode { pub base: BaseNode, pub transform: AffineTransform, @@ -315,14 +331,17 @@ pub struct RectangleNode { } #[derive(Debug, Clone)] -#[deprecated(note = "Not implemented yet")] pub struct ImageNode { pub base: BaseNode, pub transform: AffineTransform, pub size: Size, pub corner_radius: RectangularCornerRadius, - pub src: String, + pub fill: Paint, + pub stroke: Paint, + pub stroke_width: f32, pub opacity: f32, + pub effect: Option, + pub _ref: String, } #[derive(Debug, Clone)] @@ -406,6 +425,39 @@ pub struct RegularPolygonNode { pub opacity: f32, } +impl RegularPolygonNode { + pub fn to_polygon(&self) -> PolygonNode { + let cx = self.size.width / 2.0; + let cy = self.size.height / 2.0; + let r = cx.min(cy); // fit within bounding box + + let angle_offset = if self.point_count % 2 == 0 { + PI / self.point_count as f32 + } else { + -PI / 2.0 + }; + + let points: Vec<(f32, f32)> = (0..self.point_count) + .map(|i| { + let angle = (i as f32 / self.point_count as f32) * 2.0 * PI + angle_offset; + let x = cx + r * angle.cos(); + let y = cy + r * angle.sin(); + (x, y) + }) + .collect(); + + PolygonNode { + base: self.base.clone(), + transform: self.transform, + points, + fill: self.fill.clone(), + stroke: self.stroke.clone(), + stroke_width: self.stroke_width, + opacity: self.opacity, + } + } +} + /// A node representing a plain text block (non-rich). /// For multi-style content, see `RichTextNode` (not implemented yet). #[derive(Debug, Clone)] From 58605ea3f908675e786daf65ccd39ff3265f9501 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 21:27:59 +0900 Subject: [PATCH 028/262] benchmarks --- Cargo.lock | 277 +++++++++++++++++++++++++++++- crates/cg/Cargo.toml | 8 + crates/cg/benches/render_bench.rs | 208 ++++++++++++++++++++++ crates/cg/src/demo.rs | 62 ++++++- 4 files changed, 551 insertions(+), 4 deletions(-) create mode 100644 crates/cg/benches/render_bench.rs diff --git a/Cargo.lock b/Cargo.lock index c41f445f02..cfc2b263bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -68,7 +80,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -97,6 +109,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[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" @@ -132,6 +150,7 @@ name = "cg" version = "0.1.0" dependencies = [ "console_error_panic_hook", + "criterion", "reqwest", "skia-safe", "tokio", @@ -139,6 +158,33 @@ dependencies = [ "wee_alloc", ] +[[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" @@ -150,6 +196,31 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -185,6 +256,73 @@ dependencies = [ "cfg-if 1.0.0", ] +[[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 = "displaydoc" version = "0.2.5" @@ -378,6 +516,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if 1.0.0", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.15.3" @@ -390,6 +538,12 @@ 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 = "http" version = "1.3.1" @@ -641,6 +795,26 @@ dependencies = [ "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 = "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" @@ -788,6 +962,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -803,6 +986,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[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" @@ -871,6 +1060,34 @@ 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 = "potential_utf" version = "0.1.2" @@ -914,6 +1131,26 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[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.5.12" @@ -1078,6 +1315,15 @@ 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" @@ -1320,6 +1566,16 @@ dependencies = [ "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" @@ -1526,6 +1782,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[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" @@ -1659,6 +1925,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[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 = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index 824d7572e1..4a0883e6f7 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -17,6 +17,7 @@ console_error_panic_hook = "0.1.7" wee_alloc = { version = "0.4.5", optional = true } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } reqwest = "0.12.19" +criterion = "0.5" [features] default = ["wee_alloc"] @@ -25,3 +26,10 @@ default = ["wee_alloc"] rustflags = [ "-s ERROR_ON_UNDEFINED_SYMBOLS=0" ] + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "render_bench" +harness = false diff --git a/crates/cg/benches/render_bench.rs b/crates/cg/benches/render_bench.rs new file mode 100644 index 0000000000..ed149afde4 --- /dev/null +++ b/crates/cg/benches/render_bench.rs @@ -0,0 +1,208 @@ +use cg::draw::Renderer; +use cg::schema::{ + BaseNode, BlendMode, Color, Node, NodeMap, Paint, RectangleNode, RectangularCornerRadius, Size, + SolidPaint, +}; +use cg::transform::AffineTransform; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; + +fn create_rectangles(count: usize, with_effects: bool) -> (NodeMap, Vec) { + let mut nodemap = NodeMap::new(); + let mut ids = Vec::with_capacity(count); + + for i in 0..count { + let id = format!("rect_{}", i); + let rect = RectangleNode { + base: BaseNode { + id: id.clone(), + name: format!("Rectangle {}", i), + active: true, + blend_mode: if i % 2 == 0 { + BlendMode::Normal + } else { + BlendMode::Multiply + }, + }, + transform: AffineTransform::new( + (i % 100) as f32 * 10.0, // x position + (i / 100) as f32 * 10.0, // y position + (i % 4) as f32 * 90.0, // rotation + ), + size: Size { + width: 8.0, + height: 8.0, + }, + corner_radius: RectangularCornerRadius::all(2.0), + fill: Paint::Solid(SolidPaint { + color: Color( + (i * 7) as u8, // r + (i * 13) as u8, // g + (i * 17) as u8, // b + 255, // a + ), + }), + stroke: Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), + }), + stroke_width: 1.0, + opacity: 1.0, + effect: if with_effects { + Some(cg::schema::FilterEffect::DropShadow( + cg::schema::FeDropShadow { + dx: 2.0, + dy: 2.0, + blur: 4.0, + color: Color(0, 0, 0, 77), + }, + )) + } else { + None + }, + }; + nodemap.insert(id.clone(), Node::Rectangle(rect)); + ids.push(id); + } + + // Create a root group node + let root_group = cg::schema::GroupNode { + base: BaseNode { + id: "root".to_string(), + name: "Root Group".to_string(), + active: true, + blend_mode: BlendMode::Normal, + }, + transform: AffineTransform::identity(), + children: ids.clone(), + opacity: 1.0, + }; + nodemap.insert("root".to_string(), Node::Group(root_group)); + + (nodemap, ids) +} + +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 renderer = Renderer::new(); + let surface_ptr = Renderer::init(width, height); + + let (nodemap, _) = 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.render_node(surface_ptr, &"root".to_string(), &nodemap); + Renderer::free(surface_ptr); + }) + }); + + // 10K rectangles + group.bench_function("10k_basic", |b| { + b.iter(|| { + let renderer = Renderer::new(); + let surface_ptr = Renderer::init(width, height); + + let (nodemap, _) = 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.render_node(surface_ptr, &"root".to_string(), &nodemap); + Renderer::free(surface_ptr); + }) + }); + + group.bench_function("10k_with_effects", |b| { + b.iter(|| { + let renderer = Renderer::new(); + let surface_ptr = Renderer::init(width, height); + + let (nodemap, _) = 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.render_node(surface_ptr, &"root".to_string(), &nodemap); + Renderer::free(surface_ptr); + }) + }); + + // 50K rectangles + group.bench_function("50k_basic", |b| { + b.iter(|| { + let renderer = Renderer::new(); + let surface_ptr = Renderer::init(width, height); + + let (nodemap, _) = 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.render_node(surface_ptr, &"root".to_string(), &nodemap); + Renderer::free(surface_ptr); + }) + }); + + group.bench_function("50k_with_effects", |b| { + b.iter(|| { + let renderer = Renderer::new(); + let surface_ptr = Renderer::init(width, height); + + let (nodemap, _) = 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.render_node(surface_ptr, &"root".to_string(), &nodemap); + Renderer::free(surface_ptr); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_rectangles); +criterion_main!(benches); diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index c3544ad43a..22609536da 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -10,6 +10,7 @@ use cg::schema::{ use cg::transform::AffineTransform; use reqwest; use skia_safe::Image; +use std::time::Instant; #[tokio::main] async fn main() { @@ -20,10 +21,11 @@ async fn main() { let mut renderer = Renderer::new(); let surface_ptr = Renderer::init(width, height); + // Preload image before timing let demo_image_id = "demo_image"; let demo_image_url = "https://grida.co/images/abstract-placeholder.jpg".to_string(); - - // Preload the image + println!("Loading image..."); + let image_load_start = Instant::now(); if let Ok(response) = reqwest::get(&demo_image_url).await { if let Ok(bytes) = response.bytes().await { if let Some(image) = Image::from_encoded(skia_safe::Data::new_copy(&bytes)) { @@ -31,6 +33,7 @@ async fn main() { } } } + println!("Image load time: {:?}", image_load_start.elapsed()); // Create a test image node with URL let image_node = ImageNode { @@ -289,16 +292,69 @@ async fn main() { nodemap.insert("test_image".to_string(), Node::Image(image_node)); nodemap.insert("root_group".to_string(), Node::Group(root_group_node)); - // Render the root group node and its children + // Test performance with individual nodes first + println!("\nTesting individual node performance:"); + + let test_nodes = [ + ("test_rect", "Rectangle"), + ("test_ellipse", "Ellipse"), + ("test_polygon", "Polygon"), + ("test_regular_polygon", "Regular Polygon"), + ("test_text", "Text"), + ("test_line", "Line"), + ("test_image", "Image"), + ]; + + for (id, name) in test_nodes.iter() { + // Clear canvas before each render + 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, + ); + + let start = Instant::now(); + renderer.render_node(surface_ptr, &id.to_string(), &nodemap); + let time = start.elapsed(); + println!( + "{} render time: {:?} (FPS: {:.2})", + name, + time, + 1.0 / time.as_secs_f64() + ); + } + + // Now test the full scene + println!("\nTesting full scene performance:"); + + // Clear canvas before full scene render + 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, + ); + + let start_time = Instant::now(); renderer.render_node(surface_ptr, &"root_group".to_string(), &nodemap); + let render_time = start_time.elapsed(); + println!("Full scene render time: {:?}", render_time); + println!("Full scene FPS: {:.2}", 1.0 / render_time.as_secs_f64()); // Get the surface from the pointer to save the image + let save_start = Instant::now(); let surface = unsafe { &mut *surface_ptr }; let image = surface.image_snapshot(); image .encode_to_data(skia_safe::EncodedImageFormat::PNG) .and_then(|data| std::fs::write("output.png", data.as_bytes()).ok()) .expect("Failed to save PNG"); + println!("Save time: {:?}", save_start.elapsed()); // Free the surface Renderer::free(surface_ptr); From 10be1b62e3011fba074a78d1666c54bf142509c0 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Jun 2025 23:03:30 +0900 Subject: [PATCH 029/262] configure backend --- Cargo.lock | 1362 ++++++++++++++++++++++++++++- crates/cg/Cargo.toml | 7 +- crates/cg/benches/render_bench.rs | 47 +- crates/cg/output.png | Bin 3183 -> 126238 bytes crates/cg/src/demo.rs | 335 +++++-- crates/cg/src/draw.rs | 734 ++++++++-------- 6 files changed, 2015 insertions(+), 470 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfc2b263bf..11c6f0831e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # 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" @@ -17,6 +33,19 @@ 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 1.0.0", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +55,33 @@ 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 = "anes" version = "0.1.6" @@ -38,6 +94,24 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +[[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 = "atomic-waker" version = "1.1.2" @@ -77,7 +151,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags", + "bitflags 2.9.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -91,24 +165,71 @@ dependencies = [ "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 = "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" @@ -121,9 +242,17 @@ 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" @@ -145,17 +274,37 @@ 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.1.0" dependencies = [ "console_error_panic_hook", "criterion", + "gl", + "glutin", + "glutin-winit", + "raw-window-handle", "reqwest", "skia-safe", "tokio", "wasm-bindgen", "wee_alloc", + "winit", +] + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", ] [[package]] @@ -221,6 +370,25 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[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" @@ -247,6 +415,30 @@ 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" @@ -323,6 +515,28 @@ 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 = "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" @@ -334,6 +548,27 @@ dependencies = [ "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" @@ -405,7 +640,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "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]] @@ -414,6 +670,12 @@ 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" @@ -462,6 +724,16 @@ dependencies = [ "pin-utils", ] +[[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" @@ -491,12 +763,98 @@ 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 = "h2" version = "0.4.10" @@ -830,6 +1188,38 @@ 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 1.0.0", + "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" @@ -840,6 +1230,12 @@ dependencies = [ "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" @@ -868,11 +1264,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.9.1", "libc", - "redox_syscall", + "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" @@ -897,6 +1299,15 @@ 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 = "memory_units" version = "0.4.0" @@ -953,22 +1364,319 @@ dependencies = [ ] [[package]] -name = "nom" -version = "7.1.3" +name = "ndk" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "memchr", - "minimal-lexical", + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "ndk-context" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", +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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[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]] @@ -998,9 +1706,9 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags", + "bitflags 2.9.1", "cfg-if 1.0.0", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -1036,12 +1744,50 @@ dependencies = [ "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" @@ -1088,6 +1834,21 @@ 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 1.0.0", + "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" @@ -1107,6 +1868,15 @@ dependencies = [ "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" @@ -1116,6 +1886,15 @@ 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" @@ -1131,6 +1910,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[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" @@ -1151,13 +1936,22 @@ dependencies = [ "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", + "bitflags 2.9.1", ] [[package]] @@ -1257,16 +2051,29 @@ 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", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] @@ -1333,13 +2140,32 @@ 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", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -1438,7 +2264,7 @@ version = "0.86.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "008dec8a6b69f03b2a0bc4520dc06a7a8efc844e59b2a9bc024f0cb02fb60b63" dependencies = [ - "bitflags", + "bitflags 2.9.1", "lazy_static", "skia-bindings", ] @@ -1458,6 +2284,40 @@ 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" @@ -1474,6 +2334,12 @@ 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 = "subtle" version = "2.6.1" @@ -1517,7 +2383,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.9.1", "core-foundation", "system-configuration-sys", ] @@ -1552,10 +2418,55 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "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 = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if 1.0.0", + "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" @@ -1694,11 +2605,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags", + "bitflags 2.9.1", "bytes", "futures-util", "http", @@ -1747,12 +2658,24 @@ 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" @@ -1782,6 +2705,12 @@ 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" @@ -1887,6 +2816,115 @@ 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" @@ -1897,6 +2935,16 @@ dependencies = [ "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 = "wee_alloc" version = "0.4.5" @@ -1975,6 +3023,15 @@ 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" @@ -1993,6 +3050,36 @@ 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" @@ -2025,6 +3112,18 @@ dependencies = [ "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" @@ -2037,6 +3136,18 @@ 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" @@ -2049,6 +3160,18 @@ 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" @@ -2073,6 +3196,18 @@ 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" @@ -2085,6 +3220,18 @@ 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" @@ -2097,6 +3244,18 @@ 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" @@ -2109,6 +3268,18 @@ 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" @@ -2121,6 +3292,58 @@ 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" @@ -2136,7 +3359,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags", + "bitflags 2.9.1", ] [[package]] @@ -2145,6 +3368,38 @@ 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" @@ -2152,9 +3407,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", - "rustix", + "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" @@ -2179,6 +3465,26 @@ dependencies = [ "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" diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index 4a0883e6f7..47c5d0cf9b 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -12,12 +12,17 @@ path = "src/demo.rs" [dependencies] wasm-bindgen = "0.2.100" -skia-safe = "0.86.0" +skia-safe = { version = "0.86.0", features = ["gpu", "gl"] } console_error_panic_hook = "0.1.7" wee_alloc = { version = "0.4.5", optional = true } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } reqwest = "0.12.19" criterion = "0.5" +gl = "0.14.0" +glutin = "0.32.0" +glutin-winit = "0.5.0" +raw-window-handle = "0.6.0" +winit = "0.30.0" [features] default = ["wee_alloc"] diff --git a/crates/cg/benches/render_bench.rs b/crates/cg/benches/render_bench.rs index ed149afde4..f218f42c62 100644 --- a/crates/cg/benches/render_bench.rs +++ b/crates/cg/benches/render_bench.rs @@ -1,4 +1,4 @@ -use cg::draw::Renderer; +use cg::draw::{Backend, Renderer}; use cg::schema::{ BaseNode, BlendMode, Color, Node, NodeMap, Paint, RectangleNode, RectangularCornerRadius, Size, SolidPaint, @@ -91,8 +91,9 @@ fn bench_rectangles(c: &mut Criterion) { // 1K rectangles group.bench_function("1k_basic", |b| { b.iter(|| { - let renderer = Renderer::new(); - let surface_ptr = Renderer::init(width, height); + let mut renderer = Renderer::new(); + let surface_ptr = Renderer::init_raster(width, height); + renderer.set_backend(Backend::Raster(surface_ptr)); let (nodemap, _) = create_rectangles(black_box(1_000), false); @@ -106,16 +107,17 @@ fn bench_rectangles(c: &mut Criterion) { &paint, ); - renderer.render_node(surface_ptr, &"root".to_string(), &nodemap); - Renderer::free(surface_ptr); + renderer.render_node(&"root".to_string(), &nodemap); + renderer.free(); }) }); // 10K rectangles group.bench_function("10k_basic", |b| { b.iter(|| { - let renderer = Renderer::new(); - let surface_ptr = Renderer::init(width, height); + let mut renderer = Renderer::new(); + let surface_ptr = Renderer::init_raster(width, height); + renderer.set_backend(Backend::Raster(surface_ptr)); let (nodemap, _) = create_rectangles(black_box(10_000), false); @@ -129,15 +131,16 @@ fn bench_rectangles(c: &mut Criterion) { &paint, ); - renderer.render_node(surface_ptr, &"root".to_string(), &nodemap); - Renderer::free(surface_ptr); + renderer.render_node(&"root".to_string(), &nodemap); + renderer.free(); }) }); group.bench_function("10k_with_effects", |b| { b.iter(|| { - let renderer = Renderer::new(); - let surface_ptr = Renderer::init(width, height); + let mut renderer = Renderer::new(); + let surface_ptr = Renderer::init_raster(width, height); + renderer.set_backend(Backend::Raster(surface_ptr)); let (nodemap, _) = create_rectangles(black_box(10_000), true); @@ -151,16 +154,17 @@ fn bench_rectangles(c: &mut Criterion) { &paint, ); - renderer.render_node(surface_ptr, &"root".to_string(), &nodemap); - Renderer::free(surface_ptr); + renderer.render_node(&"root".to_string(), &nodemap); + renderer.free(); }) }); // 50K rectangles group.bench_function("50k_basic", |b| { b.iter(|| { - let renderer = Renderer::new(); - let surface_ptr = Renderer::init(width, height); + let mut renderer = Renderer::new(); + let surface_ptr = Renderer::init_raster(width, height); + renderer.set_backend(Backend::Raster(surface_ptr)); let (nodemap, _) = create_rectangles(black_box(50_000), false); @@ -174,15 +178,16 @@ fn bench_rectangles(c: &mut Criterion) { &paint, ); - renderer.render_node(surface_ptr, &"root".to_string(), &nodemap); - Renderer::free(surface_ptr); + renderer.render_node(&"root".to_string(), &nodemap); + renderer.free(); }) }); group.bench_function("50k_with_effects", |b| { b.iter(|| { - let renderer = Renderer::new(); - let surface_ptr = Renderer::init(width, height); + let mut renderer = Renderer::new(); + let surface_ptr = Renderer::init_raster(width, height); + renderer.set_backend(Backend::Raster(surface_ptr)); let (nodemap, _) = create_rectangles(black_box(50_000), true); @@ -196,8 +201,8 @@ fn bench_rectangles(c: &mut Criterion) { &paint, ); - renderer.render_node(surface_ptr, &"root".to_string(), &nodemap); - Renderer::free(surface_ptr); + renderer.render_node(&"root".to_string(), &nodemap); + renderer.free(); }) }); diff --git a/crates/cg/output.png b/crates/cg/output.png index a56334227c12a9a0465264d3fb18f9f42ee7ee2f..5e6146d33e48445d39e490305b6fd556e61b65ae 100644 GIT binary patch literal 126238 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¬JD9;4G}xCI7at9LxLo zz0}7g$5WyDG&|Bpov>u^Sz49+GJ5hGkK zWh2lx;geJfMhqF1h8AP{5x$}znda2Z3-f6G%f~d}1c*e#e+c+ji&FASihQi{oNC$p zC!Zy&_BlAWc&~F zcDphA`({Dir2f8hNKW+-t0LnqcG^)6ES61~Da&f`6C~*8DII7&(1!*&Zhy zd0t4ftT|+g≫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 diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index 22609536da..dc32d73c40 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -1,4 +1,4 @@ -use cg::draw::Renderer; +use cg::draw::{Backend, Renderer}; use cg::schema::FeDropShadow; use cg::schema::FilterEffect; use cg::schema::{ @@ -8,9 +8,179 @@ use cg::schema::{ TextDecoration, TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; +use console_error_panic_hook::set_once as init_panic_hook; +use gl::types::*; +use glutin::prelude::*; +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 raw_window_handle::HasRawWindowHandle; use reqwest; -use skia_safe::Image; +use skia_safe::{Image, Surface, gpu}; use std::time::Instant; +use winit::{ + application::ApplicationHandler, + dpi::{LogicalSize, PhysicalSize}, + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::{Window, WindowAttributes}, +}; + +fn init_window( + width: i32, + height: i32, +) -> ( + *mut Surface, + EventLoop<()>, + Window, + GlutinSurface, + PossiblyCurrentContext, + glutin::config::Config, + gpu::gl::FramebufferInfo, + skia_safe::gpu::DirectContext, +) { + init_panic_hook(); + + // Create event loop and window + let el = EventLoop::new().expect("Failed to create event loop"); + let window_attributes = WindowAttributes::default() + .with_title("Grida Canvas") + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_visible(true) + .with_transparent(true); + + // 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 retrieve RawWindowHandle"); + + // Create GL 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") + }) + }; + + // Create GL surface + let (width, height): (u32, u32) = window.inner_size().into(); + let attrs = SurfaceAttributesBuilder::::new().build( + raw_window_handle, + std::num::NonZeroU32::new(width).unwrap(), + std::num::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"); + + // Load GL functions + gl::load_with(|s| { + gl_config + .display() + .get_proc_address(std::ffi::CString::new(s).unwrap().as_c_str()) + }); + + // Create Skia GL 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(std::ffi::CString::new(name).unwrap().as_c_str()) + }) + .expect("Could not create interface"); + + // Create Skia GPU context + 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: 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, + ) +} #[tokio::main] async fn main() { @@ -19,7 +189,9 @@ async fn main() { // Initialize the renderer with image cache let mut renderer = Renderer::new(); - let surface_ptr = Renderer::init(width, height); + let (surface_ptr, el, window, mut gl_surface, gl_context, gl_config, fb_info, mut gr_context) = + init_window(width, height); + renderer.set_backend(Backend::GL(surface_ptr)); // Preload image before timing let demo_image_id = "demo_image"; @@ -292,72 +464,99 @@ async fn main() { nodemap.insert("test_image".to_string(), Node::Image(image_node)); nodemap.insert("root_group".to_string(), Node::Group(root_group_node)); - // Test performance with individual nodes first - println!("\nTesting individual node performance:"); - - let test_nodes = [ - ("test_rect", "Rectangle"), - ("test_ellipse", "Ellipse"), - ("test_polygon", "Polygon"), - ("test_regular_polygon", "Regular Polygon"), - ("test_text", "Text"), - ("test_line", "Line"), - ("test_image", "Image"), - ]; - - for (id, name) in test_nodes.iter() { - // Clear canvas before each render - 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, - ); - - let start = Instant::now(); - renderer.render_node(surface_ptr, &id.to_string(), &nodemap); - let time = start.elapsed(); - println!( - "{} render time: {:?} (FPS: {:.2})", - name, - time, - 1.0 / time.as_secs_f64() - ); + struct App { + renderer: Renderer, + surface_ptr: *mut Surface, + gl_surface: GlutinSurface, + gl_context: PossiblyCurrentContext, + window: Window, + nodemap: NodeMap, + gl_config: glutin::config::Config, + fb_info: gpu::gl::FramebufferInfo, + gr_context: skia_safe::gpu::DirectContext, } - // Now test the full scene - println!("\nTesting full scene performance:"); - - // Clear canvas before full scene render - 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, - ); + let mut app = App { + renderer, + surface_ptr, + gl_surface, + gl_context, + window, + nodemap, + gl_config, + fb_info, + gr_context, + }; + + 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: WindowEvent, + ) { + match event { + WindowEvent::CloseRequested => { + self.renderer.free(); + event_loop.exit(); + } + WindowEvent::Resized(new_size) => { + self.gl_surface.resize( + &self.gl_context, + new_size.width.try_into().unwrap(), + new_size.height.try_into().unwrap(), + ); + // Fix: skip if window is minimized or has zero size + if new_size.width == 0 || new_size.height == 0 { + return; + } + // Fix: ensure GL context is current before recreating surface + self.gl_context.make_current(&self.gl_surface).unwrap(); + // Recreate Skia surface to match new window size + let backend_render_target = gpu::backend_render_targets::make_gl( + (new_size.width as i32, new_size.height as i32), + self.gl_config.num_samples() as usize, + self.gl_config.stencil_size() as usize, + self.fb_info, + ); + let new_surface = gpu::surfaces::wrap_backend_render_target( + &mut self.gr_context, + &backend_render_target, + skia_safe::gpu::SurfaceOrigin::BottomLeft, + skia_safe::ColorType::RGBA8888, + None, + None, + ) + .expect("Could not recreate skia surface"); + // Free the old surface + self.renderer.free(); + // Update surface_ptr and backend + self.surface_ptr = Box::into_raw(Box::new(new_surface)); + self.renderer.set_backend(Backend::GL(self.surface_ptr)); + } + WindowEvent::RedrawRequested => { + let size = self.window.inner_size(); + let width = size.width as f32; + let height = size.height as f32; + + let surface = unsafe { &mut *self.surface_ptr }; + let canvas = surface.canvas(); + let mut paint = skia_safe::Paint::default(); + paint.set_color(skia_safe::Color::TRANSPARENT); + canvas.draw_rect(skia_safe::Rect::from_xywh(0.0, 0.0, width, height), &paint); + + self.renderer + .render_node(&"root_group".to_string(), &self.nodemap); + self.renderer.flush(); + + self.gl_surface.swap_buffers(&self.gl_context).unwrap(); + } + _ => {} + } + } + } - let start_time = Instant::now(); - renderer.render_node(surface_ptr, &"root_group".to_string(), &nodemap); - let render_time = start_time.elapsed(); - println!("Full scene render time: {:?}", render_time); - println!("Full scene FPS: {:.2}", 1.0 / render_time.as_secs_f64()); - - // Get the surface from the pointer to save the image - let save_start = Instant::now(); - let surface = unsafe { &mut *surface_ptr }; - let image = surface.image_snapshot(); - image - .encode_to_data(skia_safe::EncodedImageFormat::PNG) - .and_then(|data| std::fs::write("output.png", data.as_bytes()).ok()) - .expect("Failed to save PNG"); - println!("Save time: {:?}", save_start.elapsed()); - - // Free the surface - Renderer::free(surface_ptr); - - println!("Saved output.png"); + el.run_app(&mut app).expect("Failed to run event loop"); } diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 65e38ef4b2..f38cceef7a 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -3,7 +3,6 @@ use crate::schema::{ Node, NodeId, NodeMap, Paint, PolygonNode, RectangleNode, RectangularCornerRadius, RegularPolygonNode, TextAlign, TextAlignVertical, TextSpanNode, }; -use console_error_panic_hook::set_once as init_panic_hook; use skia_safe::{ Color, Font, FontMgr, FontStyle, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, TextBlob, Typeface, surfaces, @@ -33,82 +32,113 @@ fn default_typeface() -> Typeface { }) } +pub enum Backend { + GL(*mut Surface), + Raster(*mut Surface), +} + +impl Backend { + pub fn get_surface(&self) -> *mut Surface { + match self { + Backend::GL(ptr) | Backend::Raster(ptr) => *ptr, + } + } +} + pub struct Renderer { image_cache: HashMap, + backend: Option, } impl Renderer { pub fn new() -> Self { Self { image_cache: HashMap::new(), + backend: None, } } + pub fn init_raster(width: i32, height: i32) -> *mut Surface { + let surface = + surfaces::raster_n32_premul((width, height)).expect("Failed to create raster surface"); + Box::into_raw(Box::new(surface)) + } + + pub fn set_backend(&mut self, backend: Backend) { + self.backend = Some(backend); + } + pub fn add_image(&mut self, src: String, image: Image) { self.image_cache.insert(src, image); } - pub fn init(width: i32, height: i32) -> *mut Surface { - init_panic_hook(); - let surface = surfaces::raster_n32_premul((width, height)).unwrap(); - Box::into_raw(Box::new(surface)) - } + pub fn draw_rect(&self, x: f32, y: f32, w: f32, h: f32, r: f32, g: f32, b: f32, a: f32) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); - pub fn draw_rect( - &self, - ptr: *mut Surface, - x: f32, - y: f32, - w: f32, - h: f32, - r: f32, - g: f32, - b: f32, - a: f32, - ) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - - let color = Color::from_argb( - (a * 255.0) as u8, - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - ); - - let mut paint = SkiaPaint::default(); - paint.set_color(color); - - canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); + let color = Color::from_argb( + (a * 255.0) as u8, + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + ); + + let mut paint = SkiaPaint::default(); + paint.set_color(color); + + canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); + } } - pub fn draw_rect_node(&self, ptr: *mut Surface, node: &RectangleNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - let paint = sk_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; - // Draw drop shadow effect if present - if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { - let mut shadow_paint = SkiaPaint::default(); - let SchemaColor(r, g, b, a) = shadow.color; - shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); - shadow_paint.set_anti_alias(true); - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( - skia_safe::BlurStyle::Normal, - shadow.blur, - None, - )); + pub fn draw_rect_node(&self, node: &RectangleNode) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + let paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; + // Draw drop shadow effect if present + if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { + let mut shadow_paint = SkiaPaint::default(); + let SchemaColor(r, g, b, a) = shadow.color; + shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); + shadow_paint.set_anti_alias(true); + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + skia_safe::BlurStyle::Normal, + shadow.blur, + None, + )); + } + let offset_x = shadow.dx; + let offset_y = shadow.dy; + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + let mut shadow_rrect = rrect; + shadow_rrect.offset((offset_x, offset_y)); + canvas.draw_rrect(shadow_rrect, &shadow_paint); + } else { + let mut shadow_rect = rect; + shadow_rect.offset((offset_x, offset_y)); + canvas.draw_rect(shadow_rect, &shadow_paint); + } } - let offset_x = shadow.dx; - let offset_y = shadow.dy; + // Draw fill and stroke as before if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { let rrect = RRect::new_rect_radii( rect, @@ -119,45 +149,82 @@ impl Renderer { Point::new(bl, bl), // bottom-left ], ); - let mut shadow_rrect = rrect; - shadow_rrect.offset((offset_x, offset_y)); - canvas.draw_rrect(shadow_rrect, &shadow_paint); + let mut fill_paint = paint.clone(); + fill_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_rrect(rrect, &fill_paint); + // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint( + &node.stroke, + node.opacity, + (node.size.width, node.size.height), + ); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_rrect(rrect, &stroke_paint); + } } else { - let mut shadow_rect = rect; - shadow_rect.offset((offset_x, offset_y)); - canvas.draw_rect(shadow_rect, &shadow_paint); + let mut fill_paint = paint.clone(); + fill_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_rect(rect, &fill_paint); + // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint( + &node.stroke, + node.opacity, + (node.size.width, node.size.height), + ); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_rect(rect, &stroke_paint); + } } + canvas.restore(); } - // Draw fill and stroke as before - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left - ], + } + + pub fn draw_ellipse(&self, x: f32, y: f32, rx: f32, ry: f32, r: f32, g: f32, b: f32, a: f32) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + + let color = Color::from_argb( + (a * 255.0) as u8, + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, ); - let mut fill_paint = paint.clone(); - fill_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_rrect(rrect, &fill_paint); - // Draw stroke if stroke_width > 0 - if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint( - &node.stroke, - node.opacity, - (node.size.width, node.size.height), - ); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_rrect(rrect, &stroke_paint); - } - } else { - let mut fill_paint = paint.clone(); + + let mut paint = SkiaPaint::default(); + paint.set_color(color); + + canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); + } + } + + pub fn draw_ellipse_node(&self, node: &EllipseNode) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + let fill_paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + let rect = Rect::from_xywh( + -node.size.width / 2.0, + -node.size.height / 2.0, + node.size.width, + node.size.height, + ); + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + // Draw fill + let mut fill_paint = fill_paint.clone(); fill_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_rect(rect, &fill_paint); + canvas.draw_oval(rect, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { let mut stroke_paint = sk_paint( @@ -168,257 +235,173 @@ impl Renderer { stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); stroke_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_rect(rect, &stroke_paint); + canvas.draw_oval(rect, &stroke_paint); } + canvas.restore(); } - canvas.restore(); - } - - pub fn draw_ellipse( - &self, - ptr: *mut Surface, - x: f32, - y: f32, - rx: f32, - ry: f32, - r: f32, - g: f32, - b: f32, - a: f32, - ) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - - let color = Color::from_argb( - (a * 255.0) as u8, - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - ); - - let mut paint = SkiaPaint::default(); - paint.set_color(color); - - canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); } - pub fn draw_ellipse_node(&self, ptr: *mut Surface, node: &EllipseNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - let fill_paint = sk_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - let rect = Rect::from_xywh( - -node.size.width / 2.0, - -node.size.height / 2.0, - node.size.width, - node.size.height, - ); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - // Draw fill - let mut fill_paint = fill_paint.clone(); - fill_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_oval(rect, &fill_paint); - // Draw stroke if stroke_width > 0 - if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint( - &node.stroke, - node.opacity, - (node.size.width, node.size.height), + pub fn draw_line_node(&self, node: &LineNode) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + let mut paint = sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); + paint.set_stroke(true); + paint.set_stroke_width(node.stroke_width); + paint.set_blend_mode(node.base.blend_mode.into()); + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + canvas.draw_line( + Point::new(0.0, 0.0), + Point::new(node.size.width, 0.0), + &paint, ); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_oval(rect, &stroke_paint); + canvas.restore(); } - canvas.restore(); } - pub fn draw_line_node(&self, ptr: *mut Surface, node: &LineNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - let mut paint = sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); - paint.set_stroke(true); - paint.set_stroke_width(node.stroke_width); - paint.set_blend_mode(node.base.blend_mode.into()); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - canvas.draw_line( - Point::new(0.0, 0.0), - Point::new(node.size.width, 0.0), - &paint, - ); - canvas.restore(); - } - - pub fn draw_polygon_node(&self, ptr: *mut Surface, node: &PolygonNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - if node.points.len() < 3 { - // Not enough points to form a polygon - return; - } - let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); - let mut path = skia_safe::Path::new(); - let mut points_iter = node.points.iter(); - if let Some(&(x0, y0)) = points_iter.next() { - path.move_to((x0, y0)); - for &(x, y) in points_iter { - path.line_to((x, y)); + pub fn draw_polygon_node(&self, node: &PolygonNode) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + if node.points.len() < 3 { + // Not enough points to form a polygon + return; } - path.close(); - } - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - // Draw fill - let mut fill_paint = fill_paint.clone(); - fill_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_path(&path, &fill_paint); - // Draw stroke if stroke_width > 0 - if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_path(&path, &stroke_paint); + let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + let mut path = skia_safe::Path::new(); + let mut points_iter = node.points.iter(); + if let Some(&(x0, y0)) = points_iter.next() { + path.move_to((x0, y0)); + for &(x, y) in points_iter { + path.line_to((x, y)); + } + path.close(); + } + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + // Draw fill + let mut fill_paint = fill_paint.clone(); + fill_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_path(&path, &fill_paint); + // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_path(&path, &stroke_paint); + } + canvas.restore(); } - canvas.restore(); } - pub fn draw_regular_polygon_node(&self, ptr: *mut Surface, node: &RegularPolygonNode) { + pub fn draw_regular_polygon_node(&self, node: &RegularPolygonNode) { let poly = node.to_polygon(); - self.draw_polygon_node(ptr, &poly); + self.draw_polygon_node(&poly); } - pub fn draw_text_span_node(&self, ptr: *mut Surface, node: &TextSpanNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); - - // Create font with the specified size - let font = Font::from_typeface(default_typeface(), node.text_style.font_size); - - // Create text blob - let blob = TextBlob::from_str(&node.text, &font).unwrap(); - - // Calculate text position based on alignment - let (x, y) = match (node.text_align, node.text_align_vertical) { - (TextAlign::Left, TextAlignVertical::Top) => (0.0, node.text_style.font_size), - (TextAlign::Left, TextAlignVertical::Center) => (0.0, node.size.height / 2.0), - (TextAlign::Left, TextAlignVertical::Bottom) => (0.0, node.size.height), - (TextAlign::Center, TextAlignVertical::Top) => { - (node.size.width / 2.0, node.text_style.font_size) - } - (TextAlign::Center, TextAlignVertical::Center) => { - (node.size.width / 2.0, node.size.height / 2.0) - } - (TextAlign::Center, TextAlignVertical::Bottom) => { - (node.size.width / 2.0, node.size.height) - } - (TextAlign::Right, TextAlignVertical::Top) => { - (node.size.width, node.text_style.font_size) - } - (TextAlign::Right, TextAlignVertical::Center) => { - (node.size.width, node.size.height / 2.0) - } - (TextAlign::Right, TextAlignVertical::Bottom) => (node.size.width, node.size.height), - (TextAlign::Justify, _) => (0.0, node.text_style.font_size), // Justify not supported yet - }; - - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - - // Draw stroke if specified - if let (Some(stroke), Some(stroke_width)) = (&node.stroke, node.stroke_width) { - let mut stroke_paint = - sk_paint(stroke, node.opacity, (node.size.width, node.size.height)); - stroke_paint.set_style(skia_safe::paint::Style::Stroke); - stroke_paint.set_stroke_width(stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_text_blob(&blob, (x, y), &stroke_paint); - } + pub fn draw_text_span_node(&self, node: &TextSpanNode) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); - // Draw fill - let mut fill_paint = sk_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - fill_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_text_blob(&blob, (x, y), &fill_paint); + // Create font with the specified size + let font = Font::from_typeface(default_typeface(), node.text_style.font_size); - canvas.restore(); - } + // Create text blob + let blob = TextBlob::from_str(&node.text, &font).unwrap(); - pub fn draw_image_node(&self, ptr: *mut Surface, node: &ImageNode) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); + // Calculate text position based on alignment + let (x, y) = match (node.text_align, node.text_align_vertical) { + (TextAlign::Left, TextAlignVertical::Top) => (0.0, node.text_style.font_size), + (TextAlign::Left, TextAlignVertical::Center) => (0.0, node.size.height / 2.0), + (TextAlign::Left, TextAlignVertical::Bottom) => (0.0, node.size.height), + (TextAlign::Center, TextAlignVertical::Top) => { + (node.size.width / 2.0, node.text_style.font_size) + } + (TextAlign::Center, TextAlignVertical::Center) => { + (node.size.width / 2.0, node.size.height / 2.0) + } + (TextAlign::Center, TextAlignVertical::Bottom) => { + (node.size.width / 2.0, node.size.height) + } + (TextAlign::Right, TextAlignVertical::Top) => { + (node.size.width, node.text_style.font_size) + } + (TextAlign::Right, TextAlignVertical::Center) => { + (node.size.width, node.size.height / 2.0) + } + (TextAlign::Right, TextAlignVertical::Bottom) => { + (node.size.width, node.size.height) + } + (TextAlign::Justify, _) => (0.0, node.text_style.font_size), // Justify not supported yet + }; - if let Some(image) = self.image_cache.get(&node._ref) { canvas.save(); canvas.concat(&sk_matrix(node.transform.matrix)); - // Draw drop shadow effect if present - if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { - let mut shadow_paint = SkiaPaint::default(); - let SchemaColor(r, g, b, a) = shadow.color; - shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); - shadow_paint.set_anti_alias(true); - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( - skia_safe::BlurStyle::Normal, - shadow.blur, - None, - )); - } - let offset_x = shadow.dx; - let offset_y = shadow.dy; - let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - let mut shadow_rect = rect; - shadow_rect.offset((offset_x, offset_y)); - canvas.draw_image_rect(image, None, shadow_rect, &shadow_paint); + // Draw stroke if specified + if let (Some(stroke), Some(stroke_width)) = (&node.stroke, node.stroke_width) { + let mut stroke_paint = + sk_paint(stroke, node.opacity, (node.size.width, node.size.height)); + stroke_paint.set_style(skia_safe::paint::Style::Stroke); + stroke_paint.set_stroke_width(stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_text_blob(&blob, (x, y), &stroke_paint); } - // Draw the image - let mut paint = SkiaPaint::default(); - paint.set_anti_alias(true); - paint.set_blend_mode(node.base.blend_mode.into()); - paint.set_alpha((node.opacity * 255.0) as u8); + // Draw fill + let mut fill_paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + fill_paint.set_blend_mode(node.base.blend_mode.into()); + canvas.draw_text_blob(&blob, (x, y), &fill_paint); - let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; + canvas.restore(); + } + } - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left - ], - ); - // For rounded rectangles, we need to use a clip path + pub fn draw_image_node(&self, node: &ImageNode) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + + if let Some(image) = self.image_cache.get(&node._ref) { canvas.save(); - canvas.clip_rrect(rrect, None, true); - canvas.draw_image_rect(image, None, rect, &paint); - canvas.restore(); - } else { - canvas.draw_image_rect(image, None, rect, &paint); - } + canvas.concat(&sk_matrix(node.transform.matrix)); + + // Draw drop shadow effect if present + if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { + let mut shadow_paint = SkiaPaint::default(); + let SchemaColor(r, g, b, a) = shadow.color; + shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); + shadow_paint.set_anti_alias(true); + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + skia_safe::BlurStyle::Normal, + shadow.blur, + None, + )); + } + let offset_x = shadow.dx; + let offset_y = shadow.dy; + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let mut shadow_rect = rect; + shadow_rect.offset((offset_x, offset_y)); + canvas.draw_image_rect(image, None, shadow_rect, &shadow_paint); + } - // Draw stroke if stroke_width > 0 - if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint( - &node.stroke, - node.opacity, - (node.size.width, node.size.height), - ); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); + // Draw the image + let mut paint = SkiaPaint::default(); + paint.set_anti_alias(true); + paint.set_blend_mode(node.base.blend_mode.into()); + paint.set_alpha((node.opacity * 255.0) as u8); + + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { let rrect = RRect::new_rect_radii( @@ -430,70 +413,117 @@ impl Renderer { Point::new(bl, bl), // bottom-left ], ); - canvas.draw_rrect(rrect, &stroke_paint); + // For rounded rectangles, we need to use a clip path + canvas.save(); + canvas.clip_rrect(rrect, None, true); + canvas.draw_image_rect(image, None, rect, &paint); + canvas.restore(); } else { - canvas.draw_rect(rect, &stroke_paint); + canvas.draw_image_rect(image, None, rect, &paint); } - } - canvas.restore(); + // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint( + &node.stroke, + node.opacity, + (node.size.width, node.size.height), + ); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.base.blend_mode.into()); + + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + canvas.draw_rrect(rrect, &stroke_paint); + } else { + canvas.draw_rect(rect, &stroke_paint); + } + } + + canvas.restore(); + } } } - pub fn flush(_ptr: *mut Surface) { - // No flush needed for raster surfaces + pub fn flush(&self) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + if let Some(mut gr_context) = surface.recording_context() { + if let Some(mut direct_context) = gr_context.as_direct_context() { + direct_context.flush_and_submit(); + } + } + } } - pub fn free(ptr: *mut Surface) { - unsafe { Box::from_raw(ptr) }; + pub fn free(&mut self) { + if let Some(backend) = self.backend.take() { + let surface = unsafe { Box::from_raw(backend.get_surface()) }; + if let Some(mut gr_context) = surface.recording_context() { + if let Some(mut direct_context) = gr_context.as_direct_context() { + direct_context.abandon(); + } + } + } } - pub fn render_node(&self, ptr: *mut Surface, id: &NodeId, nodemap: &NodeMap) { + pub fn render_node(&self, id: &NodeId, nodemap: &NodeMap) { let node = match nodemap.get(id) { Some(node) => node, None => return, // Skip if node not found }; match node { - Node::Group(node) => self.draw_group_node(ptr, node, nodemap), - Node::Rectangle(node) => self.draw_rect_node(ptr, node), - Node::Ellipse(node) => self.draw_ellipse_node(ptr, node), - Node::Polygon(node) => self.draw_polygon_node(ptr, node), - Node::RegularPolygon(node) => self.draw_regular_polygon_node(ptr, node), - Node::TextSpan(node) => self.draw_text_span_node(ptr, node), - Node::Line(node) => self.draw_line_node(ptr, node), - Node::Image(node) => self.draw_image_node(ptr, node), + Node::Group(node) => self.draw_group_node(node, nodemap), + Node::Rectangle(node) => self.draw_rect_node(node), + Node::Ellipse(node) => self.draw_ellipse_node(node), + Node::Polygon(node) => self.draw_polygon_node(node), + Node::RegularPolygon(node) => self.draw_regular_polygon_node(node), + Node::TextSpan(node) => self.draw_text_span_node(node), + Node::Line(node) => self.draw_line_node(node), + Node::Image(node) => self.draw_image_node(node), _ => {} } } - pub fn draw_group_node(&self, ptr: *mut Surface, node: &GroupNode, nodemap: &NodeMap) { - let surface = unsafe { &mut *ptr }; - let canvas = surface.canvas(); + pub fn draw_group_node(&self, node: &GroupNode, nodemap: &NodeMap) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); - // Save canvas state for transform - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); + // Save canvas state for transform + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); - let needs_opacity_layer = node.opacity < 1.0; + let needs_opacity_layer = node.opacity < 1.0; - if needs_opacity_layer { - // Start new layer with opacity - canvas.save_layer_alpha(None, (node.opacity * 255.0) as u32); - } + if needs_opacity_layer { + // Start new layer with opacity + canvas.save_layer_alpha(None, (node.opacity * 255.0) as u32); + } - // Recursively render children - for child_id in &node.children { - self.render_node(ptr, child_id, nodemap); - } + // Recursively render children + for child_id in &node.children { + self.render_node(child_id, nodemap); + } + + if needs_opacity_layer { + // End opacity layer + canvas.restore(); + } - if needs_opacity_layer { - // End opacity layer + // Restore transform canvas.restore(); } - - // Restore transform - canvas.restore(); } } From 7f85f6707e46da1b39f3d8f6300129720686380f Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 00:33:34 +0900 Subject: [PATCH 030/262] demo --- crates/cg/src/demo.rs | 215 +++++++++++++++++++++++------------------- 1 file changed, 119 insertions(+), 96 deletions(-) diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index dc32d73c40..233c81ad2f 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -10,7 +10,6 @@ use cg::schema::{ use cg::transform::AffineTransform; use console_error_panic_hook::set_once as init_panic_hook; use gl::types::*; -use glutin::prelude::*; use glutin::{ config::{ConfigTemplateBuilder, GlConfig}, context::{ContextApi, ContextAttributesBuilder, PossiblyCurrentContext}, @@ -22,7 +21,7 @@ use glutin_winit::DisplayBuilder; use raw_window_handle::HasRawWindowHandle; use reqwest; use skia_safe::{Image, Surface, gpu}; -use std::time::Instant; +use std::time::{Duration, Instant}; use winit::{ application::ApplicationHandler, dpi::{LogicalSize, PhysicalSize}, @@ -43,6 +42,7 @@ fn init_window( glutin::config::Config, gpu::gl::FramebufferInfo, skia_safe::gpu::DirectContext, + f64, // scale factor ) { init_panic_hook(); @@ -51,16 +51,14 @@ fn init_window( let window_attributes = WindowAttributes::default() .with_title("Grida Canvas") .with_inner_size(LogicalSize::new(width as f64, height as f64)) - .with_visible(true) - .with_transparent(true); + .with_visible(true); // Create GL config template - let template = ConfigTemplateBuilder::new() - .with_alpha_size(8) - .with_transparency(true); + let template = ConfigTemplateBuilder::new().with_alpha_size(8); // 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 @@ -82,6 +80,13 @@ fn init_window( .raw_window_handle() .expect("Failed to retrieve RawWindowHandle"); + // --- DPI handling --- + let scale_factor = window.scale_factor(); + let logical_size = window.inner_size(); + let physical_width = (logical_size.width as f64 * scale_factor).round() as u32; + let physical_height = (logical_size.height as f64 * scale_factor).round() as u32; + // --- + // Create GL context let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle)); let fallback_context_attributes = ContextAttributesBuilder::new() @@ -101,11 +106,10 @@ fn init_window( }; // Create GL surface - let (width, height): (u32, u32) = window.inner_size().into(); let attrs = SurfaceAttributesBuilder::::new().build( raw_window_handle, - std::num::NonZeroU32::new(width).unwrap(), - std::num::NonZeroU32::new(height).unwrap(), + std::num::NonZeroU32::new(physical_width).unwrap(), + std::num::NonZeroU32::new(physical_height).unwrap(), ); let gl_surface = unsafe { @@ -179,9 +183,44 @@ fn init_window( gl_config, fb_info, gr_context, + scale_factor, ) } +struct App { + renderer: Renderer, + surface_ptr: *mut Surface, + gl_surface: GlutinSurface, + gl_context: PossiblyCurrentContext, + window: Window, + nodemap: NodeMap, +} + +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: WindowEvent, + ) { + match event { + WindowEvent::CloseRequested => { + self.renderer.free(); + event_loop.exit(); + } + WindowEvent::Resized(_) => { + // Ignore resize events + } + WindowEvent::RedrawRequested => { + // Do nothing - we only render once at startup + } + _ => {} + } + } +} + #[tokio::main] async fn main() { let width = 800; @@ -189,10 +228,60 @@ async fn main() { // Initialize the renderer with image cache let mut renderer = Renderer::new(); - let (surface_ptr, el, window, mut gl_surface, gl_context, gl_config, fb_info, mut gr_context) = - init_window(width, height); + let ( + surface_ptr, + el, + window, + gl_surface, + gl_context, + _gl_config, + _fb_info, + _gr_context, + scale_factor, + ) = init_window(width, height); renderer.set_backend(Backend::GL(surface_ptr)); + // Log DPI and size info + let logical_size = window.inner_size(); + let physical_width = (logical_size.width as f64 * scale_factor).round() as u32; + let physical_height = (logical_size.height as f64 * scale_factor).round() as u32; + println!("[DPI DEBUG] scale_factor: {}", scale_factor); + println!( + "[DPI DEBUG] logical_size: {} x {}", + logical_size.width, logical_size.height + ); + println!( + "[DPI DEBUG] physical_size: {} x {}", + physical_width, physical_height + ); + // Get logical canvas size for background + // let logical_size = window.inner_size(); + + // Add a background rectangle node + let background_rect_node = RectangleNode { + base: BaseNode { + id: "background_rect".to_string(), + name: "Background Rect".to_string(), + active: true, + blend_mode: BlendMode::Normal, + }, + opacity: 1.0, + transform: AffineTransform::identity(), + size: Size { + width: 800.0, + height: 600.0, + }, + corner_radius: RectangularCornerRadius::all(0.0), + fill: Paint::Solid(SolidPaint { + color: Color(230, 240, 255, 255), // Light blue for visibility + }), + stroke: Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 0), // No stroke + }), + stroke_width: 0.0, + effect: None, + }; + // Preload image before timing let demo_image_id = "demo_image"; let demo_image_url = "https://grida.co/images/abstract-placeholder.jpg".to_string(); @@ -439,8 +528,9 @@ async fn main() { active: true, blend_mode: BlendMode::Normal, }, - transform: AffineTransform::identity(), + transform: AffineTransform::new(0.0, 100.0, 0.0), children: vec![ + "background_rect".to_string(), "shapes_group".to_string(), "test_text".to_string(), "test_line".to_string(), @@ -451,6 +541,10 @@ async fn main() { // Create a node map and add all nodes let mut nodemap = NodeMap::new(); + nodemap.insert( + "background_rect".to_string(), + Node::Rectangle(background_rect_node), + ); nodemap.insert("test_rect".to_string(), Node::Rectangle(rect_node)); nodemap.insert("test_ellipse".to_string(), Node::Ellipse(ellipse_node)); nodemap.insert("test_polygon".to_string(), Node::Polygon(polygon_node)); @@ -464,18 +558,6 @@ async fn main() { nodemap.insert("test_image".to_string(), Node::Image(image_node)); nodemap.insert("root_group".to_string(), Node::Group(root_group_node)); - struct App { - renderer: Renderer, - surface_ptr: *mut Surface, - gl_surface: GlutinSurface, - gl_context: PossiblyCurrentContext, - window: Window, - nodemap: NodeMap, - gl_config: glutin::config::Config, - fb_info: gpu::gl::FramebufferInfo, - gr_context: skia_safe::gpu::DirectContext, - } - let mut app = App { renderer, surface_ptr, @@ -483,80 +565,21 @@ async fn main() { gl_context, window, nodemap, - gl_config, - fb_info, - gr_context, }; - 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: WindowEvent, - ) { - match event { - WindowEvent::CloseRequested => { - self.renderer.free(); - event_loop.exit(); - } - WindowEvent::Resized(new_size) => { - self.gl_surface.resize( - &self.gl_context, - new_size.width.try_into().unwrap(), - new_size.height.try_into().unwrap(), - ); - // Fix: skip if window is minimized or has zero size - if new_size.width == 0 || new_size.height == 0 { - return; - } - // Fix: ensure GL context is current before recreating surface - self.gl_context.make_current(&self.gl_surface).unwrap(); - // Recreate Skia surface to match new window size - let backend_render_target = gpu::backend_render_targets::make_gl( - (new_size.width as i32, new_size.height as i32), - self.gl_config.num_samples() as usize, - self.gl_config.stencil_size() as usize, - self.fb_info, - ); - let new_surface = gpu::surfaces::wrap_backend_render_target( - &mut self.gr_context, - &backend_render_target, - skia_safe::gpu::SurfaceOrigin::BottomLeft, - skia_safe::ColorType::RGBA8888, - None, - None, - ) - .expect("Could not recreate skia surface"); - // Free the old surface - self.renderer.free(); - // Update surface_ptr and backend - self.surface_ptr = Box::into_raw(Box::new(new_surface)); - self.renderer.set_backend(Backend::GL(self.surface_ptr)); - } - WindowEvent::RedrawRequested => { - let size = self.window.inner_size(); - let width = size.width as f32; - let height = size.height as f32; - - let surface = unsafe { &mut *self.surface_ptr }; - let canvas = surface.canvas(); - let mut paint = skia_safe::Paint::default(); - paint.set_color(skia_safe::Color::TRANSPARENT); - canvas.draw_rect(skia_safe::Rect::from_xywh(0.0, 0.0, width, height), &paint); - - self.renderer - .render_node(&"root_group".to_string(), &self.nodemap); - self.renderer.flush(); - - self.gl_surface.swap_buffers(&self.gl_context).unwrap(); - } - _ => {} - } - } + // Render once at startup + let surface = unsafe { &mut *app.surface_ptr }; + let canvas = surface.canvas(); + canvas.clear(skia_safe::Color::WHITE); + + app.renderer + .render_node(&"root_group".to_string(), &app.nodemap); + app.renderer.flush(); + if let Err(e) = app.gl_surface.swap_buffers(&app.gl_context) { + eprintln!("Error swapping buffers: {:?}", e); } + // Set up the event loop to wait for events + el.set_control_flow(ControlFlow::Wait); el.run_app(&mut app).expect("Failed to run event loop"); } From 84785c269b752286e7c7f25386a28cbb26a7f377 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 14:40:11 +0900 Subject: [PATCH 031/262] add io json --- Cargo.lock | 2 + crates/cg/Cargo.toml | 2 + crates/cg/src/demo.rs | 18 ++- crates/cg/src/draw.rs | 1 - crates/cg/src/io.rs | 260 ++++++++++++++++++++++++++++++++++++++++ crates/cg/src/lib.rs | 1 + crates/cg/src/schema.rs | 61 +++++++--- 7 files changed, 317 insertions(+), 28 deletions(-) create mode 100644 crates/cg/src/io.rs diff --git a/Cargo.lock b/Cargo.lock index 11c6f0831e..76f78843cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,6 +291,8 @@ dependencies = [ "glutin-winit", "raw-window-handle", "reqwest", + "serde", + "serde_json", "skia-safe", "tokio", "wasm-bindgen", diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index 47c5d0cf9b..97e2f64c4b 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -23,6 +23,8 @@ glutin = "0.32.0" glutin-winit = "0.5.0" raw-window-handle = "0.6.0" winit = "0.30.0" +serde = "1.0.219" +serde_json = "1.0.140" [features] default = ["wee_alloc"] diff --git a/crates/cg/src/demo.rs b/crates/cg/src/demo.rs index 233c81ad2f..6d131b7d8d 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/demo.rs @@ -3,9 +3,9 @@ use cg::schema::FeDropShadow; use cg::schema::FilterEffect; use cg::schema::{ BaseNode, BlendMode, Color, EllipseNode, FontWeight, GradientStop, GroupNode, ImageNode, - LineNode, LinearGradientPaint, Node, NodeMap, Paint, PolygonNode, RadialGradientPaint, - RectangleNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, TextAlignVertical, - TextDecoration, TextSpanNode, TextStyle, + LineNode, Node, NodeMap, Paint, PolygonNode, RadialGradientPaint, RectangleNode, + RectangularCornerRadius, Size, SolidPaint, TextAlign, TextAlignVertical, TextDecoration, + TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; use console_error_panic_hook::set_once as init_panic_hook; @@ -21,11 +21,11 @@ use glutin_winit::DisplayBuilder; use raw_window_handle::HasRawWindowHandle; use reqwest; use skia_safe::{Image, Surface, gpu}; -use std::time::{Duration, Instant}; +use std::time::Instant; use winit::{ application::ApplicationHandler, - dpi::{LogicalSize, PhysicalSize}, - event::{Event, WindowEvent}, + dpi::LogicalSize, + event::WindowEvent, event_loop::{ControlFlow, EventLoop}, window::{Window, WindowAttributes}, }; @@ -192,7 +192,6 @@ struct App { surface_ptr: *mut Surface, gl_surface: GlutinSurface, gl_context: PossiblyCurrentContext, - window: Window, nodemap: NodeMap, } @@ -405,7 +404,7 @@ async fn main() { (x, y) }) .collect::>(); - let polygon_node = cg::schema::PolygonNode { + let polygon_node = PolygonNode { base: BaseNode { id: "test_polygon".to_string(), name: "Test Polygon".to_string(), @@ -466,7 +465,7 @@ async fn main() { text_decoration: TextDecoration::None, font_family: None, font_size: 32.0, - font_weight: FontWeight::W400, + font_weight: FontWeight::default(), letter_spacing: None, line_height: None, }, @@ -563,7 +562,6 @@ async fn main() { surface_ptr, gl_surface, gl_context, - window, nodemap, }; diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index f38cceef7a..39e03ed03d 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -491,7 +491,6 @@ impl Renderer { Node::TextSpan(node) => self.draw_text_span_node(node), Node::Line(node) => self.draw_line_node(node), Node::Image(node) => self.draw_image_node(node), - _ => {} } } diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs new file mode 100644 index 0000000000..c9e5588899 --- /dev/null +++ b/crates/cg/src/io.rs @@ -0,0 +1,260 @@ +use crate::schema::{FontWeight, TextAlign, TextAlignVertical, TextDecoration}; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, Deserialize)] +pub struct CanvasFile { + pub version: String, + pub document: Document, +} + +#[derive(Debug, Deserialize)] +pub struct Document { + pub bitmaps: HashMap, + pub properties: HashMap, + pub nodes: HashMap, + pub scenes: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct Scene { + pub id: String, + pub name: String, + #[serde(rename = "type")] + pub type_name: String, + pub children: Vec, + #[serde(rename = "backgroundColor")] + pub background_color: Option, + pub guides: Option>, + pub constraints: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum Node { + #[serde(rename = "container")] + Container(ContainerNode), + #[serde(rename = "text")] + Text(TextNode), + #[serde(rename = "vector")] + Vector(VectorNode), + #[serde(rename = "ellipse")] + Ellipse(EllipseNode), + #[serde(other)] + Unknown, +} + +#[derive(Debug, Deserialize)] +pub struct ContainerNode { + pub id: String, + pub name: String, + #[serde(default = "default_active")] + pub active: bool, + #[serde(default = "default_locked")] + pub locked: bool, + #[serde(default = "default_opacity")] + pub opacity: f32, + #[serde(default = "default_rotation")] + pub rotation: f32, + #[serde(rename = "zIndex", default = "default_z_index")] + pub z_index: i32, + pub position: Option, + pub left: f32, + pub top: f32, + pub width: serde_json::Value, + pub height: serde_json::Value, + pub children: Vec, + pub expanded: Option, + pub fill: Option, + pub border: Option, + pub style: Option>, + #[serde(rename = "cornerRadius")] + pub corner_radius: Option, + pub padding: Option, + pub layout: Option, + pub direction: Option, + #[serde(rename = "mainAxisAlignment")] + pub main_axis_alignment: Option, + #[serde(rename = "crossAxisAlignment")] + pub cross_axis_alignment: Option, + #[serde(rename = "mainAxisGap")] + pub main_axis_gap: Option, + #[serde(rename = "crossAxisGap")] + pub cross_axis_gap: Option, +} + +#[derive(Debug, Deserialize)] +pub struct TextNode { + pub id: String, + pub name: String, + #[serde(default = "default_active")] + pub active: bool, + #[serde(default = "default_locked")] + pub locked: bool, + #[serde(default = "default_opacity")] + pub opacity: f32, + #[serde(default = "default_rotation")] + pub rotation: f32, + #[serde(rename = "zIndex", default = "default_z_index")] + pub z_index: i32, + pub position: Option, + pub left: f32, + pub top: f32, + pub right: Option, + pub bottom: Option, + pub width: serde_json::Value, + pub height: serde_json::Value, + pub fill: Option, + pub style: Option>, + pub text: String, + #[serde(rename = "textAlign", default = "default_text_align")] + pub text_align: TextAlign, + #[serde(rename = "textAlignVertical", default = "default_text_align_vertical")] + pub text_align_vertical: TextAlignVertical, + #[serde(rename = "textDecoration", default = "default_text_decoration")] + pub text_decoration: TextDecoration, + #[serde(rename = "lineHeight")] + pub line_height: Option, + #[serde(rename = "letterSpacing")] + pub letter_spacing: Option, + #[serde(rename = "fontSize")] + pub font_size: Option, + #[serde(rename = "fontFamily")] + pub font_family: Option, + #[serde(rename = "fontWeight", default = "default_font_weight")] + pub font_weight: FontWeight, +} + +#[derive(Debug, Deserialize)] +pub struct VectorNode { + pub id: String, + pub name: String, + #[serde(default = "default_active")] + pub active: bool, + #[serde(default = "default_locked")] + pub locked: bool, + #[serde(default = "default_opacity")] + pub opacity: f32, + #[serde(default = "default_rotation")] + pub rotation: f32, + #[serde(rename = "zIndex", default = "default_z_index")] + pub z_index: i32, + pub position: Option, + pub left: f32, + pub top: f32, + pub width: f32, + pub height: f32, + pub fill: Option, + pub paths: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct EllipseNode { + pub id: String, + pub name: String, + #[serde(default = "default_active")] + pub active: bool, + #[serde(default = "default_locked")] + pub locked: bool, + #[serde(default = "default_opacity")] + pub opacity: f32, + #[serde(default = "default_rotation")] + pub rotation: f32, + #[serde(rename = "zIndex", default = "default_z_index")] + pub z_index: i32, + pub position: Option, + pub left: f32, + pub top: f32, + pub width: f32, + pub height: f32, + pub fill: Option, + #[serde(rename = "strokeWidth")] + pub stroke_width: Option, + #[serde(rename = "strokeCap")] + pub stroke_cap: Option, + pub effects: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct Fill { + #[serde(rename = "type")] + pub kind: String, + pub color: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Border { + #[serde(rename = "borderWidth")] + pub border_width: Option, + #[serde(rename = "borderColor")] + pub border_color: Option, + #[serde(rename = "borderStyle")] + pub border_style: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Path { + pub d: String, + #[serde(rename = "fillRule")] + pub fill_rule: String, + pub fill: String, +} + +#[derive(Debug, Deserialize)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: f32, +} + +// Default value functions +fn default_active() -> bool { + true +} +fn default_locked() -> bool { + false +} +fn default_opacity() -> f32 { + 1.0 +} +fn default_rotation() -> f32 { + 0.0 +} +fn default_z_index() -> i32 { + 0 +} +fn default_text_align() -> TextAlign { + TextAlign::Left +} +fn default_text_align_vertical() -> TextAlignVertical { + TextAlignVertical::Top +} +fn default_text_decoration() -> TextDecoration { + TextDecoration::None +} +fn default_font_weight() -> FontWeight { + FontWeight::new(400) +} + +pub fn parse(file: &str) -> Result { + serde_json::from_str(file) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn parse_canvas_json() { + let data = fs::read_to_string("canvas.json").expect("failed to read file"); + let parsed: CanvasFile = serde_json::from_str(&data).expect("failed to parse JSON"); + + assert_eq!(parsed.version, "0.0.1-beta.1+20250303"); + assert!( + !parsed.document.nodes.is_empty(), + "nodes should not be empty" + ); + } +} diff --git a/crates/cg/src/lib.rs b/crates/cg/src/lib.rs index 3d73c91255..a7795cf33b 100644 --- a/crates/cg/src/lib.rs +++ b/crates/cg/src/lib.rs @@ -1,3 +1,4 @@ pub mod draw; +pub mod io; pub mod schema; pub mod transform; diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 553c9b3e4f..0cb70a81ea 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -1,4 +1,5 @@ use crate::transform::AffineTransform; +use serde::Deserialize; use std::collections::HashMap; use std::f32::consts::PI; @@ -121,9 +122,11 @@ impl From for skia_safe::BlendMode { /// /// - [Flutter](https://api.flutter.dev/flutter/dart-ui/TextDecoration-class.html) /// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration) -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Deserialize)] pub enum TextDecoration { + #[serde(rename = "none")] None, + #[serde(rename = "underline")] Underline, } @@ -133,11 +136,15 @@ pub enum TextDecoration { /// /// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/text-align) /// - [Flutter](https://api.flutter.dev/flutter/dart-ui/TextAlign.html) -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Deserialize)] pub enum TextAlign { + #[serde(rename = "left")] Left, + #[serde(rename = "right")] Right, + #[serde(rename = "center")] Center, + #[serde(rename = "justify")] Justify, } @@ -147,29 +154,50 @@ pub enum TextAlign { /// /// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content) /// - [Konva](https://konvajs.org/api/Konva.Text.html#verticalAlign) -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Deserialize)] pub enum TextAlignVertical { + #[serde(rename = "top")] Top, + #[serde(rename = "center")] Center, + #[serde(rename = "bottom")] Bottom, } -/// Supported font weights, mapped from CSS/OpenType numeric values. +/// Font weight value (1-1000). /// /// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight) /// - [Flutter](https://api.flutter.dev/flutter/dart-ui/FontWeight-class.html) /// - [OpenType spec](https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass) -#[derive(Debug, Clone, Copy)] -pub enum FontWeight { - W100, - W200, - W300, - W400, - W500, - W600, - W700, - W800, - W900, +#[derive(Debug, Clone, Copy, Deserialize)] +pub struct FontWeight(pub u16); + +impl FontWeight { + /// Creates a new font weight value. + /// + /// # Arguments + /// + /// * `value` - The font weight value (1-1000) + /// + /// # Panics + /// + /// Panics if the value is not between 1 and 1000. + pub fn new(value: u16) -> Self { + assert!( + value >= 1 && value <= 1000, + "Font weight must be between 1 and 1000" + ); + Self(value) + } + + /// Returns the font weight value. + pub fn value(&self) -> u16 { + self.0 + } + + pub fn default() -> Self { + Self(400) + } } /// A set of style properties that can be applied to a text or text span. @@ -191,8 +219,7 @@ pub struct TextStyle { /// Default is `0.0`. pub letter_spacing: Option, - /// Line height (deprecated). - #[deprecated(note = "Line height is not currently supported or recommended.")] + /// Line height pub line_height: Option, } From f209f4cc972b88fce127b78bc3e5556335da2b6b Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 15:20:15 +0900 Subject: [PATCH 032/262] mv --- crates/cg/Cargo.toml | 6 ++-- crates/cg/src/{demo.rs => dev.rs} | 49 +++++++++++++++++++------------ crates/cg/src/draw.rs | 2 +- crates/cg/src/io.rs | 1 + 4 files changed, 35 insertions(+), 23 deletions(-) rename crates/cg/src/{demo.rs => dev.rs} (93%) diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index 97e2f64c4b..1b6fdab192 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -7,8 +7,8 @@ edition = "2024" crate-type = ["cdylib", "rlib"] [[bin]] -name = "demo" -path = "src/demo.rs" +name = "dev" +path = "src/dev.rs" [dependencies] wasm-bindgen = "0.2.100" @@ -18,10 +18,10 @@ wee_alloc = { version = "0.4.5", optional = true } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } reqwest = "0.12.19" criterion = "0.5" -gl = "0.14.0" 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" serde = "1.0.219" serde_json = "1.0.140" diff --git a/crates/cg/src/demo.rs b/crates/cg/src/dev.rs similarity index 93% rename from crates/cg/src/demo.rs rename to crates/cg/src/dev.rs index 6d131b7d8d..9ca26cc018 100644 --- a/crates/cg/src/demo.rs +++ b/crates/cg/src/dev.rs @@ -10,6 +10,7 @@ use cg::schema::{ use cg::transform::AffineTransform; use console_error_panic_hook::set_once as init_panic_hook; use gl::types::*; +use gl_rs as gl; use glutin::{ config::{ConfigTemplateBuilder, GlConfig}, context::{ContextApi, ContextAttributesBuilder, PossiblyCurrentContext}, @@ -18,10 +19,15 @@ use glutin::{ surface::{Surface as GlutinSurface, SurfaceAttributesBuilder, WindowSurface}, }; use glutin_winit::DisplayBuilder; +#[allow(deprecated)] use raw_window_handle::HasRawWindowHandle; use reqwest; use skia_safe::{Image, Surface, gpu}; -use std::time::Instant; +use std::{ + ffi::CString, + num::NonZeroU32, + time::{Duration, Instant}, +}; use winit::{ application::ApplicationHandler, dpi::LogicalSize, @@ -50,21 +56,26 @@ fn init_window( let el = EventLoop::new().expect("Failed to create event loop"); let window_attributes = WindowAttributes::default() .with_title("Grida Canvas") - .with_inner_size(LogicalSize::new(width as f64, height as f64)) - .with_visible(true); + .with_inner_size(LogicalSize::new(400, 300)); // Create GL config template - let template = ConfigTemplateBuilder::new().with_alpha_size(8); + 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| { + // Find the config with the minimum number of samples. Usually Skia takes care of + // anti-aliasing and may not be able to create appropriate Surfaces for samples > 0. + // See https://github.com/rust-skia/rust-skia/issues/782 + // And https://github.com/rust-skia/rust-skia/issues/764 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 { @@ -74,7 +85,7 @@ fn init_window( .unwrap() }) .unwrap(); - + println!("Picked a config with {} samples", gl_config.num_samples()); let window = window.expect("Could not create window with OpenGL context"); let raw_window_handle = window .raw_window_handle() @@ -82,13 +93,15 @@ fn init_window( // --- DPI handling --- let scale_factor = window.scale_factor(); - let logical_size = window.inner_size(); - let physical_width = (logical_size.width as f64 * scale_factor).round() as u32; - let physical_height = (logical_size.height as f64 * scale_factor).round() as u32; // --- - // Create GL context + // The context creation part. It can be created before surface and that's how + // it's expected in multithreaded + multiwindow operation mode, since you + // can send NotCurrentContext, but not Surface. let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle)); + + // Since glutin by default tries to create OpenGL core context, which may not be + // present we should try gles. let fallback_context_attributes = ContextAttributesBuilder::new() .with_context_api(ContextApi::Gles(None)) .build(Some(raw_window_handle)); @@ -105,11 +118,12 @@ fn init_window( }) }; - // Create GL surface + let (width, height): (u32, u32) = window.inner_size().into(); + let attrs = SurfaceAttributesBuilder::::new().build( raw_window_handle, - std::num::NonZeroU32::new(physical_width).unwrap(), - std::num::NonZeroU32::new(physical_height).unwrap(), + NonZeroU32::new(width).unwrap(), + NonZeroU32::new(height).unwrap(), ); let gl_surface = unsafe { @@ -123,25 +137,22 @@ fn init_window( .make_current(&gl_surface) .expect("Could not make GL context current"); - // Load GL functions gl::load_with(|s| { gl_config .display() - .get_proc_address(std::ffi::CString::new(s).unwrap().as_c_str()) + .get_proc_address(CString::new(s).unwrap().as_c_str()) }); - // Create Skia GL 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(std::ffi::CString::new(name).unwrap().as_c_str()) + .get_proc_address(CString::new(name).unwrap().as_c_str()) }) .expect("Could not create interface"); - // Create Skia GPU context let mut gr_context = skia_safe::gpu::direct_contexts::make_gl(interface, None) .expect("Could not create direct context"); @@ -527,7 +538,7 @@ async fn main() { active: true, blend_mode: BlendMode::Normal, }, - transform: AffineTransform::new(0.0, 100.0, 0.0), + transform: AffineTransform::new(0.0, 0.0, 0.0), children: vec![ "background_rect".to_string(), "shapes_group".to_string(), diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 39e03ed03d..5fec8808fd 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,7 +1,7 @@ use crate::schema::{ Color as SchemaColor, EllipseNode, FilterEffect, GradientStop, GroupNode, ImageNode, LineNode, Node, NodeId, NodeMap, Paint, PolygonNode, RectangleNode, RectangularCornerRadius, - RegularPolygonNode, TextAlign, TextAlignVertical, TextSpanNode, + RegularPolygonNode, TextAlign, TextAlignVertical, TextNode, TextSpanNode, }; use skia_safe::{ Color, Font, FontMgr, FontStyle, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index c9e5588899..58aac40d4c 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -14,6 +14,7 @@ pub struct Document { pub properties: HashMap, pub nodes: HashMap, pub scenes: HashMap, + pub entry_scene_id: Option, } #[derive(Debug, Deserialize)] From 6709fabb4b3f5350b1b03d4c5fc4c7243818bcf2 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 18:13:46 +0900 Subject: [PATCH 033/262] paragraph --- crates/cg/Cargo.toml | 2 +- .../cg/resources/Caveat-VariableFont_wght.ttf | Bin 0 -> 394104 bytes crates/cg/src/dev.rs | 36 +++- crates/cg/src/draw.rs | 179 +++++++++++------- crates/cg/src/io.rs | 2 +- crates/cg/src/schema.rs | 35 +++- crates/cg/src/transform.rs | 7 + 7 files changed, 175 insertions(+), 86 deletions(-) create mode 100644 crates/cg/resources/Caveat-VariableFont_wght.ttf diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index 1b6fdab192..6630c00338 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -12,7 +12,7 @@ path = "src/dev.rs" [dependencies] wasm-bindgen = "0.2.100" -skia-safe = { version = "0.86.0", features = ["gpu", "gl"] } +skia-safe = { version = "0.86.0", features = ["gpu", "gl", "textlayout"] } console_error_panic_hook = "0.1.7" wee_alloc = { version = "0.4.5", optional = true } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/crates/cg/resources/Caveat-VariableFont_wght.ttf b/crates/cg/resources/Caveat-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5adc65821c428948b4789e4195ea59cfef9952bf GIT binary patch literal 394104 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

    Y8fEPOXG0H|^O8=g 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/hoR^sdg?> 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 b5s-mcp?nLKQ;j~sW4yS~&qMk%*J}R ( *mut Surface, EventLoop<()>, @@ -267,6 +268,26 @@ async fn main() { // Get logical canvas size for background // let logical_size = window.inner_size(); + let font_caveat_path: &str = "resources/Caveat-VariableFont_wght.ttf"; + let font_caveat_data = fs::read(font_caveat_path).expect("failed to read file"); + let font_caveat_family = "Caveat".to_string(); + let font_roboto_url = "https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf"; + let font_roboto_family = "Roboto".to_string(); + + // load the font + let font_load_start = Instant::now(); + let font_data = reqwest::get(font_roboto_url) + .await + .unwrap() + .bytes() + .await + .unwrap(); + + renderer.add_font(&font_caveat_data); + renderer.add_font(&font_data); + + println!("Font load time: {:?}", font_load_start.elapsed()); + // Add a background rectangle node let background_rect_node = RectangleNode { base: BaseNode { @@ -466,24 +487,25 @@ async fn main() { active: true, blend_mode: BlendMode::Normal, }, - transform: AffineTransform::identity(), + transform: AffineTransform::new(50.0, 50.0, 15.0), size: Size { width: 300.0, height: 200.0, }, text: "Grida Canvas SKIA Bindings Backend".to_string(), text_style: TextStyle { - text_decoration: TextDecoration::None, - font_family: None, + text_decoration: TextDecoration::LineThrough, + // font_family: font_roboto_family.clone(), + font_family: font_caveat_family.clone(), font_size: 32.0, - font_weight: FontWeight::default(), + font_weight: FontWeight::new(900), letter_spacing: None, line_height: None, }, text_align: TextAlign::Center, text_align_vertical: TextAlignVertical::Center, fill: Paint::Solid(SolidPaint { - color: Color(255, 255, 255, 255), // White text + color: Color(0, 0, 0, 255), // White text }), stroke: Some(Paint::Solid(SolidPaint { color: Color(0, 0, 0, 255), // Black stroke diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 5fec8808fd..8bf5f0b339 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,37 +1,16 @@ use crate::schema::{ - Color as SchemaColor, EllipseNode, FilterEffect, GradientStop, GroupNode, ImageNode, LineNode, - Node, NodeId, NodeMap, Paint, PolygonNode, RectangleNode, RectangularCornerRadius, - RegularPolygonNode, TextAlign, TextAlignVertical, TextNode, TextSpanNode, + Color as SchemaColor, EllipseNode, FilterEffect, FontWeight, GradientStop, GroupNode, + ImageNode, LineNode, Node, NodeId, NodeMap, Paint, PolygonNode, RectangleNode, + RectangularCornerRadius, RegularPolygonNode, TextAlign, TextAlignVertical, TextDecoration, + TextNode, TextSpanNode, }; use skia_safe::{ Color, Font, FontMgr, FontStyle, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, TextBlob, Typeface, surfaces, + textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle}, }; -use std::cell::RefCell; use std::collections::HashMap; -thread_local! { - static DEFAULT_TYPEFACE: RefCell> = RefCell::new(None); -} - -fn default_typeface() -> Typeface { - DEFAULT_TYPEFACE.with(|typeface| { - let mut typeface = typeface.borrow_mut(); - if typeface.is_none() { - let font_mgr = FontMgr::new(); - *typeface = Some( - font_mgr - .legacy_make_typeface(None, FontStyle::default()) - .unwrap(), - ); - } - typeface - .as_ref() - .expect("Failed to initialize default typeface") - .clone() - }) -} - pub enum Backend { GL(*mut Surface), Raster(*mut Surface), @@ -48,13 +27,21 @@ impl Backend { pub struct Renderer { image_cache: HashMap, backend: Option, + font_mgr: FontMgr, + font_collection: FontCollection, } impl Renderer { pub fn new() -> Self { + let mut font_collection = FontCollection::new(); + let font_mgr = FontMgr::new(); + font_collection.set_default_font_manager(font_mgr.clone(), None); + Self { image_cache: HashMap::new(), backend: None, + font_collection, + font_mgr, } } @@ -72,6 +59,10 @@ impl Renderer { self.image_cache.insert(src, image); } + pub fn add_font(&mut self, bytes: &[u8]) { + self.font_mgr.new_from_data(bytes, None); + } + pub fn draw_rect(&self, x: f32, y: f32, w: f32, h: f32, r: f32, g: f32, b: f32, a: f32) { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; @@ -306,61 +297,103 @@ impl Renderer { let surface = unsafe { &mut *backend.get_surface() }; let canvas = surface.canvas(); - // Create font with the specified size - let font = Font::from_typeface(default_typeface(), node.text_style.font_size); - - // Create text blob - let blob = TextBlob::from_str(&node.text, &font).unwrap(); - - // Calculate text position based on alignment - let (x, y) = match (node.text_align, node.text_align_vertical) { - (TextAlign::Left, TextAlignVertical::Top) => (0.0, node.text_style.font_size), - (TextAlign::Left, TextAlignVertical::Center) => (0.0, node.size.height / 2.0), - (TextAlign::Left, TextAlignVertical::Bottom) => (0.0, node.size.height), - (TextAlign::Center, TextAlignVertical::Top) => { - (node.size.width / 2.0, node.text_style.font_size) - } - (TextAlign::Center, TextAlignVertical::Center) => { - (node.size.width / 2.0, node.size.height / 2.0) - } - (TextAlign::Center, TextAlignVertical::Bottom) => { - (node.size.width / 2.0, node.size.height) - } - (TextAlign::Right, TextAlignVertical::Top) => { - (node.size.width, node.text_style.font_size) - } - (TextAlign::Right, TextAlignVertical::Center) => { - (node.size.width, node.size.height / 2.0) - } - (TextAlign::Right, TextAlignVertical::Bottom) => { - (node.size.width, node.size.height) - } - (TextAlign::Justify, _) => (0.0, node.text_style.font_size), // Justify not supported yet - }; - - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - - // Draw stroke if specified - if let (Some(stroke), Some(stroke_width)) = (&node.stroke, node.stroke_width) { - let mut stroke_paint = - sk_paint(stroke, node.opacity, (node.size.width, node.size.height)); - stroke_paint.set_style(skia_safe::paint::Style::Stroke); - stroke_paint.set_stroke_width(stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_text_blob(&blob, (x, y), &stroke_paint); - } - - // Draw fill + // paints let mut fill_paint = sk_paint( &node.fill, node.opacity, (node.size.width, node.size.height), ); fill_paint.set_blend_mode(node.base.blend_mode.into()); - canvas.draw_text_blob(&blob, (x, y), &fill_paint); + // font + let mut font_collection = FontCollection::new(); + font_collection.set_default_font_manager(FontMgr::new(), None); + + // paragraph + let mut paragraph_style = ParagraphStyle::new(); + paragraph_style.set_text_direction(skia_safe::textlayout::TextDirection::LTR); + paragraph_style.set_text_align(node.text_align.into()); + let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); + + // text style + let mut ts = TextStyle::new(); + ts.set_foreground_paint(&fill_paint); + ts.set_font_size(node.text_style.font_size); + if let Some(letter_spacing) = node.text_style.letter_spacing { + ts.set_letter_spacing(letter_spacing); + } + if let Some(line_height) = node.text_style.line_height { + ts.set_height(line_height); + } + let mut decoration = skia_safe::textlayout::Decoration::default(); + decoration.ty = node.text_style.text_decoration.into(); + ts.set_decoration(&decoration); + ts.set_font_families(&[&node.text_style.font_family]); + + let font_style = skia_safe::FontStyle::new( + skia_safe::font_style::Weight::from(node.text_style.font_weight.value()), + skia_safe::font_style::Width::NORMAL, + skia_safe::font_style::Slant::Upright, + ); + ts.set_font_style(font_style); + + // paragraph builder + paragraph_builder.push_style(&ts); + paragraph_builder.add_text(&node.text); + let mut paragraph = paragraph_builder.build(); + paragraph_builder.pop(); + paragraph.layout(node.size.width); + + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + paragraph.paint(canvas, Point::new(node.transform.x(), node.transform.y())); canvas.restore(); + return; + + // // Create paragraph style + // let mut paragraph_style = skia_safe::textlayout::ParagraphStyle::new(); + + // Create text style + // let mut text_style = skia_safe::textlayout::TextStyle::new(); + + // // Create paragraph builder + // let mut paragraph_builder = skia_safe::textlayout::ParagraphBuilder::new( + // ¶graph_style, + // skia_safe::textlayout::FontCollection::new(), + // ); + + // // Add text with style + // paragraph_builder.push_style(&text_style); + // paragraph_builder.pop(); + + // // Build paragraph + // let mut paragraph = paragraph_builder.build(); + + // // Calculate vertical position based on alignment + // let y = match node.text_align_vertical { + // TextAlignVertical::Top => 0.0, + // TextAlignVertical::Center => (node.size.height - paragraph.height()) / 2.0, + // TextAlignVertical::Bottom => node.size.height - paragraph.height(), + // }; + + // // Draw stroke if specified + // if let (Some(stroke), Some(stroke_width)) = (&node.stroke, node.stroke_width) { + // let mut stroke_paint = + // sk_paint(stroke, node.opacity, (node.size.width, node.size.height)); + // stroke_paint.set_style(skia_safe::paint::Style::Stroke); + // stroke_paint.set_stroke_width(stroke_width); + // stroke_paint.set_blend_mode(node.base.blend_mode.into()); + // paragraph.paint(canvas, skia_safe::Point::new(0.0, y)); + // } + + // // Draw fill + // let mut fill_paint = sk_paint( + // &node.fill, + // node.opacity, + // (node.size.width, node.size.height), + // ); + // fill_paint.set_blend_mode(node.base.blend_mode.into()); + // paragraph.paint(canvas, skia_safe::Point::new(0.0, y)); } } diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index 58aac40d4c..23e64ed1c8 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -249,7 +249,7 @@ mod tests { #[test] fn parse_canvas_json() { - let data = fs::read_to_string("canvas.json").expect("failed to read file"); + let data = fs::read_to_string("resources/document.json").expect("failed to read file"); let parsed: CanvasFile = serde_json::from_str(&data).expect("failed to parse JSON"); assert_eq!(parsed.version, "0.0.1-beta.1+20250303"); diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 0cb70a81ea..9a24a5b071 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -128,6 +128,21 @@ pub enum TextDecoration { None, #[serde(rename = "underline")] Underline, + #[serde(rename = "overline")] + Overline, + #[serde(rename = "line-through")] + LineThrough, +} + +impl From for skia_safe::textlayout::TextDecoration { + fn from(mode: TextDecoration) -> Self { + match mode { + TextDecoration::None => skia_safe::textlayout::TextDecoration::NO_DECORATION, + TextDecoration::Underline => skia_safe::textlayout::TextDecoration::UNDERLINE, + TextDecoration::Overline => skia_safe::textlayout::TextDecoration::OVERLINE, + TextDecoration::LineThrough => skia_safe::textlayout::TextDecoration::LINE_THROUGH, + } + } } /// Supported horizontal text alignment. @@ -148,6 +163,18 @@ pub enum TextAlign { Justify, } +impl From for skia_safe::textlayout::TextAlign { + fn from(mode: TextAlign) -> Self { + use skia_safe::textlayout::TextAlign::*; + match mode { + TextAlign::Left => Left, + TextAlign::Right => Right, + TextAlign::Center => Center, + TextAlign::Justify => Justify, + } + } +} + /// Supported vertical alignment values for text. /// /// In CSS, this maps to `align-content`. @@ -170,7 +197,7 @@ pub enum TextAlignVertical { /// - [Flutter](https://api.flutter.dev/flutter/dart-ui/FontWeight-class.html) /// - [OpenType spec](https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass) #[derive(Debug, Clone, Copy, Deserialize)] -pub struct FontWeight(pub u16); +pub struct FontWeight(pub i32); impl FontWeight { /// Creates a new font weight value. @@ -182,7 +209,7 @@ impl FontWeight { /// # Panics /// /// Panics if the value is not between 1 and 1000. - pub fn new(value: u16) -> Self { + pub fn new(value: i32) -> Self { assert!( value >= 1 && value <= 1000, "Font weight must be between 1 and 1000" @@ -191,7 +218,7 @@ impl FontWeight { } /// Returns the font weight value. - pub fn value(&self) -> u16 { + pub fn value(&self) -> i32 { self.0 } @@ -207,7 +234,7 @@ pub struct TextStyle { pub text_decoration: TextDecoration, /// Optional font family name (e.g. "Roboto"). - pub font_family: Option, + pub font_family: String, /// Font size in logical pixels. pub font_size: f32, diff --git a/crates/cg/src/transform.rs b/crates/cg/src/transform.rs index e2cafbbecd..8af0af00ea 100644 --- a/crates/cg/src/transform.rs +++ b/crates/cg/src/transform.rs @@ -43,6 +43,13 @@ impl AffineTransform { Self::translate(tx, ty).compose(&Self::rotate(rotation)) } + pub fn x(&self) -> f32 { + self.matrix[0][2] + } + pub fn y(&self) -> f32 { + self.matrix[1][2] + } + /// Composes this transform with another. /// /// This is equivalent to applying `other` after `self`. From 11b28779abf211fc2a86ccf57c69751dc85f1fb6 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 18:13:54 +0900 Subject: [PATCH 034/262] blendmode --- crates/cg/benches/render_bench.rs | 12 +-- crates/cg/src/dev.rs | 31 ++++-- crates/cg/src/draw.rs | 24 ++--- crates/cg/src/io.rs | 154 +++++++++++++++++++++++++++++- crates/cg/src/schema.rs | 15 ++- 5 files changed, 204 insertions(+), 32 deletions(-) diff --git a/crates/cg/benches/render_bench.rs b/crates/cg/benches/render_bench.rs index f218f42c62..aca758a8ea 100644 --- a/crates/cg/benches/render_bench.rs +++ b/crates/cg/benches/render_bench.rs @@ -17,11 +17,11 @@ fn create_rectangles(count: usize, with_effects: bool) -> (NodeMap, Vec) id: id.clone(), name: format!("Rectangle {}", i), active: true, - blend_mode: if i % 2 == 0 { - BlendMode::Normal - } else { - BlendMode::Multiply - }, + }, + blend_mode: if i % 2 == 0 { + BlendMode::Normal + } else { + BlendMode::Multiply }, transform: AffineTransform::new( (i % 100) as f32 * 10.0, // x position @@ -69,8 +69,8 @@ fn create_rectangles(count: usize, with_effects: bool) -> (NodeMap, Vec) id: "root".to_string(), name: "Root Group".to_string(), active: true, - blend_mode: BlendMode::Normal, }, + blend_mode: BlendMode::Normal, transform: AffineTransform::identity(), children: ids.clone(), opacity: 1.0, diff --git a/crates/cg/src/dev.rs b/crates/cg/src/dev.rs index 3a6037de10..5ca254b42c 100644 --- a/crates/cg/src/dev.rs +++ b/crates/cg/src/dev.rs @@ -1,4 +1,5 @@ use cg::draw::{Backend, Renderer}; +use cg::io::parse; use cg::schema::FeDropShadow; use cg::schema::FilterEffect; use cg::schema::{ @@ -232,6 +233,16 @@ impl ApplicationHandler for App { } } +async fn demo_json() { + let file: String = fs::read_to_string("resources/document.json").expect("failed to read file"); + let canvas_file = parse(&file).expect("failed to parse file"); + println!("{:?}", canvas_file); +} + +async fn demo_static() { + // +} + #[tokio::main] async fn main() { let width = 800; @@ -294,8 +305,8 @@ async fn main() { id: "background_rect".to_string(), name: "Background Rect".to_string(), active: true, - blend_mode: BlendMode::Normal, }, + blend_mode: BlendMode::Normal, opacity: 1.0, transform: AffineTransform::identity(), size: Size { @@ -333,8 +344,8 @@ async fn main() { id: "test_image".to_string(), name: "Test Image".to_string(), active: true, - blend_mode: BlendMode::Normal, }, + blend_mode: BlendMode::Normal, transform: AffineTransform::new(50.0, 50.0, 0.0), size: Size { width: 200.0, @@ -364,8 +375,8 @@ async fn main() { id: "test_rect".to_string(), name: "Test Rectangle".to_string(), active: true, - blend_mode: BlendMode::Normal, }, + blend_mode: BlendMode::Normal, opacity: 1.0, transform: AffineTransform::new(50.0, 300.0, 45.0), size: Size { @@ -394,8 +405,8 @@ async fn main() { id: "test_ellipse".to_string(), name: "Test Ellipse".to_string(), active: true, - blend_mode: BlendMode::Multiply, // Example of using a different blend mode }, + blend_mode: BlendMode::Multiply, // Example of using a different blend mode opacity: 1.0, transform: AffineTransform::new(300.0, 300.0, 45.0), // Rotated 45 degrees size: Size { @@ -441,8 +452,8 @@ async fn main() { id: "test_polygon".to_string(), name: "Test Polygon".to_string(), active: true, - blend_mode: BlendMode::Screen, // Example of using Screen blend mode }, + blend_mode: BlendMode::Screen, // Example of using Screen blend mode transform: AffineTransform::identity(), points: pentagon_points, fill: Paint::Solid(SolidPaint { @@ -461,8 +472,8 @@ async fn main() { id: "test_regular_polygon".to_string(), name: "Test Regular Polygon".to_string(), active: true, - blend_mode: BlendMode::Overlay, // Example of using Overlay blend mode }, + blend_mode: BlendMode::Overlay, // Example of using Overlay blend mode transform: AffineTransform::new(300.0, 300.0, 0.0), size: Size { width: 200.0, @@ -485,8 +496,8 @@ async fn main() { id: "test_text".to_string(), name: "Test Text".to_string(), active: true, - blend_mode: BlendMode::Normal, }, + blend_mode: BlendMode::Normal, transform: AffineTransform::new(50.0, 50.0, 15.0), size: Size { width: 300.0, @@ -520,8 +531,8 @@ async fn main() { id: "test_line".to_string(), name: "Test Line".to_string(), active: true, - blend_mode: BlendMode::Normal, }, + blend_mode: BlendMode::Normal, opacity: 0.8, transform: AffineTransform::new(0.0, height as f32 - 50.0, 0.0), size: Size { @@ -540,8 +551,8 @@ async fn main() { id: "shapes_group".to_string(), name: "Shapes Group".to_string(), active: true, - blend_mode: BlendMode::Normal, }, + blend_mode: BlendMode::Normal, transform: AffineTransform::new(0.0, 0.0, -15.0), children: vec![ "test_rect".to_string(), @@ -558,8 +569,8 @@ async fn main() { id: "root_group".to_string(), name: "Root Group".to_string(), active: true, - blend_mode: BlendMode::Normal, }, + blend_mode: BlendMode::Normal, transform: AffineTransform::new(0.0, 0.0, 0.0), children: vec![ "background_rect".to_string(), diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 8bf5f0b339..c931d09898 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -141,7 +141,7 @@ impl Renderer { ], ); let mut fill_paint = paint.clone(); - fill_paint.set_blend_mode(node.base.blend_mode.into()); + fill_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_rrect(rrect, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { @@ -152,12 +152,12 @@ impl Renderer { ); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); + stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_rrect(rrect, &stroke_paint); } } else { let mut fill_paint = paint.clone(); - fill_paint.set_blend_mode(node.base.blend_mode.into()); + fill_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_rect(rect, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { @@ -168,7 +168,7 @@ impl Renderer { ); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); + stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_rect(rect, &stroke_paint); } } @@ -214,7 +214,7 @@ impl Renderer { canvas.concat(&sk_matrix(node.transform.matrix)); // Draw fill let mut fill_paint = fill_paint.clone(); - fill_paint.set_blend_mode(node.base.blend_mode.into()); + fill_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_oval(rect, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { @@ -225,7 +225,7 @@ impl Renderer { ); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); + stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_oval(rect, &stroke_paint); } canvas.restore(); @@ -239,7 +239,7 @@ impl Renderer { let mut paint = sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); paint.set_stroke(true); paint.set_stroke_width(node.stroke_width); - paint.set_blend_mode(node.base.blend_mode.into()); + paint.set_blend_mode(node.blend_mode.into()); canvas.save(); canvas.concat(&sk_matrix(node.transform.matrix)); canvas.draw_line( @@ -273,14 +273,14 @@ impl Renderer { canvas.concat(&sk_matrix(node.transform.matrix)); // Draw fill let mut fill_paint = fill_paint.clone(); - fill_paint.set_blend_mode(node.base.blend_mode.into()); + fill_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); + stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &stroke_paint); } canvas.restore(); @@ -303,7 +303,7 @@ impl Renderer { node.opacity, (node.size.width, node.size.height), ); - fill_paint.set_blend_mode(node.base.blend_mode.into()); + fill_paint.set_blend_mode(node.blend_mode.into()); // font let mut font_collection = FontCollection::new(); @@ -430,7 +430,7 @@ impl Renderer { // Draw the image let mut paint = SkiaPaint::default(); paint.set_anti_alias(true); - paint.set_blend_mode(node.base.blend_mode.into()); + paint.set_blend_mode(node.blend_mode.into()); paint.set_alpha((node.opacity * 255.0) as u8); let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); @@ -464,7 +464,7 @@ impl Renderer { ); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.base.blend_mode.into()); + stroke_paint.set_blend_mode(node.blend_mode.into()); if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { let rrect = RRect::new_rect_radii( diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index 23e64ed1c8..d8f33c4232 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -1,5 +1,12 @@ -use crate::schema::{FontWeight, TextAlign, TextAlignVertical, TextDecoration}; +use crate::schema::{ + BaseNode, BlendMode, Color as SchemaColor, ContainerNode as SchemaContainerNode, + EllipseNode as SchemaEllipseNode, FontWeight, GroupNode, Node as SchemaNode, NodeId, Paint, + PolygonNode, RectangleNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, + TextAlignVertical, TextDecoration, TextSpanNode, TextStyle, +}; +use crate::transform::AffineTransform; use serde::Deserialize; +use serde_json::Value; use std::collections::HashMap; #[derive(Debug, Deserialize)] @@ -242,6 +249,151 @@ pub fn parse(file: &str) -> Result { serde_json::from_str(file) } +impl From for SchemaColor { + fn from(color: Color) -> Self { + SchemaColor(color.r, color.g, color.b, (color.a * 255.0) as u8) + } +} + +impl From> for Paint { + fn from(fill: Option) -> Self { + match fill { + Some(fill) => match fill.kind.as_str() { + "solid" => { + if let Some(color) = fill.color { + Paint::Solid(SolidPaint { + color: SchemaColor(color.r, color.g, color.b, (color.a * 255.0) as u8), + }) + } else { + Paint::Solid(SolidPaint { + color: SchemaColor(0, 0, 0, 0), + }) + } + } + _ => Paint::Solid(SolidPaint { + color: SchemaColor(0, 0, 0, 0), + }), + }, + None => Paint::Solid(SolidPaint { + color: SchemaColor(0, 0, 0, 0), + }), + } + } +} + +impl From for SchemaContainerNode { + fn from(node: ContainerNode) -> Self { + let width = match node.width { + Value::Number(n) => n.as_f64().unwrap_or(0.0) as f32, + _ => 0.0, + }; + let height = match node.height { + Value::Number(n) => n.as_f64().unwrap_or(0.0) as f32, + _ => 0.0, + }; + SchemaContainerNode { + base: BaseNode { + id: node.id, + name: node.name, + active: node.active, + }, + blend_mode: BlendMode::Normal, + transform: AffineTransform::new(node.left, node.top, node.rotation), + size: Size { width, height }, + children: node.children, + opacity: node.opacity, + } + } +} + +impl From for TextSpanNode { + fn from(node: TextNode) -> Self { + let width = match node.width { + Value::Number(n) => n.as_f64().unwrap_or(0.0) as f32, + _ => 0.0, + }; + let height = match node.height { + Value::Number(n) => n.as_f64().unwrap_or(0.0) as f32, + _ => 0.0, + }; + TextSpanNode { + base: BaseNode { + id: node.id, + name: node.name, + active: node.active, + }, + blend_mode: BlendMode::Normal, + transform: AffineTransform::new(node.left, node.top, node.rotation), + size: Size { width, height }, + text: node.text, + text_style: TextStyle { + text_decoration: node.text_decoration, + font_family: node.font_family.unwrap_or_else(|| "Inter".to_string()), + font_size: node.font_size.unwrap_or(14.0), + font_weight: node.font_weight, + letter_spacing: node.letter_spacing, + line_height: node.line_height, + }, + text_align: node.text_align, + text_align_vertical: node.text_align_vertical, + fill: node.fill.into(), + stroke: None, + stroke_width: None, + opacity: node.opacity, + } + } +} + +impl From for SchemaNode { + fn from(node: EllipseNode) -> Self { + let transform = AffineTransform::new(node.left, node.top, node.rotation); + + SchemaNode::Ellipse(SchemaEllipseNode { + base: BaseNode { + id: node.id, + name: node.name, + active: node.active, + }, + blend_mode: BlendMode::Normal, + transform, + size: Size { + width: node.width, + height: node.height, + }, + fill: node.fill.into(), + stroke: Paint::Solid(SolidPaint { + color: SchemaColor(0, 0, 0, 255), + }), + stroke_width: node.stroke_width.unwrap_or(0.0), + opacity: node.opacity, + }) + } +} + +impl From for SchemaNode { + fn from(node: VectorNode) -> Self { + let transform = AffineTransform::new(node.left, node.top, node.rotation); + + // For vector nodes, we'll create a polygon node with the path data + SchemaNode::Polygon(PolygonNode { + base: BaseNode { + id: node.id, + name: node.name, + active: node.active, + }, + blend_mode: BlendMode::Normal, + transform, + points: vec![], + fill: node.fill.into(), + stroke: Paint::Solid(SolidPaint { + color: SchemaColor(0, 0, 0, 255), + }), + stroke_width: 0.0, + opacity: node.opacity, + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 9a24a5b071..788d000c02 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -314,8 +314,7 @@ impl RectangularCornerRadius { // region: Scene #[derive(Debug, Clone)] -pub struct SceneNode { - pub base: BaseNode, +pub struct Scene { pub transform: AffineTransform, pub children: Vec, } @@ -341,7 +340,6 @@ pub struct BaseNode { pub id: NodeId, pub name: String, pub active: bool, - pub blend_mode: BlendMode, } #[derive(Debug, Clone)] @@ -350,6 +348,7 @@ pub struct GroupNode { pub transform: AffineTransform, pub children: Vec, pub opacity: f32, + pub blend_mode: BlendMode, } #[derive(Debug, Clone)] @@ -359,6 +358,7 @@ pub struct ContainerNode { pub size: Size, pub children: Vec, pub opacity: f32, + pub blend_mode: BlendMode, } #[derive(Debug, Clone)] @@ -369,6 +369,7 @@ pub struct LineNode { pub stroke: Paint, pub stroke_width: f32, pub opacity: f32, + pub blend_mode: BlendMode, } #[derive(Debug, Clone)] @@ -381,6 +382,7 @@ pub struct RectangleNode { pub stroke: Paint, pub stroke_width: f32, pub opacity: f32, + pub blend_mode: BlendMode, pub effect: Option, } @@ -394,6 +396,7 @@ pub struct ImageNode { pub stroke: Paint, pub stroke_width: f32, pub opacity: f32, + pub blend_mode: BlendMode, pub effect: Option, pub _ref: String, } @@ -407,6 +410,7 @@ pub struct EllipseNode { pub stroke: Paint, pub stroke_width: f32, pub opacity: f32, + pub blend_mode: BlendMode, } /// A polygon shape defined by a list of absolute 2D points, following the SVG `` model. @@ -440,6 +444,7 @@ pub struct PolygonNode { /// Opacity applied to the polygon shape (`0.0` - transparent, `1.0` - opaque). pub opacity: f32, + pub blend_mode: BlendMode, } /// A node representing a regular polygon (triangle, square, pentagon, etc.) @@ -477,6 +482,7 @@ pub struct RegularPolygonNode { /// Overall node opacity (0.0–1.0) pub opacity: f32, + pub blend_mode: BlendMode, } impl RegularPolygonNode { @@ -508,6 +514,7 @@ impl RegularPolygonNode { stroke: self.stroke.clone(), stroke_width: self.stroke_width, opacity: self.opacity, + blend_mode: self.blend_mode, } } } @@ -548,6 +555,7 @@ pub struct TextSpanNode { /// Overall node opacity. pub opacity: f32, + pub blend_mode: BlendMode, } #[derive(Debug, Clone)] @@ -560,6 +568,7 @@ pub struct TextNode { pub font_size: f32, pub fill: Paint, pub opacity: f32, + pub blend_mode: BlendMode, } // endregion From 8ece99c452285b78508624a6acad538eafefefa0 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 19:05:06 +0900 Subject: [PATCH 035/262] container node --- crates/cg/src/dev.rs | 157 +++++++++++++++++++++++++--------------- crates/cg/src/draw.rs | 133 +++++++++++++++++++++++++++++++++- crates/cg/src/io.rs | 72 +++++++++++++++++- crates/cg/src/schema.rs | 38 ++++++++-- 4 files changed, 329 insertions(+), 71 deletions(-) diff --git a/crates/cg/src/dev.rs b/crates/cg/src/dev.rs index 5ca254b42c..781ce52d3a 100644 --- a/crates/cg/src/dev.rs +++ b/crates/cg/src/dev.rs @@ -3,9 +3,9 @@ use cg::io::parse; use cg::schema::FeDropShadow; use cg::schema::FilterEffect; use cg::schema::{ - BaseNode, BlendMode, Color, EllipseNode, FontWeight, GradientStop, GroupNode, ImageNode, - LineNode, Node, NodeMap, Paint, PolygonNode, RadialGradientPaint, RectangleNode, - RectangularCornerRadius, Size, SolidPaint, TextAlign, TextAlignVertical, TextDecoration, + BaseNode, BlendMode, Color, ContainerNode, EllipseNode, FontWeight, GradientStop, GroupNode, + ImageNode, LineNode, Node, NodeMap, Paint, PolygonNode, RadialGradientPaint, RectangleNode, + RectangularCornerRadius, Scene, Size, SolidPaint, TextAlign, TextAlignVertical, TextDecoration, TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; @@ -58,7 +58,7 @@ fn init_window( let el = EventLoop::new().expect("Failed to create event loop"); let window_attributes = WindowAttributes::default() .with_title("Grida Canvas") - .with_inner_size(LogicalSize::new(400, 300)); + .with_inner_size(LogicalSize::new(1080, 1080)); // Create GL config template let template = ConfigTemplateBuilder::new() @@ -205,7 +205,6 @@ struct App { surface_ptr: *mut Surface, gl_surface: GlutinSurface, gl_context: PossiblyCurrentContext, - nodemap: NodeMap, } impl ApplicationHandler for App { @@ -233,52 +232,34 @@ impl ApplicationHandler for App { } } -async fn demo_json() { - let file: String = fs::read_to_string("resources/document.json").expect("failed to read file"); +async fn demo_json() -> Scene { + let path = "resources/document-2.json"; + // let path = "resources/document.json"; + // let path = "resources/hero-main-demo.grida"; + let file: String = fs::read_to_string(path).expect("failed to read file"); let canvas_file = parse(&file).expect("failed to parse file"); - println!("{:?}", canvas_file); -} - -async fn demo_static() { - // -} - -#[tokio::main] -async fn main() { - let width = 800; - let height = 600; - - // Initialize the renderer with image cache - let mut renderer = Renderer::new(); - let ( - surface_ptr, - el, - window, - gl_surface, - gl_context, - _gl_config, - _fb_info, - _gr_context, - scale_factor, - ) = init_window(width, height); - renderer.set_backend(Backend::GL(surface_ptr)); - - // Log DPI and size info - let logical_size = window.inner_size(); - let physical_width = (logical_size.width as f64 * scale_factor).round() as u32; - let physical_height = (logical_size.height as f64 * scale_factor).round() as u32; - println!("[DPI DEBUG] scale_factor: {}", scale_factor); - println!( - "[DPI DEBUG] logical_size: {} x {}", - logical_size.width, logical_size.height - ); - println!( - "[DPI DEBUG] physical_size: {} x {}", - physical_width, physical_height + 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(), ); - // Get logical canvas size for background - // let logical_size = window.inner_size(); + 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(), + } +} +async fn demo_static(renderer: &mut Renderer) -> Scene { let font_caveat_path: &str = "resources/Caveat-VariableFont_wght.ttf"; let font_caveat_data = fs::read(font_caveat_path).expect("failed to read file"); let font_caveat_family = "Caveat".to_string(); @@ -534,9 +515,9 @@ async fn main() { }, blend_mode: BlendMode::Normal, opacity: 0.8, - transform: AffineTransform::new(0.0, height as f32 - 50.0, 0.0), + transform: AffineTransform::new(0.0, 700.0, 0.0), size: Size { - width: width as f32, + width: 800.0, height: 0.0, // ignored }, stroke: Paint::Solid(SolidPaint { @@ -564,14 +545,14 @@ async fn main() { }; // Create a root group node containing the shapes group, text, and line - let root_group_node = GroupNode { + let root_container_node = ContainerNode { base: BaseNode { - id: "root_group".to_string(), - name: "Root Group".to_string(), + id: "root_container".to_string(), + name: "Root Container".to_string(), active: true, }, blend_mode: BlendMode::Normal, - transform: AffineTransform::new(0.0, 0.0, 0.0), + transform: AffineTransform::identity(), children: vec![ "background_rect".to_string(), "shapes_group".to_string(), @@ -580,6 +561,17 @@ async fn main() { "test_image".to_string(), ], opacity: 1.0, + size: Size { + width: 1080.0, + height: 1080.0, + }, + corner_radius: RectangularCornerRadius::all(0.0), + fill: Paint::Solid(SolidPaint { + color: Color(255, 255, 255, 255), + }), + stroke: None, + stroke_width: 0.0, + effect: None, }; // Create a node map and add all nodes @@ -599,14 +591,64 @@ async fn main() { nodemap.insert("test_text".to_string(), Node::TextSpan(text_span_node)); nodemap.insert("test_line".to_string(), Node::Line(line_node)); nodemap.insert("test_image".to_string(), Node::Image(image_node)); - nodemap.insert("root_group".to_string(), Node::Group(root_group_node)); + nodemap.insert( + "root_container".to_string(), + Node::Container(root_container_node), + ); + + Scene { + id: "scene".to_string(), + name: "Demo".to_string(), + transform: AffineTransform::identity(), + children: vec!["root_container".to_string()], + nodes: nodemap, + } +} + +#[tokio::main] +async fn main() { + let width = 1080; + let height = 1080; + + // Initialize the renderer with image cache + let mut renderer = Renderer::new(); + let ( + surface_ptr, + el, + window, + gl_surface, + gl_context, + _gl_config, + _fb_info, + _gr_context, + scale_factor, + ) = init_window(width, height); + renderer.set_backend(Backend::GL(surface_ptr)); + + // Log DPI and size info + let logical_size = window.inner_size(); + let physical_width = (logical_size.width as f64 * scale_factor).round() as u32; + let physical_height = (logical_size.height as f64 * scale_factor).round() as u32; + println!("[DPI DEBUG] scale_factor: {}", scale_factor); + println!( + "[DPI DEBUG] logical_size: {} x {}", + logical_size.width, logical_size.height + ); + println!( + "[DPI DEBUG] physical_size: {} x {}", + physical_width, physical_height + ); + // Get logical canvas size for background + // let logical_size = window.inner_size(); + + // let scene = demo_static(&mut renderer).await; + let scene = demo_json().await; let mut app = App { renderer, surface_ptr, gl_surface, gl_context, - nodemap, }; // Render once at startup @@ -614,8 +656,7 @@ async fn main() { let canvas = surface.canvas(); canvas.clear(skia_safe::Color::WHITE); - app.renderer - .render_node(&"root_group".to_string(), &app.nodemap); + app.renderer.render_scene(&scene); app.renderer.flush(); if let Err(e) = app.gl_surface.swap_buffers(&app.gl_context) { eprintln!("Error swapping buffers: {:?}", e); diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index c931d09898..4626ef06bd 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,8 +1,8 @@ use crate::schema::{ - Color as SchemaColor, EllipseNode, FilterEffect, FontWeight, GradientStop, GroupNode, - ImageNode, LineNode, Node, NodeId, NodeMap, Paint, PolygonNode, RectangleNode, - RectangularCornerRadius, RegularPolygonNode, TextAlign, TextAlignVertical, TextDecoration, - TextNode, TextSpanNode, + Color as SchemaColor, ContainerNode, EllipseNode, FilterEffect, FontWeight, GradientStop, + GroupNode, ImageNode, LineNode, Node, NodeId, NodeMap, Paint, PolygonNode, RectangleNode, + RectangularCornerRadius, RegularPolygonNode, Scene, TextAlign, TextAlignVertical, + TextDecoration, TextNode, TextSpanNode, }; use skia_safe::{ Color, Font, FontMgr, FontStyle, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, @@ -509,6 +509,12 @@ impl Renderer { } } + pub fn render_scene(&self, scene: &Scene) { + for child_id in &scene.children { + self.render_node(child_id, &scene.nodes); + } + } + pub fn render_node(&self, id: &NodeId, nodemap: &NodeMap) { let node = match nodemap.get(id) { Some(node) => node, @@ -517,6 +523,7 @@ impl Renderer { match node { Node::Group(node) => self.draw_group_node(node, nodemap), + Node::Container(node) => self.draw_container_node(node, nodemap), Node::Rectangle(node) => self.draw_rect_node(node), Node::Ellipse(node) => self.draw_ellipse_node(node), Node::Polygon(node) => self.draw_polygon_node(node), @@ -557,6 +564,124 @@ impl Renderer { canvas.restore(); } } + + pub fn draw_container_node(&self, node: &ContainerNode, nodemap: &NodeMap) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + + // Save canvas state for transform + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + + let needs_opacity_layer = node.opacity < 1.0; + + if needs_opacity_layer { + // Start new layer with opacity + canvas.save_layer_alpha(None, (node.opacity * 255.0) as u32); + } + + // Draw the background rectangle + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; + + // Draw drop shadow effect if present + if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { + let mut shadow_paint = SkiaPaint::default(); + let SchemaColor(r, g, b, a) = shadow.color; + shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); + shadow_paint.set_anti_alias(true); + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + skia_safe::BlurStyle::Normal, + shadow.blur, + None, + )); + } + let offset_x = shadow.dx; + let offset_y = shadow.dy; + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + let mut shadow_rrect = rrect; + shadow_rrect.offset((offset_x, offset_y)); + canvas.draw_rrect(shadow_rrect, &shadow_paint); + } else { + let mut shadow_rect = rect; + shadow_rect.offset((offset_x, offset_y)); + canvas.draw_rect(shadow_rect, &shadow_paint); + } + } + + // Draw fill + let fill_paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + let mut fill_paint = fill_paint.clone(); + fill_paint.set_blend_mode(node.blend_mode.into()); + + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + canvas.draw_rrect(rrect, &fill_paint); + } else { + canvas.draw_rect(rect, &fill_paint); + } + + // Draw stroke if present + if let Some(stroke) = &node.stroke { + let mut stroke_paint = + sk_paint(stroke, node.opacity, (node.size.width, node.size.height)); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.blend_mode.into()); + + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), // top-left + Point::new(tr, tr), // top-right + Point::new(br, br), // bottom-right + Point::new(bl, bl), // bottom-left + ], + ); + canvas.draw_rrect(rrect, &stroke_paint); + } else { + canvas.draw_rect(rect, &stroke_paint); + } + } + + // Recursively render children + for child_id in &node.children { + self.render_node(child_id, nodemap); + } + + if needs_opacity_layer { + // End opacity layer + canvas.restore(); + } + + // Restore transform + canvas.restore(); + } + } } fn sk_matrix(m: [[f32; 3]; 2]) -> skia_safe::Matrix { diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index d8f33c4232..9038ccd7b9 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -76,8 +76,11 @@ pub struct ContainerNode { pub fill: Option, pub border: Option, pub style: Option>, - #[serde(rename = "cornerRadius")] - pub corner_radius: Option, + #[serde( + rename = "cornerRadius", + deserialize_with = "deserialize_corner_radius" + )] + pub corner_radius: Option, pub padding: Option, pub layout: Option, pub direction: Option, @@ -91,6 +94,42 @@ pub struct ContainerNode { pub cross_axis_gap: Option, } +fn deserialize_corner_radius<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + + match value { + None => Ok(None), + Some(v) => match v { + serde_json::Value::Number(n) => { + let radius = n.as_f64().unwrap_or(0.0) as f32; + Ok(Some(RectangularCornerRadius::all(radius))) + } + serde_json::Value::Array(arr) => { + if arr.len() == 4 { + let values: Vec = arr + .into_iter() + .map(|v| v.as_f64().unwrap_or(0.0) as f32) + .collect(); + Ok(Some(RectangularCornerRadius { + tl: values[0], + tr: values[1], + bl: values[2], + br: values[3], + })) + } else { + Ok(None) + } + } + _ => Ok(None), + }, + } +} + #[derive(Debug, Deserialize)] pub struct TextNode { pub id: String, @@ -300,6 +339,13 @@ impl From for SchemaContainerNode { blend_mode: BlendMode::Normal, transform: AffineTransform::new(node.left, node.top, node.rotation), size: Size { width, height }, + corner_radius: node + .corner_radius + .unwrap_or(RectangularCornerRadius::zero()), + fill: node.fill.into(), + stroke: None, + stroke_width: 0.0, + effect: None, children: node.children, opacity: node.opacity, } @@ -394,6 +440,28 @@ impl From for SchemaNode { } } +impl From for SchemaNode { + fn from(node: Node) -> Self { + match node { + Node::Container(container) => SchemaNode::Container(container.into()), + Node::Text(text) => SchemaNode::TextSpan(text.into()), + Node::Vector(vector) => vector.into(), + Node::Ellipse(ellipse) => ellipse.into(), + Node::Unknown => SchemaNode::Group(GroupNode { + base: BaseNode { + id: "unknown".to_string(), + name: "Unknown Node".to_string(), + active: false, + }, + transform: AffineTransform::identity(), + children: vec![], + opacity: 0.0, + blend_mode: BlendMode::Normal, + }), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 788d000c02..4e044a9662 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -1,4 +1,5 @@ use crate::transform::AffineTransform; +use core::str; use serde::Deserialize; use std::collections::HashMap; use std::f32::consts::PI; @@ -315,8 +316,11 @@ impl RectangularCornerRadius { // region: Scene #[derive(Debug, Clone)] pub struct Scene { + pub id: String, + pub name: String, pub transform: AffineTransform, pub children: Vec, + pub nodes: NodeMap, } // endregion @@ -326,12 +330,14 @@ pub struct Scene { #[derive(Debug, Clone)] pub enum Node { Group(GroupNode), + Container(ContainerNode), Rectangle(RectangleNode), Ellipse(EllipseNode), Polygon(PolygonNode), RegularPolygon(RegularPolygonNode), Line(LineNode), TextSpan(TextSpanNode), + Path(PathNode), Image(ImageNode), } @@ -356,34 +362,39 @@ pub struct ContainerNode { pub base: BaseNode, pub transform: AffineTransform, pub size: Size, + pub corner_radius: RectangularCornerRadius, pub children: Vec, + pub fill: Paint, + pub stroke: Option, + pub stroke_width: f32, pub opacity: f32, pub blend_mode: BlendMode, + pub effect: Option, } #[derive(Debug, Clone)] -pub struct LineNode { +pub struct RectangleNode { pub base: BaseNode, pub transform: AffineTransform, - pub size: Size, // height is always 0 (ignored) + pub size: Size, + pub corner_radius: RectangularCornerRadius, + pub fill: Paint, pub stroke: Paint, pub stroke_width: f32, pub opacity: f32, pub blend_mode: BlendMode, + pub effect: Option, } #[derive(Debug, Clone)] -pub struct RectangleNode { +pub struct LineNode { pub base: BaseNode, pub transform: AffineTransform, - pub size: Size, - pub corner_radius: RectangularCornerRadius, - pub fill: Paint, + pub size: Size, // height is always 0 (ignored) pub stroke: Paint, pub stroke_width: f32, pub opacity: f32, pub blend_mode: BlendMode, - pub effect: Option, } #[derive(Debug, Clone)] @@ -447,6 +458,19 @@ pub struct PolygonNode { pub blend_mode: BlendMode, } +/// +/// SVG Path compatible path node. +/// +#[derive(Debug, Clone)] +pub struct PathNode { + pub base: BaseNode, + pub transform: AffineTransform, + pub size: Size, + pub fill: Paint, + pub data: String, + pub stroke: Paint, +} + /// A node representing a regular polygon (triangle, square, pentagon, etc.) /// that fits inside a bounding box defined by `size`, optionally transformed. /// From 8b48a72a9ed683105b12a9c2cad180464a23b3aa Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 19:37:29 +0900 Subject: [PATCH 036/262] path node --- crates/cg/src/dev.rs | 28 ++++++++- crates/cg/src/draw.rs | 124 ++++++++++++++++++++++++---------------- crates/cg/src/io.rs | 15 +++-- crates/cg/src/schema.rs | 3 +- 4 files changed, 111 insertions(+), 59 deletions(-) diff --git a/crates/cg/src/dev.rs b/crates/cg/src/dev.rs index 781ce52d3a..24cb916dc5 100644 --- a/crates/cg/src/dev.rs +++ b/crates/cg/src/dev.rs @@ -4,9 +4,9 @@ use cg::schema::FeDropShadow; use cg::schema::FilterEffect; use cg::schema::{ BaseNode, BlendMode, Color, ContainerNode, EllipseNode, FontWeight, GradientStop, GroupNode, - ImageNode, LineNode, Node, NodeMap, Paint, PolygonNode, RadialGradientPaint, RectangleNode, - RectangularCornerRadius, Scene, Size, SolidPaint, TextAlign, TextAlignVertical, TextDecoration, - TextSpanNode, TextStyle, + ImageNode, LineNode, Node, NodeMap, Paint, PathNode, PolygonNode, RadialGradientPaint, + RectangleNode, RectangularCornerRadius, Scene, Size, SolidPaint, TextAlign, TextAlignVertical, + TextDecoration, TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; use console_error_panic_hook::set_once as init_panic_hook; @@ -506,6 +506,26 @@ async fn demo_static(renderer: &mut Renderer) -> Scene { opacity: 1.0, }; + // Create a test path node + let path_node = PathNode { + base: BaseNode { + id: "test_path".to_string(), + name: "Test Path".to_string(), + active: true, + }, + // blend_mode: BlendMode::Normal, + opacity: 1.0, + transform: AffineTransform::new(200.0, 200.0, 0.0), + fill: Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), // Black fill + }), + data: "M50 150H0v-50h50v50ZM150 150h-50v-50h50v50ZM100 100H50V50h50v50ZM50 50H0V0h50v50ZM150 50h-50V0h50v50Z".to_string(), + stroke: Paint::Solid(SolidPaint { + color: Color(255, 0, 0, 255), // Red stroke + }), + stroke_width: 4.0, + }; + // Create a test line node with solid color let line_node = LineNode { base: BaseNode { @@ -558,6 +578,7 @@ async fn demo_static(renderer: &mut Renderer) -> Scene { "shapes_group".to_string(), "test_text".to_string(), "test_line".to_string(), + "test_path".to_string(), "test_image".to_string(), ], opacity: 1.0, @@ -591,6 +612,7 @@ async fn demo_static(renderer: &mut Renderer) -> Scene { nodemap.insert("test_text".to_string(), Node::TextSpan(text_span_node)); nodemap.insert("test_line".to_string(), Node::Line(line_node)); nodemap.insert("test_image".to_string(), Node::Image(image_node)); + nodemap.insert("test_path".to_string(), Node::Path(path_node)); nodemap.insert( "root_container".to_string(), Node::Container(root_container_node), diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 4626ef06bd..5261bb8cdf 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,8 +1,8 @@ use crate::schema::{ Color as SchemaColor, ContainerNode, EllipseNode, FilterEffect, FontWeight, GradientStop, - GroupNode, ImageNode, LineNode, Node, NodeId, NodeMap, Paint, PolygonNode, RectangleNode, - RectangularCornerRadius, RegularPolygonNode, Scene, TextAlign, TextAlignVertical, - TextDecoration, TextNode, TextSpanNode, + GroupNode, ImageNode, LineNode, Node, NodeId, NodeMap, Paint, PathNode, PolygonNode, + RectangleNode, RectangularCornerRadius, RegularPolygonNode, Scene, TextAlign, + TextAlignVertical, TextDecoration, TextNode, TextSpanNode, }; use skia_safe::{ Color, Font, FontMgr, FontStyle, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, @@ -63,6 +63,54 @@ impl Renderer { self.font_mgr.new_from_data(bytes, None); } + pub fn flush(&self) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + if let Some(mut gr_context) = surface.recording_context() { + if let Some(mut direct_context) = gr_context.as_direct_context() { + direct_context.flush_and_submit(); + } + } + } + } + + pub fn free(&mut self) { + if let Some(backend) = self.backend.take() { + let surface = unsafe { Box::from_raw(backend.get_surface()) }; + if let Some(mut gr_context) = surface.recording_context() { + if let Some(mut direct_context) = gr_context.as_direct_context() { + direct_context.abandon(); + } + } + } + } + + pub fn render_scene(&self, scene: &Scene) { + for child_id in &scene.children { + self.render_node(child_id, &scene.nodes); + } + } + + pub fn render_node(&self, id: &NodeId, nodemap: &NodeMap) { + let node = match nodemap.get(id) { + Some(node) => node, + None => return, // Skip if node not found + }; + + match node { + Node::Group(node) => self.draw_group_node(node, nodemap), + Node::Container(node) => self.draw_container_node(node, nodemap), + Node::Rectangle(node) => self.draw_rect_node(node), + Node::Ellipse(node) => self.draw_ellipse_node(node), + Node::Polygon(node) => self.draw_polygon_node(node), + Node::RegularPolygon(node) => self.draw_regular_polygon_node(node), + Node::TextSpan(node) => self.draw_text_span_node(node), + Node::Line(node) => self.draw_line_node(node), + Node::Image(node) => self.draw_image_node(node), + Node::Path(node) => self.draw_path_node(node), + } + } + pub fn draw_rect(&self, x: f32, y: f32, w: f32, h: f32, r: f32, g: f32, b: f32, a: f32) { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; @@ -251,6 +299,29 @@ impl Renderer { } } + pub fn draw_path_node(&self, node: &PathNode) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + canvas.save(); + canvas.concat(&sk_matrix(node.transform.matrix)); + + let path = skia_safe::path::Path::from_svg(&node.data).expect("path is not valid"); + + let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + if node.stroke_width > 0.0 { + let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + // stroke_paint.set_blend_mode(node.blend_mode.into()); + canvas.draw_path(&path, &stroke_paint); + } + + canvas.draw_path(&path, &fill_paint); + canvas.restore(); + } + } + pub fn draw_polygon_node(&self, node: &PolygonNode) { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; @@ -487,53 +558,6 @@ impl Renderer { } } - pub fn flush(&self) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - if let Some(mut gr_context) = surface.recording_context() { - if let Some(mut direct_context) = gr_context.as_direct_context() { - direct_context.flush_and_submit(); - } - } - } - } - - pub fn free(&mut self) { - if let Some(backend) = self.backend.take() { - let surface = unsafe { Box::from_raw(backend.get_surface()) }; - if let Some(mut gr_context) = surface.recording_context() { - if let Some(mut direct_context) = gr_context.as_direct_context() { - direct_context.abandon(); - } - } - } - } - - pub fn render_scene(&self, scene: &Scene) { - for child_id in &scene.children { - self.render_node(child_id, &scene.nodes); - } - } - - pub fn render_node(&self, id: &NodeId, nodemap: &NodeMap) { - let node = match nodemap.get(id) { - Some(node) => node, - None => return, // Skip if node not found - }; - - match node { - Node::Group(node) => self.draw_group_node(node, nodemap), - Node::Container(node) => self.draw_container_node(node, nodemap), - Node::Rectangle(node) => self.draw_rect_node(node), - Node::Ellipse(node) => self.draw_ellipse_node(node), - Node::Polygon(node) => self.draw_polygon_node(node), - Node::RegularPolygon(node) => self.draw_regular_polygon_node(node), - Node::TextSpan(node) => self.draw_text_span_node(node), - Node::Line(node) => self.draw_line_node(node), - Node::Image(node) => self.draw_image_node(node), - } - } - pub fn draw_group_node(&self, node: &GroupNode, nodemap: &NodeMap) { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index 9038ccd7b9..747e31ba0e 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -1,7 +1,7 @@ use crate::schema::{ BaseNode, BlendMode, Color as SchemaColor, ContainerNode as SchemaContainerNode, EllipseNode as SchemaEllipseNode, FontWeight, GroupNode, Node as SchemaNode, NodeId, Paint, - PolygonNode, RectangleNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, + PathNode, PolygonNode, RectangleNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, TextAlignVertical, TextDecoration, TextSpanNode, TextStyle, }; use crate::transform::AffineTransform; @@ -420,17 +420,22 @@ impl From for SchemaNode { fn from(node: VectorNode) -> Self { let transform = AffineTransform::new(node.left, node.top, node.rotation); - // For vector nodes, we'll create a polygon node with the path data - SchemaNode::Polygon(PolygonNode { + // For vector nodes, we'll create a path node with the path data + SchemaNode::Path(PathNode { base: BaseNode { id: node.id, name: node.name, active: node.active, }, - blend_mode: BlendMode::Normal, transform, - points: vec![], fill: node.fill.into(), + data: node.paths.map_or("".to_string(), |paths| { + paths + .iter() + .map(|path| path.d.clone()) + .collect::>() + .join(" ") + }), stroke: Paint::Solid(SolidPaint { color: SchemaColor(0, 0, 0, 255), }), diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 4e044a9662..6159ce98fe 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -465,10 +465,11 @@ pub struct PolygonNode { pub struct PathNode { pub base: BaseNode, pub transform: AffineTransform, - pub size: Size, pub fill: Paint, pub data: String, pub stroke: Paint, + pub stroke_width: f32, + pub opacity: f32, } /// A node representing a regular polygon (triangle, square, pentagon, etc.) From 945f55ad9b014c0ca64ce7f51816b28db084c502 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 19:42:39 +0900 Subject: [PATCH 037/262] update dev cli --- Cargo.lock | 90 ++++++++++++++++++++++++++++++++++++++++++++ crates/cg/Cargo.toml | 1 + crates/cg/src/dev.rs | 46 +++++++++++++++++----- 3 files changed, 128 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76f78843cd..1b6a473863 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,12 +88,56 @@ 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" @@ -284,6 +328,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" name = "cg" version = "0.1.0" dependencies = [ + "clap", "console_error_panic_hook", "criterion", "gl", @@ -354,6 +399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -362,8 +408,22 @@ 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]] @@ -372,6 +432,12 @@ 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" @@ -1166,6 +1232,12 @@ dependencies = [ "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" @@ -1696,6 +1768,12 @@ 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" @@ -2342,6 +2420,12 @@ 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" @@ -2701,6 +2785,12 @@ 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 = "vcpkg" version = "0.2.15" diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index 6630c00338..58eb64711a 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -25,6 +25,7 @@ gl-rs = { version = "0.14.0", package = "gl" } winit = "0.30.0" serde = "1.0.219" serde_json = "1.0.140" +clap = { version = "4.5.39", features = ["derive"] } [features] default = ["wee_alloc"] diff --git a/crates/cg/src/dev.rs b/crates/cg/src/dev.rs index 24cb916dc5..02bd70ecce 100644 --- a/crates/cg/src/dev.rs +++ b/crates/cg/src/dev.rs @@ -9,6 +9,7 @@ use cg::schema::{ TextDecoration, TextSpanNode, TextStyle, }; use cg::transform::AffineTransform; +use clap::{Parser, Subcommand}; use console_error_panic_hook::set_once as init_panic_hook; use gl::types::*; use gl_rs as gl; @@ -38,6 +39,27 @@ use winit::{ window::{Window, WindowAttributes}, }; +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Load an example scene + Example { + /// Name of the example to load + name: String, + }, + /// Load a scene from a file + File { + /// Path to the file to load + path: String, + }, +} + fn init_window( _width: i32, _height: i32, @@ -232,11 +254,8 @@ impl ApplicationHandler for App { } } -async fn demo_json() -> Scene { - let path = "resources/document-2.json"; - // let path = "resources/document.json"; - // let path = "resources/hero-main-demo.grida"; - let file: String = fs::read_to_string(path).expect("failed to read file"); +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] @@ -629,6 +648,7 @@ async fn demo_static(renderer: &mut Renderer) -> Scene { #[tokio::main] async fn main() { + let cli = Cli::parse(); let width = 1080; let height = 1080; @@ -660,11 +680,19 @@ async fn main() { "[DPI DEBUG] physical_size: {} x {}", physical_width, physical_height ); - // Get logical canvas size for background - // let logical_size = window.inner_size(); - // let scene = demo_static(&mut renderer).await; - let scene = demo_json().await; + // Load the appropriate scene based on command line arguments + let scene = match cli.command { + Commands::Example { name } => match name.as_str() { + "basic" => demo_static(&mut renderer).await, + _ => { + eprintln!("Unknown example: {}", name); + eprintln!("Available examples: basic"); + std::process::exit(1); + } + }, + Commands::File { path } => load_scene_from_file(&path).await, + }; let mut app = App { renderer, From 9b4e0fb05e4dbb85d441e34c83a474540479bb64 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 20:38:19 +0900 Subject: [PATCH 038/262] init factory --- crates/cg/src/dev.rs | 413 ++++++++++++++++----------------------- crates/cg/src/factory.rs | 179 +++++++++++++++++ crates/cg/src/lib.rs | 1 + 3 files changed, 344 insertions(+), 249 deletions(-) create mode 100644 crates/cg/src/factory.rs diff --git a/crates/cg/src/dev.rs b/crates/cg/src/dev.rs index 02bd70ecce..33b20130fd 100644 --- a/crates/cg/src/dev.rs +++ b/crates/cg/src/dev.rs @@ -1,4 +1,5 @@ use cg::draw::{Backend, Renderer}; +use cg::factory::NodeFactory; use cg::io::parse; use cg::schema::FeDropShadow; use cg::schema::FilterEffect; @@ -278,7 +279,7 @@ async fn load_scene_from_file(file_path: &str) -> Scene { } } -async fn demo_static(renderer: &mut Renderer) -> Scene { +async fn demo_basic(renderer: &mut Renderer) -> Scene { let font_caveat_path: &str = "resources/Caveat-VariableFont_wght.ttf"; let font_caveat_data = fs::read(font_caveat_path).expect("failed to read file"); let font_caveat_family = "Caveat".to_string(); @@ -300,29 +301,20 @@ async fn demo_static(renderer: &mut Renderer) -> Scene { println!("Font load time: {:?}", font_load_start.elapsed()); // Add a background rectangle node - let background_rect_node = RectangleNode { - base: BaseNode { - id: "background_rect".to_string(), - name: "Background Rect".to_string(), - active: true, - }, - blend_mode: BlendMode::Normal, - opacity: 1.0, - transform: AffineTransform::identity(), - size: Size { - width: 800.0, - height: 600.0, - }, - corner_radius: RectangularCornerRadius::all(0.0), - fill: Paint::Solid(SolidPaint { - color: Color(230, 240, 255, 255), // Light blue for visibility - }), - stroke: Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 0), // No stroke - }), - stroke_width: 0.0, - effect: None, + let mut background_rect_node = NodeFactory::create_rectangle_node(); + background_rect_node.base.id = "background_rect".to_string(); + background_rect_node.base.name = "Background Rect".to_string(); + background_rect_node.size = Size { + width: 800.0, + height: 600.0, }; + background_rect_node.fill = Paint::Solid(SolidPaint { + color: Color(230, 240, 255, 255), // Light blue for visibility + }); + background_rect_node.stroke = Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 0), // No stroke + }); + background_rect_node.stroke_width = 0.0; // Preload image before timing let demo_image_id = "demo_image"; @@ -339,103 +331,74 @@ async fn demo_static(renderer: &mut Renderer) -> Scene { println!("Image load time: {:?}", image_load_start.elapsed()); // Create a test image node with URL - let image_node = ImageNode { - base: BaseNode { - id: "test_image".to_string(), - name: "Test Image".to_string(), - active: true, - }, - blend_mode: BlendMode::Normal, - transform: AffineTransform::new(50.0, 50.0, 0.0), - size: Size { - width: 200.0, - height: 200.0, - }, - corner_radius: RectangularCornerRadius::all(20.0), - fill: Paint::Solid(SolidPaint { - color: Color(255, 255, 255, 255), - }), - stroke: Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), - }), - stroke_width: 2.0, - effect: Some(FilterEffect::DropShadow(FeDropShadow { - dx: 4.0, - dy: 4.0, - blur: 8.0, - color: Color(0, 0, 0, 77), - })), - opacity: 1.0, - _ref: demo_image_id.to_string(), + let mut image_node = NodeFactory::create_image_node(); + image_node.base.id = "test_image".to_string(); + 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 rect_node = RectangleNode { - base: BaseNode { - id: "test_rect".to_string(), - name: "Test Rectangle".to_string(), - active: true, - }, - blend_mode: BlendMode::Normal, - opacity: 1.0, - transform: AffineTransform::new(50.0, 300.0, 45.0), - size: Size { - width: 200.0, - height: 100.0, - }, - corner_radius: RectangularCornerRadius::all(10.0), - fill: Paint::Solid(SolidPaint { - color: Color(255, 0, 0, 255), // Red fill - }), - stroke: Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), // Black stroke - }), - stroke_width: 2.0, - effect: Some(FilterEffect::DropShadow(FeDropShadow { - dx: 4.0, - dy: 4.0, - blur: 8.0, - color: Color(0, 0, 0, 77), // Semi-transparent black (0.3 * 255 ≈ 77) - })), + let mut rect_node = NodeFactory::create_rectangle_node(); + rect_node.base.id = "test_rect".to_string(); + rect_node.base.name = "Test Rectangle".to_string(); + rect_node.transform = AffineTransform::new(50.0, 300.0, 45.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 + }); + 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 ellipse_node = EllipseNode { - base: BaseNode { - id: "test_ellipse".to_string(), - name: "Test Ellipse".to_string(), - active: true, - }, - blend_mode: BlendMode::Multiply, // Example of using a different blend mode - opacity: 1.0, - transform: AffineTransform::new(300.0, 300.0, 45.0), // Rotated 45 degrees - size: Size { - width: 200.0, - height: 200.0, - }, - fill: Paint::RadialGradient(RadialGradientPaint { - id: "gradient2".to_string(), - 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 - }, - ], - }), - stroke: Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), // Black stroke - }), - stroke_width: 6.0, + let mut ellipse_node = NodeFactory::create_ellipse_node(); + ellipse_node.base.id = "test_ellipse".to_string(); + ellipse_node.base.name = "Test Ellipse".to_string(); + ellipse_node.blend_mode = BlendMode::Multiply; + ellipse_node.transform = AffineTransform::new(300.0, 300.0, 45.0); + ellipse_node.size = Size { + width: 200.0, + height: 200.0, }; + ellipse_node.fill = Paint::RadialGradient(RadialGradientPaint { + id: "gradient2".to_string(), + 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 + }, + ], + }); + ellipse_node.stroke_width = 6.0; // Create a test polygon node (pentagon) let pentagon_points = (0..5) @@ -453,7 +416,7 @@ async fn demo_static(renderer: &mut Renderer) -> Scene { name: "Test Polygon".to_string(), active: true, }, - blend_mode: BlendMode::Screen, // Example of using Screen blend mode + blend_mode: BlendMode::Screen, transform: AffineTransform::identity(), points: pentagon_points, fill: Paint::Solid(SolidPaint { @@ -467,152 +430,104 @@ async fn demo_static(renderer: &mut Renderer) -> Scene { }; // Create a test regular polygon node (hexagon) - let regular_polygon_node = cg::schema::RegularPolygonNode { - base: BaseNode { - id: "test_regular_polygon".to_string(), - name: "Test Regular Polygon".to_string(), - active: true, - }, - blend_mode: BlendMode::Overlay, // Example of using Overlay blend mode - transform: AffineTransform::new(300.0, 300.0, 0.0), - size: Size { - width: 200.0, - height: 200.0, - }, - point_count: 6, // hexagon - fill: Paint::Solid(SolidPaint { - color: Color(0, 200, 255, 255), // Cyan fill - }), - stroke: Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), // Black stroke - }), - stroke_width: 4.0, - opacity: 0.5, + let mut regular_polygon_node = NodeFactory::create_regular_polygon_node(); + regular_polygon_node.base.id = "test_regular_polygon".to_string(); + regular_polygon_node.base.name = "Test Regular Polygon".to_string(); + regular_polygon_node.blend_mode = BlendMode::Overlay; + regular_polygon_node.transform = AffineTransform::new(300.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 + }); + regular_polygon_node.stroke_width = 4.0; + regular_polygon_node.opacity = 0.5; // Create a test text span node - let text_span_node = TextSpanNode { - base: BaseNode { - id: "test_text".to_string(), - name: "Test Text".to_string(), - active: true, - }, - blend_mode: BlendMode::Normal, - transform: AffineTransform::new(50.0, 50.0, 15.0), - size: Size { - width: 300.0, - height: 200.0, - }, - text: "Grida Canvas SKIA Bindings Backend".to_string(), - text_style: TextStyle { - text_decoration: TextDecoration::LineThrough, - // font_family: font_roboto_family.clone(), - font_family: font_caveat_family.clone(), - font_size: 32.0, - font_weight: FontWeight::new(900), - letter_spacing: None, - line_height: None, - }, - text_align: TextAlign::Center, - text_align_vertical: TextAlignVertical::Center, - fill: Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), // White text - }), - stroke: Some(Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), // Black stroke - })), - stroke_width: Some(4.0), - opacity: 1.0, + let mut text_span_node = NodeFactory::create_text_span_node(); + text_span_node.base.id = "test_text".to_string(); + text_span_node.base.name = "Test Text".to_string(); + text_span_node.transform = AffineTransform::new(50.0, 50.0, 15.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), + letter_spacing: None, + line_height: 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 + })); + text_span_node.stroke_width = Some(4.0); // Create a test path node - let path_node = PathNode { - base: BaseNode { - id: "test_path".to_string(), - name: "Test Path".to_string(), - active: true, - }, - // blend_mode: BlendMode::Normal, - opacity: 1.0, - transform: AffineTransform::new(200.0, 200.0, 0.0), - fill: Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), // Black fill - }), - data: "M50 150H0v-50h50v50ZM150 150h-50v-50h50v50ZM100 100H50V50h50v50ZM50 50H0V0h50v50ZM150 50h-50V0h50v50Z".to_string(), - stroke: Paint::Solid(SolidPaint { - color: Color(255, 0, 0, 255), // Red stroke - }), - stroke_width: 4.0, - }; + let mut path_node = NodeFactory::create_path_node(); + path_node.base.id = "test_path".to_string(); + path_node.base.name = "Test Path".to_string(); + path_node.transform = AffineTransform::new(200.0, 200.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 + }); + path_node.stroke_width = 4.0; // Create a test line node with solid color - let line_node = LineNode { - base: BaseNode { - id: "test_line".to_string(), - name: "Test Line".to_string(), - active: true, - }, - blend_mode: BlendMode::Normal, - opacity: 0.8, - transform: AffineTransform::new(0.0, 700.0, 0.0), - size: Size { - width: 800.0, - height: 0.0, // ignored - }, - stroke: Paint::Solid(SolidPaint { - color: Color(0, 255, 0, 255), // Green color - }), - stroke_width: 4.0, + let mut line_node = NodeFactory::create_line_node(); + line_node.base.id = "test_line".to_string(); + line_node.base.name = "Test Line".to_string(); + line_node.opacity = 0.8; + line_node.transform = AffineTransform::new(0.0, 700.0, 0.0); + line_node.size = Size { + width: 800.0, + height: 0.0, // ignored }; + line_node.stroke = Paint::Solid(SolidPaint { + color: Color(0, 255, 0, 255), // Green color + }); + line_node.stroke_width = 4.0; // Create a group node for the shapes (rectangle, ellipse, polygon) - let shapes_group_node = GroupNode { - base: BaseNode { - id: "shapes_group".to_string(), - name: "Shapes Group".to_string(), - active: true, - }, - blend_mode: BlendMode::Normal, - transform: AffineTransform::new(0.0, 0.0, -15.0), - children: vec![ - "test_rect".to_string(), - "test_ellipse".to_string(), - "test_polygon".to_string(), - "test_regular_polygon".to_string(), - ], - opacity: 0.8, - }; - - // Create a root group node containing the shapes group, text, and line - let root_container_node = ContainerNode { - base: BaseNode { - id: "root_container".to_string(), - name: "Root Container".to_string(), - active: true, - }, - blend_mode: BlendMode::Normal, - transform: AffineTransform::identity(), - children: vec![ - "background_rect".to_string(), - "shapes_group".to_string(), - "test_text".to_string(), - "test_line".to_string(), - "test_path".to_string(), - "test_image".to_string(), - ], - opacity: 1.0, - size: Size { - width: 1080.0, - height: 1080.0, - }, - corner_radius: RectangularCornerRadius::all(0.0), - fill: Paint::Solid(SolidPaint { - color: Color(255, 255, 255, 255), - }), - stroke: None, - stroke_width: 0.0, - effect: None, + let mut shapes_group_node = NodeFactory::create_group_node(); + shapes_group_node.base.id = "shapes_group".to_string(); + shapes_group_node.base.name = "Shapes Group".to_string(); + shapes_group_node.transform = AffineTransform::new(0.0, 0.0, -15.0); + shapes_group_node.children = vec![ + "test_rect".to_string(), + "test_ellipse".to_string(), + "test_polygon".to_string(), + "test_regular_polygon".to_string(), + ]; + shapes_group_node.opacity = 0.8; + + // Create a root container node containing the shapes group, text, and line + let mut root_container_node = NodeFactory::create_container_node(); + root_container_node.base.id = "root_container".to_string(); + root_container_node.base.name = "Root Container".to_string(); + root_container_node.children = vec![ + "background_rect".to_string(), + "shapes_group".to_string(), + "test_text".to_string(), + "test_line".to_string(), + "test_path".to_string(), + "test_image".to_string(), + ]; + root_container_node.size = Size { + width: 1080.0, + height: 1080.0, }; + root_container_node.stroke = None; + root_container_node.stroke_width = 0.0; // Create a node map and add all nodes let mut nodemap = NodeMap::new(); @@ -684,7 +599,7 @@ async fn main() { // Load the appropriate scene based on command line arguments let scene = match cli.command { Commands::Example { name } => match name.as_str() { - "basic" => demo_static(&mut renderer).await, + "basic" => demo_basic(&mut renderer).await, _ => { eprintln!("Unknown example: {}", name); eprintln!("Available examples: basic"); diff --git a/crates/cg/src/factory.rs b/crates/cg/src/factory.rs new file mode 100644 index 0000000000..1cfbe89794 --- /dev/null +++ b/crates/cg/src/factory.rs @@ -0,0 +1,179 @@ +use crate::schema::*; +use crate::transform::AffineTransform; + +/// Factory for creating nodes with default values +pub struct NodeFactory; + +impl NodeFactory { + // Internal factory defaults + const DEFAULT_SIZE: Size = Size { + width: 100.0, + height: 100.0, + }; + + const DEFAULT_BASE: BaseNode = BaseNode { + id: String::new(), + name: String::new(), + active: true, + }; + + const DEFAULT_COLOR: Color = Color(255, 255, 255, 255); + const DEFAULT_STROKE_COLOR: Color = Color(0, 0, 0, 255); + const DEFAULT_STROKE_WIDTH: f32 = 1.0; + const DEFAULT_OPACITY: f32 = 1.0; + + fn default_base_node() -> BaseNode { + Self::DEFAULT_BASE.clone() + } + + fn default_solid_paint(color: Color) -> Paint { + Paint::Solid(SolidPaint { color }) + } + + /// Creates a new rectangle node with default values + pub fn create_rectangle_node() -> RectangleNode { + RectangleNode { + base: Self::default_base_node(), + transform: AffineTransform::identity(), + size: Self::DEFAULT_SIZE, + corner_radius: RectangularCornerRadius::zero(), + fill: Self::default_solid_paint(Self::DEFAULT_COLOR), + stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), + stroke_width: Self::DEFAULT_STROKE_WIDTH, + opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + effect: None, + } + } + + /// Creates a new ellipse node with default values + pub fn create_ellipse_node() -> EllipseNode { + EllipseNode { + base: Self::default_base_node(), + transform: AffineTransform::identity(), + size: Self::DEFAULT_SIZE, + fill: Self::default_solid_paint(Self::DEFAULT_COLOR), + stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), + stroke_width: Self::DEFAULT_STROKE_WIDTH, + opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + } + } + + /// Creates a new line node with default values + pub fn create_line_node() -> LineNode { + LineNode { + base: Self::default_base_node(), + transform: AffineTransform::identity(), + size: Size { + width: Self::DEFAULT_SIZE.width, + height: 0.0, + }, + stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), + stroke_width: Self::DEFAULT_STROKE_WIDTH, + opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + } + } + + /// Creates a new text span node with default values + pub fn create_text_span_node() -> TextSpanNode { + TextSpanNode { + base: Self::default_base_node(), + transform: AffineTransform::identity(), + size: Size { + width: Self::DEFAULT_SIZE.width, + height: 20.0, + }, + text: String::new(), + text_style: TextStyle { + text_decoration: TextDecoration::None, + font_family: String::from("Arial"), + font_size: 16.0, + font_weight: FontWeight::default(), + letter_spacing: None, + line_height: None, + }, + text_align: TextAlign::Left, + text_align_vertical: TextAlignVertical::Top, + fill: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), + stroke: None, + stroke_width: None, + opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + } + } + + /// Creates a new group node with default values + pub fn create_group_node() -> GroupNode { + GroupNode { + base: Self::default_base_node(), + transform: AffineTransform::identity(), + children: Vec::new(), + opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + } + } + + /// Creates a new container node with default values + pub fn create_container_node() -> ContainerNode { + ContainerNode { + base: Self::default_base_node(), + transform: AffineTransform::identity(), + size: Self::DEFAULT_SIZE, + corner_radius: RectangularCornerRadius::zero(), + children: Vec::new(), + fill: Self::default_solid_paint(Self::DEFAULT_COLOR), + stroke: None, + stroke_width: Self::DEFAULT_STROKE_WIDTH, + opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + effect: None, + } + } + + /// Creates a new regular polygon node with default values + pub fn create_regular_polygon_node() -> RegularPolygonNode { + RegularPolygonNode { + base: Self::default_base_node(), + transform: AffineTransform::identity(), + size: Self::DEFAULT_SIZE, + point_count: 3, // Triangle by default + fill: Self::default_solid_paint(Self::DEFAULT_COLOR), + stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), + stroke_width: Self::DEFAULT_STROKE_WIDTH, + opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + } + } + + /// Creates a new path node with default values + pub fn create_path_node() -> PathNode { + PathNode { + base: Self::default_base_node(), + transform: AffineTransform::identity(), + fill: Self::default_solid_paint(Self::DEFAULT_COLOR), + data: String::new(), + stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), + stroke_width: Self::DEFAULT_STROKE_WIDTH, + opacity: Self::DEFAULT_OPACITY, + } + } + + /// Creates a new image node with default values + pub fn create_image_node() -> ImageNode { + ImageNode { + base: Self::default_base_node(), + transform: AffineTransform::identity(), + size: Self::DEFAULT_SIZE, + corner_radius: RectangularCornerRadius::zero(), + fill: Self::default_solid_paint(Self::DEFAULT_COLOR), + stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), + stroke_width: Self::DEFAULT_STROKE_WIDTH, + opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + effect: None, + _ref: String::new(), + } + } +} diff --git a/crates/cg/src/lib.rs b/crates/cg/src/lib.rs index a7795cf33b..b190db2225 100644 --- a/crates/cg/src/lib.rs +++ b/crates/cg/src/lib.rs @@ -1,4 +1,5 @@ pub mod draw; +pub mod factory; pub mod io; pub mod schema; pub mod transform; From dc87b8a42533d08bdfe3c9c227d5423129621599 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 21:23:49 +0900 Subject: [PATCH 039/262] repository --- Cargo.lock | 12 ++ crates/cg/Cargo.toml | 1 + crates/cg/benches/render_bench.rs | 64 +++++----- crates/cg/resources/.gitignore | 1 + crates/cg/src/dev.rs | 188 ++++++++++++++---------------- crates/cg/src/draw.rs | 89 ++++---------- crates/cg/src/factory.rs | 80 ++++++++----- crates/cg/src/lib.rs | 1 + crates/cg/src/repository.rs | 83 +++++++++++++ crates/cg/src/schema.rs | 7 +- 10 files changed, 289 insertions(+), 237 deletions(-) create mode 100644 crates/cg/resources/.gitignore create mode 100644 crates/cg/src/repository.rs diff --git a/Cargo.lock b/Cargo.lock index 1b6a473863..32261c06e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,6 +340,7 @@ dependencies = [ "serde_json", "skia-safe", "tokio", + "uuid", "wasm-bindgen", "wee_alloc", "winit", @@ -2791,6 +2792,17 @@ 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" diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index 58eb64711a..ca342f2026 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -26,6 +26,7 @@ winit = "0.30.0" serde = "1.0.219" serde_json = "1.0.140" clap = { version = "4.5.39", features = ["derive"] } +uuid = { version = "1.17.0", features = ["v4"] } [features] default = ["wee_alloc"] diff --git a/crates/cg/benches/render_bench.rs b/crates/cg/benches/render_bench.rs index aca758a8ea..536bca4e1f 100644 --- a/crates/cg/benches/render_bench.rs +++ b/crates/cg/benches/render_bench.rs @@ -1,83 +1,75 @@ use cg::draw::{Backend, Renderer}; +use cg::repository::NodeRepository; use cg::schema::{ - BaseNode, BlendMode, Color, Node, NodeMap, Paint, RectangleNode, RectangularCornerRadius, Size, + BaseNode, BlendMode, Color, Node, NodeId, Paint, RectangleNode, RectangularCornerRadius, Size, SolidPaint, }; use cg::transform::AffineTransform; use criterion::{Criterion, black_box, criterion_group, criterion_main}; -fn create_rectangles(count: usize, with_effects: bool) -> (NodeMap, Vec) { - let mut nodemap = NodeMap::new(); - let mut ids = Vec::with_capacity(count); +fn create_rectangles(count: usize, with_effects: bool) -> (NodeRepository, Vec) { + let mut repository = NodeRepository::new(); + let mut ids = Vec::new(); + // Create rectangles for i in 0..count { - let id = format!("rect_{}", i); + let id = format!("rect-{}", i); + ids.push(id.clone()); + let rect = RectangleNode { base: BaseNode { id: id.clone(), name: format!("Rectangle {}", i), active: true, }, - blend_mode: if i % 2 == 0 { - BlendMode::Normal - } else { - BlendMode::Multiply - }, - transform: AffineTransform::new( - (i % 100) as f32 * 10.0, // x position - (i / 100) as f32 * 10.0, // y position - (i % 4) as f32 * 90.0, // rotation - ), + transform: AffineTransform::identity(), size: Size { - width: 8.0, - height: 8.0, + width: 100.0, + height: 100.0, }, - corner_radius: RectangularCornerRadius::all(2.0), + corner_radius: RectangularCornerRadius::zero(), fill: Paint::Solid(SolidPaint { - color: Color( - (i * 7) as u8, // r - (i * 13) as u8, // g - (i * 17) as u8, // b - 255, // a - ), + color: Color(255, 0, 0, 255), }), stroke: Paint::Solid(SolidPaint { color: Color(0, 0, 0, 255), }), stroke_width: 1.0, opacity: 1.0, + blend_mode: BlendMode::Normal, effect: if with_effects { Some(cg::schema::FilterEffect::DropShadow( cg::schema::FeDropShadow { dx: 2.0, dy: 2.0, blur: 4.0, - color: Color(0, 0, 0, 77), + color: Color(0, 0, 0, 128), }, )) } else { None }, }; - nodemap.insert(id.clone(), Node::Rectangle(rect)); - ids.push(id); + + repository.insert(Node::Rectangle(rect)); } - // Create a root group node + // Create root group let root_group = cg::schema::GroupNode { base: BaseNode { id: "root".to_string(), name: "Root Group".to_string(), active: true, }, - blend_mode: BlendMode::Normal, transform: AffineTransform::identity(), children: ids.clone(), opacity: 1.0, + blend_mode: BlendMode::Normal, }; - nodemap.insert("root".to_string(), Node::Group(root_group)); - (nodemap, ids) + repository.insert(Node::Group(root_group)); + + (repository, ids) } fn bench_rectangles(c: &mut Criterion) { @@ -91,7 +83,7 @@ fn bench_rectangles(c: &mut Criterion) { // 1K rectangles group.bench_function("1k_basic", |b| { b.iter(|| { - let mut renderer = Renderer::new(); + let mut renderer = Renderer::new(1.0); let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); @@ -115,7 +107,7 @@ fn bench_rectangles(c: &mut Criterion) { // 10K rectangles group.bench_function("10k_basic", |b| { b.iter(|| { - let mut renderer = Renderer::new(); + let mut renderer = Renderer::new(1.0); let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); @@ -138,7 +130,7 @@ fn bench_rectangles(c: &mut Criterion) { group.bench_function("10k_with_effects", |b| { b.iter(|| { - let mut renderer = Renderer::new(); + let mut renderer = Renderer::new(1.0); let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); @@ -162,7 +154,7 @@ fn bench_rectangles(c: &mut Criterion) { // 50K rectangles group.bench_function("50k_basic", |b| { b.iter(|| { - let mut renderer = Renderer::new(); + let mut renderer = Renderer::new(1.0); let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); @@ -185,7 +177,7 @@ fn bench_rectangles(c: &mut Criterion) { group.bench_function("50k_with_effects", |b| { b.iter(|| { - let mut renderer = Renderer::new(); + let mut renderer = Renderer::new(1.0); let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); diff --git a/crates/cg/resources/.gitignore b/crates/cg/resources/.gitignore new file mode 100644 index 0000000000..c2c027fec1 --- /dev/null +++ b/crates/cg/resources/.gitignore @@ -0,0 +1 @@ +local \ No newline at end of file diff --git a/crates/cg/src/dev.rs b/crates/cg/src/dev.rs index 33b20130fd..85c533d280 100644 --- a/crates/cg/src/dev.rs +++ b/crates/cg/src/dev.rs @@ -1,14 +1,10 @@ use cg::draw::{Backend, Renderer}; use cg::factory::NodeFactory; use cg::io::parse; +use cg::repository::NodeRepository; use cg::schema::FeDropShadow; use cg::schema::FilterEffect; -use cg::schema::{ - BaseNode, BlendMode, Color, ContainerNode, EllipseNode, FontWeight, GradientStop, GroupNode, - ImageNode, LineNode, Node, NodeMap, Paint, PathNode, PolygonNode, RadialGradientPaint, - RectangleNode, RectangularCornerRadius, Scene, Size, SolidPaint, TextAlign, TextAlignVertical, - TextDecoration, TextSpanNode, TextStyle, -}; +use cg::schema::*; use cg::transform::AffineTransform; use clap::{Parser, Subcommand}; use console_error_panic_hook::set_once as init_panic_hook; @@ -300,13 +296,14 @@ async fn demo_basic(renderer: &mut Renderer) -> Scene { println!("Font load time: {:?}", font_load_start.elapsed()); + let nf = NodeFactory::new(); + // Add a background rectangle node - let mut background_rect_node = NodeFactory::create_rectangle_node(); - background_rect_node.base.id = "background_rect".to_string(); + let mut background_rect_node = nf.create_rectangle_node(); background_rect_node.base.name = "Background Rect".to_string(); background_rect_node.size = Size { - width: 800.0, - height: 600.0, + width: 1080.0, + height: 1080.0, }; background_rect_node.fill = Paint::Solid(SolidPaint { color: Color(230, 240, 255, 255), // Light blue for visibility @@ -331,8 +328,7 @@ async fn demo_basic(renderer: &mut Renderer) -> Scene { println!("Image load time: {:?}", image_load_start.elapsed()); // Create a test image node with URL - let mut image_node = NodeFactory::create_image_node(); - image_node.base.id = "test_image".to_string(); + 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 { @@ -350,10 +346,9 @@ async fn demo_basic(renderer: &mut Renderer) -> Scene { image_node._ref = demo_image_id.to_string(); // Create a test rectangle node with linear gradient - let mut rect_node = NodeFactory::create_rectangle_node(); - rect_node.base.id = "test_rect".to_string(); + let mut rect_node = nf.create_rectangle_node(); rect_node.base.name = "Test Rectangle".to_string(); - rect_node.transform = AffineTransform::new(50.0, 300.0, 45.0); + rect_node.transform = AffineTransform::new(300.0, 50.0, 0.0); rect_node.size = Size { width: 200.0, height: 100.0, @@ -371,11 +366,10 @@ async fn demo_basic(renderer: &mut Renderer) -> Scene { })); // Create a test ellipse node with radial gradient and a visible stroke - let mut ellipse_node = NodeFactory::create_ellipse_node(); - ellipse_node.base.id = "test_ellipse".to_string(); + 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(300.0, 300.0, 45.0); + ellipse_node.transform = AffineTransform::new(550.0, 50.0, 0.0); ellipse_node.size = Size { width: 200.0, height: 200.0, @@ -405,36 +399,30 @@ async fn demo_basic(renderer: &mut Renderer) -> Scene { .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 = 550.0 + radius * angle.cos(); - let y = 150.0 + radius * angle.sin(); + let x = radius * angle.cos(); + let y = radius * angle.sin(); (x, y) }) .collect::>(); - let polygon_node = PolygonNode { - base: BaseNode { - id: "test_polygon".to_string(), - name: "Test Polygon".to_string(), - active: true, - }, - blend_mode: BlendMode::Screen, - transform: AffineTransform::identity(), - points: pentagon_points, - fill: Paint::Solid(SolidPaint { - color: Color(255, 200, 0, 255), // Orange fill - }), - stroke: Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), // Black stroke - }), - stroke_width: 5.0, - opacity: 1.0, - }; + + 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 + }); + polygon_node.stroke = Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), // Black stroke + }); + polygon_node.stroke_width = 5.0; // Create a test regular polygon node (hexagon) - let mut regular_polygon_node = NodeFactory::create_regular_polygon_node(); - regular_polygon_node.base.id = "test_regular_polygon".to_string(); + 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(300.0, 300.0, 0.0); + regular_polygon_node.transform = AffineTransform::new(50.0, 300.0, 0.0); regular_polygon_node.size = Size { width: 200.0, height: 200.0, @@ -447,10 +435,9 @@ async fn demo_basic(renderer: &mut Renderer) -> Scene { regular_polygon_node.opacity = 0.5; // Create a test text span node - let mut text_span_node = NodeFactory::create_text_span_node(); - text_span_node.base.id = "test_text".to_string(); + 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(50.0, 50.0, 15.0); + text_span_node.transform = AffineTransform::new(300.0, 300.0, 0.0); text_span_node.size = Size { width: 300.0, height: 200.0, @@ -472,10 +459,9 @@ async fn demo_basic(renderer: &mut Renderer) -> Scene { text_span_node.stroke_width = Some(4.0); // Create a test path node - let mut path_node = NodeFactory::create_path_node(); - path_node.base.id = "test_path".to_string(); + let mut path_node = nf.create_path_node(); path_node.base.name = "Test Path".to_string(); - path_node.transform = AffineTransform::new(200.0, 200.0, 0.0); + 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 @@ -483,13 +469,12 @@ async fn demo_basic(renderer: &mut Renderer) -> Scene { path_node.stroke_width = 4.0; // Create a test line node with solid color - let mut line_node = NodeFactory::create_line_node(); - line_node.base.id = "test_line".to_string(); + 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(0.0, 700.0, 0.0); + line_node.transform = AffineTransform::new(800.0, 300.0, 0.0); line_node.size = Size { - width: 800.0, + width: 200.0, height: 0.0, // ignored }; line_node.stroke = Paint::Solid(SolidPaint { @@ -498,66 +483,62 @@ async fn demo_basic(renderer: &mut Renderer) -> Scene { line_node.stroke_width = 4.0; // Create a group node for the shapes (rectangle, ellipse, polygon) - let mut shapes_group_node = NodeFactory::create_group_node(); - shapes_group_node.base.id = "shapes_group".to_string(); + 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, -15.0); - shapes_group_node.children = vec![ - "test_rect".to_string(), - "test_ellipse".to_string(), - "test_polygon".to_string(), - "test_regular_polygon".to_string(), - ]; - shapes_group_node.opacity = 0.8; + 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 = NodeFactory::create_container_node(); - root_container_node.base.id = "root_container".to_string(); + let mut root_container_node = nf.create_container_node(); root_container_node.base.name = "Root Container".to_string(); - root_container_node.children = vec![ - "background_rect".to_string(), - "shapes_group".to_string(), - "test_text".to_string(), - "test_line".to_string(), - "test_path".to_string(), - "test_image".to_string(), - ]; - root_container_node.size = Size { - width: 1080.0, - height: 1080.0, - }; - root_container_node.stroke = None; - root_container_node.stroke_width = 0.0; // Create a node map and add all nodes - let mut nodemap = NodeMap::new(); - nodemap.insert( - "background_rect".to_string(), - Node::Rectangle(background_rect_node), - ); - nodemap.insert("test_rect".to_string(), Node::Rectangle(rect_node)); - nodemap.insert("test_ellipse".to_string(), Node::Ellipse(ellipse_node)); - nodemap.insert("test_polygon".to_string(), Node::Polygon(polygon_node)); - nodemap.insert( - "test_regular_polygon".to_string(), - Node::RegularPolygon(regular_polygon_node), - ); - nodemap.insert("shapes_group".to_string(), Node::Group(shapes_group_node)); - nodemap.insert("test_text".to_string(), Node::TextSpan(text_span_node)); - nodemap.insert("test_line".to_string(), Node::Line(line_node)); - nodemap.insert("test_image".to_string(), Node::Image(image_node)); - nodemap.insert("test_path".to_string(), Node::Path(path_node)); - nodemap.insert( - "root_container".to_string(), - Node::Container(root_container_node), - ); + let mut repository = NodeRepository::new(); + + // First, collect all the IDs we'll need + let background_rect_id = background_rect_node.base.id.clone(); + 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(background_rect_node)); + 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![ + background_rect_id, + 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".to_string()], - nodes: nodemap, + children: vec![root_container_id], + nodes: repository, } } @@ -567,8 +548,6 @@ async fn main() { let width = 1080; let height = 1080; - // Initialize the renderer with image cache - let mut renderer = Renderer::new(); let ( surface_ptr, el, @@ -580,7 +559,6 @@ async fn main() { _gr_context, scale_factor, ) = init_window(width, height); - renderer.set_backend(Backend::GL(surface_ptr)); // Log DPI and size info let logical_size = window.inner_size(); @@ -596,6 +574,10 @@ async fn main() { physical_width, physical_height ); + let mut renderer = Renderer::new(scale_factor as f32); + + renderer.set_backend(Backend::GL(surface_ptr)); + // Load the appropriate scene based on command line arguments let scene = match cli.command { Commands::Example { name } => match name.as_str() { diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 5261bb8cdf..f6bf20f0e2 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,6 +1,7 @@ +use crate::repository::NodeRepository; use crate::schema::{ - Color as SchemaColor, ContainerNode, EllipseNode, FilterEffect, FontWeight, GradientStop, - GroupNode, ImageNode, LineNode, Node, NodeId, NodeMap, Paint, PathNode, PolygonNode, + BlendMode, Color as SchemaColor, ContainerNode, EllipseNode, FilterEffect, FontWeight, + GradientStop, GroupNode, ImageNode, LineNode, Node, NodeId, Paint, PathNode, PolygonNode, RectangleNode, RectangularCornerRadius, RegularPolygonNode, Scene, TextAlign, TextAlignVertical, TextDecoration, TextNode, TextSpanNode, }; @@ -29,10 +30,11 @@ pub struct Renderer { backend: Option, font_mgr: FontMgr, font_collection: FontCollection, + dpi: f32, } impl Renderer { - pub fn new() -> Self { + pub fn new(dpi: f32) -> Self { let mut font_collection = FontCollection::new(); let font_mgr = FontMgr::new(); font_collection.set_default_font_manager(font_mgr.clone(), None); @@ -42,6 +44,7 @@ impl Renderer { backend: None, font_collection, font_mgr, + dpi, } } @@ -86,20 +89,26 @@ impl Renderer { } pub fn render_scene(&self, scene: &Scene) { - for child_id in &scene.children { - self.render_node(child_id, &scene.nodes); + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + canvas.save(); + canvas.scale((self.dpi, self.dpi)); + for child_id in &scene.children { + self.render_node(child_id, &scene.nodes); + } } } - pub fn render_node(&self, id: &NodeId, nodemap: &NodeMap) { - let node = match nodemap.get(id) { + pub fn render_node(&self, id: &NodeId, repository: &NodeRepository) { + let node = match repository.get(id) { Some(node) => node, - None => return, // Skip if node not found + None => return, }; match node { - Node::Group(node) => self.draw_group_node(node, nodemap), - Node::Container(node) => self.draw_container_node(node, nodemap), + Node::Group(node) => self.draw_group_node(node, repository), + Node::Container(node) => self.draw_container_node(node, repository), Node::Rectangle(node) => self.draw_rect_node(node), Node::Ellipse(node) => self.draw_ellipse_node(node), Node::Polygon(node) => self.draw_polygon_node(node), @@ -376,15 +385,12 @@ impl Renderer { ); fill_paint.set_blend_mode(node.blend_mode.into()); - // font - let mut font_collection = FontCollection::new(); - font_collection.set_default_font_manager(FontMgr::new(), None); - // paragraph let mut paragraph_style = ParagraphStyle::new(); paragraph_style.set_text_direction(skia_safe::textlayout::TextDirection::LTR); paragraph_style.set_text_align(node.text_align.into()); - let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); + let mut paragraph_builder = + ParagraphBuilder::new(¶graph_style, &self.font_collection); // text style let mut ts = TextStyle::new(); @@ -420,51 +426,6 @@ impl Renderer { paragraph.paint(canvas, Point::new(node.transform.x(), node.transform.y())); canvas.restore(); return; - - // // Create paragraph style - // let mut paragraph_style = skia_safe::textlayout::ParagraphStyle::new(); - - // Create text style - // let mut text_style = skia_safe::textlayout::TextStyle::new(); - - // // Create paragraph builder - // let mut paragraph_builder = skia_safe::textlayout::ParagraphBuilder::new( - // ¶graph_style, - // skia_safe::textlayout::FontCollection::new(), - // ); - - // // Add text with style - // paragraph_builder.push_style(&text_style); - // paragraph_builder.pop(); - - // // Build paragraph - // let mut paragraph = paragraph_builder.build(); - - // // Calculate vertical position based on alignment - // let y = match node.text_align_vertical { - // TextAlignVertical::Top => 0.0, - // TextAlignVertical::Center => (node.size.height - paragraph.height()) / 2.0, - // TextAlignVertical::Bottom => node.size.height - paragraph.height(), - // }; - - // // Draw stroke if specified - // if let (Some(stroke), Some(stroke_width)) = (&node.stroke, node.stroke_width) { - // let mut stroke_paint = - // sk_paint(stroke, node.opacity, (node.size.width, node.size.height)); - // stroke_paint.set_style(skia_safe::paint::Style::Stroke); - // stroke_paint.set_stroke_width(stroke_width); - // stroke_paint.set_blend_mode(node.base.blend_mode.into()); - // paragraph.paint(canvas, skia_safe::Point::new(0.0, y)); - // } - - // // Draw fill - // let mut fill_paint = sk_paint( - // &node.fill, - // node.opacity, - // (node.size.width, node.size.height), - // ); - // fill_paint.set_blend_mode(node.base.blend_mode.into()); - // paragraph.paint(canvas, skia_safe::Point::new(0.0, y)); } } @@ -558,7 +519,7 @@ impl Renderer { } } - pub fn draw_group_node(&self, node: &GroupNode, nodemap: &NodeMap) { + pub fn draw_group_node(&self, node: &GroupNode, repository: &NodeRepository) { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; let canvas = surface.canvas(); @@ -576,7 +537,7 @@ impl Renderer { // Recursively render children for child_id in &node.children { - self.render_node(child_id, nodemap); + self.render_node(child_id, repository); } if needs_opacity_layer { @@ -589,7 +550,7 @@ impl Renderer { } } - pub fn draw_container_node(&self, node: &ContainerNode, nodemap: &NodeMap) { + pub fn draw_container_node(&self, node: &ContainerNode, repository: &NodeRepository) { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; let canvas = surface.canvas(); @@ -694,7 +655,7 @@ impl Renderer { // Recursively render children for child_id in &node.children { - self.render_node(child_id, nodemap); + self.render_node(child_id, repository); } if needs_opacity_layer { diff --git a/crates/cg/src/factory.rs b/crates/cg/src/factory.rs index 1cfbe89794..2d5c20d717 100644 --- a/crates/cg/src/factory.rs +++ b/crates/cg/src/factory.rs @@ -1,29 +1,38 @@ use crate::schema::*; use crate::transform::AffineTransform; +use uuid::Uuid; /// Factory for creating nodes with default values pub struct NodeFactory; impl NodeFactory { + pub fn new() -> Self { + Self {} + } + + fn id(&self) -> String { + // random id + let id = Uuid::new_v4(); + id.to_string() + } + // Internal factory defaults const DEFAULT_SIZE: Size = Size { width: 100.0, height: 100.0, }; - const DEFAULT_BASE: BaseNode = BaseNode { - id: String::new(), - name: String::new(), - active: true, - }; - const DEFAULT_COLOR: Color = Color(255, 255, 255, 255); const DEFAULT_STROKE_COLOR: Color = Color(0, 0, 0, 255); const DEFAULT_STROKE_WIDTH: f32 = 1.0; const DEFAULT_OPACITY: f32 = 1.0; - fn default_base_node() -> BaseNode { - Self::DEFAULT_BASE.clone() + fn default_base_node(&self) -> BaseNode { + BaseNode { + id: self.id(), + name: String::new(), + active: true, + } } fn default_solid_paint(color: Color) -> Paint { @@ -31,9 +40,9 @@ impl NodeFactory { } /// Creates a new rectangle node with default values - pub fn create_rectangle_node() -> RectangleNode { + pub fn create_rectangle_node(&self) -> RectangleNode { RectangleNode { - base: Self::default_base_node(), + base: self.default_base_node(), transform: AffineTransform::identity(), size: Self::DEFAULT_SIZE, corner_radius: RectangularCornerRadius::zero(), @@ -47,9 +56,9 @@ impl NodeFactory { } /// Creates a new ellipse node with default values - pub fn create_ellipse_node() -> EllipseNode { + pub fn create_ellipse_node(&self) -> EllipseNode { EllipseNode { - base: Self::default_base_node(), + base: self.default_base_node(), transform: AffineTransform::identity(), size: Self::DEFAULT_SIZE, fill: Self::default_solid_paint(Self::DEFAULT_COLOR), @@ -61,9 +70,9 @@ impl NodeFactory { } /// Creates a new line node with default values - pub fn create_line_node() -> LineNode { + pub fn create_line_node(&self) -> LineNode { LineNode { - base: Self::default_base_node(), + base: self.default_base_node(), transform: AffineTransform::identity(), size: Size { width: Self::DEFAULT_SIZE.width, @@ -77,9 +86,9 @@ impl NodeFactory { } /// Creates a new text span node with default values - pub fn create_text_span_node() -> TextSpanNode { + pub fn create_text_span_node(&self) -> TextSpanNode { TextSpanNode { - base: Self::default_base_node(), + base: self.default_base_node(), transform: AffineTransform::identity(), size: Size { width: Self::DEFAULT_SIZE.width, @@ -105,9 +114,9 @@ impl NodeFactory { } /// Creates a new group node with default values - pub fn create_group_node() -> GroupNode { + pub fn create_group_node(&self) -> GroupNode { GroupNode { - base: Self::default_base_node(), + base: self.default_base_node(), transform: AffineTransform::identity(), children: Vec::new(), opacity: Self::DEFAULT_OPACITY, @@ -116,9 +125,9 @@ impl NodeFactory { } /// Creates a new container node with default values - pub fn create_container_node() -> ContainerNode { + pub fn create_container_node(&self) -> ContainerNode { ContainerNode { - base: Self::default_base_node(), + base: self.default_base_node(), transform: AffineTransform::identity(), size: Self::DEFAULT_SIZE, corner_radius: RectangularCornerRadius::zero(), @@ -132,10 +141,23 @@ impl NodeFactory { } } + /// Creates a new path node with default values + pub fn create_path_node(&self) -> PathNode { + PathNode { + base: self.default_base_node(), + transform: AffineTransform::identity(), + fill: Self::default_solid_paint(Self::DEFAULT_COLOR), + data: String::new(), + stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), + stroke_width: Self::DEFAULT_STROKE_WIDTH, + opacity: Self::DEFAULT_OPACITY, + } + } + /// Creates a new regular polygon node with default values - pub fn create_regular_polygon_node() -> RegularPolygonNode { + pub fn create_regular_polygon_node(&self) -> RegularPolygonNode { RegularPolygonNode { - base: Self::default_base_node(), + base: self.default_base_node(), transform: AffineTransform::identity(), size: Self::DEFAULT_SIZE, point_count: 3, // Triangle by default @@ -147,23 +169,23 @@ impl NodeFactory { } } - /// Creates a new path node with default values - pub fn create_path_node() -> PathNode { - PathNode { - base: Self::default_base_node(), + pub fn create_polygon_node(&self) -> PolygonNode { + PolygonNode { + base: self.default_base_node(), transform: AffineTransform::identity(), + points: Vec::new(), fill: Self::default_solid_paint(Self::DEFAULT_COLOR), - data: String::new(), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, } } /// Creates a new image node with default values - pub fn create_image_node() -> ImageNode { + pub fn create_image_node(&self) -> ImageNode { ImageNode { - base: Self::default_base_node(), + base: self.default_base_node(), transform: AffineTransform::identity(), size: Self::DEFAULT_SIZE, corner_radius: RectangularCornerRadius::zero(), diff --git a/crates/cg/src/lib.rs b/crates/cg/src/lib.rs index b190db2225..1e7a01f1ec 100644 --- a/crates/cg/src/lib.rs +++ b/crates/cg/src/lib.rs @@ -1,5 +1,6 @@ pub mod draw; pub mod factory; pub mod io; +pub mod repository; pub mod schema; pub mod transform; diff --git a/crates/cg/src/repository.rs b/crates/cg/src/repository.rs new file mode 100644 index 0000000000..003b295aaa --- /dev/null +++ b/crates/cg/src/repository.rs @@ -0,0 +1,83 @@ +use crate::schema::{Node, NodeId}; +use std::collections::HashMap; + +/// A repository for managing nodes with automatic ID indexing. +#[derive(Debug, Clone)] +pub struct NodeRepository { + /// The map of all nodes indexed by their IDs + nodes: HashMap, +} + +impl NodeRepository { + /// Creates a new empty node repository + pub fn new() -> Self { + Self { + nodes: HashMap::new(), + } + } + + /// Inserts a node into the repository, automatically indexing it by its ID. + /// Returns the node's ID. + pub fn insert(&mut self, node: Node) -> NodeId { + let id = match &node { + Node::Group(n) => n.base.id.clone(), + Node::Container(n) => n.base.id.clone(), + Node::Rectangle(n) => n.base.id.clone(), + Node::Ellipse(n) => n.base.id.clone(), + Node::Polygon(n) => n.base.id.clone(), + Node::RegularPolygon(n) => n.base.id.clone(), + Node::Line(n) => n.base.id.clone(), + Node::TextSpan(n) => n.base.id.clone(), + Node::Path(n) => n.base.id.clone(), + Node::Image(n) => n.base.id.clone(), + }; + self.nodes.insert(id.clone(), node); + id + } + + /// Gets a reference to a node by its ID + pub fn get(&self, id: &NodeId) -> Option<&Node> { + self.nodes.get(id) + } + + /// Gets a mutable reference to a node by its ID + pub fn get_mut(&mut self, id: &NodeId) -> Option<&mut Node> { + self.nodes.get_mut(id) + } + + /// Removes a node from the repository by its ID + pub fn remove(&mut self, id: &NodeId) -> Option { + self.nodes.remove(id) + } + + /// Returns an iterator over all nodes in the repository + pub fn iter(&self) -> impl Iterator { + self.nodes.iter() + } + + /// Returns the number of nodes in the repository + pub fn len(&self) -> usize { + self.nodes.len() + } + + /// Returns true if the repository is empty + pub fn is_empty(&self) -> bool { + self.nodes.is_empty() + } +} + +impl Default for NodeRepository { + fn default() -> Self { + Self::new() + } +} + +impl FromIterator<(NodeId, Node)> for NodeRepository { + fn from_iter>(iter: T) -> Self { + let mut repo = Self::new(); + for (_, node) in iter { + repo.insert(node); + } + repo + } +} diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 6159ce98fe..71fe9d8766 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -1,7 +1,7 @@ +use crate::repository::NodeRepository; use crate::transform::AffineTransform; use core::str; use serde::Deserialize; -use std::collections::HashMap; use std::f32::consts::PI; pub type NodeId = String; @@ -320,7 +320,7 @@ pub struct Scene { pub name: String, pub transform: AffineTransform, pub children: Vec, - pub nodes: NodeMap, + pub nodes: NodeRepository, } // endregion @@ -597,6 +597,3 @@ pub struct TextNode { } // endregion - -// Example doc tree container -pub type NodeMap = HashMap; From 2ec2c84a61cfc997f58d85e288e04ba92cead14a Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 22:48:20 +0900 Subject: [PATCH 040/262] update examples --- crates/cg/Cargo.toml | 5 +- crates/cg/README.md | 29 + .../{render_bench.rs => bench_rectangles.rs} | 0 crates/cg/examples/basic.rs | 264 ++++++++ crates/cg/examples/json.rs | 17 + crates/cg/examples/nested.rs | 97 +++ crates/cg/examples/window.rs | 326 ++++++++++ crates/cg/src/dev.rs | 615 ------------------ crates/cg/src/draw.rs | 1 + 9 files changed, 735 insertions(+), 619 deletions(-) rename crates/cg/benches/{render_bench.rs => bench_rectangles.rs} (100%) create mode 100644 crates/cg/examples/basic.rs create mode 100644 crates/cg/examples/json.rs create mode 100644 crates/cg/examples/nested.rs create mode 100644 crates/cg/examples/window.rs delete mode 100644 crates/cg/src/dev.rs diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index ca342f2026..a50694c59c 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -6,9 +6,6 @@ edition = "2024" [lib] crate-type = ["cdylib", "rlib"] -[[bin]] -name = "dev" -path = "src/dev.rs" [dependencies] wasm-bindgen = "0.2.100" @@ -40,5 +37,5 @@ rustflags = [ criterion = "0.5" [[bench]] -name = "render_bench" +name = "bench_rectangles" harness = false diff --git a/crates/cg/README.md b/crates/cg/README.md index bff1d0d120..759e1a5c61 100644 --- a/crates/cg/README.md +++ b/crates/cg/README.md @@ -1 +1,30 @@ # `cg` Grida Rendering Backend + +**2D Nodes** + +- [ ] TextSpan +- [ ] Text (Text with mixed styles) +- [ ] Image +- [ ] Bitmap (for bitmap drawing) +- [ ] Group +- [ ] Container +- [ ] Rectangle +- [ ] Ellipse +- [ ] Polygon +- [ ] RegularPolygon +- [ ] Path +- [ ] Line + +**Styles & Effects** + +- [ ] SolidPaint +- [ ] LinearGradientPaint +- [ ] RadialGradientPaint +- [ ] DropShadow +- [ ] BoxShadow +- [ ] BlendMode + +**Pipeline & API** + +- [ ] load font +- [ ] load image diff --git a/crates/cg/benches/render_bench.rs b/crates/cg/benches/bench_rectangles.rs similarity index 100% rename from crates/cg/benches/render_bench.rs rename to crates/cg/benches/bench_rectangles.rs diff --git a/crates/cg/examples/basic.rs b/crates/cg/examples/basic.rs new file mode 100644 index 0000000000..42feeb9aa0 --- /dev/null +++ b/crates/cg/examples/basic.rs @@ -0,0 +1,264 @@ +use cg::factory::NodeFactory; +use cg::repository::NodeRepository; +use cg::schema::*; +use cg::transform::AffineTransform; + +mod window; + +async fn demo_basic() -> Scene { + let font_caveat_path: &str = "resources/Caveat-VariableFont_wght.ttf"; + let font_caveat_data = window::fetch_font_data(font_caveat_path).await; + let font_caveat_family = "Caveat".to_string(); + let font_roboto_url = "https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf"; + let font_roboto_family = "Roboto".to_string(); + + // load the font + let font_data = window::fetch_font_data(font_roboto_url).await; + + // renderer.add_font(&font_caveat_data); + // renderer.add_font(&font_data); + + let nf = NodeFactory::new(); + + // Add a background rectangle node + let mut background_rect_node = nf.create_rectangle_node(); + background_rect_node.base.name = "Background Rect".to_string(); + background_rect_node.size = Size { + width: 1080.0, + height: 1080.0, + }; + background_rect_node.fill = Paint::Solid(SolidPaint { + color: Color(230, 240, 255, 255), // Light blue for visibility + }); + background_rect_node.stroke = Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 0), // No stroke + }); + background_rect_node.stroke_width = 0.0; + + // Preload image before timing + let demo_image_id = "demo_image"; + let demo_image_url = "https://grida.co/images/abstract-placeholder.jpg".to_string(); + let demo_image_data = window::fetch_image_data(&demo_image_url).await; + let image = skia_safe::Image::from_encoded(skia_safe::Data::new_copy(&demo_image_data)); + + // 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 + }); + 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 { + id: "gradient2".to_string(), + 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 + }, + ], + }); + 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(); + (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 + }); + polygon_node.stroke = Paint::Solid(SolidPaint { + color: Color(0, 0, 0, 255), // Black stroke + }); + 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 + }); + 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), + letter_spacing: None, + line_height: 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 + })); + 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 + }); + 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 + }); + 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.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 background_rect_id = background_rect_node.base.id.clone(); + 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(background_rect_node)); + 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![ + background_rect_id, + 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, + } +} + +#[tokio::main] +async fn main() { + let scene = demo_basic().await; + + window::run_demo_window(scene).await; +} diff --git a/crates/cg/examples/json.rs b/crates/cg/examples/json.rs new file mode 100644 index 0000000000..715049fa0f --- /dev/null +++ b/crates/cg/examples/json.rs @@ -0,0 +1,17 @@ +mod window; +use clap::Parser; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + // take path to json file + path: String, +} + +#[tokio::main] +async fn main() { + let cli = Cli::parse(); + let scene = window::load_scene_from_file(&cli.path).await; + + window::run_demo_window(scene).await; +} diff --git a/crates/cg/examples/nested.rs b/crates/cg/examples/nested.rs new file mode 100644 index 0000000000..1d6d0edcad --- /dev/null +++ b/crates/cg/examples/nested.rs @@ -0,0 +1,97 @@ +use cg::factory::NodeFactory; +use cg::repository::NodeRepository; +use cg::schema::*; +use cg::transform::AffineTransform; + +mod window; + +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), + }); + 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 + }); + 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 + }); + 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, + } +} + +#[tokio::main] +async fn main() { + let scene = demo_nested().await; + + window::run_demo_window(scene).await; +} diff --git a/crates/cg/examples/window.rs b/crates/cg/examples/window.rs new file mode 100644 index 0000000000..8f8f39bd48 --- /dev/null +++ b/crates/cg/examples/window.rs @@ -0,0 +1,326 @@ +use cg::draw::{Backend, Renderer}; +use cg::io::parse; +use cg::schema::*; +use cg::transform::AffineTransform; +use console_error_panic_hook::set_once as init_panic_hook; +use gl::types::*; +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; +#[allow(deprecated)] +use raw_window_handle::HasRawWindowHandle; +use reqwest; +use skia_safe::{Surface, gpu}; +use std::fs; +use std::{ffi::CString, num::NonZeroU32}; +use winit::{ + application::ApplicationHandler, + dpi::LogicalSize, + event::WindowEvent, + event_loop::{ControlFlow, EventLoop}, + window::{Window, WindowAttributes}, +}; + +pub async fn fetch_font_data(path: &str) -> Vec { + // read from file or url + if path.starts_with("http") { + let response = reqwest::get(path).await.unwrap(); + response.bytes().await.unwrap().to_vec() + } else { + fs::read(path).expect("failed to read file") + } +} + +pub async fn fetch_image_data(path: &str) -> Vec { + if path.starts_with("http") { + let response = reqwest::get(path).await.unwrap(); + response.bytes().await.unwrap().to_vec() + } else { + fs::read(path).expect("failed to read file") + } +} + +fn init_window( + _width: i32, + _height: i32, +) -> ( + *mut Surface, + EventLoop<()>, + Window, + GlutinSurface, + PossiblyCurrentContext, + glutin::config::Config, + gpu::gl::FramebufferInfo, + skia_safe::gpu::DirectContext, + f64, // scale factor +) { + init_panic_hook(); + + // Create event loop and window + let el = EventLoop::new().expect("Failed to create event loop"); + let window_attributes = WindowAttributes::default() + .with_title("Grida Canvas") + .with_inner_size(LogicalSize::new(1080, 1080)); + + // 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| { + // Find the config with the minimum number of samples. Usually Skia takes care of + // anti-aliasing and may not be able to create appropriate Surfaces for samples > 0. + // See https://github.com/rust-skia/rust-skia/issues/782 + // And https://github.com/rust-skia/rust-skia/issues/764 + 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(); + println!("Picked a config with {} samples", gl_config.num_samples()); + let window = window.expect("Could not create window with OpenGL context"); + let raw_window_handle = window + .raw_window_handle() + .expect("Failed to retrieve RawWindowHandle"); + + // --- DPI handling --- + let scale_factor = window.scale_factor(); + // --- + + // The context creation part. It can be created before surface and that's how + // it's expected in multithreaded + multiwindow operation mode, since you + // can send NotCurrentContext, but not Surface. + let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle)); + + // Since glutin by default tries to create OpenGL core context, which may not be + // present we should try gles. + 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()) + }); + + 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: 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, + ) +} + +struct App { + renderer: Renderer, + surface_ptr: *mut Surface, + gl_surface: GlutinSurface, + gl_context: PossiblyCurrentContext, +} + +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: WindowEvent, + ) { + match event { + WindowEvent::CloseRequested => { + self.renderer.free(); + event_loop.exit(); + } + WindowEvent::Resized(_) => { + // Ignore resize events + } + WindowEvent::RedrawRequested => { + // Do nothing - we only render once at startup + } + _ => {} + } + } +} + +pub async fn run_demo_window(scene: Scene) { + let width = 1080; + let height = 1080; + + let ( + surface_ptr, + el, + window, + gl_surface, + gl_context, + _gl_config, + _fb_info, + _gr_context, + scale_factor, + ) = init_window(width, height); + + // Log DPI and size info + let logical_size = window.inner_size(); + let physical_width = (logical_size.width as f64 * scale_factor).round() as u32; + let physical_height = (logical_size.height as f64 * scale_factor).round() as u32; + println!("[DPI DEBUG] scale_factor: {}", scale_factor); + println!( + "[DPI DEBUG] logical_size: {} x {}", + logical_size.width, logical_size.height + ); + println!( + "[DPI DEBUG] physical_size: {} x {}", + physical_width, physical_height + ); + + let mut renderer = Renderer::new(scale_factor as f32); + renderer.set_backend(Backend::GL(surface_ptr)); + + let mut app = App { + renderer, + surface_ptr, + gl_surface, + gl_context, + }; + + // Render once at startup + let surface = unsafe { &mut *app.surface_ptr }; + let canvas = surface.canvas(); + canvas.clear(skia_safe::Color::WHITE); + + app.renderer.render_scene(&scene); + app.renderer.flush(); + if let Err(e) = app.gl_surface.swap_buffers(&app.gl_context) { + eprintln!("Error swapping buffers: {:?}", e); + } + + // Set up the event loop to wait for events + el.set_control_flow(ControlFlow::Wait); + el.run_app(&mut app).expect("Failed to run event loop"); +} + +pub 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(), + } +} + +fn main() { + println!("No-op"); + // no-op +} diff --git a/crates/cg/src/dev.rs b/crates/cg/src/dev.rs deleted file mode 100644 index 85c533d280..0000000000 --- a/crates/cg/src/dev.rs +++ /dev/null @@ -1,615 +0,0 @@ -use cg::draw::{Backend, Renderer}; -use cg::factory::NodeFactory; -use cg::io::parse; -use cg::repository::NodeRepository; -use cg::schema::FeDropShadow; -use cg::schema::FilterEffect; -use cg::schema::*; -use cg::transform::AffineTransform; -use clap::{Parser, Subcommand}; -use console_error_panic_hook::set_once as init_panic_hook; -use gl::types::*; -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; -#[allow(deprecated)] -use raw_window_handle::HasRawWindowHandle; -use reqwest; -use skia_safe::{Image, Surface, gpu}; -use std::fs; -use std::{ - ffi::CString, - num::NonZeroU32, - time::{Duration, Instant}, -}; -use winit::{ - application::ApplicationHandler, - dpi::LogicalSize, - event::WindowEvent, - event_loop::{ControlFlow, EventLoop}, - window::{Window, WindowAttributes}, -}; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Load an example scene - Example { - /// Name of the example to load - name: String, - }, - /// Load a scene from a file - File { - /// Path to the file to load - path: String, - }, -} - -fn init_window( - _width: i32, - _height: i32, -) -> ( - *mut Surface, - EventLoop<()>, - Window, - GlutinSurface, - PossiblyCurrentContext, - glutin::config::Config, - gpu::gl::FramebufferInfo, - skia_safe::gpu::DirectContext, - f64, // scale factor -) { - init_panic_hook(); - - // Create event loop and window - let el = EventLoop::new().expect("Failed to create event loop"); - let window_attributes = WindowAttributes::default() - .with_title("Grida Canvas") - .with_inner_size(LogicalSize::new(1080, 1080)); - - // 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| { - // Find the config with the minimum number of samples. Usually Skia takes care of - // anti-aliasing and may not be able to create appropriate Surfaces for samples > 0. - // See https://github.com/rust-skia/rust-skia/issues/782 - // And https://github.com/rust-skia/rust-skia/issues/764 - 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(); - println!("Picked a config with {} samples", gl_config.num_samples()); - let window = window.expect("Could not create window with OpenGL context"); - let raw_window_handle = window - .raw_window_handle() - .expect("Failed to retrieve RawWindowHandle"); - - // --- DPI handling --- - let scale_factor = window.scale_factor(); - // --- - - // The context creation part. It can be created before surface and that's how - // it's expected in multithreaded + multiwindow operation mode, since you - // can send NotCurrentContext, but not Surface. - let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle)); - - // Since glutin by default tries to create OpenGL core context, which may not be - // present we should try gles. - 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()) - }); - - 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: 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, - ) -} - -struct App { - renderer: Renderer, - surface_ptr: *mut Surface, - gl_surface: GlutinSurface, - gl_context: PossiblyCurrentContext, -} - -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: WindowEvent, - ) { - match event { - WindowEvent::CloseRequested => { - self.renderer.free(); - event_loop.exit(); - } - WindowEvent::Resized(_) => { - // Ignore resize events - } - WindowEvent::RedrawRequested => { - // Do nothing - we only render once at startup - } - _ => {} - } - } -} - -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(), - } -} - -async fn demo_basic(renderer: &mut Renderer) -> Scene { - let font_caveat_path: &str = "resources/Caveat-VariableFont_wght.ttf"; - let font_caveat_data = fs::read(font_caveat_path).expect("failed to read file"); - let font_caveat_family = "Caveat".to_string(); - let font_roboto_url = "https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf"; - let font_roboto_family = "Roboto".to_string(); - - // load the font - let font_load_start = Instant::now(); - let font_data = reqwest::get(font_roboto_url) - .await - .unwrap() - .bytes() - .await - .unwrap(); - - renderer.add_font(&font_caveat_data); - renderer.add_font(&font_data); - - println!("Font load time: {:?}", font_load_start.elapsed()); - - let nf = NodeFactory::new(); - - // Add a background rectangle node - let mut background_rect_node = nf.create_rectangle_node(); - background_rect_node.base.name = "Background Rect".to_string(); - background_rect_node.size = Size { - width: 1080.0, - height: 1080.0, - }; - background_rect_node.fill = Paint::Solid(SolidPaint { - color: Color(230, 240, 255, 255), // Light blue for visibility - }); - background_rect_node.stroke = Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 0), // No stroke - }); - background_rect_node.stroke_width = 0.0; - - // Preload image before timing - let demo_image_id = "demo_image"; - let demo_image_url = "https://grida.co/images/abstract-placeholder.jpg".to_string(); - println!("Loading image..."); - let image_load_start = Instant::now(); - if let Ok(response) = reqwest::get(&demo_image_url).await { - if let Ok(bytes) = response.bytes().await { - if let Some(image) = Image::from_encoded(skia_safe::Data::new_copy(&bytes)) { - renderer.add_image(demo_image_id.to_string(), image); - } - } - } - println!("Image load time: {:?}", image_load_start.elapsed()); - - // 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 - }); - 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 { - id: "gradient2".to_string(), - 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 - }, - ], - }); - 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(); - (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 - }); - polygon_node.stroke = Paint::Solid(SolidPaint { - color: Color(0, 0, 0, 255), // Black stroke - }); - 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 - }); - 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), - letter_spacing: None, - line_height: 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 - })); - 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 - }); - 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 - }); - 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.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 background_rect_id = background_rect_node.base.id.clone(); - 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(background_rect_node)); - 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![ - background_rect_id, - 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, - } -} - -#[tokio::main] -async fn main() { - let cli = Cli::parse(); - let width = 1080; - let height = 1080; - - let ( - surface_ptr, - el, - window, - gl_surface, - gl_context, - _gl_config, - _fb_info, - _gr_context, - scale_factor, - ) = init_window(width, height); - - // Log DPI and size info - let logical_size = window.inner_size(); - let physical_width = (logical_size.width as f64 * scale_factor).round() as u32; - let physical_height = (logical_size.height as f64 * scale_factor).round() as u32; - println!("[DPI DEBUG] scale_factor: {}", scale_factor); - println!( - "[DPI DEBUG] logical_size: {} x {}", - logical_size.width, logical_size.height - ); - println!( - "[DPI DEBUG] physical_size: {} x {}", - physical_width, physical_height - ); - - let mut renderer = Renderer::new(scale_factor as f32); - - renderer.set_backend(Backend::GL(surface_ptr)); - - // Load the appropriate scene based on command line arguments - let scene = match cli.command { - Commands::Example { name } => match name.as_str() { - "basic" => demo_basic(&mut renderer).await, - _ => { - eprintln!("Unknown example: {}", name); - eprintln!("Available examples: basic"); - std::process::exit(1); - } - }, - Commands::File { path } => load_scene_from_file(&path).await, - }; - - let mut app = App { - renderer, - surface_ptr, - gl_surface, - gl_context, - }; - - // Render once at startup - let surface = unsafe { &mut *app.surface_ptr }; - let canvas = surface.canvas(); - canvas.clear(skia_safe::Color::WHITE); - - app.renderer.render_scene(&scene); - app.renderer.flush(); - if let Err(e) = app.gl_surface.swap_buffers(&app.gl_context) { - eprintln!("Error swapping buffers: {:?}", e); - } - - // Set up the event loop to wait for events - el.set_control_flow(ControlFlow::Wait); - el.run_app(&mut app).expect("Failed to run event loop"); -} diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index f6bf20f0e2..fe917c3ba4 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -97,6 +97,7 @@ impl Renderer { for child_id in &scene.children { self.render_node(child_id, &scene.nodes); } + canvas.restore(); } } From 52afa91496f6817f8744182c49419f4452de8b0f Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 23:01:24 +0900 Subject: [PATCH 041/262] update io --- Cargo.lock | 83 +++++++------------------------- crates/cg/Cargo.toml | 26 +++++----- crates/cg/src/io.rs | 110 +++++++++++++++++++++---------------------- 3 files changed, 86 insertions(+), 133 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32261c06e0..fc60824c57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "getrandom 0.3.3", "once_cell", "version_check", @@ -175,7 +175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cfg-if 1.0.0", + "cfg-if", "libc", "miniz_oxide", "object", @@ -306,12 +306,6 @@ 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" @@ -326,7 +320,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "cg" -version = "0.1.0" +version = "0.0.0" dependencies = [ "clap", "console_error_panic_hook", @@ -342,7 +336,6 @@ dependencies = [ "tokio", "uuid", "wasm-bindgen", - "wee_alloc", "winit", ] @@ -464,7 +457,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen", ] @@ -514,7 +507,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -650,7 +643,7 @@ version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -681,7 +674,7 @@ version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "libredox", "windows-sys 0.59.0", @@ -809,7 +802,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] @@ -820,7 +813,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", @@ -949,7 +942,7 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crunchy", ] @@ -1270,7 +1263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", - "cfg-if 1.0.0", + "cfg-if", "combine", "jni-sys", "log", @@ -1329,7 +1322,7 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "windows-targets 0.53.0", ] @@ -1383,12 +1376,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memory_units" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" - [[package]] name = "mime" version = "0.3.17" @@ -1788,7 +1775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ "bitflags 2.9.1", - "cfg-if 1.0.0", + "cfg-if", "foreign-types 0.3.2", "libc", "once_cell", @@ -1921,7 +1908,7 @@ version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", @@ -2113,7 +2100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if 1.0.0", + "cfg-if", "getrandom 0.2.16", "libc", "untrusted", @@ -2538,7 +2525,7 @@ dependencies = [ "arrayref", "arrayvec", "bytemuck", - "cfg-if 1.0.0", + "cfg-if", "log", "tiny-skia-path", ] @@ -2855,7 +2842,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -2881,7 +2868,7 @@ version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "once_cell", "wasm-bindgen", @@ -3049,34 +3036,6 @@ dependencies = [ "wasm-bindgen", ] -[[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-util" version = "0.1.9" @@ -3086,12 +3045,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[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-link" version = "0.1.1" diff --git a/crates/cg/Cargo.toml b/crates/cg/Cargo.toml index a50694c59c..1162a7b82a 100644 --- a/crates/cg/Cargo.toml +++ b/crates/cg/Cargo.toml @@ -1,32 +1,36 @@ [package] name = "cg" -version = "0.1.0" +version = "0.0.0" edition = "2024" [lib] crate-type = ["cdylib", "rlib"] - [dependencies] -wasm-bindgen = "0.2.100" -skia-safe = { version = "0.86.0", features = ["gpu", "gl", "textlayout"] } -console_error_panic_hook = "0.1.7" -wee_alloc = { version = "0.4.5", optional = true } +# Core dependencies +serde = "1.0.219" +serde_json = "1.0.140" +uuid = { version = "1.17.0", features = ["v4"] } + +# WASM-specific dependencies +wasm-bindgen = { version = "0.2.100", optional = true } +console_error_panic_hook = { version = "0.1.7", optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +# Native-only dependencies tokio = { version = "1", features = ["macros", "rt-multi-thread"] } reqwest = "0.12.19" -criterion = "0.5" +skia-safe = { version = "0.86.0", features = ["gpu", "gl", "textlayout"] } 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" -serde = "1.0.219" -serde_json = "1.0.140" clap = { version = "4.5.39", features = ["derive"] } -uuid = { version = "1.17.0", features = ["v4"] } [features] -default = ["wee_alloc"] +default = ["wasm-bindgen", "console_error_panic_hook"] +wasm = ["wasm-bindgen", "console_error_panic_hook"] [target.wasm32-unknown-emscripten] rustflags = [ diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index 747e31ba0e..ad7bc394d0 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -1,59 +1,54 @@ -use crate::schema::{ - BaseNode, BlendMode, Color as SchemaColor, ContainerNode as SchemaContainerNode, - EllipseNode as SchemaEllipseNode, FontWeight, GroupNode, Node as SchemaNode, NodeId, Paint, - PathNode, PolygonNode, RectangleNode, RectangularCornerRadius, Size, SolidPaint, TextAlign, - TextAlignVertical, TextDecoration, TextSpanNode, TextStyle, -}; +use crate::schema::*; use crate::transform::AffineTransform; use serde::Deserialize; use serde_json::Value; use std::collections::HashMap; #[derive(Debug, Deserialize)] -pub struct CanvasFile { +pub struct IOCanvasFile { pub version: String, - pub document: Document, + pub document: IODocument, } #[derive(Debug, Deserialize)] -pub struct Document { +pub struct IODocument { pub bitmaps: HashMap, pub properties: HashMap, - pub nodes: HashMap, - pub scenes: HashMap, + pub nodes: HashMap, + pub scenes: HashMap, pub entry_scene_id: Option, } #[derive(Debug, Deserialize)] -pub struct Scene { +pub struct IOScene { pub id: String, pub name: String, #[serde(rename = "type")] pub type_name: String, pub children: Vec, #[serde(rename = "backgroundColor")] - pub background_color: Option, + pub background_color: Option, pub guides: Option>, pub constraints: Option>, } #[derive(Debug, Deserialize)] #[serde(tag = "type")] -pub enum Node { +pub enum IONode { #[serde(rename = "container")] - Container(ContainerNode), + Container(IOContainerNode), #[serde(rename = "text")] - Text(TextNode), + Text(IOTextNode), #[serde(rename = "vector")] - Vector(VectorNode), + Vector(IOVectorNode), #[serde(rename = "ellipse")] - Ellipse(EllipseNode), + Ellipse(IOEllipseNode), #[serde(other)] Unknown, } #[derive(Debug, Deserialize)] -pub struct ContainerNode { +pub struct IOContainerNode { pub id: String, pub name: String, #[serde(default = "default_active")] @@ -131,7 +126,7 @@ where } #[derive(Debug, Deserialize)] -pub struct TextNode { +pub struct IOTextNode { pub id: String, pub name: String, #[serde(default = "default_active")] @@ -173,7 +168,7 @@ pub struct TextNode { } #[derive(Debug, Deserialize)] -pub struct VectorNode { +pub struct IOVectorNode { pub id: String, pub name: String, #[serde(default = "default_active")] @@ -192,11 +187,11 @@ pub struct VectorNode { pub width: f32, pub height: f32, pub fill: Option, - pub paths: Option>, + pub paths: Option>, } #[derive(Debug, Deserialize)] -pub struct EllipseNode { +pub struct IOEllipseNode { pub id: String, pub name: String, #[serde(default = "default_active")] @@ -226,7 +221,7 @@ pub struct EllipseNode { pub struct Fill { #[serde(rename = "type")] pub kind: String, - pub color: Option, + pub color: Option, } #[derive(Debug, Deserialize)] @@ -234,13 +229,13 @@ pub struct Border { #[serde(rename = "borderWidth")] pub border_width: Option, #[serde(rename = "borderColor")] - pub border_color: Option, + pub border_color: Option, #[serde(rename = "borderStyle")] pub border_style: Option, } #[derive(Debug, Deserialize)] -pub struct Path { +pub struct IOPath { pub d: String, #[serde(rename = "fillRule")] pub fill_rule: String, @@ -248,7 +243,7 @@ pub struct Path { } #[derive(Debug, Deserialize)] -pub struct Color { +pub struct RGBA { pub r: u8, pub g: u8, pub b: u8, @@ -284,13 +279,13 @@ fn default_font_weight() -> FontWeight { FontWeight::new(400) } -pub fn parse(file: &str) -> Result { +pub fn parse(file: &str) -> Result { serde_json::from_str(file) } -impl From for SchemaColor { - fn from(color: Color) -> Self { - SchemaColor(color.r, color.g, color.b, (color.a * 255.0) as u8) +impl From for Color { + fn from(color: RGBA) -> Self { + Color(color.r, color.g, color.b, (color.a * 255.0) as u8) } } @@ -301,27 +296,27 @@ impl From> for Paint { "solid" => { if let Some(color) = fill.color { Paint::Solid(SolidPaint { - color: SchemaColor(color.r, color.g, color.b, (color.a * 255.0) as u8), + color: Color(color.r, color.g, color.b, (color.a * 255.0) as u8), }) } else { Paint::Solid(SolidPaint { - color: SchemaColor(0, 0, 0, 0), + color: Color(0, 0, 0, 0), }) } } _ => Paint::Solid(SolidPaint { - color: SchemaColor(0, 0, 0, 0), + color: Color(0, 0, 0, 0), }), }, None => Paint::Solid(SolidPaint { - color: SchemaColor(0, 0, 0, 0), + color: Color(0, 0, 0, 0), }), } } } -impl From for SchemaContainerNode { - fn from(node: ContainerNode) -> Self { +impl From for ContainerNode { + fn from(node: IOContainerNode) -> Self { let width = match node.width { Value::Number(n) => n.as_f64().unwrap_or(0.0) as f32, _ => 0.0, @@ -330,7 +325,7 @@ impl From for SchemaContainerNode { Value::Number(n) => n.as_f64().unwrap_or(0.0) as f32, _ => 0.0, }; - SchemaContainerNode { + ContainerNode { base: BaseNode { id: node.id, name: node.name, @@ -352,8 +347,8 @@ impl From for SchemaContainerNode { } } -impl From for TextSpanNode { - fn from(node: TextNode) -> Self { +impl From for TextSpanNode { + fn from(node: IOTextNode) -> Self { let width = match node.width { Value::Number(n) => n.as_f64().unwrap_or(0.0) as f32, _ => 0.0, @@ -390,11 +385,11 @@ impl From for TextSpanNode { } } -impl From for SchemaNode { - fn from(node: EllipseNode) -> Self { +impl From for Node { + fn from(node: IOEllipseNode) -> Self { let transform = AffineTransform::new(node.left, node.top, node.rotation); - SchemaNode::Ellipse(SchemaEllipseNode { + Node::Ellipse(EllipseNode { base: BaseNode { id: node.id, name: node.name, @@ -408,7 +403,7 @@ impl From for SchemaNode { }, fill: node.fill.into(), stroke: Paint::Solid(SolidPaint { - color: SchemaColor(0, 0, 0, 255), + color: Color(0, 0, 0, 255), }), stroke_width: node.stroke_width.unwrap_or(0.0), opacity: node.opacity, @@ -416,12 +411,12 @@ impl From for SchemaNode { } } -impl From for SchemaNode { - fn from(node: VectorNode) -> Self { +impl From for Node { + fn from(node: IOVectorNode) -> Self { let transform = AffineTransform::new(node.left, node.top, node.rotation); // For vector nodes, we'll create a path node with the path data - SchemaNode::Path(PathNode { + Node::Path(PathNode { base: BaseNode { id: node.id, name: node.name, @@ -437,7 +432,7 @@ impl From for SchemaNode { .join(" ") }), stroke: Paint::Solid(SolidPaint { - color: SchemaColor(0, 0, 0, 255), + color: Color(0, 0, 0, 255), }), stroke_width: 0.0, opacity: node.opacity, @@ -445,14 +440,14 @@ impl From for SchemaNode { } } -impl From for SchemaNode { - fn from(node: Node) -> Self { +impl From for Node { + fn from(node: IONode) -> Self { match node { - Node::Container(container) => SchemaNode::Container(container.into()), - Node::Text(text) => SchemaNode::TextSpan(text.into()), - Node::Vector(vector) => vector.into(), - Node::Ellipse(ellipse) => ellipse.into(), - Node::Unknown => SchemaNode::Group(GroupNode { + IONode::Container(container) => Node::Container(container.into()), + IONode::Text(text) => Node::TextSpan(text.into()), + IONode::Vector(vector) => vector.into(), + IONode::Ellipse(ellipse) => ellipse.into(), + IONode::Unknown => Node::Group(GroupNode { base: BaseNode { id: "unknown".to_string(), name: "Unknown Node".to_string(), @@ -474,8 +469,9 @@ mod tests { #[test] fn parse_canvas_json() { - let data = fs::read_to_string("resources/document.json").expect("failed to read file"); - let parsed: CanvasFile = serde_json::from_str(&data).expect("failed to parse JSON"); + let data = + fs::read_to_string("resources/local/document.json").expect("failed to read file"); + let parsed: IOCanvasFile = serde_json::from_str(&data).expect("failed to parse JSON"); assert_eq!(parsed.version, "0.0.1-beta.1+20250303"); assert!( From fd125046490ff9f6fe1cec0b8108f86f44e1e2ee Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 23:15:41 +0900 Subject: [PATCH 042/262] add primitive shapes example --- crates/cg/examples/shapes.rs | 180 +++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 crates/cg/examples/shapes.rs diff --git a/crates/cg/examples/shapes.rs b/crates/cg/examples/shapes.rs new file mode 100644 index 0000000000..b2d242586a --- /dev/null +++ b/crates/cg/examples/shapes.rs @@ -0,0 +1,180 @@ +use cg::factory::NodeFactory; +use cg::repository::NodeRepository; +use cg::schema::*; +use cg::transform::AffineTransform; + +mod window; + +async fn demo_shapes() -> Scene { + let nf = NodeFactory::new(); + + // Add a background rectangle node + let mut background_rect_node = nf.create_rectangle_node(); + background_rect_node.base.name = "Background Rect".to_string(); + background_rect_node.size = Size { + width: 1080.0, + height: 1080.0, + }; + background_rect_node.fill = Paint::Solid(SolidPaint { + color: Color(240, 240, 240, 255), // Light gray background + }); + + // Create a root container node + let mut root_container_node = nf.create_container_node(); + root_container_node.base.name = "Root Container".to_string(); + + let mut repository = NodeRepository::new(); + let background_rect_id = background_rect_node.base.id.clone(); + repository.insert(Node::Rectangle(background_rect_node)); + + 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 + }); + 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 + }); + 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(); + (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.fill = Paint::Solid(SolidPaint { + color: Color( + 200 - (i * 20) as u8, + 200 - (i * 20) as u8, + 200 - (i * 20) as u8, + 255, + ), // Fading gray + }); + 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 + }); + 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 + }); + all_shape_ids.push(path.base.id.clone()); + repository.insert(Node::Path(path)); + } + + // Set up the root container + root_container_node.children = vec![background_rect_id]; + 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, + } +} + +#[tokio::main] +async fn main() { + let scene = demo_shapes().await; + window::run_demo_window(scene).await; +} From 88a5dc5c51f10280fa2dabdc579ea8665b61e352 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 23:44:51 +0900 Subject: [PATCH 043/262] ellipse position --- crates/cg/src/draw.rs | 4 ++-- crates/cg/src/schema.rs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index fe917c3ba4..f4dbfc1f5b 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -263,8 +263,8 @@ impl Renderer { (node.size.width, node.size.height), ); let rect = Rect::from_xywh( - -node.size.width / 2.0, - -node.size.height / 2.0, + 0.0, // x starts at 0 (top-left) + 0.0, // y starts at 0 (top-left) node.size.width, node.size.height, ); diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 71fe9d8766..ab3f475b1a 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -412,6 +412,10 @@ pub struct ImageNode { pub _ref: String, } +/// A node representing an ellipse shape. +/// +/// Like RectangleNode, uses a top-left based coordinate system (x,y,width,height). +/// The ellipse is drawn within the bounding box defined by these coordinates. #[derive(Debug, Clone)] pub struct EllipseNode { pub base: BaseNode, From 4f25aab381093cbd22ee1029609e096bde15e398 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 4 Jun 2025 23:56:48 +0900 Subject: [PATCH 044/262] add text example --- crates/cg/examples/texts.rs | 138 ++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 crates/cg/examples/texts.rs diff --git a/crates/cg/examples/texts.rs b/crates/cg/examples/texts.rs new file mode 100644 index 0000000000..5013bfe30e --- /dev/null +++ b/crates/cg/examples/texts.rs @@ -0,0 +1,138 @@ +use cg::factory::NodeFactory; +use cg::repository::NodeRepository; +use cg::schema::*; +use cg::transform::AffineTransform; + +mod window; + +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. +"#; + +async fn demo_texts() -> Scene { + let nf = NodeFactory::new(); + + // Create a background rectangle node + let mut background_rect_node = nf.create_rectangle_node(); + background_rect_node.base.name = "Background Rect".to_string(); + background_rect_node.size = Size { + width: 1080.0, + height: 1080.0, + }; + background_rect_node.fill = Paint::Solid(SolidPaint { + color: Color(230, 240, 255, 255), // Light blue background + }); + + // 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, + font_weight: FontWeight::new(700), // Bold + letter_spacing: None, + line_height: None, + }; + word_text_node.stroke = Some(Paint::Solid(SolidPaint { + color: Color(255, 255, 255, 255), + })); + 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, 100.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, + font_weight: FontWeight::new(400), // Regular + letter_spacing: None, + line_height: 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, 150.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, + font_weight: FontWeight::new(400), // Regular + letter_spacing: None, + line_height: Some(1.5), // 1.5 line height for better readability + }; + paragraph_text_node.text_align = TextAlign::Left; + paragraph_text_node.text_align_vertical = TextAlignVertical::Top; + + // Create a root container node + let mut root_container_node = nf.create_container_node(); + root_container_node.base.name = "Root Container".to_string(); + + // Create a node repository and add all nodes + let mut repository = NodeRepository::new(); + + // Collect all the IDs + let background_rect_id = background_rect_node.base.id.clone(); + 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(); + + // Add all nodes to the repository + repository.insert(Node::Rectangle(background_rect_node)); + repository.insert(Node::TextSpan(word_text_node)); + repository.insert(Node::TextSpan(sentence_text_node)); + repository.insert(Node::TextSpan(paragraph_text_node)); + + // Set up the root container with all IDs + root_container_node.children = vec![ + background_rect_id, + word_text_id, + sentence_text_id, + 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, + } +} + +#[tokio::main] +async fn main() { + let scene = demo_texts().await; + window::run_demo_window(scene).await; +} From b567b707490765fad5d5f233db7f8a63b8b8be93 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 16:07:20 +0900 Subject: [PATCH 045/262] regular star node --- crates/cg/README.md | 1 + crates/cg/examples/shapes.rs | 23 +++++++++++ crates/cg/src/draw.rs | 10 ++++- crates/cg/src/factory.rs | 15 +++++++ crates/cg/src/repository.rs | 1 + crates/cg/src/schema.rs | 79 ++++++++++++++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 2 deletions(-) diff --git a/crates/cg/README.md b/crates/cg/README.md index 759e1a5c61..11d71f9b4c 100644 --- a/crates/cg/README.md +++ b/crates/cg/README.md @@ -12,6 +12,7 @@ - [ ] Ellipse - [ ] Polygon - [ ] RegularPolygon +- [ ] RegularStarPolygon - [ ] Path - [ ] Line diff --git a/crates/cg/examples/shapes.rs b/crates/cg/examples/shapes.rs index b2d242586a..d1f5717db7 100644 --- a/crates/cg/examples/shapes.rs +++ b/crates/cg/examples/shapes.rs @@ -158,6 +158,29 @@ async fn demo_shapes() -> Scene { 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.3 + (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 + }); + all_shape_ids.push(star.base.id.clone()); + repository.insert(Node::RegularStarPolygon(star)); + } + // Set up the root container root_container_node.children = vec![background_rect_id]; root_container_node.children.extend(all_shape_ids); diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index f4dbfc1f5b..930e0a80c1 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -2,8 +2,8 @@ use crate::repository::NodeRepository; use crate::schema::{ BlendMode, Color as SchemaColor, ContainerNode, EllipseNode, FilterEffect, FontWeight, GradientStop, GroupNode, ImageNode, LineNode, Node, NodeId, Paint, PathNode, PolygonNode, - RectangleNode, RectangularCornerRadius, RegularPolygonNode, Scene, TextAlign, - TextAlignVertical, TextDecoration, TextNode, TextSpanNode, + RectangleNode, RectangularCornerRadius, RegularPolygonNode, RegularStarPolygonNode, Scene, + TextAlign, TextAlignVertical, TextDecoration, TextNode, TextSpanNode, }; use skia_safe::{ Color, Font, FontMgr, FontStyle, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, @@ -118,6 +118,7 @@ impl Renderer { Node::Line(node) => self.draw_line_node(node), Node::Image(node) => self.draw_image_node(node), Node::Path(node) => self.draw_path_node(node), + Node::RegularStarPolygon(node) => self.draw_regular_star_polygon_node(node), } } @@ -668,6 +669,11 @@ impl Renderer { canvas.restore(); } } + + pub fn draw_regular_star_polygon_node(&self, node: &RegularStarPolygonNode) { + let poly = node.to_polygon(); + self.draw_polygon_node(&poly); + } } fn sk_matrix(m: [[f32; 3]; 2]) -> skia_safe::Matrix { diff --git a/crates/cg/src/factory.rs b/crates/cg/src/factory.rs index 2d5c20d717..6017681e7d 100644 --- a/crates/cg/src/factory.rs +++ b/crates/cg/src/factory.rs @@ -169,6 +169,21 @@ impl NodeFactory { } } + pub fn create_regular_star_polygon_node(&self) -> RegularStarPolygonNode { + RegularStarPolygonNode { + base: self.default_base_node(), + transform: AffineTransform::identity(), + size: Self::DEFAULT_SIZE, + point_count: 5, // 5-pointed star by default + inner_radius: 0.4, // Default inner radius + fill: Self::default_solid_paint(Self::DEFAULT_COLOR), + stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), + stroke_width: Self::DEFAULT_STROKE_WIDTH, + opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + } + } + pub fn create_polygon_node(&self) -> PolygonNode { PolygonNode { base: self.default_base_node(), diff --git a/crates/cg/src/repository.rs b/crates/cg/src/repository.rs index 003b295aaa..7da9a1e964 100644 --- a/crates/cg/src/repository.rs +++ b/crates/cg/src/repository.rs @@ -26,6 +26,7 @@ impl NodeRepository { Node::Ellipse(n) => n.base.id.clone(), Node::Polygon(n) => n.base.id.clone(), Node::RegularPolygon(n) => n.base.id.clone(), + Node::RegularStarPolygon(n) => n.base.id.clone(), Node::Line(n) => n.base.id.clone(), Node::TextSpan(n) => n.base.id.clone(), Node::Path(n) => n.base.id.clone(), diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index ab3f475b1a..a6554cdca5 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -335,6 +335,7 @@ pub enum Node { Ellipse(EllipseNode), Polygon(PolygonNode), RegularPolygon(RegularPolygonNode), + RegularStarPolygon(RegularStarPolygonNode), Line(LineNode), TextSpan(TextSpanNode), Path(PathNode), @@ -548,6 +549,84 @@ impl RegularPolygonNode { } } +/// A regular star polygon node rendered within a bounding box. +/// +/// This node represents a geometric star shape composed of alternating outer and inner vertices evenly spaced around a center, +/// forming a symmetric star with `point_count` spikes. Each spike is constructed by alternating between an outer point +/// (determined by the bounding box) and an inner point (scaled by `inner_radius`). +/// +/// For details on star polygon mathematics, see: +#[derive(Debug, Clone)] +pub struct RegularStarPolygonNode { + /// Core identity + metadata + pub base: BaseNode, + + /// Affine transform applied to this node + pub transform: AffineTransform, + + /// Bounding box size the polygon is fit into + pub size: Size, + + /// Number of equally spaced points (>= 3) + pub point_count: usize, + + /// The `inner_radius` defines the radius of the inner vertices of the star, relative to the center. + /// + /// It controls the sharpness of the star's angles: + /// - A smaller value (closer to 0) results in sharper, spikier points. + /// - A larger value (closer to or greater than the outer radius) makes the shape closer to a regular polygon with 2 × point_count edges. + /// + /// The outer radius is defined by the bounding box (`size`), while the `inner_radius` places the inner points on a second concentric circle. + /// Unlike `corner_radius`, which affects the rounding of outer corners, `inner_radius` controls the depth of the inner angles between the points. + pub inner_radius: f32, + + /// Fill paint (solid or gradient) + pub fill: Paint, + + /// The stroke paint used to outline the polygon. + pub stroke: Paint, + + /// The stroke width used to outline the polygon. + pub stroke_width: f32, + + /// Overall node opacity (0.0–1.0) + pub opacity: f32, + pub blend_mode: BlendMode, +} + +impl RegularStarPolygonNode { + pub fn to_polygon(&self) -> PolygonNode { + let w = self.size.width; + let h = self.size.height; + let cx = w / 2.0; + let cy = h / 2.0; + let outer_r = cx.min(cy); + let inner_r = outer_r * self.inner_radius; + let step = std::f32::consts::PI / self.point_count as f32; + let start_angle = -std::f32::consts::PI / 2.0; + + let mut points = Vec::with_capacity(self.point_count * 2); + for i in 0..(self.point_count * 2) { + let angle = start_angle + i as f32 * step; + let r = if i % 2 == 0 { outer_r } else { inner_r }; + let x = cx + r * angle.cos(); + let y = cy + r * angle.sin(); + points.push((x, y)); + } + + PolygonNode { + base: self.base.clone(), + transform: self.transform, + points, + fill: self.fill.clone(), + stroke: self.stroke.clone(), + stroke_width: self.stroke_width, + opacity: self.opacity, + blend_mode: self.blend_mode, + } + } +} + /// A node representing a plain text block (non-rich). /// For multi-style content, see `RichTextNode` (not implemented yet). #[derive(Debug, Clone)] From 5b6dc7436ae70bfb31d621a0b89c7e048d39698c Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 16:54:18 +0900 Subject: [PATCH 046/262] point strcut --- crates/cg/examples/basic.rs | 2 +- crates/cg/examples/shapes.rs | 4 +- crates/cg/src/draw.rs | 8 +-- crates/cg/src/factory.rs | 3 + crates/cg/src/lib.rs | 1 + crates/cg/src/schema.rs | 82 +++++++++++++++-------- crates/cg/src/sk_polygon_corner_radius.rs | 77 +++++++++++++++++++++ 7 files changed, 143 insertions(+), 34 deletions(-) create mode 100644 crates/cg/src/sk_polygon_corner_radius.rs diff --git a/crates/cg/examples/basic.rs b/crates/cg/examples/basic.rs index 42feeb9aa0..a2f23ee9d1 100644 --- a/crates/cg/examples/basic.rs +++ b/crates/cg/examples/basic.rs @@ -115,7 +115,7 @@ async fn demo_basic() -> Scene { let radius = 100.0; let x = radius * angle.cos(); let y = radius * angle.sin(); - (x, y) + Point { x, y } }) .collect::>(); diff --git a/crates/cg/examples/shapes.rs b/crates/cg/examples/shapes.rs index d1f5717db7..5c8d5178f8 100644 --- a/crates/cg/examples/shapes.rs +++ b/crates/cg/examples/shapes.rs @@ -86,7 +86,7 @@ async fn demo_shapes() -> Scene { let radius = base_size / 2.0; let x = radius * angle.cos(); let y = radius * angle.sin(); - (x, y) + Point { x, y } }) .collect::>(); @@ -168,7 +168,7 @@ async fn demo_shapes() -> Scene { height: base_size, }; star.point_count = 3 + i; // 3 to 12 points - star.inner_radius = 0.3 + (i as f32 * 0.05); // 0.3 to 0.75 inner radius + 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, diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 930e0a80c1..ddbd7c6737 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -344,10 +344,10 @@ impl Renderer { let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); let mut path = skia_safe::Path::new(); let mut points_iter = node.points.iter(); - if let Some(&(x0, y0)) = points_iter.next() { - path.move_to((x0, y0)); - for &(x, y) in points_iter { - path.line_to((x, y)); + if let Some(&point) = points_iter.next() { + path.move_to((point.x, point.y)); + for point in points_iter { + path.line_to((point.x, point.y)); } path.close(); } diff --git a/crates/cg/src/factory.rs b/crates/cg/src/factory.rs index 6017681e7d..fb2ba9f42e 100644 --- a/crates/cg/src/factory.rs +++ b/crates/cg/src/factory.rs @@ -161,6 +161,7 @@ impl NodeFactory { transform: AffineTransform::identity(), size: Self::DEFAULT_SIZE, point_count: 3, // Triangle by default + corner_radius: 0.0, fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, @@ -176,6 +177,7 @@ impl NodeFactory { size: Self::DEFAULT_SIZE, point_count: 5, // 5-pointed star by default inner_radius: 0.4, // Default inner radius + corner_radius: 0.0, fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, @@ -189,6 +191,7 @@ impl NodeFactory { base: self.default_base_node(), transform: AffineTransform::identity(), points: Vec::new(), + corner_radius: 0.0, fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, diff --git a/crates/cg/src/lib.rs b/crates/cg/src/lib.rs index 1e7a01f1ec..309f454ede 100644 --- a/crates/cg/src/lib.rs +++ b/crates/cg/src/lib.rs @@ -3,4 +3,5 @@ pub mod factory; pub mod io; pub mod repository; pub mod schema; +pub mod sk_polygon_corner_radius; pub mod transform; diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index a6554cdca5..54d9defbdd 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -1,11 +1,18 @@ use crate::repository::NodeRepository; +use crate::sk_polygon_corner_radius; use crate::transform::AffineTransform; use core::str; use serde::Deserialize; -use std::f32::consts::PI; pub type NodeId = String; +/// A 2D point with x and y coordinates. +#[derive(Debug, Clone, Copy)] +pub struct Point { + pub x: f32, + pub y: f32, +} + #[derive(Debug, Clone, Copy)] pub struct Color(pub u8, pub u8, pub u8, pub u8); @@ -429,6 +436,20 @@ pub struct EllipseNode { pub blend_mode: BlendMode, } +/// +/// SVG Path compatible path node. +/// +#[derive(Debug, Clone)] +pub struct PathNode { + pub base: BaseNode, + pub transform: AffineTransform, + pub fill: Paint, + pub data: String, + pub stroke: Paint, + pub stroke_width: f32, + pub opacity: f32, +} + /// A polygon shape defined by a list of absolute 2D points, following the SVG `` model. /// /// ## Characteristics @@ -446,8 +467,11 @@ pub struct PolygonNode { /// 2D affine transform matrix applied to the shape. pub transform: AffineTransform, - /// The list of absolute coordinates (x, y) defining the polygon vertices. - pub points: Vec<(f32, f32)>, + /// The list of points defining the polygon vertices. + pub points: Vec, + + /// The corner radius of the polygon. + pub corner_radius: f32, /// The paint used to fill the interior of the polygon. pub fill: Paint, @@ -463,18 +487,10 @@ pub struct PolygonNode { pub blend_mode: BlendMode, } -/// -/// SVG Path compatible path node. -/// -#[derive(Debug, Clone)] -pub struct PathNode { - pub base: BaseNode, - pub transform: AffineTransform, - pub fill: Paint, - pub data: String, - pub stroke: Paint, - pub stroke_width: f32, - pub opacity: f32, +impl PolygonNode { + pub fn to_path(&self) -> skia_safe::Path { + sk_polygon_corner_radius::rounded_polygon_path(&self.points, self.corner_radius) + } } /// A node representing a regular polygon (triangle, square, pentagon, etc.) @@ -487,6 +503,8 @@ pub struct PathNode { /// - Even `point_count` aligns the top edge flat. /// /// The actual rendering is derived, not stored. Rotation should be applied via `transform`. +/// +/// For details on regular polygon mathematics, see: (implementation varies) #[derive(Debug, Clone)] pub struct RegularPolygonNode { /// Core identity + metadata @@ -501,6 +519,9 @@ pub struct RegularPolygonNode { /// Number of equally spaced points (>= 3) pub point_count: usize, + /// The corner radius of the polygon. + pub corner_radius: f32, + /// Fill paint (solid or gradient) pub fill: Paint, @@ -517,22 +538,24 @@ pub struct RegularPolygonNode { impl RegularPolygonNode { pub fn to_polygon(&self) -> PolygonNode { - let cx = self.size.width / 2.0; - let cy = self.size.height / 2.0; - let r = cx.min(cy); // fit within bounding box - + let w = self.size.width; + let h = self.size.height; + let cx = w / 2.0; + let cy = h / 2.0; + let r = w.min(h) / 2.0; let angle_offset = if self.point_count % 2 == 0 { - PI / self.point_count as f32 + std::f32::consts::PI / self.point_count as f32 } else { - -PI / 2.0 + -std::f32::consts::PI / 2.0 }; - let points: Vec<(f32, f32)> = (0..self.point_count) + let points: Vec = (0..self.point_count) .map(|i| { - let angle = (i as f32 / self.point_count as f32) * 2.0 * PI + angle_offset; - let x = cx + r * angle.cos(); - let y = cy + r * angle.sin(); - (x, y) + let theta = (i as f32 / self.point_count as f32) * 2.0 * std::f32::consts::PI + + angle_offset; + let x = cx + r * theta.cos(); + let y = cy + r * theta.sin(); + Point { x, y } }) .collect(); @@ -540,6 +563,7 @@ impl RegularPolygonNode { base: self.base.clone(), transform: self.transform, points, + corner_radius: self.corner_radius, fill: self.fill.clone(), stroke: self.stroke.clone(), stroke_width: self.stroke_width, @@ -580,6 +604,9 @@ pub struct RegularStarPolygonNode { /// Unlike `corner_radius`, which affects the rounding of outer corners, `inner_radius` controls the depth of the inner angles between the points. pub inner_radius: f32, + /// The corner radius of the polygon. + pub corner_radius: f32, + /// Fill paint (solid or gradient) pub fill: Paint, @@ -611,13 +638,14 @@ impl RegularStarPolygonNode { let r = if i % 2 == 0 { outer_r } else { inner_r }; let x = cx + r * angle.cos(); let y = cy + r * angle.sin(); - points.push((x, y)); + points.push(Point { x, y }); } PolygonNode { base: self.base.clone(), transform: self.transform, points, + corner_radius: self.corner_radius, fill: self.fill.clone(), stroke: self.stroke.clone(), stroke_width: self.stroke_width, diff --git a/crates/cg/src/sk_polygon_corner_radius.rs b/crates/cg/src/sk_polygon_corner_radius.rs new file mode 100644 index 0000000000..5c26f286cd --- /dev/null +++ b/crates/cg/src/sk_polygon_corner_radius.rs @@ -0,0 +1,77 @@ +use crate::schema::Point; +use skia_safe; + +// Helper function to handle vector operations +fn vector_ops(a: Point, b: Point, scale: f32) -> Point { + Point { + x: a.x - b.x * scale, + y: a.y - b.y * scale, + } +} + +// Given: +// - `pts`: Vec with your polygon's vertices in order +// - `r`: the corner‐radius +// +// Build a Path that walks each edge but rounds each "sharp" corner: +pub fn rounded_polygon_path(pts: &[Point], r: f32) -> skia_safe::Path { + let n = pts.len(); + assert!(n >= 3); + + let mut path = skia_safe::Path::new(); + + // Start at the first vertex, but moveTo a point + // that's `r` away from the first corner along the first edge. + // (We'll compute those "offset" points below.) + + // Compute the "offset" point on the last edge that leads into pts[0]: + let last = pts[n - 1]; + let first = pts[0]; + + // 1) Find direction from last→first, then move `r` along that: + let dir_a = Point { + x: (first.x - last.x) / ((first.x - last.x).powi(2) + (first.y - last.y).powi(2)).sqrt(), + y: (first.y - last.y) / ((first.x - last.x).powi(2) + (first.y - last.y).powi(2)).sqrt(), + }; + let move_into_first = vector_ops(first, dir_a, r); + + path.move_to(skia_safe::Point::new(move_into_first.x, move_into_first.y)); + + for i in 0..n { + // Current "corner" is pts[i], + // "incoming" edge is (pts[i−1] → pts[i]), + // "outgoing" edge is (pts[i] → pts[i+1]). + let curr = pts[i]; + let prev = pts[(i + n - 1) % n]; + let next = pts[(i + 1) % n]; + + // Compute offset along incoming edge (to where arc starts): + let dir_in = Point { + x: (curr.x - prev.x) / ((curr.x - prev.x).powi(2) + (curr.y - prev.y).powi(2)).sqrt(), + y: (curr.y - prev.y) / ((curr.x - prev.x).powi(2) + (curr.y - prev.y).powi(2)).sqrt(), + }; + let start_arc = vector_ops(curr, dir_in, r); + + // Compute offset along outgoing edge (to where arc ends): + let dir_out = Point { + x: (next.x - curr.x) / ((next.x - curr.x).powi(2) + (next.y - curr.y).powi(2)).sqrt(), + y: (next.y - curr.y) / ((next.x - curr.x).powi(2) + (next.y - curr.y).powi(2)).sqrt(), + }; + let end_arc = Point { + x: curr.x + dir_out.x * r, + y: curr.y + dir_out.y * r, + }; + + // Line from previous offset → start_arc + path.line_to(skia_safe::Point::new(start_arc.x, start_arc.y)); + + // Add the rounded corner (arc) from start_arc → end_arc, tangent at curr: + path.quad_to( + skia_safe::Point::new(curr.x, curr.y), + skia_safe::Point::new(end_arc.x, end_arc.y), + ); + } + + path.close(); + path +} From 0052b43720d395c3819eac8af6e6f9e97deb55c8 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 16:57:52 +0900 Subject: [PATCH 047/262] polygon corner radius --- crates/cg/examples/shapes.rs | 1 + crates/cg/src/draw.rs | 29 ++++++++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/cg/examples/shapes.rs b/crates/cg/examples/shapes.rs index 5c8d5178f8..3b76af4e3a 100644 --- a/crates/cg/examples/shapes.rs +++ b/crates/cg/examples/shapes.rs @@ -94,6 +94,7 @@ async fn demo_shapes() -> Scene { 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, diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index ddbd7c6737..b1891bcef8 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -342,21 +342,31 @@ impl Renderer { return; } let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); - let mut path = skia_safe::Path::new(); - let mut points_iter = node.points.iter(); - if let Some(&point) = points_iter.next() { - path.move_to((point.x, point.y)); - for point in points_iter { - path.line_to((point.x, point.y)); - } - path.close(); - } canvas.save(); canvas.concat(&sk_matrix(node.transform.matrix)); + + // If corner_radius > 0, use the rounded polygon path + let path = if node.corner_radius > 0.0 { + node.to_path() + } else { + // Otherwise create a regular polygon path + let mut path = skia_safe::Path::new(); + let mut points_iter = node.points.iter(); + if let Some(&point) = points_iter.next() { + path.move_to((point.x, point.y)); + for point in points_iter { + path.line_to((point.x, point.y)); + } + path.close(); + } + path + }; + // Draw fill let mut fill_paint = fill_paint.clone(); fill_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &fill_paint); + // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); @@ -365,6 +375,7 @@ impl Renderer { stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &stroke_paint); } + canvas.restore(); } } From e17d40ccb03cdf6a54342a917a5482331500bcfc Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 17:22:05 +0900 Subject: [PATCH 048/262] camera --- crates/cg/README.md | 22 ++++++++++- crates/cg/examples/window.rs | 72 ++++++++++++++++++++++++++++++------ crates/cg/src/camera.rs | 47 +++++++++++++++++++++++ crates/cg/src/draw.rs | 34 +++++++++++++---- crates/cg/src/lib.rs | 1 + crates/cg/src/transform.rs | 22 +++++++++++ 6 files changed, 177 insertions(+), 21 deletions(-) create mode 100644 crates/cg/src/camera.rs diff --git a/crates/cg/README.md b/crates/cg/README.md index 11d71f9b4c..d3e7bdf205 100644 --- a/crates/cg/README.md +++ b/crates/cg/README.md @@ -1,5 +1,7 @@ # `cg` Grida Rendering Backend +## Rendering + **2D Nodes** - [ ] TextSpan @@ -7,15 +9,21 @@ - [ ] Image - [ ] Bitmap (for bitmap drawing) - [ ] Group -- [ ] Container +- [ ] Container (Frame) - [ ] Rectangle - [ ] Ellipse - [ ] Polygon - [ ] RegularPolygon - [ ] RegularStarPolygon -- [ ] Path +- [ ] Path (SVG Path) +- [ ] Vector (Vector Network) - [ ] Line +**Meta** + +- [ ] Mask +- [ ] Clip + **Styles & Effects** - [ ] SolidPaint @@ -25,7 +33,17 @@ - [ ] BoxShadow - [ ] BlendMode +## API + +**Camera** + +- [ ] 2D Camera + **Pipeline & API** - [ ] load font - [ ] load image + +## Interactivity + +- [ ] Hit testing diff --git a/crates/cg/examples/window.rs b/crates/cg/examples/window.rs index 8f8f39bd48..62cd87bf72 100644 --- a/crates/cg/examples/window.rs +++ b/crates/cg/examples/window.rs @@ -1,3 +1,4 @@ +use cg::camera::Camera; use cg::draw::{Backend, Renderer}; use cg::io::parse; use cg::schema::*; @@ -22,7 +23,7 @@ use std::{ffi::CString, num::NonZeroU32}; use winit::{ application::ApplicationHandler, dpi::LogicalSize, - event::WindowEvent, + event::{ElementState, MouseScrollDelta, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::{Window, WindowAttributes}, }; @@ -213,6 +214,8 @@ struct App { surface_ptr: *mut Surface, gl_surface: GlutinSurface, gl_context: PossiblyCurrentContext, + camera: Camera, + scene: Scene, } impl ApplicationHandler for App { @@ -232,14 +235,57 @@ impl ApplicationHandler for App { WindowEvent::Resized(_) => { // Ignore resize events } + WindowEvent::MouseWheel { delta, .. } => { + match delta { + MouseScrollDelta::LineDelta(x, y) => { + // Handle pan only - inverted direction + let pan_speed = 10.0; + let current_x = self.camera.transform.x(); + let current_y = self.camera.transform.y(); + self.camera + .set_position(current_x + x * pan_speed, current_y + y * pan_speed); + } + MouseScrollDelta::PixelDelta(delta) => { + // Handle pan only - inverted direction + let pan_speed = 0.5; + let current_x = self.camera.transform.x(); + let current_y = self.camera.transform.y(); + self.camera.set_position( + current_x + delta.x as f32 * pan_speed, + current_y + delta.y as f32 * pan_speed, + ); + } + } + + // Update camera in renderer + self.renderer.set_camera(self.camera.clone()); + + // Redraw + self.redraw(); + } WindowEvent::RedrawRequested => { - // Do nothing - we only render once at startup + self.redraw(); } _ => {} } } } +impl App { + fn redraw(&mut self) { + let surface = unsafe { &mut *self.surface_ptr }; + let canvas = surface.canvas(); + canvas.clear(skia_safe::Color::WHITE); + + self.renderer.render_scene(&self.scene); + self.renderer.flush(); + + if let Err(e) = self.gl_surface.swap_buffers(&self.gl_context) { + eprintln!("Error swapping buffers: {:?}", e); + } + } +} + pub async fn run_demo_window(scene: Scene) { let width = 1080; let height = 1080; @@ -273,23 +319,25 @@ pub async fn run_demo_window(scene: Scene) { let mut renderer = Renderer::new(scale_factor as f32); renderer.set_backend(Backend::GL(surface_ptr)); + // Create and set up camera + let viewport_size = Size { + width: physical_width as f32, + height: physical_height as f32, + }; + let camera = Camera::new(viewport_size); + renderer.set_camera(camera.clone()); + let mut app = App { renderer, surface_ptr, gl_surface, gl_context, + camera, + scene, }; - // Render once at startup - let surface = unsafe { &mut *app.surface_ptr }; - let canvas = surface.canvas(); - canvas.clear(skia_safe::Color::WHITE); - - app.renderer.render_scene(&scene); - app.renderer.flush(); - if let Err(e) = app.gl_surface.swap_buffers(&app.gl_context) { - eprintln!("Error swapping buffers: {:?}", e); - } + // Initial render + app.redraw(); // Set up the event loop to wait for events el.set_control_flow(ControlFlow::Wait); diff --git a/crates/cg/src/camera.rs b/crates/cg/src/camera.rs new file mode 100644 index 0000000000..ba791eab4f --- /dev/null +++ b/crates/cg/src/camera.rs @@ -0,0 +1,47 @@ +use crate::{schema::Size, transform::AffineTransform}; + +/// A camera that defines the view transformation for rendering. +/// The camera's transform is inverse-applied to create the view matrix. +#[derive(Debug, Clone)] +pub struct Camera { + /// The camera's transform in world space + pub transform: AffineTransform, + + /// The viewport size in pixels + pub viewport_size: Size, + + /// The zoom level (1.0 = 100%) + pub zoom: f32, +} + +impl Camera { + pub fn new(viewport_size: Size) -> Self { + Self { + transform: AffineTransform::identity(), + viewport_size, + zoom: 1.0, + } + } + + /// Creates a view matrix by inverse-applying the camera's transform + pub fn view_matrix(&self) -> AffineTransform { + let mut view = self.transform; + view.inverse(); + view + } + + /// Sets the camera's position + pub fn set_position(&mut self, x: f32, y: f32) { + self.transform.set_translation(x, y); + } + + /// Sets the camera's rotation in radians + pub fn set_rotation(&mut self, angle: f32) { + self.transform.set_rotation(angle); + } + + /// Sets the camera's zoom level + pub fn set_zoom(&mut self, zoom: f32) { + self.zoom = zoom; + } +} diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index b1891bcef8..a9378e1616 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,13 +1,12 @@ -use crate::repository::NodeRepository; use crate::schema::{ - BlendMode, Color as SchemaColor, ContainerNode, EllipseNode, FilterEffect, FontWeight, - GradientStop, GroupNode, ImageNode, LineNode, Node, NodeId, Paint, PathNode, PolygonNode, - RectangleNode, RectangularCornerRadius, RegularPolygonNode, RegularStarPolygonNode, Scene, - TextAlign, TextAlignVertical, TextDecoration, TextNode, TextSpanNode, + Color as SchemaColor, ContainerNode, EllipseNode, FilterEffect, GradientStop, GroupNode, + ImageNode, LineNode, Node, NodeId, Paint, PathNode, PolygonNode, RectangleNode, + RectangularCornerRadius, RegularPolygonNode, RegularStarPolygonNode, Scene, TextSpanNode, }; +use crate::{camera::Camera, repository::NodeRepository}; use skia_safe::{ - Color, Font, FontMgr, FontStyle, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, - Shader, Surface, TextBlob, Typeface, surfaces, + Color, FontMgr, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, + surfaces, textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle}, }; use std::collections::HashMap; @@ -31,6 +30,7 @@ pub struct Renderer { font_mgr: FontMgr, font_collection: FontCollection, dpi: f32, + camera: Option, } impl Renderer { @@ -45,6 +45,7 @@ impl Renderer { font_collection, font_mgr, dpi, + camera: None, } } @@ -88,15 +89,34 @@ impl Renderer { } } + pub fn set_camera(&mut self, camera: Camera) { + self.camera = Some(camera); + } + pub fn render_scene(&self, scene: &Scene) { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; let canvas = surface.canvas(); canvas.save(); + + // Apply DPI scaling canvas.scale((self.dpi, self.dpi)); + + // Apply camera transform if present + if let Some(camera) = &self.camera { + let view_matrix = camera.view_matrix(); + canvas.concat(&sk_matrix(view_matrix.matrix)); + + // Apply zoom + let zoom = camera.zoom; + canvas.scale((zoom, zoom)); + } + + // Render scene nodes for child_id in &scene.children { self.render_node(child_id, &scene.nodes); } + canvas.restore(); } } diff --git a/crates/cg/src/lib.rs b/crates/cg/src/lib.rs index 309f454ede..10ef6d0aee 100644 --- a/crates/cg/src/lib.rs +++ b/crates/cg/src/lib.rs @@ -1,3 +1,4 @@ +pub mod camera; pub mod draw; pub mod factory; pub mod io; diff --git a/crates/cg/src/transform.rs b/crates/cg/src/transform.rs index 8af0af00ea..e857973664 100644 --- a/crates/cg/src/transform.rs +++ b/crates/cg/src/transform.rs @@ -98,4 +98,26 @@ impl AffineTransform { matrix: [[a_inv, c_inv, tx_inv], [b_inv, d_inv, ty_inv]], }) } + + /// Sets the translation components of the transform. + /// This preserves any existing rotation. + pub fn set_translation(&mut self, tx: f32, ty: f32) { + self.matrix[0][2] = tx; + self.matrix[1][2] = ty; + } + + /// Sets the rotation of the transform in radians. + /// This preserves any existing translation. + pub fn set_rotation(&mut self, angle: f32) { + let (sin, cos) = angle.sin_cos(); + self.matrix[0][0] = cos; + self.matrix[0][1] = -sin; + self.matrix[1][0] = sin; + self.matrix[1][1] = cos; + } + + /// Returns the rotation angle in radians. + pub fn rotation(&self) -> f32 { + self.matrix[1][0].atan2(self.matrix[0][0]) + } } From baf65d00647a7a1d2370f526748fe1215a23f15c Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 17:44:46 +0900 Subject: [PATCH 049/262] camera zoom --- crates/cg/examples/texts.rs | 4 +- crates/cg/examples/window.rs | 99 ++++++++++++++++++++++++------------ crates/cg/src/draw.rs | 3 +- 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/crates/cg/examples/texts.rs b/crates/cg/examples/texts.rs index 5013bfe30e..c6075ef6bc 100644 --- a/crates/cg/examples/texts.rs +++ b/crates/cg/examples/texts.rs @@ -54,7 +54,7 @@ async fn demo_texts() -> Scene { // 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, 100.0, 0.0); + sentence_text_node.transform = AffineTransform::new(50.0, 150.0, 0.0); sentence_text_node.size = Size { width: 500.0, height: 100.0, @@ -76,7 +76,7 @@ async fn demo_texts() -> Scene { // 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, 150.0, 0.0); + paragraph_text_node.transform = AffineTransform::new(50.0, 250.0, 0.0); paragraph_text_node.size = Size { width: 800.0, height: 300.0, diff --git a/crates/cg/examples/window.rs b/crates/cg/examples/window.rs index 62cd87bf72..1f4c25311f 100644 --- a/crates/cg/examples/window.rs +++ b/crates/cg/examples/window.rs @@ -20,14 +20,63 @@ use reqwest; use skia_safe::{Surface, gpu}; use std::fs; use std::{ffi::CString, num::NonZeroU32}; +use winit::event::{ElementState, Event, KeyEvent, MouseScrollDelta, WindowEvent}; +use winit::keyboard::Key; use winit::{ application::ApplicationHandler, dpi::LogicalSize, - event::{ElementState, MouseScrollDelta, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::{Window, WindowAttributes}, }; +#[derive(Debug)] +enum Command { + Close, + ZoomIn, + ZoomOut, + Pan { x: f32, y: f32 }, + Redraw, + None, +} + +fn handle_window_event(event: WindowEvent) -> Command { + match event { + WindowEvent::CloseRequested => Command::Close, + WindowEvent::Resized(_) => Command::None, + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: key, + state: ElementState::Pressed, + .. + }, + .. + } => match key { + Key::Character(c) if c == "=" => Command::ZoomIn, + Key::Character(c) if c == "-" => Command::ZoomOut, + _ => Command::None, + }, + WindowEvent::MouseWheel { delta, .. } => match delta { + MouseScrollDelta::LineDelta(x, y) => { + let pan_speed = 10.0; + Command::Pan { + x: x * pan_speed, + y: y * pan_speed, + } + } + MouseScrollDelta::PixelDelta(delta) => { + let pan_speed = 0.5; + Command::Pan { + x: delta.x as f32 * pan_speed, + y: delta.y as f32 * pan_speed, + } + } + }, + WindowEvent::RedrawRequested => Command::Redraw, + _ => Command::None, + } +} + pub async fn fetch_font_data(path: &str) -> Vec { // read from file or url if path.starts_with("http") { @@ -227,46 +276,32 @@ impl ApplicationHandler for App { _window_id: winit::window::WindowId, event: WindowEvent, ) { - match event { - WindowEvent::CloseRequested => { + match handle_window_event(event) { + Command::Close => { self.renderer.free(); event_loop.exit(); } - WindowEvent::Resized(_) => { - // Ignore resize events + Command::ZoomIn => { + self.camera.zoom *= 1.1; + self.renderer.set_camera(self.camera.clone()); + self.redraw(); } - WindowEvent::MouseWheel { delta, .. } => { - match delta { - MouseScrollDelta::LineDelta(x, y) => { - // Handle pan only - inverted direction - let pan_speed = 10.0; - let current_x = self.camera.transform.x(); - let current_y = self.camera.transform.y(); - self.camera - .set_position(current_x + x * pan_speed, current_y + y * pan_speed); - } - MouseScrollDelta::PixelDelta(delta) => { - // Handle pan only - inverted direction - let pan_speed = 0.5; - let current_x = self.camera.transform.x(); - let current_y = self.camera.transform.y(); - self.camera.set_position( - current_x + delta.x as f32 * pan_speed, - current_y + delta.y as f32 * pan_speed, - ); - } - } - - // Update camera in renderer + Command::ZoomOut => { + self.camera.zoom *= 0.9; + self.renderer.set_camera(self.camera.clone()); + self.redraw(); + } + Command::Pan { x, y } => { + let current_x = self.camera.transform.x(); + let current_y = self.camera.transform.y(); + self.camera.set_position(current_x + x, current_y + y); self.renderer.set_camera(self.camera.clone()); - - // Redraw self.redraw(); } - WindowEvent::RedrawRequested => { + Command::Redraw => { self.redraw(); } - _ => {} + Command::None => {} } } } diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index a9378e1616..7bd07724e9 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -456,7 +456,8 @@ impl Renderer { canvas.save(); canvas.concat(&sk_matrix(node.transform.matrix)); - paragraph.paint(canvas, Point::new(node.transform.x(), node.transform.y())); + // Paint at origin since transform is already applied + paragraph.paint(canvas, Point::new(0.0, 0.0)); canvas.restore(); return; } From 828c70b219bc67dc4af8d38a28cb8795bbc6c491 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 19:26:44 +0900 Subject: [PATCH 050/262] refactor --- crates/cg/benches/bench_rectangles.rs | 35 +- crates/cg/examples/100k.rs | 76 +++ crates/cg/src/draw.rs | 904 +++++++++++--------------- 3 files changed, 490 insertions(+), 525 deletions(-) create mode 100644 crates/cg/examples/100k.rs diff --git a/crates/cg/benches/bench_rectangles.rs b/crates/cg/benches/bench_rectangles.rs index 536bca4e1f..feb59d057b 100644 --- a/crates/cg/benches/bench_rectangles.rs +++ b/crates/cg/benches/bench_rectangles.rs @@ -1,13 +1,10 @@ use cg::draw::{Backend, Renderer}; use cg::repository::NodeRepository; -use cg::schema::{ - BaseNode, BlendMode, Color, Node, NodeId, Paint, RectangleNode, RectangularCornerRadius, Size, - SolidPaint, -}; +use cg::schema::*; use cg::transform::AffineTransform; use criterion::{Criterion, black_box, criterion_group, criterion_main}; -fn create_rectangles(count: usize, with_effects: bool) -> (NodeRepository, Vec) { +fn create_rectangles(count: usize, with_effects: bool) -> Scene { let mut repository = NodeRepository::new(); let mut ids = Vec::new(); @@ -69,7 +66,13 @@ fn create_rectangles(count: usize, with_effects: bool) -> (NodeRepository, Vec 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(); + + 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 a gradient of colors + let intensity = (i % 255) as u8; + rect.fill = Paint::Solid(SolidPaint { + color: Color(intensity, intensity, intensity, 255), + }); + + all_shape_ids.push(rect.base.id.clone()); + repository.insert(Node::Rectangle(rect)); + } + + // Set up the root container + root_container_node.children = 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: format!("{} Shapes Performance Test", n), + transform: AffineTransform::identity(), + children: vec![root_container_id], + nodes: repository, + } +} + +#[tokio::main] +async fn main() { + let scene = demo_n_shapes(100_000).await; + window::run_demo_window(scene).await; +} diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 7bd07724e9..bd6816a6a1 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -5,8 +5,8 @@ use crate::schema::{ }; use crate::{camera::Camera, repository::NodeRepository}; use skia_safe::{ - Color, FontMgr, Image, MaskFilter, Paint as SkiaPaint, Point, RRect, Rect, Shader, Surface, - surfaces, + Color, FontMgr, Image, MaskFilter, Paint as SkiaPaint, Picture, PictureRecorder, Point, RRect, + Rect, Shader, Surface, surfaces, textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle}, }; use std::collections::HashMap; @@ -24,41 +24,28 @@ impl Backend { } } -pub struct Renderer { - image_cache: HashMap, - backend: Option, - font_mgr: FontMgr, +/// A painter that handles all drawing operations for nodes. +/// This struct is responsible for the actual painting operations, +/// while Renderer manages the high-level rendering flow. +pub struct Painter { font_collection: FontCollection, - dpi: f32, - camera: Option, + font_mgr: FontMgr, + image_cache: HashMap, } -impl Renderer { - pub fn new(dpi: f32) -> Self { +impl Painter { + pub fn new() -> Self { let mut font_collection = FontCollection::new(); let font_mgr = FontMgr::new(); font_collection.set_default_font_manager(font_mgr.clone(), None); Self { - image_cache: HashMap::new(), - backend: None, font_collection, font_mgr, - dpi, - camera: None, + image_cache: HashMap::new(), } } - pub fn init_raster(width: i32, height: i32) -> *mut Surface { - let surface = - surfaces::raster_n32_premul((width, height)).expect("Failed to create raster surface"); - Box::into_raw(Box::new(surface)) - } - - pub fn set_backend(&mut self, backend: Backend) { - self.backend = Some(backend); - } - pub fn add_image(&mut self, src: String, image: Image) { self.image_cache.insert(src, image); } @@ -67,230 +54,266 @@ impl Renderer { self.font_mgr.new_from_data(bytes, None); } - pub fn flush(&self) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - if let Some(mut gr_context) = surface.recording_context() { - if let Some(mut direct_context) = gr_context.as_direct_context() { - direct_context.flush_and_submit(); - } - } - } + // --- Helper methods for internal use --- + fn with_canvas_state( + &self, + canvas: &skia_safe::Canvas, + transform: &[[f32; 3]; 2], + f: F, + ) { + canvas.save(); + canvas.concat(&sk_matrix(*transform)); + f(); + canvas.restore(); } - pub fn free(&mut self) { - if let Some(backend) = self.backend.take() { - let surface = unsafe { Box::from_raw(backend.get_surface()) }; - if let Some(mut gr_context) = surface.recording_context() { - if let Some(mut direct_context) = gr_context.as_direct_context() { - direct_context.abandon(); - } - } + fn with_opacity_layer(&self, canvas: &skia_safe::Canvas, opacity: f32, f: F) { + if opacity < 1.0 { + canvas.save_layer_alpha(None, (opacity * 255.0) as u32); + f(); + canvas.restore(); + } else { + f(); } } - pub fn set_camera(&mut self, camera: Camera) { - self.camera = Some(camera); - } - - pub fn render_scene(&self, scene: &Scene) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); - canvas.save(); - - // Apply DPI scaling - canvas.scale((self.dpi, self.dpi)); - - // Apply camera transform if present - if let Some(camera) = &self.camera { - let view_matrix = camera.view_matrix(); - canvas.concat(&sk_matrix(view_matrix.matrix)); - - // Apply zoom - let zoom = camera.zoom; - canvas.scale((zoom, zoom)); + fn draw_drop_shadow( + &self, + canvas: &skia_safe::Canvas, + rect: Rect, + radii: &RectangularCornerRadius, + shadow: &FilterEffect, + ) { + if let FilterEffect::DropShadow(shadow) = shadow { + let mut shadow_paint = SkiaPaint::default(); + let SchemaColor(r, g, b, a) = shadow.color; + shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); + shadow_paint.set_anti_alias(true); + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + skia_safe::BlurStyle::Normal, + shadow.blur, + None, + )); } - - // Render scene nodes - for child_id in &scene.children { - self.render_node(child_id, &scene.nodes); + let offset_x = shadow.dx; + let offset_y = shadow.dy; + let RectangularCornerRadius { tl, tr, bl, br } = *radii; + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), + Point::new(tr, tr), + Point::new(br, br), + Point::new(bl, bl), + ], + ); + let mut shadow_rrect = rrect; + shadow_rrect.offset((offset_x, offset_y)); + canvas.draw_rrect(shadow_rrect, &shadow_paint); + } else { + let mut shadow_rect = rect; + shadow_rect.offset((offset_x, offset_y)); + canvas.draw_rect(shadow_rect, &shadow_paint); } - - canvas.restore(); - } - } - - pub fn render_node(&self, id: &NodeId, repository: &NodeRepository) { - let node = match repository.get(id) { - Some(node) => node, - None => return, - }; - - match node { - Node::Group(node) => self.draw_group_node(node, repository), - Node::Container(node) => self.draw_container_node(node, repository), - Node::Rectangle(node) => self.draw_rect_node(node), - Node::Ellipse(node) => self.draw_ellipse_node(node), - Node::Polygon(node) => self.draw_polygon_node(node), - Node::RegularPolygon(node) => self.draw_regular_polygon_node(node), - Node::TextSpan(node) => self.draw_text_span_node(node), - Node::Line(node) => self.draw_line_node(node), - Node::Image(node) => self.draw_image_node(node), - Node::Path(node) => self.draw_path_node(node), - Node::RegularStarPolygon(node) => self.draw_regular_star_polygon_node(node), } } - pub fn draw_rect(&self, x: f32, y: f32, w: f32, h: f32, r: f32, g: f32, b: f32, a: f32) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); - - let color = Color::from_argb( - (a * 255.0) as u8, - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, + fn draw_fill_and_stroke( + &self, + canvas: &skia_safe::Canvas, + rect: Rect, + radii: &RectangularCornerRadius, + fill: &Paint, + stroke: Option<&Paint>, + stroke_width: f32, + blend_mode: crate::schema::BlendMode, + opacity: f32, + ) { + let RectangularCornerRadius { tl, tr, bl, br } = *radii; + let mut fill_paint = sk_paint(fill, opacity, (rect.width(), rect.height())); + fill_paint.set_blend_mode(blend_mode.into()); + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), + Point::new(tr, tr), + Point::new(br, br), + Point::new(bl, bl), + ], ); - - let mut paint = SkiaPaint::default(); - paint.set_color(color); - - canvas.draw_rect(Rect::from_xywh(x, y, w, h), &paint); + canvas.draw_rrect(rrect, &fill_paint); + if let Some(stroke) = stroke { + if stroke_width > 0.0 { + let mut stroke_paint = sk_paint(stroke, opacity, (rect.width(), rect.height())); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(stroke_width); + stroke_paint.set_blend_mode(blend_mode.into()); + canvas.draw_rrect(rrect, &stroke_paint); + } + } + } else { + canvas.draw_rect(rect, &fill_paint); + if let Some(stroke) = stroke { + if stroke_width > 0.0 { + let mut stroke_paint = sk_paint(stroke, opacity, (rect.width(), rect.height())); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(stroke_width); + stroke_paint.set_blend_mode(blend_mode.into()); + canvas.draw_rect(rect, &stroke_paint); + } + } } } - pub fn draw_rect_node(&self, node: &RectangleNode) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); - let paint = sk_paint( + // --- Node drawing methods --- + pub fn draw_rect_node(&self, canvas: &skia_safe::Canvas, node: &RectangleNode) { + self.with_canvas_state(canvas, &node.transform.matrix, || { + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let radii = node.corner_radius; + if let Some(effect) = &node.effect { + self.draw_drop_shadow(canvas, rect, &radii, effect); + } + self.draw_fill_and_stroke( + canvas, + rect, + &radii, &node.fill, + Some(&node.stroke), + node.stroke_width, + node.blend_mode, node.opacity, - (node.size.width, node.size.height), ); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; - // Draw drop shadow effect if present - if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { - let mut shadow_paint = SkiaPaint::default(); - let SchemaColor(r, g, b, a) = shadow.color; - shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); - shadow_paint.set_anti_alias(true); - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( - skia_safe::BlurStyle::Normal, - shadow.blur, - None, - )); + }); + } + + pub fn draw_container_node( + &self, + canvas: &skia_safe::Canvas, + node: &ContainerNode, + repository: &NodeRepository, + ) { + self.with_canvas_state(canvas, &node.transform.matrix, || { + self.with_opacity_layer(canvas, node.opacity, || { + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let radii = node.corner_radius; + if let Some(effect) = &node.effect { + self.draw_drop_shadow(canvas, rect, &radii, effect); } - let offset_x = shadow.dx; - let offset_y = shadow.dy; - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + self.draw_fill_and_stroke( + canvas, + rect, + &radii, + &node.fill, + node.stroke.as_ref(), + node.stroke_width, + node.blend_mode, + node.opacity, + ); + for child_id in &node.children { + if let Some(child) = repository.get(child_id) { + self.draw_node(canvas, child, repository); + } + } + }); + }); + } + + pub fn draw_image_node(&self, canvas: &skia_safe::Canvas, node: &ImageNode) { + if let Some(image) = self.image_cache.get(&node._ref) { + self.with_canvas_state(canvas, &node.transform.matrix, || { + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + let radii = node.corner_radius; + if let Some(effect) = &node.effect { + self.draw_drop_shadow(canvas, rect, &radii, effect); + } + // Draw the image (with optional rounded corners) + let mut paint = SkiaPaint::default(); + paint.set_anti_alias(true); + paint.set_blend_mode(node.blend_mode.into()); + paint.set_alpha((node.opacity * 255.0) as u8); + if radii.tl > 0.0 || radii.tr > 0.0 || radii.bl > 0.0 || radii.br > 0.0 { let rrect = RRect::new_rect_radii( rect, &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left + Point::new(radii.tl, radii.tl), + Point::new(radii.tr, radii.tr), + Point::new(radii.br, radii.br), + Point::new(radii.bl, radii.bl), ], ); - let mut shadow_rrect = rrect; - shadow_rrect.offset((offset_x, offset_y)); - canvas.draw_rrect(shadow_rrect, &shadow_paint); + canvas.save(); + canvas.clip_rrect(rrect, None, true); + canvas.draw_image_rect(image, None, rect, &paint); + canvas.restore(); } else { - let mut shadow_rect = rect; - shadow_rect.offset((offset_x, offset_y)); - canvas.draw_rect(shadow_rect, &shadow_paint); - } - } - // Draw fill and stroke as before - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left - ], - ); - let mut fill_paint = paint.clone(); - fill_paint.set_blend_mode(node.blend_mode.into()); - canvas.draw_rrect(rrect, &fill_paint); - // Draw stroke if stroke_width > 0 - if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint( - &node.stroke, - node.opacity, - (node.size.width, node.size.height), - ); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.blend_mode.into()); - canvas.draw_rrect(rrect, &stroke_paint); + canvas.draw_image_rect(image, None, rect, &paint); } - } else { - let mut fill_paint = paint.clone(); - fill_paint.set_blend_mode(node.blend_mode.into()); - canvas.draw_rect(rect, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint( + self.draw_fill_and_stroke( + canvas, + rect, + &radii, &node.stroke, + None, + node.stroke_width, + node.blend_mode, node.opacity, - (node.size.width, node.size.height), ); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.blend_mode.into()); - canvas.draw_rect(rect, &stroke_paint); } - } - canvas.restore(); + }); } } - pub fn draw_ellipse(&self, x: f32, y: f32, rx: f32, ry: f32, r: f32, g: f32, b: f32, a: f32) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); - - let color = Color::from_argb( - (a * 255.0) as u8, - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - ); - - let mut paint = SkiaPaint::default(); - paint.set_color(color); + pub fn draw_group_node( + &self, + canvas: &skia_safe::Canvas, + node: &GroupNode, + repository: &NodeRepository, + ) { + self.with_canvas_state(canvas, &node.transform.matrix, || { + self.with_opacity_layer(canvas, node.opacity, || { + for child_id in &node.children { + if let Some(child) = repository.get(child_id) { + self.draw_node(canvas, child, repository); + } + } + }); + }); + } - canvas.draw_oval(Rect::from_xywh(x - rx, y - ry, rx * 2.0, ry * 2.0), &paint); + pub fn draw_node(&self, canvas: &skia_safe::Canvas, node: &Node, repository: &NodeRepository) { + match node { + Node::Group(node) => self.draw_group_node(canvas, node, repository), + Node::Container(node) => self.draw_container_node(canvas, node, repository), + Node::Rectangle(node) => self.draw_rect_node(canvas, node), + Node::Ellipse(node) => self.draw_ellipse_node(canvas, node), + Node::Polygon(node) => self.draw_polygon_node(canvas, node), + Node::RegularPolygon(node) => self.draw_regular_polygon_node(canvas, node), + Node::TextSpan(node) => self.draw_text_span_node(canvas, node), + Node::Line(node) => self.draw_line_node(canvas, node), + Node::Image(node) => self.draw_image_node(canvas, node), + Node::Path(node) => self.draw_path_node(canvas, node), + Node::RegularStarPolygon(node) => self.draw_regular_star_polygon_node(canvas, node), } } - pub fn draw_ellipse_node(&self, node: &EllipseNode) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); - let fill_paint = sk_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - let rect = Rect::from_xywh( - 0.0, // x starts at 0 (top-left) - 0.0, // y starts at 0 (top-left) - node.size.width, - node.size.height, - ); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); + pub fn draw_ellipse_node(&self, canvas: &skia_safe::Canvas, node: &EllipseNode) { + let fill_paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + let rect = Rect::from_xywh( + 0.0, // x starts at 0 (top-left) + 0.0, // y starts at 0 (top-left) + node.size.width, + node.size.height, + ); + self.with_canvas_state(canvas, &node.transform.matrix, || { // Draw fill let mut fill_paint = fill_paint.clone(); fill_paint.set_blend_mode(node.blend_mode.into()); @@ -307,36 +330,25 @@ impl Renderer { stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_oval(rect, &stroke_paint); } - canvas.restore(); - } + }); } - pub fn draw_line_node(&self, node: &LineNode) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); - let mut paint = sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); - paint.set_stroke(true); - paint.set_stroke_width(node.stroke_width); - paint.set_blend_mode(node.blend_mode.into()); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); + pub fn draw_line_node(&self, canvas: &skia_safe::Canvas, node: &LineNode) { + let mut paint = sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); + paint.set_stroke(true); + paint.set_stroke_width(node.stroke_width); + paint.set_blend_mode(node.blend_mode.into()); + self.with_canvas_state(canvas, &node.transform.matrix, || { canvas.draw_line( Point::new(0.0, 0.0), Point::new(node.size.width, 0.0), &paint, ); - canvas.restore(); - } + }); } - pub fn draw_path_node(&self, node: &PathNode) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - + pub fn draw_path_node(&self, canvas: &skia_safe::Canvas, node: &PathNode) { + self.with_canvas_state(canvas, &node.transform.matrix, || { let path = skia_safe::path::Path::from_svg(&node.data).expect("path is not valid"); let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); @@ -344,27 +356,20 @@ impl Renderer { let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); - // stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &stroke_paint); } canvas.draw_path(&path, &fill_paint); - canvas.restore(); - } + }); } - pub fn draw_polygon_node(&self, node: &PolygonNode) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); - if node.points.len() < 3 { - // Not enough points to form a polygon - return; - } - let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - + pub fn draw_polygon_node(&self, canvas: &skia_safe::Canvas, node: &PolygonNode) { + if node.points.len() < 3 { + // Not enough points to form a polygon + return; + } + let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + self.with_canvas_state(canvas, &node.transform.matrix, || { // If corner_radius > 0, use the rounded polygon path let path = if node.corner_radius > 0.0 { node.to_path() @@ -395,316 +400,197 @@ impl Renderer { stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &stroke_paint); } - - canvas.restore(); - } + }); } - pub fn draw_regular_polygon_node(&self, node: &RegularPolygonNode) { + pub fn draw_regular_polygon_node(&self, canvas: &skia_safe::Canvas, node: &RegularPolygonNode) { let poly = node.to_polygon(); - self.draw_polygon_node(&poly); + self.draw_polygon_node(canvas, &poly); } - pub fn draw_text_span_node(&self, node: &TextSpanNode) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); - - // paints - let mut fill_paint = sk_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - fill_paint.set_blend_mode(node.blend_mode.into()); - - // paragraph - let mut paragraph_style = ParagraphStyle::new(); - paragraph_style.set_text_direction(skia_safe::textlayout::TextDirection::LTR); - paragraph_style.set_text_align(node.text_align.into()); - let mut paragraph_builder = - ParagraphBuilder::new(¶graph_style, &self.font_collection); - - // text style - let mut ts = TextStyle::new(); - ts.set_foreground_paint(&fill_paint); - ts.set_font_size(node.text_style.font_size); - if let Some(letter_spacing) = node.text_style.letter_spacing { - ts.set_letter_spacing(letter_spacing); - } - if let Some(line_height) = node.text_style.line_height { - ts.set_height(line_height); - } - let mut decoration = skia_safe::textlayout::Decoration::default(); - decoration.ty = node.text_style.text_decoration.into(); - ts.set_decoration(&decoration); - ts.set_font_families(&[&node.text_style.font_family]); - - let font_style = skia_safe::FontStyle::new( - skia_safe::font_style::Weight::from(node.text_style.font_weight.value()), - skia_safe::font_style::Width::NORMAL, - skia_safe::font_style::Slant::Upright, - ); - ts.set_font_style(font_style); - - // paragraph builder - paragraph_builder.push_style(&ts); - paragraph_builder.add_text(&node.text); - let mut paragraph = paragraph_builder.build(); - paragraph_builder.pop(); - paragraph.layout(node.size.width); + pub fn draw_regular_star_polygon_node( + &self, + canvas: &skia_safe::Canvas, + node: &RegularStarPolygonNode, + ) { + let poly = node.to_polygon(); + self.draw_polygon_node(canvas, &poly); + } - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); + pub fn draw_text_span_node(&self, canvas: &skia_safe::Canvas, node: &TextSpanNode) { + // paints + let mut fill_paint = sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); + fill_paint.set_blend_mode(node.blend_mode.into()); + + // paragraph + let mut paragraph_style = ParagraphStyle::new(); + paragraph_style.set_text_direction(skia_safe::textlayout::TextDirection::LTR); + paragraph_style.set_text_align(node.text_align.into()); + let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, &self.font_collection); + + // text style + let mut ts = TextStyle::new(); + ts.set_foreground_paint(&fill_paint); + ts.set_font_size(node.text_style.font_size); + if let Some(letter_spacing) = node.text_style.letter_spacing { + ts.set_letter_spacing(letter_spacing); + } + if let Some(line_height) = node.text_style.line_height { + ts.set_height(line_height); + } + let mut decoration = skia_safe::textlayout::Decoration::default(); + decoration.ty = node.text_style.text_decoration.into(); + ts.set_decoration(&decoration); + ts.set_font_families(&[&node.text_style.font_family]); + + let font_style = skia_safe::FontStyle::new( + skia_safe::font_style::Weight::from(node.text_style.font_weight.value()), + skia_safe::font_style::Width::NORMAL, + skia_safe::font_style::Slant::Upright, + ); + ts.set_font_style(font_style); + + // paragraph builder + paragraph_builder.push_style(&ts); + paragraph_builder.add_text(&node.text); + let mut paragraph = paragraph_builder.build(); + paragraph_builder.pop(); + paragraph.layout(node.size.width); + + self.with_canvas_state(canvas, &node.transform.matrix, || { // Paint at origin since transform is already applied paragraph.paint(canvas, Point::new(0.0, 0.0)); - canvas.restore(); - return; + }); + } +} + +pub struct Renderer { + painter: Painter, + backend: Option, + dpi: f32, + camera: Option, +} + +impl Renderer { + pub fn new(dpi: f32) -> Self { + Self { + painter: Painter::new(), + backend: None, + dpi, + camera: None, } } - pub fn draw_image_node(&self, node: &ImageNode) { - if let Some(backend) = &self.backend { - let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); + pub fn init_raster(width: i32, height: i32) -> *mut Surface { + let surface = + surfaces::raster_n32_premul((width, height)).expect("Failed to create raster surface"); + Box::into_raw(Box::new(surface)) + } - if let Some(image) = self.image_cache.get(&node._ref) { - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - - // Draw drop shadow effect if present - if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { - let mut shadow_paint = SkiaPaint::default(); - let SchemaColor(r, g, b, a) = shadow.color; - shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); - shadow_paint.set_anti_alias(true); - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( - skia_safe::BlurStyle::Normal, - shadow.blur, - None, - )); - } - let offset_x = shadow.dx; - let offset_y = shadow.dy; - let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - let mut shadow_rect = rect; - shadow_rect.offset((offset_x, offset_y)); - canvas.draw_image_rect(image, None, shadow_rect, &shadow_paint); - } + pub fn set_backend(&mut self, backend: Backend) { + self.backend = Some(backend); + } - // Draw the image - let mut paint = SkiaPaint::default(); - paint.set_anti_alias(true); - paint.set_blend_mode(node.blend_mode.into()); - paint.set_alpha((node.opacity * 255.0) as u8); + pub fn add_image(&mut self, src: String, image: Image) { + self.painter.add_image(src, image); + } - let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; + pub fn add_font(&mut self, bytes: &[u8]) { + self.painter.add_font(bytes); + } - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left - ], - ); - // For rounded rectangles, we need to use a clip path - canvas.save(); - canvas.clip_rrect(rrect, None, true); - canvas.draw_image_rect(image, None, rect, &paint); - canvas.restore(); - } else { - canvas.draw_image_rect(image, None, rect, &paint); + pub fn flush(&self) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + if let Some(mut gr_context) = surface.recording_context() { + if let Some(mut direct_context) = gr_context.as_direct_context() { + direct_context.flush_and_submit(); } + } + } + } - // Draw stroke if stroke_width > 0 - if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint( - &node.stroke, - node.opacity, - (node.size.width, node.size.height), - ); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.blend_mode.into()); - - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left - ], - ); - canvas.draw_rrect(rrect, &stroke_paint); - } else { - canvas.draw_rect(rect, &stroke_paint); - } + pub fn free(&mut self) { + if let Some(backend) = self.backend.take() { + let surface = unsafe { Box::from_raw(backend.get_surface()) }; + if let Some(mut gr_context) = surface.recording_context() { + if let Some(mut direct_context) = gr_context.as_direct_context() { + direct_context.abandon(); } - - canvas.restore(); } } } - pub fn draw_group_node(&self, node: &GroupNode, repository: &NodeRepository) { + pub fn set_camera(&mut self, camera: Camera) { + self.camera = Some(camera); + } + + // Record the scene content without any camera transforms + pub fn record_scene(&self, scene: &Scene) -> Option { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; - let canvas = surface.canvas(); + let mut recorder = PictureRecorder::new(); - // Save canvas state for transform - canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); + // Use the surface dimensions for the recording bounds + let bounds = Rect::new(0.0, 0.0, surface.width() as f32, surface.height() as f32); + let canvas = recorder.begin_recording(bounds, None); - let needs_opacity_layer = node.opacity < 1.0; - - if needs_opacity_layer { - // Start new layer with opacity - canvas.save_layer_alpha(None, (node.opacity * 255.0) as u32); - } - - // Recursively render children - for child_id in &node.children { - self.render_node(child_id, repository); - } + // Apply DPI scaling only + canvas.scale((self.dpi, self.dpi)); - if needs_opacity_layer { - // End opacity layer - canvas.restore(); + // Render scene nodes directly (without camera transform) + for child_id in &scene.children { + self.render_node(child_id, &scene.nodes); } - // Restore transform - canvas.restore(); + // End recording and return the picture + recorder.finish_recording_as_picture(None) + } else { + None } } - pub fn draw_container_node(&self, node: &ContainerNode, repository: &NodeRepository) { + // Render the scene + pub fn render_scene(&self, scene: &Scene) { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; let canvas = surface.canvas(); - - // Save canvas state for transform canvas.save(); - canvas.concat(&sk_matrix(node.transform.matrix)); - - let needs_opacity_layer = node.opacity < 1.0; - - if needs_opacity_layer { - // Start new layer with opacity - canvas.save_layer_alpha(None, (node.opacity * 255.0) as u32); - } - - // Draw the background rectangle - let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - let RectangularCornerRadius { tl, tr, bl, br } = node.corner_radius; - - // Draw drop shadow effect if present - if let Some(FilterEffect::DropShadow(shadow)) = &node.effect { - let mut shadow_paint = SkiaPaint::default(); - let SchemaColor(r, g, b, a) = shadow.color; - shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); - shadow_paint.set_anti_alias(true); - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( - skia_safe::BlurStyle::Normal, - shadow.blur, - None, - )); - } - let offset_x = shadow.dx; - let offset_y = shadow.dy; - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left - ], - ); - let mut shadow_rrect = rrect; - shadow_rrect.offset((offset_x, offset_y)); - canvas.draw_rrect(shadow_rrect, &shadow_paint); - } else { - let mut shadow_rect = rect; - shadow_rect.offset((offset_x, offset_y)); - canvas.draw_rect(shadow_rect, &shadow_paint); - } - } - - // Draw fill - let fill_paint = sk_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - let mut fill_paint = fill_paint.clone(); - fill_paint.set_blend_mode(node.blend_mode.into()); - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left - ], - ); - canvas.draw_rrect(rrect, &fill_paint); - } else { - canvas.draw_rect(rect, &fill_paint); - } - - // Draw stroke if present - if let Some(stroke) = &node.stroke { - let mut stroke_paint = - sk_paint(stroke, node.opacity, (node.size.width, node.size.height)); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.blend_mode.into()); + // Apply DPI scaling + canvas.scale((self.dpi, self.dpi)); - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), // top-left - Point::new(tr, tr), // top-right - Point::new(br, br), // bottom-right - Point::new(bl, bl), // bottom-left - ], - ); - canvas.draw_rrect(rrect, &stroke_paint); - } else { - canvas.draw_rect(rect, &stroke_paint); - } - } + // Apply camera transform if present + if let Some(camera) = &self.camera { + let view_matrix = camera.view_matrix(); + canvas.concat(&sk_matrix(view_matrix.matrix)); - // Recursively render children - for child_id in &node.children { - self.render_node(child_id, repository); + // Apply zoom + let zoom = camera.zoom; + canvas.scale((zoom, zoom)); } - if needs_opacity_layer { - // End opacity layer - canvas.restore(); + // Render scene nodes + for child_id in &scene.children { + self.render_node(child_id, &scene.nodes); } - // Restore transform canvas.restore(); } } - pub fn draw_regular_star_polygon_node(&self, node: &RegularStarPolygonNode) { - let poly = node.to_polygon(); - self.draw_polygon_node(&poly); + fn render_node(&self, id: &NodeId, repository: &NodeRepository) { + if let Some(backend) = &self.backend { + let surface = unsafe { &mut *backend.get_surface() }; + let canvas = surface.canvas(); + if let Some(node) = repository.get(id) { + self.painter.draw_node(canvas, node, repository); + } + } } } From 2dc105adccad0109e4261485ea4e619f5d768ed8 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 19:27:48 +0900 Subject: [PATCH 051/262] mv --- crates/cg/src/{sk_polygon_corner_radius.rs => cvt.rs} | 2 +- crates/cg/src/lib.rs | 2 +- crates/cg/src/schema.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename crates/cg/src/{sk_polygon_corner_radius.rs => cvt.rs} (97%) diff --git a/crates/cg/src/sk_polygon_corner_radius.rs b/crates/cg/src/cvt.rs similarity index 97% rename from crates/cg/src/sk_polygon_corner_radius.rs rename to crates/cg/src/cvt.rs index 5c26f286cd..6ab273d5c2 100644 --- a/crates/cg/src/sk_polygon_corner_radius.rs +++ b/crates/cg/src/cvt.rs @@ -14,7 +14,7 @@ fn vector_ops(a: Point, b: Point, scale: f32) -> Point { // - `r`: the corner‐radius // // Build a Path that walks each edge but rounds each "sharp" corner: -pub fn rounded_polygon_path(pts: &[Point], r: f32) -> skia_safe::Path { +pub fn sk_polygon_path(pts: &[Point], r: f32) -> skia_safe::Path { let n = pts.len(); assert!(n >= 3); diff --git a/crates/cg/src/lib.rs b/crates/cg/src/lib.rs index 10ef6d0aee..e8d33e6963 100644 --- a/crates/cg/src/lib.rs +++ b/crates/cg/src/lib.rs @@ -1,8 +1,8 @@ pub mod camera; +pub mod cvt; pub mod draw; pub mod factory; pub mod io; pub mod repository; pub mod schema; -pub mod sk_polygon_corner_radius; pub mod transform; diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 54d9defbdd..410350dd48 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -1,5 +1,5 @@ +use crate::cvt; use crate::repository::NodeRepository; -use crate::sk_polygon_corner_radius; use crate::transform::AffineTransform; use core::str; use serde::Deserialize; @@ -489,7 +489,7 @@ pub struct PolygonNode { impl PolygonNode { pub fn to_path(&self) -> skia_safe::Path { - sk_polygon_corner_radius::rounded_polygon_path(&self.points, self.corner_radius) + cvt::sk_polygon_path(&self.points, self.corner_radius) } } From 652762d9ff017ff395238727ef54591cdb517098 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 19:33:40 +0900 Subject: [PATCH 052/262] chore --- crates/cg/src/cvt.rs | 76 +++++++++++++++++++++++++--- crates/cg/src/draw.rs | 107 +++++++++------------------------------- crates/cg/src/schema.rs | 19 +++++++ 3 files changed, 109 insertions(+), 93 deletions(-) diff --git a/crates/cg/src/cvt.rs b/crates/cg/src/cvt.rs index 6ab273d5c2..f976b9992f 100644 --- a/crates/cg/src/cvt.rs +++ b/crates/cg/src/cvt.rs @@ -1,12 +1,72 @@ -use crate::schema::Point; +use crate::schema::*; use skia_safe; -// Helper function to handle vector operations -fn vector_ops(a: Point, b: Point, scale: f32) -> Point { - Point { - x: a.x - b.x * scale, - y: a.y - b.y * scale, +fn cg_build_gradient_stops( + stops: &[GradientStop], + opacity: f32, +) -> (Vec, Vec) { + let mut colors = Vec::with_capacity(stops.len()); + let mut positions = Vec::with_capacity(stops.len()); + + for stop in stops { + let Color(r, g, b, a) = stop.color; + let alpha = (a as f32 * opacity).round().clamp(0.0, 255.0) as u8; + colors.push(skia_safe::Color::from_argb(alpha, r, g, b)); + positions.push(stop.offset); + } + + (colors, positions) +} + +pub fn sk_matrix(m: [[f32; 3]; 2]) -> skia_safe::Matrix { + let [[a, c, tx], [b, d, ty]] = m; + skia_safe::Matrix::from_affine(&[a, b, c, d, tx, ty]) +} + +pub fn sk_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> skia_safe::Paint { + let mut skia_paint = skia_safe::Paint::default(); + skia_paint.set_anti_alias(true); + let (width, height) = size; + match paint { + Paint::Solid(solid) => { + let Color(r, g, b, a) = solid.color; + let final_alpha = (a as f32 * opacity) as u8; + skia_paint.set_color(skia_safe::Color::from_argb(final_alpha, r, g, b)); + } + Paint::LinearGradient(gradient) => { + let (colors, positions) = cg_build_gradient_stops(&gradient.stops, opacity); + let shader = skia_safe::Shader::linear_gradient( + ( + skia_safe::Point::new(0.0, 0.0), + skia_safe::Point::new(width, 0.0), + ), + &colors[..], + Some(&positions[..]), + skia_safe::TileMode::Clamp, + None, + Some(&sk_matrix(gradient.transform.matrix)), + ) + .unwrap(); + skia_paint.set_shader(shader); + } + Paint::RadialGradient(gradient) => { + let (colors, positions) = cg_build_gradient_stops(&gradient.stops, opacity); + let center = skia_safe::Point::new(width / 2.0, height / 2.0); + let radius = width.min(height) / 2.0; + let shader = skia_safe::Shader::radial_gradient( + center, + radius, + &colors[..], + Some(&positions[..]), + skia_safe::TileMode::Clamp, + None, + Some(&sk_matrix(gradient.transform.matrix)), + ) + .unwrap(); + skia_paint.set_shader(shader); + } } + skia_paint } // Given: @@ -33,7 +93,7 @@ pub fn sk_polygon_path(pts: &[Point], r: f32) -> skia_safe::Path { x: (first.x - last.x) / ((first.x - last.x).powi(2) + (first.y - last.y).powi(2)).sqrt(), y: (first.y - last.y) / ((first.x - last.x).powi(2) + (first.y - last.y).powi(2)).sqrt(), }; - let move_into_first = vector_ops(first, dir_a, r); + let move_into_first = first.subtract_scaled(dir_a, r); path.move_to(skia_safe::Point::new(move_into_first.x, move_into_first.y)); @@ -50,7 +110,7 @@ pub fn sk_polygon_path(pts: &[Point], r: f32) -> skia_safe::Path { x: (curr.x - prev.x) / ((curr.x - prev.x).powi(2) + (curr.y - prev.y).powi(2)).sqrt(), y: (curr.y - prev.y) / ((curr.x - prev.x).powi(2) + (curr.y - prev.y).powi(2)).sqrt(), }; - let start_arc = vector_ops(curr, dir_in, r); + let start_arc = curr.subtract_scaled(dir_in, r); // Compute offset along outgoing edge (to where arc ends): let dir_out = Point { diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index bd6816a6a1..5b3d6fd417 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,12 +1,9 @@ -use crate::schema::{ - Color as SchemaColor, ContainerNode, EllipseNode, FilterEffect, GradientStop, GroupNode, - ImageNode, LineNode, Node, NodeId, Paint, PathNode, PolygonNode, RectangleNode, - RectangularCornerRadius, RegularPolygonNode, RegularStarPolygonNode, Scene, TextSpanNode, -}; +use crate::cvt; +use crate::schema::*; use crate::{camera::Camera, repository::NodeRepository}; use skia_safe::{ - Color, FontMgr, Image, MaskFilter, Paint as SkiaPaint, Picture, PictureRecorder, Point, RRect, - Rect, Shader, Surface, surfaces, + FontMgr, Image, MaskFilter, Paint as SkPaint, Picture, PictureRecorder, Point, RRect, Rect, + Surface, surfaces, textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle}, }; use std::collections::HashMap; @@ -62,7 +59,7 @@ impl Painter { f: F, ) { canvas.save(); - canvas.concat(&sk_matrix(*transform)); + canvas.concat(&cvt::sk_matrix(*transform)); f(); canvas.restore(); } @@ -85,8 +82,8 @@ impl Painter { shadow: &FilterEffect, ) { if let FilterEffect::DropShadow(shadow) = shadow { - let mut shadow_paint = SkiaPaint::default(); - let SchemaColor(r, g, b, a) = shadow.color; + let mut shadow_paint = SkPaint::default(); + let Color(r, g, b, a) = shadow.color; shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); shadow_paint.set_anti_alias(true); if shadow.blur > 0.0 { @@ -132,7 +129,7 @@ impl Painter { opacity: f32, ) { let RectangularCornerRadius { tl, tr, bl, br } = *radii; - let mut fill_paint = sk_paint(fill, opacity, (rect.width(), rect.height())); + let mut fill_paint = cvt::sk_paint(fill, opacity, (rect.width(), rect.height())); fill_paint.set_blend_mode(blend_mode.into()); if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { let rrect = RRect::new_rect_radii( @@ -147,7 +144,8 @@ impl Painter { canvas.draw_rrect(rrect, &fill_paint); if let Some(stroke) = stroke { if stroke_width > 0.0 { - let mut stroke_paint = sk_paint(stroke, opacity, (rect.width(), rect.height())); + let mut stroke_paint = + cvt::sk_paint(stroke, opacity, (rect.width(), rect.height())); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(stroke_width); stroke_paint.set_blend_mode(blend_mode.into()); @@ -158,7 +156,8 @@ impl Painter { canvas.draw_rect(rect, &fill_paint); if let Some(stroke) = stroke { if stroke_width > 0.0 { - let mut stroke_paint = sk_paint(stroke, opacity, (rect.width(), rect.height())); + let mut stroke_paint = + cvt::sk_paint(stroke, opacity, (rect.width(), rect.height())); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(stroke_width); stroke_paint.set_blend_mode(blend_mode.into()); @@ -230,7 +229,7 @@ impl Painter { self.draw_drop_shadow(canvas, rect, &radii, effect); } // Draw the image (with optional rounded corners) - let mut paint = SkiaPaint::default(); + let mut paint = SkPaint::default(); paint.set_anti_alias(true); paint.set_blend_mode(node.blend_mode.into()); paint.set_alpha((node.opacity * 255.0) as u8); @@ -302,7 +301,7 @@ impl Painter { } pub fn draw_ellipse_node(&self, canvas: &skia_safe::Canvas, node: &EllipseNode) { - let fill_paint = sk_paint( + let fill_paint = cvt::sk_paint( &node.fill, node.opacity, (node.size.width, node.size.height), @@ -320,7 +319,7 @@ impl Painter { canvas.draw_oval(rect, &fill_paint); // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint( + let mut stroke_paint = cvt::sk_paint( &node.stroke, node.opacity, (node.size.width, node.size.height), @@ -334,7 +333,7 @@ impl Painter { } pub fn draw_line_node(&self, canvas: &skia_safe::Canvas, node: &LineNode) { - let mut paint = sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); + let mut paint = cvt::sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); paint.set_stroke(true); paint.set_stroke_width(node.stroke_width); paint.set_blend_mode(node.blend_mode.into()); @@ -351,9 +350,9 @@ impl Painter { self.with_canvas_state(canvas, &node.transform.matrix, || { let path = skia_safe::path::Path::from_svg(&node.data).expect("path is not valid"); - let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + let fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); + let mut stroke_paint = cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); canvas.draw_path(&path, &stroke_paint); @@ -368,7 +367,7 @@ impl Painter { // Not enough points to form a polygon return; } - let fill_paint = sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + let fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); self.with_canvas_state(canvas, &node.transform.matrix, || { // If corner_radius > 0, use the rounded polygon path let path = if node.corner_radius > 0.0 { @@ -394,7 +393,7 @@ impl Painter { // Draw stroke if stroke_width > 0 if node.stroke_width > 0.0 { - let mut stroke_paint = sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); + let mut stroke_paint = cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); stroke_paint.set_stroke(true); stroke_paint.set_stroke_width(node.stroke_width); stroke_paint.set_blend_mode(node.blend_mode.into()); @@ -419,7 +418,7 @@ impl Painter { pub fn draw_text_span_node(&self, canvas: &skia_safe::Canvas, node: &TextSpanNode) { // paints - let mut fill_paint = sk_paint( + let mut fill_paint = cvt::sk_paint( &node.fill, node.opacity, (node.size.width, node.size.height), @@ -567,7 +566,7 @@ impl Renderer { // Apply camera transform if present if let Some(camera) = &self.camera { let view_matrix = camera.view_matrix(); - canvas.concat(&sk_matrix(view_matrix.matrix)); + canvas.concat(&cvt::sk_matrix(view_matrix.matrix)); // Apply zoom let zoom = camera.zoom; @@ -593,65 +592,3 @@ impl Renderer { } } } - -fn sk_matrix(m: [[f32; 3]; 2]) -> skia_safe::Matrix { - let [[a, c, tx], [b, d, ty]] = m; - skia_safe::Matrix::from_affine(&[a, b, c, d, tx, ty]) -} - -fn sk_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> SkiaPaint { - let mut skia_paint = SkiaPaint::default(); - skia_paint.set_anti_alias(true); - let (width, height) = size; - match paint { - Paint::Solid(solid) => { - let SchemaColor(r, g, b, a) = solid.color; - let final_alpha = (a as f32 * opacity) as u8; - skia_paint.set_color(Color::from_argb(final_alpha, r, g, b)); - } - Paint::LinearGradient(gradient) => { - let (colors, positions) = cg_build_gradient_stops(&gradient.stops, opacity); - let shader = Shader::linear_gradient( - (Point::new(0.0, 0.0), Point::new(width, 0.0)), - &colors[..], - Some(&positions[..]), - skia_safe::TileMode::Clamp, - None, - Some(&sk_matrix(gradient.transform.matrix)), - ) - .unwrap(); - skia_paint.set_shader(shader); - } - Paint::RadialGradient(gradient) => { - let (colors, positions) = cg_build_gradient_stops(&gradient.stops, opacity); - let center = Point::new(width / 2.0, height / 2.0); - let radius = width.min(height) / 2.0; - let shader = Shader::radial_gradient( - center, - radius, - &colors[..], - Some(&positions[..]), - skia_safe::TileMode::Clamp, - None, - Some(&sk_matrix(gradient.transform.matrix)), - ) - .unwrap(); - skia_paint.set_shader(shader); - } - } - skia_paint -} - -fn cg_build_gradient_stops(stops: &[GradientStop], opacity: f32) -> (Vec, Vec) { - let mut colors = Vec::with_capacity(stops.len()); - let mut positions = Vec::with_capacity(stops.len()); - - for stop in stops { - let SchemaColor(r, g, b, a) = stop.color; - let alpha = (a as f32 * opacity).round().clamp(0.0, 255.0) as u8; - colors.push(Color::from_argb(alpha, r, g, b)); - positions.push(stop.offset); - } - - (colors, positions) -} diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 410350dd48..956966de95 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -13,6 +13,25 @@ pub struct Point { pub y: f32, } +impl Point { + /// Subtracts a scaled vector from this point. + /// + /// # Arguments + /// + /// * `other` - The point to subtract + /// * `scale` - The scale factor to apply to the other point + /// + /// # Returns + /// + /// A new point representing the result of the vector operation + pub fn subtract_scaled(&self, other: Point, scale: f32) -> Point { + Point { + x: self.x - other.x * scale, + y: self.y - other.y * scale, + } + } +} + #[derive(Debug, Clone, Copy)] pub struct Color(pub u8, pub u8, pub u8, pub u8); From f96d749f90d224bde42499384ade0125c5e404f4 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 20:03:50 +0900 Subject: [PATCH 053/262] resize window --- crates/cg/benches/bench_rectangles.rs | 14 ++++-- crates/cg/examples/window.rs | 69 +++++++++++++++++++++++++-- crates/cg/src/draw.rs | 18 ++++++- 3 files changed, 90 insertions(+), 11 deletions(-) diff --git a/crates/cg/benches/bench_rectangles.rs b/crates/cg/benches/bench_rectangles.rs index feb59d057b..391ee2ca83 100644 --- a/crates/cg/benches/bench_rectangles.rs +++ b/crates/cg/benches/bench_rectangles.rs @@ -86,7 +86,7 @@ fn bench_rectangles(c: &mut Criterion) { // 1K rectangles group.bench_function("1k_basic", |b| { b.iter(|| { - let mut renderer = Renderer::new(1.0); + let mut renderer = Renderer::new(1000.0, 1000.0, 1.0); let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); @@ -110,7 +110,8 @@ fn bench_rectangles(c: &mut Criterion) { // 10K rectangles group.bench_function("10k_basic", |b| { b.iter(|| { - let mut renderer = Renderer::new(1.0); + let mut renderer = Renderer::new(1000.0, 1000.0, 1.0); + let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); @@ -133,7 +134,8 @@ fn bench_rectangles(c: &mut Criterion) { group.bench_function("10k_with_effects", |b| { b.iter(|| { - let mut renderer = Renderer::new(1.0); + let mut renderer = Renderer::new(1000.0, 1000.0, 1.0); + let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); @@ -157,7 +159,8 @@ fn bench_rectangles(c: &mut Criterion) { // 50K rectangles group.bench_function("50k_basic", |b| { b.iter(|| { - let mut renderer = Renderer::new(1.0); + let mut renderer = Renderer::new(1000.0, 1000.0, 1.0); + let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); @@ -180,7 +183,8 @@ fn bench_rectangles(c: &mut Criterion) { group.bench_function("50k_with_effects", |b| { b.iter(|| { - let mut renderer = Renderer::new(1.0); + let mut renderer = Renderer::new(1000.0, 1000.0, 1.0); + let surface_ptr = Renderer::init_raster(width, height); renderer.set_backend(Backend::Raster(surface_ptr)); diff --git a/crates/cg/examples/window.rs b/crates/cg/examples/window.rs index 1f4c25311f..96ec8a22d8 100644 --- a/crates/cg/examples/window.rs +++ b/crates/cg/examples/window.rs @@ -36,13 +36,17 @@ enum Command { ZoomOut, Pan { x: f32, y: f32 }, Redraw, + Resize { width: u32, height: u32 }, None, } fn handle_window_event(event: WindowEvent) -> Command { match event { WindowEvent::CloseRequested => Command::Close, - WindowEvent::Resized(_) => Command::None, + WindowEvent::Resized(size) => Command::Resize { + width: size.width, + height: size.height, + }, WindowEvent::KeyboardInput { event: KeyEvent { @@ -263,8 +267,12 @@ struct App { surface_ptr: *mut Surface, gl_surface: GlutinSurface, gl_context: PossiblyCurrentContext, + gl_config: glutin::config::Config, + fb_info: gpu::gl::FramebufferInfo, + gr_context: skia_safe::gpu::DirectContext, camera: Camera, scene: Scene, + window: Window, } impl ApplicationHandler for App { @@ -298,6 +306,9 @@ impl ApplicationHandler for App { self.renderer.set_camera(self.camera.clone()); self.redraw(); } + Command::Resize { width, height } => { + self.resize(width, height); + } Command::Redraw => { self.redraw(); } @@ -319,6 +330,46 @@ impl App { eprintln!("Error swapping buffers: {:?}", e); } } + + fn resize(&mut self, width: u32, height: u32) { + // Recreate GL surface + let attrs = SurfaceAttributesBuilder::::new().build( + self.window + .raw_window_handle() + .expect("Failed to get window handle"), + NonZeroU32::new(width).unwrap(), + NonZeroU32::new(height).unwrap(), + ); + self.gl_surface = unsafe { + self.gl_config + .display() + .create_window_surface(&self.gl_config, &attrs) + .expect("Could not create gl window surface") + }; + + // Recreate Skia surface + let backend_render_target = gpu::backend_render_targets::make_gl( + (width as i32, height as i32), + self.gl_config.num_samples() as usize, + self.gl_config.stencil_size() as usize, + self.fb_info, + ); + let surface = gpu::surfaces::wrap_backend_render_target( + &mut self.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(self.surface_ptr) }; + self.surface_ptr = Box::into_raw(Box::new(surface)); + self.renderer.set_backend(Backend::GL(self.surface_ptr)); + self.redraw(); + } } pub async fn run_demo_window(scene: Scene) { @@ -331,9 +382,9 @@ pub async fn run_demo_window(scene: Scene) { window, gl_surface, gl_context, - _gl_config, - _fb_info, - _gr_context, + gl_config, + fb_info, + gr_context, scale_factor, ) = init_window(width, height); @@ -351,7 +402,11 @@ pub async fn run_demo_window(scene: Scene) { physical_width, physical_height ); - let mut renderer = Renderer::new(scale_factor as f32); + let mut renderer = Renderer::new( + logical_size.width as f32, + logical_size.height as f32, + scale_factor as f32, + ); renderer.set_backend(Backend::GL(surface_ptr)); // Create and set up camera @@ -367,8 +422,12 @@ pub async fn run_demo_window(scene: Scene) { surface_ptr, gl_surface, gl_context, + gl_config, + fb_info, + gr_context, camera, scene, + window, }; // Initial render diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 5b3d6fd417..58b32554ce 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -471,19 +471,28 @@ pub struct Renderer { painter: Painter, backend: Option, dpi: f32, + logical_width: f32, + logical_height: f32, camera: Option, } impl Renderer { - pub fn new(dpi: f32) -> Self { + pub fn new(width: f32, height: f32, dpi: f32) -> Self { Self { painter: Painter::new(), backend: None, dpi, + logical_width: width, + logical_height: height, camera: None, } } + pub fn set_logical_size(&mut self, width: f32, height: f32) { + self.logical_width = width; + self.logical_height = height; + } + pub fn init_raster(width: i32, height: i32) -> *mut Surface { let surface = surfaces::raster_n32_premul((width, height)).expect("Failed to create raster surface"); @@ -557,9 +566,16 @@ impl Renderer { pub fn render_scene(&self, scene: &Scene) { if let Some(backend) = &self.backend { let surface = unsafe { &mut *backend.get_surface() }; + let width = surface.width() as f32; + let height = surface.height() as f32; let canvas = surface.canvas(); canvas.save(); + // Scale to logical size + let scale_x = self.logical_width / width; + let scale_y = self.logical_height / height; + canvas.scale((scale_x, scale_y)); + // Apply DPI scaling canvas.scale((self.dpi, self.dpi)); From 09252ded06b133f99f22d4c346d39d5d33ccea63 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 20:48:00 +0900 Subject: [PATCH 054/262] effects demo --- crates/cg/examples/effects.rs | 158 +++++++++++++++++++++++++++++ crates/cg/src/draw.rs | 185 ++++++++++++++++++++++++++-------- crates/cg/src/schema.rs | 10 ++ 3 files changed, 310 insertions(+), 43 deletions(-) create mode 100644 crates/cg/examples/effects.rs diff --git a/crates/cg/examples/effects.rs b/crates/cg/examples/effects.rs new file mode 100644 index 0000000000..ec1bef979c --- /dev/null +++ b/crates/cg/examples/effects.rs @@ -0,0 +1,158 @@ +use cg::factory::NodeFactory; +use cg::repository::NodeRepository; +use cg::schema::*; +use cg::transform::AffineTransform; + +mod window; + +async fn demo_effects() -> Scene { + let nf = NodeFactory::new(); + + // Add a background rectangle node with a gradient + let mut background_rect_node = nf.create_rectangle_node(); + background_rect_node.base.name = "Background Rect".to_string(); + background_rect_node.size = Size { + width: 1080.0, + height: 1080.0, + }; + background_rect_node.fill = Paint::LinearGradient(LinearGradientPaint { + id: "bg_gradient".to_string(), + transform: AffineTransform::identity(), + stops: vec![ + GradientStop { + offset: 0.0, + color: Color(240, 240, 240, 255), // Light gray + }, + GradientStop { + offset: 1.0, + color: Color(200, 200, 200, 255), // Darker gray + }, + ], + }); + + // Create a root container node + let mut root_container_node = nf.create_container_node(); + root_container_node.base.name = "Root Container".to_string(); + + let mut repository = NodeRepository::new(); + let background_rect_id = background_rect_node.base.id.clone(); + repository.insert(Node::Rectangle(background_rect_node)); + + let mut all_effect_ids = Vec::new(); + let spacing = 200.0; + let start_x = 100.0; + let base_size = 150.0; + + // Row 1: Drop Shadow Variations + for i in 0..4 { + let mut rect = nf.create_rectangle_node(); + rect.base.name = format!("Drop Shadow {}", 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 + }); + rect.effect = Some(FilterEffect::DropShadow(FeDropShadow { + dx: 5.0 * (i + 1) as f32, + dy: 5.0 * (i + 1) as f32, + blur: 10.0 * (i + 1) as f32, + color: Color(0, 0, 0, 128), + })); + all_effect_ids.push(rect.base.id.clone()); + repository.insert(Node::Rectangle(rect)); + } + + // 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, 330.0, 0.0); // y middle of row 2 + vivid_gradient_rect.size = Size { + width: 1080.0, + height: 90.0, + }; + vivid_gradient_rect.fill = Paint::LinearGradient(LinearGradientPaint { + id: "vivid_row2".to_string(), + 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 + ], + }); + let vivid_gradient_rect_id = vivid_gradient_rect.base.id.clone(); + repository.insert(Node::Rectangle(vivid_gradient_rect)); + + // Row 2: Backdrop Blur Variations + for i in 0..4 { + // Create a semi-transparent rectangle with backdrop blur + let mut blur_rect = nf.create_rectangle_node(); + blur_rect.base.name = format!("Backdrop Blur {}", i + 1); + blur_rect.transform = AffineTransform::new(start_x + spacing * i as f32, 300.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 + }); + blur_rect.effect = Some(FilterEffect::BackdropBlur(FeBackdropBlur { + radius: 16.0 * (i + 1) as f32, + })); + all_effect_ids.push(blur_rect.base.id.clone()); + repository.insert(Node::Rectangle(blur_rect)); + } + + // Row 3: Gaussian Blur Variations + for i in 0..4 { + let mut rect = nf.create_rectangle_node(); + rect.base.name = format!("Gaussian Blur {}", 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(20.0); + rect.fill = Paint::Solid(SolidPaint { + color: Color(255, 255, 255, 255), // White + }); + rect.effect = Some(FilterEffect::GaussianBlur(FeGaussianBlur { + radius: 5.0 * (i + 1) as f32, + })); + all_effect_ids.push(rect.base.id.clone()); + repository.insert(Node::Rectangle(rect)); + } + + // Set up the root container + root_container_node.children = vec![background_rect_id, 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, + } +} + +#[tokio::main] +async fn main() { + let scene = demo_effects().await; + window::run_demo_window(scene).await; +} diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 58b32554ce..eca934fcaf 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -2,8 +2,10 @@ use crate::cvt; use crate::schema::*; use crate::{camera::Camera, repository::NodeRepository}; use skia_safe::{ - FontMgr, Image, MaskFilter, Paint as SkPaint, Picture, PictureRecorder, Point, RRect, Rect, - Surface, surfaces, + FontMgr, Image, ImageFilter, MaskFilter, Paint as SkPaint, Picture, PictureRecorder, Point, + RRect, Rect, Surface, + canvas::SaveLayerRec, + surfaces, textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle}, }; use std::collections::HashMap; @@ -81,42 +83,118 @@ impl Painter { radii: &RectangularCornerRadius, shadow: &FilterEffect, ) { - if let FilterEffect::DropShadow(shadow) = shadow { - let mut shadow_paint = SkPaint::default(); - let Color(r, g, b, a) = shadow.color; - shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); - shadow_paint.set_anti_alias(true); - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( + match shadow { + FilterEffect::DropShadow(shadow) => { + let mut shadow_paint = SkPaint::default(); + let Color(r, g, b, a) = shadow.color; + shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); + shadow_paint.set_anti_alias(true); + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + skia_safe::BlurStyle::Normal, + shadow.blur, + None, + )); + } + let offset_x = shadow.dx; + let offset_y = shadow.dy; + let RectangularCornerRadius { tl, tr, bl, br } = *radii; + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), + Point::new(tr, tr), + Point::new(br, br), + Point::new(bl, bl), + ], + ); + let mut shadow_rrect = rrect; + shadow_rrect.offset((offset_x, offset_y)); + canvas.draw_rrect(shadow_rrect, &shadow_paint); + } else { + let mut shadow_rect = rect; + shadow_rect.offset((offset_x, offset_y)); + canvas.draw_rect(shadow_rect, &shadow_paint); + } + } + FilterEffect::BackdropBlur(blur) => { + self.draw_backdrop_blur(canvas, rect, radii, blur); + } + FilterEffect::GaussianBlur(blur) => { + let mut paint = SkPaint::default(); + paint.set_anti_alias(true); + paint.set_mask_filter(MaskFilter::blur( skia_safe::BlurStyle::Normal, - shadow.blur, + blur.radius, None, )); + + let RectangularCornerRadius { tl, tr, bl, br } = *radii; + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), + Point::new(tr, tr), + Point::new(br, br), + Point::new(bl, bl), + ], + ); + canvas.draw_rrect(rrect, &paint); + } else { + canvas.draw_rect(rect, &paint); + } } - let offset_x = shadow.dx; - let offset_y = shadow.dy; - let RectangularCornerRadius { tl, tr, bl, br } = *radii; - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), - Point::new(tr, tr), - Point::new(br, br), - Point::new(bl, bl), - ], - ); - let mut shadow_rrect = rrect; - shadow_rrect.offset((offset_x, offset_y)); - canvas.draw_rrect(shadow_rrect, &shadow_paint); - } else { - let mut shadow_rect = rect; - shadow_rect.offset((offset_x, offset_y)); - canvas.draw_rect(shadow_rect, &shadow_paint); - } } } + fn draw_backdrop_blur( + &self, + canvas: &skia_safe::Canvas, + rect: Rect, + radii: &RectangularCornerRadius, + blur: &FeBackdropBlur, + ) { + // Create a layer for the backdrop blur effect + let mut paint = SkPaint::default(); + paint.set_mask_filter(MaskFilter::blur( + skia_safe::BlurStyle::Normal, + blur.radius, + None, + )); + + // Save the current canvas state + canvas.save(); + + // Apply rounded corners if needed + let RectangularCornerRadius { tl, tr, bl, br } = *radii; + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), + Point::new(tr, tr), + Point::new(br, br), + Point::new(bl, bl), + ], + ); + canvas.clip_rrect(rrect, None, true); + } else { + canvas.clip_rect(rect, None, true); + } + + // Create a layer for the blur effect + canvas.save_layer_alpha(None, 255); + + // Draw the blurred content + canvas.draw_rect(rect, &paint); + + // Restore the canvas state + canvas.restore(); + canvas.restore(); + } + fn draw_fill_and_stroke( &self, canvas: &skia_safe::Canvas, @@ -172,19 +250,40 @@ impl Painter { self.with_canvas_state(canvas, &node.transform.matrix, || { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let radii = node.corner_radius; - if let Some(effect) = &node.effect { - self.draw_drop_shadow(canvas, rect, &radii, effect); + if let Some(FilterEffect::GaussianBlur(blur)) = &node.effect { + // First draw the content + self.draw_fill_and_stroke( + canvas, + rect, + &radii, + &node.fill, + Some(&node.stroke), + node.stroke_width, + node.blend_mode, + node.opacity, + ); + + // Then apply the blur filter + let image_filter = + skia_safe::image_filters::blur((blur.radius, blur.radius), None, None, None); + let mut paint = SkPaint::default(); + paint.set_image_filter(image_filter); + canvas.draw_rect(rect, &paint); + } else { + if let Some(effect) = &node.effect { + self.draw_drop_shadow(canvas, rect, &radii, effect); + } + self.draw_fill_and_stroke( + canvas, + rect, + &radii, + &node.fill, + Some(&node.stroke), + node.stroke_width, + node.blend_mode, + node.opacity, + ); } - self.draw_fill_and_stroke( - canvas, - rect, - &radii, - &node.fill, - Some(&node.stroke), - node.stroke_width, - node.blend_mode, - node.opacity, - ); }); } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 956966de95..be80d68cc3 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -47,6 +47,16 @@ pub enum FilterEffect { /// Gaussian blur filter: blur only GaussianBlur(FeGaussianBlur), + + /// Background blur filter: blur only + BackdropBlur(FeBackdropBlur), +} + +/// A background blur effect, similar to CSS `backdrop-filter: blur(...)` +#[derive(Debug, Clone, Copy)] +pub struct FeBackdropBlur { + /// Blur radius in logical pixels. + pub radius: f32, } /// A drop shadow filter effect (``) From 78105bef1949b3aa1480aac7f371f045532b40b4 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 20:48:11 +0900 Subject: [PATCH 055/262] image, font repository --- crates/cg/src/draw.rs | 71 +++++++++++++++++++++---------------- crates/cg/src/repository.rs | 58 ++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 31 deletions(-) diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index eca934fcaf..743d56023e 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,6 +1,9 @@ use crate::cvt; use crate::schema::*; -use crate::{camera::Camera, repository::NodeRepository}; +use crate::{ + camera::Camera, + repository::{FontRepository, ImageRepository, NodeRepository}, +}; use skia_safe::{ FontMgr, Image, ImageFilter, MaskFilter, Paint as SkPaint, Picture, PictureRecorder, Point, RRect, Rect, Surface, @@ -28,29 +31,14 @@ impl Backend { /// while Renderer manages the high-level rendering flow. pub struct Painter { font_collection: FontCollection, - font_mgr: FontMgr, - image_cache: HashMap, } impl Painter { - pub fn new() -> Self { + pub fn new(font_repository: &FontRepository) -> Self { let mut font_collection = FontCollection::new(); - let font_mgr = FontMgr::new(); - font_collection.set_default_font_manager(font_mgr.clone(), None); - - Self { - font_collection, - font_mgr, - image_cache: HashMap::new(), - } - } + font_collection.set_default_font_manager(font_repository.font_mgr().clone(), None); - pub fn add_image(&mut self, src: String, image: Image) { - self.image_cache.insert(src, image); - } - - pub fn add_font(&mut self, bytes: &[u8]) { - self.font_mgr.new_from_data(bytes, None); + Self { font_collection } } // --- Helper methods for internal use --- @@ -292,6 +280,7 @@ impl Painter { canvas: &skia_safe::Canvas, node: &ContainerNode, repository: &NodeRepository, + image_repository: &ImageRepository, ) { self.with_canvas_state(canvas, &node.transform.matrix, || { self.with_opacity_layer(canvas, node.opacity, || { @@ -312,15 +301,20 @@ impl Painter { ); for child_id in &node.children { if let Some(child) = repository.get(child_id) { - self.draw_node(canvas, child, repository); + self.draw_node(canvas, child, repository, image_repository); } } }); }); } - pub fn draw_image_node(&self, canvas: &skia_safe::Canvas, node: &ImageNode) { - if let Some(image) = self.image_cache.get(&node._ref) { + pub fn draw_image_node( + &self, + canvas: &skia_safe::Canvas, + node: &ImageNode, + image_repository: &ImageRepository, + ) { + if let Some(image) = image_repository.get(&node._ref) { self.with_canvas_state(canvas, &node.transform.matrix, || { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let radii = node.corner_radius; @@ -371,29 +365,38 @@ impl Painter { canvas: &skia_safe::Canvas, node: &GroupNode, repository: &NodeRepository, + image_repository: &ImageRepository, ) { self.with_canvas_state(canvas, &node.transform.matrix, || { self.with_opacity_layer(canvas, node.opacity, || { for child_id in &node.children { if let Some(child) = repository.get(child_id) { - self.draw_node(canvas, child, repository); + self.draw_node(canvas, child, repository, image_repository); } } }); }); } - pub fn draw_node(&self, canvas: &skia_safe::Canvas, node: &Node, repository: &NodeRepository) { + pub fn draw_node( + &self, + canvas: &skia_safe::Canvas, + node: &Node, + repository: &NodeRepository, + image_repository: &ImageRepository, + ) { match node { - Node::Group(node) => self.draw_group_node(canvas, node, repository), - Node::Container(node) => self.draw_container_node(canvas, node, repository), + Node::Group(node) => self.draw_group_node(canvas, node, repository, image_repository), + Node::Container(node) => { + self.draw_container_node(canvas, node, repository, image_repository) + } Node::Rectangle(node) => self.draw_rect_node(canvas, node), Node::Ellipse(node) => self.draw_ellipse_node(canvas, node), Node::Polygon(node) => self.draw_polygon_node(canvas, node), Node::RegularPolygon(node) => self.draw_regular_polygon_node(canvas, node), Node::TextSpan(node) => self.draw_text_span_node(canvas, node), Node::Line(node) => self.draw_line_node(canvas, node), - Node::Image(node) => self.draw_image_node(canvas, node), + Node::Image(node) => self.draw_image_node(canvas, node, image_repository), Node::Path(node) => self.draw_path_node(canvas, node), Node::RegularStarPolygon(node) => self.draw_regular_star_polygon_node(canvas, node), } @@ -573,17 +576,22 @@ pub struct Renderer { logical_width: f32, logical_height: f32, camera: Option, + image_repository: ImageRepository, + font_repository: FontRepository, } impl Renderer { pub fn new(width: f32, height: f32, dpi: f32) -> Self { + let font_repository = FontRepository::new(); Self { - painter: Painter::new(), + painter: Painter::new(&font_repository), backend: None, dpi, logical_width: width, logical_height: height, camera: None, + image_repository: ImageRepository::new(), + font_repository, } } @@ -603,11 +611,11 @@ impl Renderer { } pub fn add_image(&mut self, src: String, image: Image) { - self.painter.add_image(src, image); + self.image_repository.add(src, image); } pub fn add_font(&mut self, bytes: &[u8]) { - self.painter.add_font(bytes); + self.font_repository.add(bytes); } pub fn flush(&self) { @@ -702,7 +710,8 @@ impl Renderer { let surface = unsafe { &mut *backend.get_surface() }; let canvas = surface.canvas(); if let Some(node) = repository.get(id) { - self.painter.draw_node(canvas, node, repository); + self.painter + .draw_node(canvas, node, repository, &self.image_repository); } } } diff --git a/crates/cg/src/repository.rs b/crates/cg/src/repository.rs index 7da9a1e964..dbfe380345 100644 --- a/crates/cg/src/repository.rs +++ b/crates/cg/src/repository.rs @@ -1,4 +1,5 @@ use crate::schema::{Node, NodeId}; +use skia_safe::{FontMgr, Image}; use std::collections::HashMap; /// A repository for managing nodes with automatic ID indexing. @@ -8,6 +9,20 @@ pub struct NodeRepository { nodes: HashMap, } +/// A repository for managing images with automatic ID indexing. +#[derive(Debug, Clone)] +pub struct ImageRepository { + /// The map of all images indexed by their source URLs + images: HashMap, +} + +/// A repository for managing fonts. +#[derive(Debug, Clone)] +pub struct FontRepository { + /// The font manager for handling font data + font_mgr: FontMgr, +} + impl NodeRepository { /// Creates a new empty node repository pub fn new() -> Self { @@ -67,6 +82,49 @@ impl NodeRepository { } } +impl ImageRepository { + /// Creates a new empty image repository + pub fn new() -> Self { + Self { + images: HashMap::new(), + } + } + + /// Adds an image to the repository + pub fn add(&mut self, src: String, image: Image) { + self.images.insert(src, image); + } + + /// Gets a reference to an image by its source URL + pub fn get(&self, src: &str) -> Option<&Image> { + self.images.get(src) + } + + /// Removes an image from the repository by its source URL + pub fn remove(&mut self, src: &str) -> Option { + self.images.remove(src) + } +} + +impl FontRepository { + /// Creates a new empty font repository + pub fn new() -> Self { + Self { + font_mgr: FontMgr::new(), + } + } + + /// Adds a font to the repository + pub fn add(&mut self, bytes: &[u8]) { + self.font_mgr.new_from_data(bytes, None); + } + + /// Gets a reference to the font manager + pub fn font_mgr(&self) -> &FontMgr { + &self.font_mgr + } +} + impl Default for NodeRepository { fn default() -> Self { Self::new() From e155a5cd3ab7c9cfbdffa5f3b489d9bba97ff235 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 21:17:55 +0900 Subject: [PATCH 056/262] layer blur & pipeline --- crates/cg/examples/effects.rs | 46 +-- crates/cg/src/draw.rs | 622 +++++++++++++++++++++------------- crates/cg/src/factory.rs | 6 + crates/cg/src/io.rs | 3 + crates/cg/src/schema.rs | 8 + 5 files changed, 420 insertions(+), 265 deletions(-) diff --git a/crates/cg/examples/effects.rs b/crates/cg/examples/effects.rs index ec1bef979c..9fcecc4af6 100644 --- a/crates/cg/examples/effects.rs +++ b/crates/cg/examples/effects.rs @@ -66,10 +66,31 @@ async fn demo_effects() -> Scene { repository.insert(Node::Rectangle(rect)); } + // Row 2: Gaussian Blur Variations + for i in 0..4 { + let mut rect = nf.create_rectangle_node(); + rect.base.name = format!("Gaussian Blur {}", 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(255, 255, 255, 255), // White + }); + rect.effect = Some(FilterEffect::GaussianBlur(FeGaussianBlur { + radius: 5.0 * (i + 1) as f32, + })); + all_effect_ids.push(rect.base.id.clone()); + repository.insert(Node::Rectangle(rect)); + } + + // 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, 330.0, 0.0); // y middle of row 2 + vivid_gradient_rect.transform = AffineTransform::new(0.0, 530.0, 0.0); // y middle of row 2 vivid_gradient_rect.size = Size { width: 1080.0, height: 90.0, @@ -95,12 +116,11 @@ async fn demo_effects() -> Scene { let vivid_gradient_rect_id = vivid_gradient_rect.base.id.clone(); repository.insert(Node::Rectangle(vivid_gradient_rect)); - // Row 2: Backdrop Blur Variations for i in 0..4 { // Create a semi-transparent rectangle with backdrop blur let mut blur_rect = nf.create_rectangle_node(); blur_rect.base.name = format!("Backdrop Blur {}", i + 1); - blur_rect.transform = AffineTransform::new(start_x + spacing * i as f32, 300.0, 0.0); + 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, @@ -116,26 +136,6 @@ async fn demo_effects() -> Scene { repository.insert(Node::Rectangle(blur_rect)); } - // Row 3: Gaussian Blur Variations - for i in 0..4 { - let mut rect = nf.create_rectangle_node(); - rect.base.name = format!("Gaussian Blur {}", 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(20.0); - rect.fill = Paint::Solid(SolidPaint { - color: Color(255, 255, 255, 255), // White - }); - rect.effect = Some(FilterEffect::GaussianBlur(FeGaussianBlur { - radius: 5.0 * (i + 1) as f32, - })); - all_effect_ids.push(rect.base.id.clone()); - repository.insert(Node::Rectangle(rect)); - } - // Set up the root container root_container_node.children = vec![background_rect_id, vivid_gradient_rect_id]; root_container_node.children.extend(all_effect_ids); diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 743d56023e..4f2eabdf4a 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,3 +1,5 @@ +// + use crate::cvt; use crate::schema::*; use crate::{ @@ -5,14 +7,11 @@ use crate::{ repository::{FontRepository, ImageRepository, NodeRepository}, }; use skia_safe::{ - FontMgr, Image, ImageFilter, MaskFilter, Paint as SkPaint, Picture, PictureRecorder, Point, - RRect, Rect, Surface, - canvas::SaveLayerRec, - surfaces, - textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle}, + Image, MaskFilter, Paint as SkPaint, Picture, PictureRecorder, Point, RRect, Rect, Surface, + canvas::SaveLayerRec, image_filters::blur, surfaces, textlayout::*, }; -use std::collections::HashMap; +/// Choice of GPU vs. raster backend pub enum Backend { GL(*mut Surface), Raster(*mut Surface), @@ -26,22 +25,25 @@ impl Backend { } } -/// A painter that handles all drawing operations for nodes. -/// This struct is responsible for the actual painting operations, -/// while Renderer manages the high-level rendering flow. +/// A painter that handles all drawing operations for nodes, +/// with proper effect ordering and layer-blur pipeline. pub struct Painter { font_collection: FontCollection, } impl Painter { + /// Create a new Painter, using fonts from the FontRepository pub fn new(font_repository: &FontRepository) -> Self { let mut font_collection = FontCollection::new(); font_collection.set_default_font_manager(font_repository.font_mgr().clone(), None); - Self { font_collection } } - // --- Helper methods for internal use --- + // ============================ + // === Helper Methods ======== + // ============================ + + /// Save/restore transform state and apply a 2×3 matrix fn with_canvas_state( &self, canvas: &skia_safe::Canvas, @@ -54,6 +56,7 @@ impl Painter { canvas.restore(); } + /// If opacity < 1.0, wrap drawing in a save_layer_alpha, else draw directly. fn with_opacity_layer(&self, canvas: &skia_safe::Canvas, opacity: f32, f: F) { if opacity < 1.0 { canvas.save_layer_alpha(None, (opacity * 255.0) as u32); @@ -64,79 +67,63 @@ impl Painter { } } - fn draw_drop_shadow( + /// Wrap a closure `f` in a layer that applies a Gaussian blur to everything drawn inside. + fn with_layer_blur(&self, canvas: &skia_safe::Canvas, radius: f32, f: F) { + let image_filter = blur((radius, radius), None, None, None); + let mut paint = SkPaint::default(); + paint.set_image_filter(image_filter); + canvas.save_layer(&SaveLayerRec::default().paint(&paint)); + f(); + canvas.restore(); + } + + /// Draw a drop shadow behind the content at `rect` with corner radii. + fn draw_shadow( &self, canvas: &skia_safe::Canvas, rect: Rect, radii: &RectangularCornerRadius, - shadow: &FilterEffect, + shadow: &FeDropShadow, ) { - match shadow { - FilterEffect::DropShadow(shadow) => { - let mut shadow_paint = SkPaint::default(); - let Color(r, g, b, a) = shadow.color; - shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); - shadow_paint.set_anti_alias(true); - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( - skia_safe::BlurStyle::Normal, - shadow.blur, - None, - )); - } - let offset_x = shadow.dx; - let offset_y = shadow.dy; - let RectangularCornerRadius { tl, tr, bl, br } = *radii; - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), - Point::new(tr, tr), - Point::new(br, br), - Point::new(bl, bl), - ], - ); - let mut shadow_rrect = rrect; - shadow_rrect.offset((offset_x, offset_y)); - canvas.draw_rrect(shadow_rrect, &shadow_paint); - } else { - let mut shadow_rect = rect; - shadow_rect.offset((offset_x, offset_y)); - canvas.draw_rect(shadow_rect, &shadow_paint); - } - } - FilterEffect::BackdropBlur(blur) => { - self.draw_backdrop_blur(canvas, rect, radii, blur); - } - FilterEffect::GaussianBlur(blur) => { - let mut paint = SkPaint::default(); - paint.set_anti_alias(true); - paint.set_mask_filter(MaskFilter::blur( - skia_safe::BlurStyle::Normal, - blur.radius, - None, - )); - - let RectangularCornerRadius { tl, tr, bl, br } = *radii; - if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(tl, tl), - Point::new(tr, tr), - Point::new(br, br), - Point::new(bl, bl), - ], - ); - canvas.draw_rrect(rrect, &paint); - } else { - canvas.draw_rect(rect, &paint); - } - } + let mut shadow_paint = SkPaint::default(); + let Color(r, g, b, a) = shadow.color; + shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); + shadow_paint.set_anti_alias(true); + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + skia_safe::BlurStyle::Normal, + shadow.blur, + None, + )); + } + + let offset_x = shadow.dx; + let offset_y = shadow.dy; + let RectangularCornerRadius { tl, tr, bl, br } = *radii; + + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + // Rounded rect shadow + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(tl, tl), + Point::new(tr, tr), + Point::new(br, br), + Point::new(bl, bl), + ], + ); + let mut shadow_rrect = rrect; + shadow_rrect.offset((offset_x, offset_y)); + canvas.draw_rrect(shadow_rrect, &shadow_paint); + } else { + // Regular rect shadow + let mut shadow_rect = rect; + shadow_rect.offset((offset_x, offset_y)); + canvas.draw_rect(shadow_rect, &shadow_paint); } } + /// Draw a backdrop blur: blur what's behind `rect`, clipped to rounded-corner area. fn draw_backdrop_blur( &self, canvas: &skia_safe::Canvas, @@ -144,7 +131,7 @@ impl Painter { radii: &RectangularCornerRadius, blur: &FeBackdropBlur, ) { - // Create a layer for the backdrop blur effect + // Create a paint that blurs let mut paint = SkPaint::default(); paint.set_mask_filter(MaskFilter::blur( skia_safe::BlurStyle::Normal, @@ -152,10 +139,7 @@ impl Painter { None, )); - // Save the current canvas state - canvas.save(); - - // Apply rounded corners if needed + // Clip to the shape's rounded rectangle (or rect) so blur only inside let RectangularCornerRadius { tl, tr, bl, br } = *radii; if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { let rrect = RRect::new_rect_radii( @@ -167,22 +151,24 @@ impl Painter { Point::new(bl, bl), ], ); + canvas.save(); canvas.clip_rrect(rrect, None, true); } else { + canvas.save(); canvas.clip_rect(rect, None, true); } - // Create a layer for the blur effect + // Draw a rectangle filled with the blurred background + // Here, draw_rect with a blur mask filter effectively blurs everything behind the rect canvas.save_layer_alpha(None, 255); - - // Draw the blurred content canvas.draw_rect(rect, &paint); - - // Restore the canvas state canvas.restore(); + + // Restore from clipping canvas.restore(); } + /// Draw fill and stroke for a shape at `rect` with `radii`, using given paints. fn draw_fill_and_stroke( &self, canvas: &skia_safe::Canvas, @@ -191,13 +177,15 @@ impl Painter { fill: &Paint, stroke: Option<&Paint>, stroke_width: f32, - blend_mode: crate::schema::BlendMode, + blend_mode: BlendMode, opacity: f32, ) { let RectangularCornerRadius { tl, tr, bl, br } = *radii; let mut fill_paint = cvt::sk_paint(fill, opacity, (rect.width(), rect.height())); fill_paint.set_blend_mode(blend_mode.into()); + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { + // Rounded rect fill let rrect = RRect::new_rect_radii( rect, &[ @@ -208,6 +196,8 @@ impl Painter { ], ); canvas.draw_rrect(rrect, &fill_paint); + + // Stroke if present if let Some(stroke) = stroke { if stroke_width > 0.0 { let mut stroke_paint = @@ -219,7 +209,10 @@ impl Painter { } } } else { + // Regular rect fill canvas.draw_rect(rect, &fill_paint); + + // Stroke if present if let Some(stroke) = stroke { if stroke_width > 0.0 { let mut stroke_paint = @@ -233,34 +226,60 @@ impl Painter { } } - // --- Node drawing methods --- - pub fn draw_rect_node(&self, canvas: &skia_safe::Canvas, node: &RectangleNode) { + /// Central dispatcher that applies the effect (drop‐shadow, layer‐blur, backdrop‐blur) + /// around drawing the content (closure `draw_content`). + fn apply_effect( + &self, + canvas: &skia_safe::Canvas, + effect: &FilterEffect, + rect: Rect, + radii: &RectangularCornerRadius, + draw_content: F, + ) { + match effect { + FilterEffect::GaussianBlur(blur) => { + // Layer‐blur: blur everything drawn inside the closure + self.with_layer_blur(canvas, blur.radius, draw_content); + } + FilterEffect::DropShadow(shadow) => { + // Drop shadow behind content, then draw content normally + self.draw_shadow(canvas, rect, radii, shadow); + draw_content(); + } + FilterEffect::BackdropBlur(blur) => { + // Backdrop blur behind content, then draw content normally + self.draw_backdrop_blur(canvas, rect, radii, blur); + draw_content(); + } + } + } + + // ============================ + // === Node Drawing Methods === + // ============================ + + /// Draw a RectangleNode, respecting its transform, effect, fill, stroke, blend mode, opacity + fn draw_rect_node(&self, canvas: &skia_safe::Canvas, node: &RectangleNode) { self.with_canvas_state(canvas, &node.transform.matrix, || { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let radii = node.corner_radius; - if let Some(FilterEffect::GaussianBlur(blur)) = &node.effect { - // First draw the content - self.draw_fill_and_stroke( - canvas, - rect, - &radii, - &node.fill, - Some(&node.stroke), - node.stroke_width, - node.blend_mode, - node.opacity, - ); - // Then apply the blur filter - let image_filter = - skia_safe::image_filters::blur((blur.radius, blur.radius), None, None, None); - let mut paint = SkPaint::default(); - paint.set_image_filter(image_filter); - canvas.draw_rect(rect, &paint); + // If there's an effect, wrap draw in apply_effect + if let Some(effect) = &node.effect { + self.apply_effect(canvas, effect, rect, &radii, || { + self.draw_fill_and_stroke( + canvas, + rect, + &radii, + &node.fill, + Some(&node.stroke), + node.stroke_width, + node.blend_mode, + node.opacity, + ); + }); } else { - if let Some(effect) = &node.effect { - self.draw_drop_shadow(canvas, rect, &radii, effect); - } + // No effect: just draw fill + stroke self.draw_fill_and_stroke( canvas, rect, @@ -275,6 +294,7 @@ impl Painter { }); } + /// Draw a ContainerNode (background + stroke + children) pub fn draw_container_node( &self, canvas: &skia_safe::Canvas, @@ -283,22 +303,41 @@ impl Painter { image_repository: &ImageRepository, ) { self.with_canvas_state(canvas, &node.transform.matrix, || { + // Respect container opacity by wrapping in a save_layer_alpha self.with_opacity_layer(canvas, node.opacity, || { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let radii = node.corner_radius; + + // If there's an effect, wrap draw in apply_effect if let Some(effect) = &node.effect { - self.draw_drop_shadow(canvas, rect, &radii, effect); + self.apply_effect(canvas, effect, rect, &radii, || { + // Draw fill + stroke + self.draw_fill_and_stroke( + canvas, + rect, + &radii, + &node.fill, + node.stroke.as_ref(), + node.stroke_width, + node.blend_mode, + node.opacity, + ); + }); + } else { + // No effect: just draw fill + stroke + self.draw_fill_and_stroke( + canvas, + rect, + &radii, + &node.fill, + node.stroke.as_ref(), + node.stroke_width, + node.blend_mode, + node.opacity, + ); } - self.draw_fill_and_stroke( - canvas, - rect, - &radii, - &node.fill, - node.stroke.as_ref(), - node.stroke_width, - node.blend_mode, - node.opacity, - ); + + // Draw children on top for child_id in &node.children { if let Some(child) = repository.get(child_id) { self.draw_node(canvas, child, repository, image_repository); @@ -308,6 +347,7 @@ impl Painter { }); } + /// Draw an ImageNode, respecting transform, effect, rounded corners, blend mode, opacity pub fn draw_image_node( &self, canvas: &skia_safe::Canvas, @@ -318,48 +358,91 @@ impl Painter { self.with_canvas_state(canvas, &node.transform.matrix, || { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let radii = node.corner_radius; + + // If there's an effect, wrap draw in apply_effect if let Some(effect) = &node.effect { - self.draw_drop_shadow(canvas, rect, &radii, effect); - } - // Draw the image (with optional rounded corners) - let mut paint = SkPaint::default(); - paint.set_anti_alias(true); - paint.set_blend_mode(node.blend_mode.into()); - paint.set_alpha((node.opacity * 255.0) as u8); - if radii.tl > 0.0 || radii.tr > 0.0 || radii.bl > 0.0 || radii.br > 0.0 { - let rrect = RRect::new_rect_radii( - rect, - &[ - Point::new(radii.tl, radii.tl), - Point::new(radii.tr, radii.tr), - Point::new(radii.br, radii.br), - Point::new(radii.bl, radii.bl), - ], - ); - canvas.save(); - canvas.clip_rrect(rrect, None, true); - canvas.draw_image_rect(image, None, rect, &paint); - canvas.restore(); + self.apply_effect(canvas, effect, rect, &radii, || { + // Draw the image with rounded-rect clipping + let mut paint = SkPaint::default(); + paint.set_anti_alias(true); + paint.set_blend_mode(node.blend_mode.into()); + paint.set_alpha((node.opacity * 255.0) as u8); + + if radii.tl > 0.0 || radii.tr > 0.0 || radii.bl > 0.0 || radii.br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(radii.tl, radii.tl), + Point::new(radii.tr, radii.tr), + Point::new(radii.br, radii.br), + Point::new(radii.bl, radii.bl), + ], + ); + canvas.save(); + canvas.clip_rrect(rrect, None, true); + canvas.draw_image_rect(image, None, rect, &paint); + canvas.restore(); + } else { + canvas.draw_image_rect(image, None, rect, &paint); + } + + // Draw stroke if needed + if node.stroke_width > 0.0 { + self.draw_fill_and_stroke( + canvas, + rect, + &radii, + &node.stroke, + None, + node.stroke_width, + node.blend_mode, + node.opacity, + ); + } + }); } else { - canvas.draw_image_rect(image, None, rect, &paint); - } - // Draw stroke if stroke_width > 0 - if node.stroke_width > 0.0 { - self.draw_fill_and_stroke( - canvas, - rect, - &radii, - &node.stroke, - None, - node.stroke_width, - node.blend_mode, - node.opacity, - ); + // No effect: draw image + stroke directly + let mut paint = SkPaint::default(); + paint.set_anti_alias(true); + paint.set_blend_mode(node.blend_mode.into()); + paint.set_alpha((node.opacity * 255.0) as u8); + + if radii.tl > 0.0 || radii.tr > 0.0 || radii.bl > 0.0 || radii.br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(radii.tl, radii.tl), + Point::new(radii.tr, radii.tr), + Point::new(radii.br, radii.br), + Point::new(radii.bl, radii.bl), + ], + ); + canvas.save(); + canvas.clip_rrect(rrect, None, true); + canvas.draw_image_rect(image, None, rect, &paint); + canvas.restore(); + } else { + canvas.draw_image_rect(image, None, rect, &paint); + } + + if node.stroke_width > 0.0 { + self.draw_fill_and_stroke( + canvas, + rect, + &radii, + &node.stroke, + None, + node.stroke_width, + node.blend_mode, + node.opacity, + ); + } } }); } } + /// Draw a GroupNode: no shape of its own, only children, but apply transform + opacity pub fn draw_group_node( &self, canvas: &skia_safe::Canvas, @@ -378,48 +461,20 @@ impl Painter { }); } - pub fn draw_node( - &self, - canvas: &skia_safe::Canvas, - node: &Node, - repository: &NodeRepository, - image_repository: &ImageRepository, - ) { - match node { - Node::Group(node) => self.draw_group_node(canvas, node, repository, image_repository), - Node::Container(node) => { - self.draw_container_node(canvas, node, repository, image_repository) - } - Node::Rectangle(node) => self.draw_rect_node(canvas, node), - Node::Ellipse(node) => self.draw_ellipse_node(canvas, node), - Node::Polygon(node) => self.draw_polygon_node(canvas, node), - Node::RegularPolygon(node) => self.draw_regular_polygon_node(canvas, node), - Node::TextSpan(node) => self.draw_text_span_node(canvas, node), - Node::Line(node) => self.draw_line_node(canvas, node), - Node::Image(node) => self.draw_image_node(canvas, node, image_repository), - Node::Path(node) => self.draw_path_node(canvas, node), - Node::RegularStarPolygon(node) => self.draw_regular_star_polygon_node(canvas, node), - } - } - + /// Draw an EllipseNode pub fn draw_ellipse_node(&self, canvas: &skia_safe::Canvas, node: &EllipseNode) { - let fill_paint = cvt::sk_paint( - &node.fill, - node.opacity, - (node.size.width, node.size.height), - ); - let rect = Rect::from_xywh( - 0.0, // x starts at 0 (top-left) - 0.0, // y starts at 0 (top-left) - node.size.width, - node.size.height, - ); self.with_canvas_state(canvas, &node.transform.matrix, || { - // Draw fill - let mut fill_paint = fill_paint.clone(); + let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); + + // No effect on ellipse for now; you could extend similarly to rect + let mut fill_paint = cvt::sk_paint( + &node.fill, + node.opacity, + (node.size.width, node.size.height), + ); fill_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_oval(rect, &fill_paint); - // Draw stroke if stroke_width > 0 + if node.stroke_width > 0.0 { let mut stroke_paint = cvt::sk_paint( &node.stroke, @@ -434,6 +489,7 @@ impl Painter { }); } + /// Draw a LineNode pub fn draw_line_node(&self, canvas: &skia_safe::Canvas, node: &LineNode) { let mut paint = cvt::sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); paint.set_stroke(true); @@ -448,67 +504,123 @@ impl Painter { }); } + /// Draw a PathNode (SVG path data) pub fn draw_path_node(&self, canvas: &skia_safe::Canvas, node: &PathNode) { self.with_canvas_state(canvas, &node.transform.matrix, || { - let path = skia_safe::path::Path::from_svg(&node.data).expect("path is not valid"); + // Build the Skia path from SVG data + let path = skia_safe::path::Path::from_svg(&node.data).expect("invalid SVG path"); + + // Compute bounding rect of path + let bounds = path.compute_tight_bounds(); + let rect = + Rect::from_xywh(bounds.left(), bounds.top(), bounds.width(), bounds.height()); + let radii = RectangularCornerRadius::zero(); // no corner radii for generic path + + // If there is an effect, wrap in apply_effect + if let Some(effect) = &node.effect { + self.apply_effect(canvas, effect, rect, &radii, || { + // Draw fill + let mut fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + fill_paint.set_blend_mode(node.blend_mode.into()); + canvas.draw_path(&path, &fill_paint); + + // Draw stroke if needed + if node.stroke_width > 0.0 { + let mut stroke_paint = + cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.blend_mode.into()); + canvas.draw_path(&path, &stroke_paint); + } + }); + } else { + // No effect: draw fill + stroke directly + let mut fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + fill_paint.set_blend_mode(node.blend_mode.into()); + canvas.draw_path(&path, &fill_paint); - let fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); - if node.stroke_width > 0.0 { - let mut stroke_paint = cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - canvas.draw_path(&path, &stroke_paint); + if node.stroke_width > 0.0 { + let mut stroke_paint = cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.blend_mode.into()); + canvas.draw_path(&path, &stroke_paint); + } } - - canvas.draw_path(&path, &fill_paint); }); } + /// Draw a PolygonNode (arbitrary polygon with optional corner radius) pub fn draw_polygon_node(&self, canvas: &skia_safe::Canvas, node: &PolygonNode) { if node.points.len() < 3 { - // Not enough points to form a polygon return; } - let fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); self.with_canvas_state(canvas, &node.transform.matrix, || { - // If corner_radius > 0, use the rounded polygon path + // Build path let path = if node.corner_radius > 0.0 { node.to_path() } else { - // Otherwise create a regular polygon path - let mut path = skia_safe::Path::new(); - let mut points_iter = node.points.iter(); - if let Some(&point) = points_iter.next() { - path.move_to((point.x, point.y)); - for point in points_iter { - path.line_to((point.x, point.y)); + let mut p = skia_safe::path::Path::new(); + let mut iter = node.points.iter(); + if let Some(&pt) = iter.next() { + p.move_to((pt.x, pt.y)); + for &pt in iter { + p.line_to((pt.x, pt.y)); } - path.close(); + p.close(); } - path + p }; - // Draw fill - let mut fill_paint = fill_paint.clone(); - fill_paint.set_blend_mode(node.blend_mode.into()); - canvas.draw_path(&path, &fill_paint); + // Compute bounds + radii + let bounds = path.compute_tight_bounds(); + let rect = + Rect::from_xywh(bounds.left(), bounds.top(), bounds.width(), bounds.height()); + let radii = RectangularCornerRadius::all(node.corner_radius); + + // If effect, wrap + if let Some(effect) = &node.effect { + self.apply_effect(canvas, effect, rect, &radii, || { + // Draw fill + let mut fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + fill_paint.set_blend_mode(node.blend_mode.into()); + canvas.draw_path(&path, &fill_paint); + + // Stroke + if node.stroke_width > 0.0 { + let mut stroke_paint = + cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.blend_mode.into()); + canvas.draw_path(&path, &stroke_paint); + } + }); + } else { + // No effect + let mut fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); + fill_paint.set_blend_mode(node.blend_mode.into()); + canvas.draw_path(&path, &fill_paint); - // Draw stroke if stroke_width > 0 - if node.stroke_width > 0.0 { - let mut stroke_paint = cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); - stroke_paint.set_blend_mode(node.blend_mode.into()); - canvas.draw_path(&path, &stroke_paint); + if node.stroke_width > 0.0 { + let mut stroke_paint = cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); + stroke_paint.set_stroke(true); + stroke_paint.set_stroke_width(node.stroke_width); + stroke_paint.set_blend_mode(node.blend_mode.into()); + canvas.draw_path(&path, &stroke_paint); + } } }); } + /// Draw a RegularPolygonNode by converting to a PolygonNode pub fn draw_regular_polygon_node(&self, canvas: &skia_safe::Canvas, node: &RegularPolygonNode) { let poly = node.to_polygon(); self.draw_polygon_node(canvas, &poly); } + /// Draw a RegularStarPolygonNode by converting to a PolygonNode pub fn draw_regular_star_polygon_node( &self, canvas: &skia_safe::Canvas, @@ -518,8 +630,9 @@ impl Painter { self.draw_polygon_node(canvas, &poly); } + /// Draw a TextSpanNode (simple text block) pub fn draw_text_span_node(&self, canvas: &skia_safe::Canvas, node: &TextSpanNode) { - // paints + // Prepare paint for fill let mut fill_paint = cvt::sk_paint( &node.fill, node.opacity, @@ -527,14 +640,15 @@ impl Painter { ); fill_paint.set_blend_mode(node.blend_mode.into()); - // paragraph + // Build paragraph style let mut paragraph_style = ParagraphStyle::new(); - paragraph_style.set_text_direction(skia_safe::textlayout::TextDirection::LTR); + paragraph_style.set_text_direction(TextDirection::LTR); paragraph_style.set_text_align(node.text_align.into()); - let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, &self.font_collection); - // text style - let mut ts = TextStyle::new(); + let mut para_builder = ParagraphBuilder::new(¶graph_style, &self.font_collection); + + // Build text style + let mut ts = skia_safe::textlayout::TextStyle::new(); ts.set_foreground_paint(&fill_paint); ts.set_font_size(node.text_style.font_size); if let Some(letter_spacing) = node.text_style.letter_spacing { @@ -543,11 +657,10 @@ impl Painter { if let Some(line_height) = node.text_style.line_height { ts.set_height(line_height); } - let mut decoration = skia_safe::textlayout::Decoration::default(); - decoration.ty = node.text_style.text_decoration.into(); - ts.set_decoration(&decoration); + let mut decor = skia_safe::textlayout::Decoration::default(); + decor.ty = node.text_style.text_decoration.into(); + ts.set_decoration(&decor); ts.set_font_families(&[&node.text_style.font_family]); - let font_style = skia_safe::FontStyle::new( skia_safe::font_style::Weight::from(node.text_style.font_weight.value()), skia_safe::font_style::Width::NORMAL, @@ -555,18 +668,39 @@ impl Painter { ); ts.set_font_style(font_style); - // paragraph builder - paragraph_builder.push_style(&ts); - paragraph_builder.add_text(&node.text); - let mut paragraph = paragraph_builder.build(); - paragraph_builder.pop(); + para_builder.push_style(&ts); + para_builder.add_text(&node.text); + let mut paragraph = para_builder.build(); + para_builder.pop(); paragraph.layout(node.size.width); self.with_canvas_state(canvas, &node.transform.matrix, || { - // Paint at origin since transform is already applied paragraph.paint(canvas, Point::new(0.0, 0.0)); }); } + + /// Dispatch to the correct node‐type draw method + pub fn draw_node( + &self, + canvas: &skia_safe::Canvas, + node: &Node, + repository: &NodeRepository, + image_repository: &ImageRepository, + ) { + match node { + Node::Group(n) => self.draw_group_node(canvas, n, repository, image_repository), + Node::Container(n) => self.draw_container_node(canvas, n, repository, image_repository), + Node::Rectangle(n) => self.draw_rect_node(canvas, n), + Node::Ellipse(n) => self.draw_ellipse_node(canvas, n), + Node::Polygon(n) => self.draw_polygon_node(canvas, n), + Node::RegularPolygon(n) => self.draw_regular_polygon_node(canvas, n), + Node::TextSpan(n) => self.draw_text_span_node(canvas, n), + Node::Line(n) => self.draw_line_node(canvas, n), + Node::Image(n) => self.draw_image_node(canvas, n, image_repository), + Node::Path(n) => self.draw_path_node(canvas, n), + Node::RegularStarPolygon(n) => self.draw_regular_star_polygon_node(canvas, n), + } + } } pub struct Renderer { @@ -580,6 +714,10 @@ pub struct Renderer { font_repository: FontRepository, } +/// --------------------------------------------------------------------------- +/// Renderer: manages backend, DPI, camera, and iterates over scene children +/// --------------------------------------------------------------------------- + impl Renderer { pub fn new(width: f32, height: f32, dpi: f32) -> Self { let font_repository = FontRepository::new(); diff --git a/crates/cg/src/factory.rs b/crates/cg/src/factory.rs index fb2ba9f42e..9c12736a15 100644 --- a/crates/cg/src/factory.rs +++ b/crates/cg/src/factory.rs @@ -66,6 +66,7 @@ impl NodeFactory { stroke_width: Self::DEFAULT_STROKE_WIDTH, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, + effect: None, } } @@ -151,6 +152,8 @@ impl NodeFactory { stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, opacity: Self::DEFAULT_OPACITY, + blend_mode: BlendMode::Normal, + effect: None, } } @@ -167,6 +170,7 @@ impl NodeFactory { stroke_width: Self::DEFAULT_STROKE_WIDTH, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, + effect: None, } } @@ -183,6 +187,7 @@ impl NodeFactory { stroke_width: Self::DEFAULT_STROKE_WIDTH, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, + effect: None, } } @@ -197,6 +202,7 @@ impl NodeFactory { stroke_width: Self::DEFAULT_STROKE_WIDTH, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, + effect: None, } } diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index ad7bc394d0..37dfa1be8d 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -406,6 +406,7 @@ impl From for Node { color: Color(0, 0, 0, 255), }), stroke_width: node.stroke_width.unwrap_or(0.0), + effect: None, opacity: node.opacity, }) } @@ -422,6 +423,7 @@ impl From for Node { name: node.name, active: node.active, }, + blend_mode: BlendMode::Normal, transform, fill: node.fill.into(), data: node.paths.map_or("".to_string(), |paths| { @@ -436,6 +438,7 @@ impl From for Node { }), stroke_width: 0.0, opacity: node.opacity, + effect: None, }) } } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index be80d68cc3..f4c1ebe670 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -463,6 +463,7 @@ pub struct EllipseNode { pub stroke_width: f32, pub opacity: f32, pub blend_mode: BlendMode, + pub effect: Option, } /// @@ -477,6 +478,8 @@ pub struct PathNode { pub stroke: Paint, pub stroke_width: f32, pub opacity: f32, + pub blend_mode: BlendMode, + pub effect: Option, } /// A polygon shape defined by a list of absolute 2D points, following the SVG `` model. @@ -514,6 +517,7 @@ pub struct PolygonNode { /// Opacity applied to the polygon shape (`0.0` - transparent, `1.0` - opaque). pub opacity: f32, pub blend_mode: BlendMode, + pub effect: Option, } impl PolygonNode { @@ -563,6 +567,7 @@ pub struct RegularPolygonNode { /// Overall node opacity (0.0–1.0) pub opacity: f32, pub blend_mode: BlendMode, + pub effect: Option, } impl RegularPolygonNode { @@ -598,6 +603,7 @@ impl RegularPolygonNode { stroke_width: self.stroke_width, opacity: self.opacity, blend_mode: self.blend_mode, + effect: self.effect.clone(), } } } @@ -648,6 +654,7 @@ pub struct RegularStarPolygonNode { /// Overall node opacity (0.0–1.0) pub opacity: f32, pub blend_mode: BlendMode, + pub effect: Option, } impl RegularStarPolygonNode { @@ -680,6 +687,7 @@ impl RegularStarPolygonNode { stroke_width: self.stroke_width, opacity: self.opacity, blend_mode: self.blend_mode, + effect: self.effect.clone(), } } } From 5e8e3270053a9aa31cc83da77337a90da714febc Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Jun 2025 21:37:17 +0900 Subject: [PATCH 057/262] backdrop blur --- crates/cg/src/draw.rs | 86 +++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 52 deletions(-) diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 4f2eabdf4a..9d4b9c0da5 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -1,5 +1,3 @@ -// - use crate::cvt; use crate::schema::*; use crate::{ @@ -8,7 +6,7 @@ use crate::{ }; use skia_safe::{ Image, MaskFilter, Paint as SkPaint, Picture, PictureRecorder, Point, RRect, Rect, Surface, - canvas::SaveLayerRec, image_filters::blur, surfaces, textlayout::*, + canvas::SaveLayerRec, surfaces, textlayout::*, }; /// Choice of GPU vs. raster backend @@ -26,7 +24,7 @@ impl Backend { } /// A painter that handles all drawing operations for nodes, -/// with proper effect ordering and layer-blur pipeline. +/// with proper effect ordering and a layer‐blur/backdrop‐blur pipeline. pub struct Painter { font_collection: FontCollection, } @@ -56,7 +54,7 @@ impl Painter { canvas.restore(); } - /// If opacity < 1.0, wrap drawing in a save_layer_alpha, else draw directly. + /// If opacity < 1.0, wrap drawing in a save_layer_alpha; else draw directly. fn with_opacity_layer(&self, canvas: &skia_safe::Canvas, opacity: f32, f: F) { if opacity < 1.0 { canvas.save_layer_alpha(None, (opacity * 255.0) as u32); @@ -69,7 +67,7 @@ impl Painter { /// Wrap a closure `f` in a layer that applies a Gaussian blur to everything drawn inside. fn with_layer_blur(&self, canvas: &skia_safe::Canvas, radius: f32, f: F) { - let image_filter = blur((radius, radius), None, None, None); + let image_filter = skia_safe::image_filters::blur((radius, radius), None, None, None); let mut paint = SkPaint::default(); paint.set_image_filter(image_filter); canvas.save_layer(&SaveLayerRec::default().paint(&paint)); @@ -85,20 +83,24 @@ impl Painter { radii: &RectangularCornerRadius, shadow: &FeDropShadow, ) { - let mut shadow_paint = SkPaint::default(); let Color(r, g, b, a) = shadow.color; - shadow_paint.set_color(skia_safe::Color::from_argb(a, r, g, b)); + let color = skia_safe::Color::from_argb(a, r, g, b); + + // Create drop shadow filter + let image_filter = skia_safe::image_filters::drop_shadow( + (shadow.dx, shadow.dy), // offset as tuple + (shadow.blur, shadow.blur), // sigma as tuple + color, // color + None, // color_space + None, // input + None, // crop_rect + ); + + // Create paint with the drop shadow filter + let mut shadow_paint = SkPaint::default(); + shadow_paint.set_image_filter(image_filter); shadow_paint.set_anti_alias(true); - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( - skia_safe::BlurStyle::Normal, - shadow.blur, - None, - )); - } - let offset_x = shadow.dx; - let offset_y = shadow.dy; let RectangularCornerRadius { tl, tr, bl, br } = *radii; if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { @@ -112,18 +114,14 @@ impl Painter { Point::new(bl, bl), ], ); - let mut shadow_rrect = rrect; - shadow_rrect.offset((offset_x, offset_y)); - canvas.draw_rrect(shadow_rrect, &shadow_paint); + canvas.draw_rrect(rrect, &shadow_paint); } else { // Regular rect shadow - let mut shadow_rect = rect; - shadow_rect.offset((offset_x, offset_y)); - canvas.draw_rect(shadow_rect, &shadow_paint); + canvas.draw_rect(rect, &shadow_paint); } } - /// Draw a backdrop blur: blur what's behind `rect`, clipped to rounded-corner area. + /// Draw a backdrop blur: blur what's behind `rect`, clipped to a rounded‐corner area. fn draw_backdrop_blur( &self, canvas: &skia_safe::Canvas, @@ -131,16 +129,13 @@ impl Painter { radii: &RectangularCornerRadius, blur: &FeBackdropBlur, ) { - // Create a paint that blurs - let mut paint = SkPaint::default(); - paint.set_mask_filter(MaskFilter::blur( - skia_safe::BlurStyle::Normal, - blur.radius, - None, - )); + // 1) Build a Gaussian‐blur filter for the backdrop + let image_filter = + skia_safe::image_filters::blur((blur.radius, blur.radius), None, None, None).unwrap(); - // Clip to the shape's rounded rectangle (or rect) so blur only inside + // 2) Clip to the shape (rounded rect or plain rect) let RectangularCornerRadius { tl, tr, bl, br } = *radii; + canvas.save(); if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { let rrect = RRect::new_rect_radii( rect, @@ -151,21 +146,18 @@ impl Painter { Point::new(bl, bl), ], ); - canvas.save(); canvas.clip_rrect(rrect, None, true); } else { - canvas.save(); canvas.clip_rect(rect, None, true); } - // Draw a rectangle filled with the blurred background - // Here, draw_rect with a blur mask filter effectively blurs everything behind the rect - canvas.save_layer_alpha(None, 255); - canvas.draw_rect(rect, &paint); - canvas.restore(); + // 3) Use a SaveLayerRec with a backdrop filter so that everything behind is blurred + let layer_rec = SaveLayerRec::default().backdrop(&image_filter); + canvas.save_layer(&layer_rec); - // Restore from clipping - canvas.restore(); + // We don't draw any content here—just pushing and popping the layer + canvas.restore(); // pop the SaveLayer + canvas.restore(); // pop the clip } /// Draw fill and stroke for a shape at `rect` with `radii`, using given paints. @@ -264,7 +256,6 @@ impl Painter { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let radii = node.corner_radius; - // If there's an effect, wrap draw in apply_effect if let Some(effect) = &node.effect { self.apply_effect(canvas, effect, rect, &radii, || { self.draw_fill_and_stroke( @@ -279,7 +270,6 @@ impl Painter { ); }); } else { - // No effect: just draw fill + stroke self.draw_fill_and_stroke( canvas, rect, @@ -303,15 +293,12 @@ impl Painter { image_repository: &ImageRepository, ) { self.with_canvas_state(canvas, &node.transform.matrix, || { - // Respect container opacity by wrapping in a save_layer_alpha self.with_opacity_layer(canvas, node.opacity, || { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let radii = node.corner_radius; - // If there's an effect, wrap draw in apply_effect if let Some(effect) = &node.effect { self.apply_effect(canvas, effect, rect, &radii, || { - // Draw fill + stroke self.draw_fill_and_stroke( canvas, rect, @@ -324,7 +311,6 @@ impl Painter { ); }); } else { - // No effect: just draw fill + stroke self.draw_fill_and_stroke( canvas, rect, @@ -359,10 +345,9 @@ impl Painter { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let radii = node.corner_radius; - // If there's an effect, wrap draw in apply_effect if let Some(effect) = &node.effect { self.apply_effect(canvas, effect, rect, &radii, || { - // Draw the image with rounded-rect clipping + // Draw the image with rounded‐rect clipping let mut paint = SkPaint::default(); paint.set_anti_alias(true); paint.set_blend_mode(node.blend_mode.into()); @@ -466,7 +451,6 @@ impl Painter { self.with_canvas_state(canvas, &node.transform.matrix, || { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); - // No effect on ellipse for now; you could extend similarly to rect let mut fill_paint = cvt::sk_paint( &node.fill, node.opacity, @@ -516,7 +500,6 @@ impl Painter { Rect::from_xywh(bounds.left(), bounds.top(), bounds.width(), bounds.height()); let radii = RectangularCornerRadius::zero(); // no corner radii for generic path - // If there is an effect, wrap in apply_effect if let Some(effect) = &node.effect { self.apply_effect(canvas, effect, rect, &radii, || { // Draw fill @@ -579,7 +562,6 @@ impl Painter { Rect::from_xywh(bounds.left(), bounds.top(), bounds.width(), bounds.height()); let radii = RectangularCornerRadius::all(node.corner_radius); - // If effect, wrap if let Some(effect) = &node.effect { self.apply_effect(canvas, effect, rect, &radii, || { // Draw fill From 597c0b569e103d5046c9300eaa549e2dfa05a365 Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Jun 2025 15:45:10 +0900 Subject: [PATCH 058/262] paint opacity --- crates/cg/benches/bench_rectangles.rs | 2 + crates/cg/examples/100k.rs | 1 + crates/cg/examples/basic.rs | 11 +- crates/cg/examples/effects.rs | 7 +- crates/cg/examples/nested.rs | 3 + crates/cg/examples/paint.rs | 262 ++++++++++++++++++++++++++ crates/cg/examples/shapes.rs | 7 + crates/cg/examples/texts.rs | 2 + crates/cg/src/cvt.rs | 8 +- crates/cg/src/draw.rs | 2 +- crates/cg/src/factory.rs | 5 +- crates/cg/src/io.rs | 6 + crates/cg/src/schema.rs | 29 ++- 13 files changed, 335 insertions(+), 10 deletions(-) create mode 100644 crates/cg/examples/paint.rs diff --git a/crates/cg/benches/bench_rectangles.rs b/crates/cg/benches/bench_rectangles.rs index 391ee2ca83..f0db04ebd3 100644 --- a/crates/cg/benches/bench_rectangles.rs +++ b/crates/cg/benches/bench_rectangles.rs @@ -27,9 +27,11 @@ fn create_rectangles(count: usize, with_effects: bool) -> Scene { 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, opacity: 1.0, diff --git a/crates/cg/examples/100k.rs b/crates/cg/examples/100k.rs index d7cfb28d41..da16cbd729 100644 --- a/crates/cg/examples/100k.rs +++ b/crates/cg/examples/100k.rs @@ -49,6 +49,7 @@ async fn demo_n_shapes(n: usize) -> Scene { let intensity = (i % 255) as u8; rect.fill = Paint::Solid(SolidPaint { color: Color(intensity, intensity, intensity, 255), + opacity: 1.0, }); all_shape_ids.push(rect.base.id.clone()); diff --git a/crates/cg/examples/basic.rs b/crates/cg/examples/basic.rs index a2f23ee9d1..4e936b9302 100644 --- a/crates/cg/examples/basic.rs +++ b/crates/cg/examples/basic.rs @@ -29,9 +29,11 @@ async fn demo_basic() -> Scene { }; background_rect_node.fill = Paint::Solid(SolidPaint { color: Color(230, 240, 255, 255), // Light blue for visibility + opacity: 1.0, }); background_rect_node.stroke = Paint::Solid(SolidPaint { color: Color(0, 0, 0, 0), // No stroke + opacity: 1.0, }); background_rect_node.stroke_width = 0.0; @@ -70,6 +72,7 @@ async fn demo_basic() -> Scene { 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 { @@ -89,7 +92,6 @@ async fn demo_basic() -> Scene { height: 200.0, }; ellipse_node.fill = Paint::RadialGradient(RadialGradientPaint { - id: "gradient2".to_string(), transform: AffineTransform::identity(), stops: vec![ GradientStop { @@ -105,6 +107,7 @@ async fn demo_basic() -> Scene { color: Color(255, 0, 255, 255), // Magenta }, ], + opacity: 1.0, }); ellipse_node.stroke_width = 6.0; @@ -126,9 +129,11 @@ async fn demo_basic() -> Scene { 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; @@ -144,6 +149,7 @@ async fn demo_basic() -> Scene { 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; @@ -169,6 +175,7 @@ async fn demo_basic() -> Scene { 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); @@ -179,6 +186,7 @@ async fn demo_basic() -> Scene { 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; @@ -193,6 +201,7 @@ async fn demo_basic() -> Scene { }; line_node.stroke = Paint::Solid(SolidPaint { color: Color(0, 255, 0, 255), // Green color + opacity: 1.0, }); line_node.stroke_width = 4.0; diff --git a/crates/cg/examples/effects.rs b/crates/cg/examples/effects.rs index 9fcecc4af6..e25e4c0744 100644 --- a/crates/cg/examples/effects.rs +++ b/crates/cg/examples/effects.rs @@ -16,7 +16,6 @@ async fn demo_effects() -> Scene { height: 1080.0, }; background_rect_node.fill = Paint::LinearGradient(LinearGradientPaint { - id: "bg_gradient".to_string(), transform: AffineTransform::identity(), stops: vec![ GradientStop { @@ -28,6 +27,7 @@ async fn demo_effects() -> Scene { color: Color(200, 200, 200, 255), // Darker gray }, ], + opacity: 1.0, }); // Create a root container node @@ -55,6 +55,7 @@ async fn demo_effects() -> Scene { 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: 5.0 * (i + 1) as f32, @@ -78,6 +79,7 @@ async fn demo_effects() -> Scene { 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::GaussianBlur(FeGaussianBlur { radius: 5.0 * (i + 1) as f32, @@ -96,7 +98,6 @@ async fn demo_effects() -> Scene { height: 90.0, }; vivid_gradient_rect.fill = Paint::LinearGradient(LinearGradientPaint { - id: "vivid_row2".to_string(), transform: AffineTransform::identity(), stops: vec![ GradientStop { @@ -112,6 +113,7 @@ async fn demo_effects() -> Scene { 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)); @@ -128,6 +130,7 @@ async fn demo_effects() -> Scene { 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: 16.0 * (i + 1) as f32, diff --git a/crates/cg/examples/nested.rs b/crates/cg/examples/nested.rs index 1d6d0edcad..64f2aefa8a 100644 --- a/crates/cg/examples/nested.rs +++ b/crates/cg/examples/nested.rs @@ -19,6 +19,7 @@ async fn demo_nested() -> Scene { }; 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)); @@ -44,6 +45,7 @@ async fn demo_nested() -> Scene { }; 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)); @@ -70,6 +72,7 @@ async fn demo_nested() -> Scene { }; 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)); diff --git a/crates/cg/examples/paint.rs b/crates/cg/examples/paint.rs new file mode 100644 index 0000000000..bc2e590f08 --- /dev/null +++ b/crates/cg/examples/paint.rs @@ -0,0 +1,262 @@ +use cg::factory::NodeFactory; +use cg::repository::NodeRepository; +use cg::schema::*; +use cg::transform::AffineTransform; + +mod window; + +async fn demo_paints() -> Scene { + let nf = NodeFactory::new(); + + // Add a background rectangle node + let mut background_rect_node = nf.create_rectangle_node(); + background_rect_node.base.name = "Background Rect".to_string(); + background_rect_node.size = Size { + width: 1080.0, + height: 1080.0, + }; + background_rect_node.fill = Paint::Solid(SolidPaint { + color: Color(240, 240, 240, 255), // Light gray background + 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(); + + let mut repository = NodeRepository::new(); + let background_rect_id = background_rect_node.base.id.clone(); + repository.insert(Node::Rectangle(background_rect_node)); + + 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 = vec![background_rect_id]; + 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, + } +} + +#[tokio::main] +async fn main() { + let scene = demo_paints().await; + window::run_demo_window(scene).await; +} diff --git a/crates/cg/examples/shapes.rs b/crates/cg/examples/shapes.rs index 3b76af4e3a..372aca4f93 100644 --- a/crates/cg/examples/shapes.rs +++ b/crates/cg/examples/shapes.rs @@ -17,6 +17,7 @@ async fn demo_shapes() -> Scene { }; background_rect_node.fill = Paint::Solid(SolidPaint { color: Color(240, 240, 240, 255), // Light gray background + opacity: 1.0, }); // Create a root container node @@ -50,6 +51,7 @@ async fn demo_shapes() -> Scene { 200 - (i * 20) as u8, 255, ), // Fading gray + opacity: 1.0, }); all_shape_ids.push(rect.base.id.clone()); repository.insert(Node::Rectangle(rect)); @@ -71,6 +73,7 @@ async fn demo_shapes() -> Scene { 200 - (i * 20) as u8, 255, ), // Fading gray + opacity: 1.0, }); all_shape_ids.push(ellipse.base.id.clone()); repository.insert(Node::Ellipse(ellipse)); @@ -102,6 +105,7 @@ async fn demo_shapes() -> Scene { 200 - (i * 20) as u8, 255, ), // Fading gray + opacity: 1.0, }); all_shape_ids.push(polygon.base.id.clone()); repository.insert(Node::Polygon(polygon)); @@ -124,6 +128,7 @@ async fn demo_shapes() -> Scene { 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)); @@ -154,6 +159,7 @@ async fn demo_shapes() -> Scene { 200 - (i * 20) as u8, 255, ), // Fading gray + opacity: 1.0, }); all_shape_ids.push(path.base.id.clone()); repository.insert(Node::Path(path)); @@ -177,6 +183,7 @@ async fn demo_shapes() -> Scene { 200 - (i * 20) as u8, 255, ), // Fading gray + opacity: 1.0, }); all_shape_ids.push(star.base.id.clone()); repository.insert(Node::RegularStarPolygon(star)); diff --git a/crates/cg/examples/texts.rs b/crates/cg/examples/texts.rs index c6075ef6bc..ca90753c72 100644 --- a/crates/cg/examples/texts.rs +++ b/crates/cg/examples/texts.rs @@ -25,6 +25,7 @@ async fn demo_texts() -> Scene { }; background_rect_node.fill = Paint::Solid(SolidPaint { color: Color(230, 240, 255, 255), // Light blue background + opacity: 1.0, }); // Create a single word text span @@ -46,6 +47,7 @@ async fn demo_texts() -> Scene { }; 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; diff --git a/crates/cg/src/cvt.rs b/crates/cg/src/cvt.rs index f976b9992f..b5c578293a 100644 --- a/crates/cg/src/cvt.rs +++ b/crates/cg/src/cvt.rs @@ -30,11 +30,12 @@ pub fn sk_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> skia_safe::Pai match paint { Paint::Solid(solid) => { let Color(r, g, b, a) = solid.color; - let final_alpha = (a as f32 * opacity) as u8; + let final_alpha = (a as f32 * opacity * solid.opacity) as u8; skia_paint.set_color(skia_safe::Color::from_argb(final_alpha, r, g, b)); } Paint::LinearGradient(gradient) => { - let (colors, positions) = cg_build_gradient_stops(&gradient.stops, opacity); + let (colors, positions) = + cg_build_gradient_stops(&gradient.stops, opacity * gradient.opacity); let shader = skia_safe::Shader::linear_gradient( ( skia_safe::Point::new(0.0, 0.0), @@ -50,7 +51,8 @@ pub fn sk_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> skia_safe::Pai skia_paint.set_shader(shader); } Paint::RadialGradient(gradient) => { - let (colors, positions) = cg_build_gradient_stops(&gradient.stops, opacity); + let (colors, positions) = + cg_build_gradient_stops(&gradient.stops, opacity * gradient.opacity); let center = skia_safe::Point::new(width / 2.0, height / 2.0); let radius = width.min(height) / 2.0; let shader = skia_safe::Shader::radial_gradient( diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 9d4b9c0da5..c9499c44d2 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -5,7 +5,7 @@ use crate::{ repository::{FontRepository, ImageRepository, NodeRepository}, }; use skia_safe::{ - Image, MaskFilter, Paint as SkPaint, Picture, PictureRecorder, Point, RRect, Rect, Surface, + Image, Paint as SkPaint, Picture, PictureRecorder, Point, RRect, Rect, Surface, canvas::SaveLayerRec, surfaces, textlayout::*, }; diff --git a/crates/cg/src/factory.rs b/crates/cg/src/factory.rs index 9c12736a15..56e1f8eb45 100644 --- a/crates/cg/src/factory.rs +++ b/crates/cg/src/factory.rs @@ -36,7 +36,10 @@ impl NodeFactory { } fn default_solid_paint(color: Color) -> Paint { - Paint::Solid(SolidPaint { color }) + Paint::Solid(SolidPaint { + color, + opacity: 1.0, + }) } /// Creates a new rectangle node with default values diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index 37dfa1be8d..c65ea94615 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -297,19 +297,23 @@ impl From> for Paint { if let Some(color) = fill.color { Paint::Solid(SolidPaint { color: Color(color.r, color.g, color.b, (color.a * 255.0) as u8), + opacity: 1.0, }) } else { Paint::Solid(SolidPaint { color: Color(0, 0, 0, 0), + opacity: 1.0, }) } } _ => Paint::Solid(SolidPaint { color: Color(0, 0, 0, 0), + opacity: 1.0, }), }, None => Paint::Solid(SolidPaint { color: Color(0, 0, 0, 0), + opacity: 1.0, }), } } @@ -404,6 +408,7 @@ impl From for Node { fill: node.fill.into(), stroke: Paint::Solid(SolidPaint { color: Color(0, 0, 0, 255), + opacity: 1.0, }), stroke_width: node.stroke_width.unwrap_or(0.0), effect: None, @@ -435,6 +440,7 @@ impl From for Node { }), stroke: Paint::Solid(SolidPaint { color: Color(0, 0, 0, 255), + opacity: 1.0, }), stroke_width: 0.0, opacity: node.opacity, diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index f4c1ebe670..f38dc90d59 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -32,6 +32,21 @@ impl Point { } } +/// Supported fit modes. +/// +/// Only `Contain`, `Cover`, and `None` are supported in the current version. +/// +/// - `None` may have unexpected results depending on the environment. +/// +/// @see https://api.flutter.dev/flutter/painting/BoxFit.html +/// @see https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BoxFit { + Contain, + Cover, + None, +} + #[derive(Debug, Clone, Copy)] pub struct Color(pub u8, pub u8, pub u8, pub u8); @@ -299,27 +314,37 @@ pub enum Paint { Solid(SolidPaint), LinearGradient(LinearGradientPaint), RadialGradient(RadialGradientPaint), + // Image(ImagePaint), } #[derive(Debug, Clone)] pub struct SolidPaint { pub color: Color, + pub opacity: f32, } #[derive(Debug, Clone)] pub struct LinearGradientPaint { - pub id: String, pub transform: super::transform::AffineTransform, pub stops: Vec, + pub opacity: f32, } #[derive(Debug, Clone)] pub struct RadialGradientPaint { - pub id: String, pub transform: super::transform::AffineTransform, pub stops: Vec, + pub opacity: f32, } +// #[derive(Debug, Clone)] +// pub struct ImagePaint { +// pub transform: super::transform::AffineTransform, +// pub _ref: String, +// pub fit: BoxFit, +// pub opacity: f32, +// } + #[derive(Debug, Clone)] pub struct Size { pub width: f32, From 829d806775f424f59ea6bfb0c44956d677c75190 Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Jun 2025 16:17:01 +0900 Subject: [PATCH 059/262] stroke dash --- crates/cg/benches/bench_rectangles.rs | 2 + crates/cg/examples/stroke.rs | 286 ++++++++++++++++++++++++++ crates/cg/src/cvt.rs | 22 ++ crates/cg/src/draw.rs | 163 ++++++++++++--- crates/cg/src/factory.rs | 20 ++ crates/cg/src/io.rs | 7 + crates/cg/src/schema.rs | 37 +++- 7 files changed, 504 insertions(+), 33 deletions(-) create mode 100644 crates/cg/examples/stroke.rs diff --git a/crates/cg/benches/bench_rectangles.rs b/crates/cg/benches/bench_rectangles.rs index f0db04ebd3..47e9c81af8 100644 --- a/crates/cg/benches/bench_rectangles.rs +++ b/crates/cg/benches/bench_rectangles.rs @@ -34,6 +34,8 @@ fn create_rectangles(count: usize, with_effects: bool) -> Scene { 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 { diff --git a/crates/cg/examples/stroke.rs b/crates/cg/examples/stroke.rs new file mode 100644 index 0000000000..32c0675e32 --- /dev/null +++ b/crates/cg/examples/stroke.rs @@ -0,0 +1,286 @@ +use cg::factory::NodeFactory; +use cg::repository::NodeRepository; +use cg::schema::*; +use cg::transform::AffineTransform; + +mod window; + +async fn demo_strokes() -> Scene { + let nf = NodeFactory::new(); + + // Add a background rectangle node + let mut background_rect_node = nf.create_rectangle_node(); + background_rect_node.base.name = "Background Rect".to_string(); + background_rect_node.size = Size { + width: 1080.0, + height: 1080.0, + }; + background_rect_node.fill = Paint::Solid(SolidPaint { + color: Color(240, 240, 240, 255), // Light gray background + 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(); + + let mut repository = NodeRepository::new(); + let background_rect_id = background_rect_node.base.id.clone(); + repository.insert(Node::Rectangle(background_rect_node)); + + 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)); + } + + // Set up the root container + root_container_node.children = vec![background_rect_id]; + 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, + } +} + +#[tokio::main] +async fn main() { + let scene = demo_strokes().await; + window::run_demo_window(scene).await; +} diff --git a/crates/cg/src/cvt.rs b/crates/cg/src/cvt.rs index b5c578293a..5ac031bf67 100644 --- a/crates/cg/src/cvt.rs +++ b/crates/cg/src/cvt.rs @@ -71,6 +71,28 @@ pub fn sk_paint(paint: &Paint, opacity: f32, size: (f32, f32)) -> skia_safe::Pai skia_paint } +pub fn sk_paint_with_stroke( + paint: &Paint, + opacity: f32, + size: (f32, f32), + stroke_width: f32, + stroke_align: StrokeAlign, + stroke_dash_array: Option<&Vec>, +) -> skia_safe::Paint { + let mut paint = sk_paint(paint, opacity, size); + paint.set_stroke(true); + paint.set_stroke_width(stroke_width); + + // Apply dash pattern if present + if let Some(dash_array) = stroke_dash_array { + if let Some(path_effect) = skia_safe::dash_path_effect::new(dash_array, 0.0) { + paint.set_path_effect(path_effect); + } + } + + paint +} + // Given: // - `pts`: Vec with your polygon's vertices in order // - `r`: the corner‐radius diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index c9499c44d2..4449f4b559 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -169,6 +169,8 @@ impl Painter { fill: &Paint, stroke: Option<&Paint>, stroke_width: f32, + stroke_align: StrokeAlign, + stroke_dash_array: Option<&Vec>, blend_mode: BlendMode, opacity: f32, ) { @@ -176,10 +178,29 @@ impl Painter { let mut fill_paint = cvt::sk_paint(fill, opacity, (rect.width(), rect.height())); fill_paint.set_blend_mode(blend_mode.into()); + // Calculate stroke offset based on alignment + let stroke_offset = match stroke_align { + StrokeAlign::Inside => 0.0, + StrokeAlign::Center => stroke_width / 2.0, + StrokeAlign::Outside => stroke_width, + }; + + // Adjust rect for stroke alignment + let adjusted_rect = if stroke_offset > 0.0 { + Rect::new( + rect.left() - stroke_offset, + rect.top() - stroke_offset, + rect.right() + stroke_offset, + rect.bottom() + stroke_offset, + ) + } else { + rect + }; + if tl > 0.0 || tr > 0.0 || bl > 0.0 || br > 0.0 { // Rounded rect fill let rrect = RRect::new_rect_radii( - rect, + adjusted_rect, &[ Point::new(tl, tl), Point::new(tr, tr), @@ -192,27 +213,35 @@ impl Painter { // Stroke if present if let Some(stroke) = stroke { if stroke_width > 0.0 { - let mut stroke_paint = - cvt::sk_paint(stroke, opacity, (rect.width(), rect.height())); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(stroke_width); + let mut stroke_paint = cvt::sk_paint_with_stroke( + stroke, + opacity, + (rect.width(), rect.height()), + stroke_width, + stroke_align, + stroke_dash_array, + ); stroke_paint.set_blend_mode(blend_mode.into()); canvas.draw_rrect(rrect, &stroke_paint); } } } else { // Regular rect fill - canvas.draw_rect(rect, &fill_paint); + canvas.draw_rect(adjusted_rect, &fill_paint); // Stroke if present if let Some(stroke) = stroke { if stroke_width > 0.0 { - let mut stroke_paint = - cvt::sk_paint(stroke, opacity, (rect.width(), rect.height())); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(stroke_width); + let mut stroke_paint = cvt::sk_paint_with_stroke( + stroke, + opacity, + (rect.width(), rect.height()), + stroke_width, + stroke_align, + stroke_dash_array, + ); stroke_paint.set_blend_mode(blend_mode.into()); - canvas.draw_rect(rect, &stroke_paint); + canvas.draw_rect(adjusted_rect, &stroke_paint); } } } @@ -265,6 +294,8 @@ impl Painter { &node.fill, Some(&node.stroke), node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), node.blend_mode, node.opacity, ); @@ -277,6 +308,8 @@ impl Painter { &node.fill, Some(&node.stroke), node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), node.blend_mode, node.opacity, ); @@ -306,6 +339,8 @@ impl Painter { &node.fill, node.stroke.as_ref(), node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), node.blend_mode, node.opacity, ); @@ -318,6 +353,8 @@ impl Painter { &node.fill, node.stroke.as_ref(), node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), node.blend_mode, node.opacity, ); @@ -380,6 +417,8 @@ impl Painter { &node.stroke, None, node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), node.blend_mode, node.opacity, ); @@ -418,6 +457,8 @@ impl Painter { &node.stroke, None, node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), node.blend_mode, node.opacity, ); @@ -475,10 +516,16 @@ impl Painter { /// Draw a LineNode pub fn draw_line_node(&self, canvas: &skia_safe::Canvas, node: &LineNode) { - let mut paint = cvt::sk_paint(&node.stroke, node.opacity, (node.size.width, 0.0)); - paint.set_stroke(true); - paint.set_stroke_width(node.stroke_width); + let mut paint = cvt::sk_paint_with_stroke( + &node.stroke, + node.opacity, + (node.size.width, 0.0), + node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), + ); paint.set_blend_mode(node.blend_mode.into()); + self.with_canvas_state(canvas, &node.transform.matrix, || { canvas.draw_line( Point::new(0.0, 0.0), @@ -500,8 +547,27 @@ impl Painter { Rect::from_xywh(bounds.left(), bounds.top(), bounds.width(), bounds.height()); let radii = RectangularCornerRadius::zero(); // no corner radii for generic path + // Calculate stroke offset based on alignment + let stroke_offset = match node.stroke_align { + StrokeAlign::Inside => 0.0, + StrokeAlign::Center => node.stroke_width / 2.0, + StrokeAlign::Outside => node.stroke_width, + }; + + // Adjust rect for stroke alignment + let adjusted_rect = if stroke_offset > 0.0 { + Rect::new( + rect.left() - stroke_offset, + rect.top() - stroke_offset, + rect.right() + stroke_offset, + rect.bottom() + stroke_offset, + ) + } else { + rect + }; + if let Some(effect) = &node.effect { - self.apply_effect(canvas, effect, rect, &radii, || { + self.apply_effect(canvas, effect, adjusted_rect, &radii, || { // Draw fill let mut fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); fill_paint.set_blend_mode(node.blend_mode.into()); @@ -509,10 +575,14 @@ impl Painter { // Draw stroke if needed if node.stroke_width > 0.0 { - let mut stroke_paint = - cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); + let mut stroke_paint = cvt::sk_paint_with_stroke( + &node.stroke, + node.opacity, + (1.0, 1.0), + node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), + ); stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &stroke_paint); } @@ -524,9 +594,14 @@ impl Painter { canvas.draw_path(&path, &fill_paint); if node.stroke_width > 0.0 { - let mut stroke_paint = cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); + let mut stroke_paint = cvt::sk_paint_with_stroke( + &node.stroke, + node.opacity, + (1.0, 1.0), + node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), + ); stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &stroke_paint); } @@ -562,8 +637,27 @@ impl Painter { Rect::from_xywh(bounds.left(), bounds.top(), bounds.width(), bounds.height()); let radii = RectangularCornerRadius::all(node.corner_radius); + // Calculate stroke offset based on alignment + let stroke_offset = match node.stroke_align { + StrokeAlign::Inside => 0.0, + StrokeAlign::Center => node.stroke_width / 2.0, + StrokeAlign::Outside => node.stroke_width, + }; + + // Adjust rect for stroke alignment + let adjusted_rect = if stroke_offset > 0.0 { + Rect::new( + rect.left() - stroke_offset, + rect.top() - stroke_offset, + rect.right() + stroke_offset, + rect.bottom() + stroke_offset, + ) + } else { + rect + }; + if let Some(effect) = &node.effect { - self.apply_effect(canvas, effect, rect, &radii, || { + self.apply_effect(canvas, effect, adjusted_rect, &radii, || { // Draw fill let mut fill_paint = cvt::sk_paint(&node.fill, node.opacity, (1.0, 1.0)); fill_paint.set_blend_mode(node.blend_mode.into()); @@ -571,10 +665,14 @@ impl Painter { // Stroke if node.stroke_width > 0.0 { - let mut stroke_paint = - cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); + let mut stroke_paint = cvt::sk_paint_with_stroke( + &node.stroke, + node.opacity, + (1.0, 1.0), + node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), + ); stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &stroke_paint); } @@ -586,9 +684,14 @@ impl Painter { canvas.draw_path(&path, &fill_paint); if node.stroke_width > 0.0 { - let mut stroke_paint = cvt::sk_paint(&node.stroke, node.opacity, (1.0, 1.0)); - stroke_paint.set_stroke(true); - stroke_paint.set_stroke_width(node.stroke_width); + let mut stroke_paint = cvt::sk_paint_with_stroke( + &node.stroke, + node.opacity, + (1.0, 1.0), + node.stroke_width, + node.stroke_align, + node.stroke_dash_array.as_ref(), + ); stroke_paint.set_blend_mode(node.blend_mode.into()); canvas.draw_path(&path, &stroke_paint); } diff --git a/crates/cg/src/factory.rs b/crates/cg/src/factory.rs index 56e1f8eb45..6dd60a5133 100644 --- a/crates/cg/src/factory.rs +++ b/crates/cg/src/factory.rs @@ -25,6 +25,7 @@ impl NodeFactory { const DEFAULT_COLOR: Color = Color(255, 255, 255, 255); const DEFAULT_STROKE_COLOR: Color = Color(0, 0, 0, 255); const DEFAULT_STROKE_WIDTH: f32 = 1.0; + const DEFAULT_STROKE_ALIGN: StrokeAlign = StrokeAlign::Inside; const DEFAULT_OPACITY: f32 = 1.0; fn default_base_node(&self) -> BaseNode { @@ -52,6 +53,8 @@ impl NodeFactory { fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_dash_array: None, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, effect: None, @@ -67,6 +70,8 @@ impl NodeFactory { fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_dash_array: None, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, effect: None, @@ -84,6 +89,8 @@ impl NodeFactory { }, stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_dash_array: None, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, } @@ -112,6 +119,7 @@ impl NodeFactory { fill: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke: None, stroke_width: None, + stroke_align: Self::DEFAULT_STROKE_ALIGN, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, } @@ -139,6 +147,8 @@ impl NodeFactory { fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: None, stroke_width: Self::DEFAULT_STROKE_WIDTH, + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_dash_array: None, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, effect: None, @@ -154,6 +164,8 @@ impl NodeFactory { data: String::new(), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_dash_array: None, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, effect: None, @@ -171,6 +183,8 @@ impl NodeFactory { fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_dash_array: None, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, effect: None, @@ -188,6 +202,8 @@ impl NodeFactory { fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_dash_array: None, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, effect: None, @@ -203,6 +219,8 @@ impl NodeFactory { fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_dash_array: None, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, effect: None, @@ -219,6 +237,8 @@ impl NodeFactory { fill: Self::default_solid_paint(Self::DEFAULT_COLOR), stroke: Self::default_solid_paint(Self::DEFAULT_STROKE_COLOR), stroke_width: Self::DEFAULT_STROKE_WIDTH, + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_dash_array: None, opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, effect: None, diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index c65ea94615..c048a32012 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -344,6 +344,8 @@ impl From for ContainerNode { fill: node.fill.into(), stroke: None, stroke_width: 0.0, + stroke_align: StrokeAlign::Inside, + stroke_dash_array: None, effect: None, children: node.children, opacity: node.opacity, @@ -384,6 +386,7 @@ impl From for TextSpanNode { fill: node.fill.into(), stroke: None, stroke_width: None, + stroke_align: StrokeAlign::Inside, opacity: node.opacity, } } @@ -411,6 +414,8 @@ impl From for Node { opacity: 1.0, }), stroke_width: node.stroke_width.unwrap_or(0.0), + stroke_align: StrokeAlign::Inside, + stroke_dash_array: None, effect: None, opacity: node.opacity, }) @@ -443,6 +448,8 @@ impl From for Node { opacity: 1.0, }), stroke_width: 0.0, + stroke_align: StrokeAlign::Inside, + stroke_dash_array: None, opacity: node.opacity, effect: None, }) diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index f38dc90d59..ebd18dfc90 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -47,6 +47,17 @@ pub enum BoxFit { None, } +/// Stroke alignment. +/// +/// - [Flutter](https://api.flutter.dev/flutter/painting/BorderSide/strokeAlign.html) +/// - [Figma](https://www.figma.com/plugin-docs/api/properties/nodes-strokealign/) +#[derive(Debug, Clone, Copy)] +pub enum StrokeAlign { + Inside, + Center, + Outside, +} + #[derive(Debug, Clone, Copy)] pub struct Color(pub u8, pub u8, pub u8, pub u8); @@ -429,6 +440,8 @@ pub struct ContainerNode { pub fill: Paint, pub stroke: Option, pub stroke_width: f32, + pub stroke_align: StrokeAlign, + pub stroke_dash_array: Option>, pub opacity: f32, pub blend_mode: BlendMode, pub effect: Option, @@ -443,6 +456,8 @@ pub struct RectangleNode { pub fill: Paint, pub stroke: Paint, pub stroke_width: f32, + pub stroke_align: StrokeAlign, + pub stroke_dash_array: Option>, pub opacity: f32, pub blend_mode: BlendMode, pub effect: Option, @@ -455,6 +470,8 @@ pub struct LineNode { pub size: Size, // height is always 0 (ignored) pub stroke: Paint, pub stroke_width: f32, + pub stroke_align: StrokeAlign, + pub stroke_dash_array: Option>, pub opacity: f32, pub blend_mode: BlendMode, } @@ -468,6 +485,8 @@ pub struct ImageNode { pub fill: Paint, pub stroke: Paint, pub stroke_width: f32, + pub stroke_align: StrokeAlign, + pub stroke_dash_array: Option>, pub opacity: f32, pub blend_mode: BlendMode, pub effect: Option, @@ -486,6 +505,8 @@ pub struct EllipseNode { pub fill: Paint, pub stroke: Paint, pub stroke_width: f32, + pub stroke_align: StrokeAlign, + pub stroke_dash_array: Option>, pub opacity: f32, pub blend_mode: BlendMode, pub effect: Option, @@ -502,6 +523,8 @@ pub struct PathNode { pub data: String, pub stroke: Paint, pub stroke_width: f32, + pub stroke_align: StrokeAlign, + pub stroke_dash_array: Option>, pub opacity: f32, pub blend_mode: BlendMode, pub effect: Option, @@ -538,11 +561,13 @@ pub struct PolygonNode { /// The stroke width used to outline the polygon. pub stroke_width: f32, + pub stroke_align: StrokeAlign, /// Opacity applied to the polygon shape (`0.0` - transparent, `1.0` - opaque). pub opacity: f32, pub blend_mode: BlendMode, pub effect: Option, + pub stroke_dash_array: Option>, } impl PolygonNode { @@ -588,11 +613,12 @@ pub struct RegularPolygonNode { /// The stroke width used to outline the polygon. pub stroke_width: f32, - + pub stroke_align: StrokeAlign, /// Overall node opacity (0.0–1.0) pub opacity: f32, pub blend_mode: BlendMode, pub effect: Option, + pub stroke_dash_array: Option>, } impl RegularPolygonNode { @@ -626,9 +652,11 @@ impl RegularPolygonNode { fill: self.fill.clone(), stroke: self.stroke.clone(), stroke_width: self.stroke_width, + stroke_align: self.stroke_align, opacity: self.opacity, blend_mode: self.blend_mode, effect: self.effect.clone(), + stroke_dash_array: self.stroke_dash_array.clone(), } } } @@ -675,11 +703,12 @@ pub struct RegularStarPolygonNode { /// The stroke width used to outline the polygon. pub stroke_width: f32, - + pub stroke_align: StrokeAlign, /// Overall node opacity (0.0–1.0) pub opacity: f32, pub blend_mode: BlendMode, pub effect: Option, + pub stroke_dash_array: Option>, } impl RegularStarPolygonNode { @@ -710,9 +739,11 @@ impl RegularStarPolygonNode { fill: self.fill.clone(), stroke: self.stroke.clone(), stroke_width: self.stroke_width, + stroke_align: self.stroke_align, opacity: self.opacity, blend_mode: self.blend_mode, effect: self.effect.clone(), + stroke_dash_array: self.stroke_dash_array.clone(), } } } @@ -750,7 +781,7 @@ pub struct TextSpanNode { /// Stroke width pub stroke_width: Option, - + pub stroke_align: StrokeAlign, /// Overall node opacity. pub opacity: f32, pub blend_mode: BlendMode, From f72d00122c8b6a7c341fdda432332c3e50c11796 Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Jun 2025 16:31:52 +0900 Subject: [PATCH 060/262] container clip --- crates/cg/examples/container.rs | 79 +++++++++++++++++++++++++++++++++ crates/cg/src/draw.rs | 47 ++++++++++++++++++-- crates/cg/src/factory.rs | 1 + crates/cg/src/io.rs | 1 + crates/cg/src/schema.rs | 1 + 5 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 crates/cg/examples/container.rs diff --git a/crates/cg/examples/container.rs b/crates/cg/examples/container.rs new file mode 100644 index 0000000000..102e0c5ac1 --- /dev/null +++ b/crates/cg/examples/container.rs @@ -0,0 +1,79 @@ +use cg::factory::NodeFactory; +use cg::repository::NodeRepository; +use cg::schema::*; +use cg::transform::AffineTransform; + +mod window; + +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, + } +} + +#[tokio::main] +async fn main() { + let scene = demo_clip().await; + window::run_demo_window(scene).await; +} diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 4449f4b559..293aaf2066 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -275,6 +275,33 @@ impl Painter { } } + /// Helper method to apply clipping to a region with optional corner radius + fn with_clip( + &self, + canvas: &skia_safe::Canvas, + rect: Rect, + radii: &RectangularCornerRadius, + f: F, + ) { + canvas.save(); + if radii.tl > 0.0 || radii.tr > 0.0 || radii.bl > 0.0 || radii.br > 0.0 { + let rrect = RRect::new_rect_radii( + rect, + &[ + Point::new(radii.tl, radii.tl), + Point::new(radii.tr, radii.tr), + Point::new(radii.br, radii.br), + Point::new(radii.bl, radii.bl), + ], + ); + canvas.clip_rrect(rrect, None, true); + } else { + canvas.clip_rect(rect, None, true); + } + f(); + canvas.restore(); + } + // ============================ // === Node Drawing Methods === // ============================ @@ -330,6 +357,7 @@ impl Painter { let rect = Rect::from_xywh(0.0, 0.0, node.size.width, node.size.height); let radii = node.corner_radius; + // Draw effects first (if any) - these won't be clipped if let Some(effect) = &node.effect { self.apply_effect(canvas, effect, rect, &radii, || { self.draw_fill_and_stroke( @@ -360,10 +388,21 @@ impl Painter { ); } - // Draw children on top - for child_id in &node.children { - if let Some(child) = repository.get(child_id) { - self.draw_node(canvas, child, repository, image_repository); + // Draw children with clipping if enabled + if node.clip { + self.with_clip(canvas, rect, &radii, || { + for child_id in &node.children { + if let Some(child) = repository.get(child_id) { + self.draw_node(canvas, child, repository, image_repository); + } + } + }); + } else { + // Draw children without clipping + for child_id in &node.children { + if let Some(child) = repository.get(child_id) { + self.draw_node(canvas, child, repository, image_repository); + } } } }); diff --git a/crates/cg/src/factory.rs b/crates/cg/src/factory.rs index 6dd60a5133..bf278882ab 100644 --- a/crates/cg/src/factory.rs +++ b/crates/cg/src/factory.rs @@ -152,6 +152,7 @@ impl NodeFactory { opacity: Self::DEFAULT_OPACITY, blend_mode: BlendMode::Normal, effect: None, + clip: true, } } diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index c048a32012..5517423949 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -349,6 +349,7 @@ impl From for ContainerNode { effect: None, children: node.children, opacity: node.opacity, + clip: true, } } } diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index ebd18dfc90..8f69dae3ec 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -445,6 +445,7 @@ pub struct ContainerNode { pub opacity: f32, pub blend_mode: BlendMode, pub effect: Option, + pub clip: bool, } #[derive(Debug, Clone)] From 594b7d4705917c892ef0ae50303ff1b97a7d7cee Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Jun 2025 16:53:48 +0900 Subject: [PATCH 061/262] text transform --- crates/cg/examples/basic.rs | 1 + crates/cg/examples/effects.rs | 4 ++ crates/cg/examples/texts.rs | 7 ++ crates/cg/src/draw.rs | 5 +- crates/cg/src/factory.rs | 1 + crates/cg/src/io.rs | 1 + crates/cg/src/lib.rs | 1 + crates/cg/src/schema.rs | 17 +++++ crates/cg/src/text_transform.rs | 122 ++++++++++++++++++++++++++++++++ 9 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 crates/cg/src/text_transform.rs diff --git a/crates/cg/examples/basic.rs b/crates/cg/examples/basic.rs index 4e936b9302..dd9e84d0b9 100644 --- a/crates/cg/examples/basic.rs +++ b/crates/cg/examples/basic.rs @@ -170,6 +170,7 @@ async fn demo_basic() -> Scene { font_weight: FontWeight::new(900), 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; diff --git a/crates/cg/examples/effects.rs b/crates/cg/examples/effects.rs index e25e4c0744..50fdf2badb 100644 --- a/crates/cg/examples/effects.rs +++ b/crates/cg/examples/effects.rs @@ -32,6 +32,10 @@ async fn demo_effects() -> Scene { // Create a root container node 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(); let mut repository = NodeRepository::new(); diff --git a/crates/cg/examples/texts.rs b/crates/cg/examples/texts.rs index ca90753c72..3a3dc4cad7 100644 --- a/crates/cg/examples/texts.rs +++ b/crates/cg/examples/texts.rs @@ -44,6 +44,7 @@ async fn demo_texts() -> Scene { 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), @@ -71,6 +72,7 @@ async fn demo_texts() -> Scene { 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; @@ -91,6 +93,7 @@ async fn demo_texts() -> Scene { 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; @@ -98,6 +101,10 @@ async fn demo_texts() -> Scene { // 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(); diff --git a/crates/cg/src/draw.rs b/crates/cg/src/draw.rs index 293aaf2066..9f483f4edc 100644 --- a/crates/cg/src/draw.rs +++ b/crates/cg/src/draw.rs @@ -793,7 +793,10 @@ impl Painter { ts.set_font_style(font_style); para_builder.push_style(&ts); - para_builder.add_text(&node.text); + // Apply text transform before adding text + let transformed_text = + crate::text_transform::transform_text(&node.text, node.text_style.text_transform); + para_builder.add_text(&transformed_text); let mut paragraph = para_builder.build(); para_builder.pop(); paragraph.layout(node.size.width); diff --git a/crates/cg/src/factory.rs b/crates/cg/src/factory.rs index bf278882ab..df0c9ed2d0 100644 --- a/crates/cg/src/factory.rs +++ b/crates/cg/src/factory.rs @@ -113,6 +113,7 @@ impl NodeFactory { font_weight: FontWeight::default(), letter_spacing: None, line_height: None, + text_transform: TextTransform::None, }, text_align: TextAlign::Left, text_align_vertical: TextAlignVertical::Top, diff --git a/crates/cg/src/io.rs b/crates/cg/src/io.rs index 5517423949..5d8796b409 100644 --- a/crates/cg/src/io.rs +++ b/crates/cg/src/io.rs @@ -381,6 +381,7 @@ impl From for TextSpanNode { font_weight: node.font_weight, letter_spacing: node.letter_spacing, line_height: node.line_height, + text_transform: TextTransform::None, }, text_align: node.text_align, text_align_vertical: node.text_align_vertical, diff --git a/crates/cg/src/lib.rs b/crates/cg/src/lib.rs index e8d33e6963..cb061e9985 100644 --- a/crates/cg/src/lib.rs +++ b/crates/cg/src/lib.rs @@ -5,4 +5,5 @@ pub mod factory; pub mod io; pub mod repository; pub mod schema; +pub mod text_transform; pub mod transform; diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 8f69dae3ec..071c872a79 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -179,6 +179,20 @@ impl From for skia_safe::BlendMode { } } +/// Text Transform (Text Case) +/// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/text-transform) +#[derive(Debug, Clone, Copy, Deserialize)] +pub enum TextTransform { + #[serde(rename = "none")] + None, + #[serde(rename = "uppercase")] + Uppercase, + #[serde(rename = "lowercase")] + Lowercase, + #[serde(rename = "capitalize")] + Capitalize, +} + /// Supported text decoration modes. /// /// Only `Underline` and `None` are supported in the current version. @@ -311,6 +325,9 @@ pub struct TextStyle { /// Line height pub line_height: Option, + + /// Text transform (e.g. uppercase, lowercase, capitalize) + pub text_transform: TextTransform, } #[derive(Debug, Clone, Copy)] diff --git a/crates/cg/src/text_transform.rs b/crates/cg/src/text_transform.rs new file mode 100644 index 0000000000..3c2661d326 --- /dev/null +++ b/crates/cg/src/text_transform.rs @@ -0,0 +1,122 @@ +use crate::schema::TextTransform; + +/// Applies text transformation according to CSS text-transform property. +/// +/// # Arguments +/// +/// * `text` - The input text to transform +/// * `transform` - The transformation to apply +/// +/// # Returns +/// +/// The transformed text string +/// +/// # Examples +/// +/// ``` +/// use cg::schema::TextTransform; +/// use cg::text_transform::transform_text; +/// +/// let text = "Hello World"; +/// assert_eq!(transform_text(text, TextTransform::Uppercase), "HELLO WORLD"); +/// assert_eq!(transform_text(text, TextTransform::Lowercase), "hello world"); +/// assert_eq!(transform_text(text, TextTransform::Capitalize), "Hello World"); +/// assert_eq!(transform_text(text, TextTransform::None), "Hello World"); +/// ``` +pub fn transform_text(text: &str, transform: TextTransform) -> String { + match transform { + TextTransform::None => text.to_string(), + TextTransform::Uppercase => text.to_uppercase(), + TextTransform::Lowercase => text.to_lowercase(), + TextTransform::Capitalize => { + let mut result = String::with_capacity(text.len()); + let mut capitalize_next = true; + + for c in text.chars() { + if capitalize_next && c.is_alphabetic() { + result.push(c.to_uppercase().next().unwrap()); + capitalize_next = false; + } else { + result.push(c); + // Consider a word boundary to be any non-alphanumeric character + capitalize_next = !c.is_alphanumeric(); + } + } + result + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_none_transform() { + let text = "Hello World"; + assert_eq!(transform_text(text, TextTransform::None), text); + } + + #[test] + fn test_uppercase_transform() { + let text = "Hello World"; + assert_eq!( + transform_text(text, TextTransform::Uppercase), + "HELLO WORLD" + ); + } + + #[test] + fn test_lowercase_transform() { + let text = "Hello World"; + assert_eq!( + transform_text(text, TextTransform::Lowercase), + "hello world" + ); + } + + #[test] + fn test_capitalize_transform() { + let text = "hello world"; + assert_eq!( + transform_text(text, TextTransform::Capitalize), + "Hello World" + ); + + let text = "hello WORLD"; + assert_eq!( + transform_text(text, TextTransform::Capitalize), + "Hello WORLD" + ); + + let text = "hello world"; + assert_eq!( + transform_text(text, TextTransform::Capitalize), + "Hello World" + ); + + let text = "hello.world"; + assert_eq!( + transform_text(text, TextTransform::Capitalize), + "Hello.World" + ); + + let text = "hello.world.test"; + assert_eq!( + transform_text(text, TextTransform::Capitalize), + "Hello.World.Test" + ); + + let text = "hello-world"; + assert_eq!( + transform_text(text, TextTransform::Capitalize), + "Hello-World" + ); + + let text = "hello_world"; + assert_eq!( + transform_text(text, TextTransform::Capitalize), + "Hello_World" + ); + } +} From 7abaf205f3159b4513f4ec9e74d65b6997691f59 Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Jun 2025 16:56:44 +0900 Subject: [PATCH 062/262] ref --- crates/cg/.ref/figma-rest-api.d.ts | 7351 +++++++++++++++++ .../cg}/.ref/figma.d.ts | 0 2 files changed, 7351 insertions(+) create mode 100644 crates/cg/.ref/figma-rest-api.d.ts rename {packages/grida-canvas-schema => crates/cg}/.ref/figma.d.ts (100%) diff --git a/crates/cg/.ref/figma-rest-api.d.ts b/crates/cg/.ref/figma-rest-api.d.ts new file mode 100644 index 0000000000..e8dab55cc7 --- /dev/null +++ b/crates/cg/.ref/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/packages/grida-canvas-schema/.ref/figma.d.ts b/crates/cg/.ref/figma.d.ts similarity index 100% rename from packages/grida-canvas-schema/.ref/figma.d.ts rename to crates/cg/.ref/figma.d.ts From 7e91abe79ca1616e04d43e2172a0a1a5b22d29b1 Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Jun 2025 17:53:11 +0900 Subject: [PATCH 063/262] path op --- crates/cg/src/schema.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/cg/src/schema.rs b/crates/cg/src/schema.rs index 071c872a79..f26995ddb5 100644 --- a/crates/cg/src/schema.rs +++ b/crates/cg/src/schema.rs @@ -32,6 +32,15 @@ impl Point { } } +/// Boolean path operation. +#[derive(Debug, Clone)] +pub enum BooleanPathOperation { + Union, // A ∪ B + Intersection, // A ∩ B + Difference, // A - B + Xor, // A ⊕ B +} + /// Supported fit modes. /// /// Only `Contain`, `Cover`, and `None` are supported in the current version. @@ -530,6 +539,14 @@ pub struct EllipseNode { pub effect: Option, } +#[derive(Debug, Clone)] +#[deprecated(note = "Not implemented yet")] +pub struct BooleanPathOperationNode { + pub base: BaseNode, + pub transform: AffineTransform, + pub op: BooleanPathOperation, +} + /// /// SVG Path compatible path node. /// From 4358da0c2a1e1e4e61551de492e974314801b65f Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Jun 2025 18:05:37 +0900 Subject: [PATCH 064/262] init io figma --- Cargo.lock | 9 + Cargo.toml | 1 + crates/io-figma/Cargo.toml | 12 ++ crates/io-figma/README.md | 1 + crates/io-figma/src/lib.rs | 1 + crates/io-figma/src/rest.rs | 379 ++++++++++++++++++++++++++++++++++++ 6 files changed, 403 insertions(+) create mode 100644 crates/io-figma/Cargo.toml create mode 100644 crates/io-figma/README.md create mode 100644 crates/io-figma/src/lib.rs create mode 100644 crates/io-figma/src/rest.rs diff --git a/Cargo.lock b/Cargo.lock index fc60824c57..8d731cc6c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -668,6 +668,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "figma" +version = "0.0.0" +dependencies = [ + "reqwest", + "serde", + "serde_json", +] + [[package]] name = "filetime" version = "0.2.25" diff --git a/Cargo.toml b/Cargo.toml index ec38312726..15c848c583 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] members = [ "crates/cg", + "crates/io-figma", ] \ No newline at end of file diff --git a/crates/io-figma/Cargo.toml b/crates/io-figma/Cargo.toml new file mode 100644 index 0000000000..94b3b1f8da --- /dev/null +++ b/crates/io-figma/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "figma" +version = "0.0.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +reqwest = "0.12.19" \ No newline at end of file diff --git a/crates/io-figma/README.md b/crates/io-figma/README.md new file mode 100644 index 0000000000..f8e1152778 --- /dev/null +++ b/crates/io-figma/README.md @@ -0,0 +1 @@ +# Figma API Rust bindings diff --git a/crates/io-figma/src/lib.rs b/crates/io-figma/src/lib.rs new file mode 100644 index 0000000000..2a7079e6c3 --- /dev/null +++ b/crates/io-figma/src/lib.rs @@ -0,0 +1 @@ +pub mod rest; diff --git a/crates/io-figma/src/rest.rs b/crates/io-figma/src/rest.rs new file mode 100644 index 0000000000..c1790673d8 --- /dev/null +++ b/crates/io-figma/src/rest.rs @@ -0,0 +1,379 @@ +use serde::Deserialize; +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RGBA { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Rectangle { + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ScrollBehavior { + Scrolls, + Fixed, + StickyScrolls, + #[serde(other)] + Unknown, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Vector { + pub x: f32, + pub y: f32, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LayoutConstraint { + pub vertical: Option, + pub horizontal: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LayoutGrid { + pub pattern: Option, + pub section_size: Option, + pub visible: Option, + pub color: Option, + pub alignment: Option, + pub gutter_size: Option, + pub offset: Option, + pub count: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Hyperlink { + pub r#type: String, + pub url: Option, + #[serde(rename = "nodeID")] + pub node_id: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LayerBase { + pub id: String, + pub name: String, + #[serde(default)] + pub visible: Option, + #[serde(default)] + pub locked: Option, + #[serde(default)] + pub rotation: Option, + #[serde(default)] + pub scroll_behavior: Option, + #[serde(default)] + pub component_property_references: Option>, + #[serde(default)] + pub plugin_data: Option, + #[serde(default)] + pub shared_plugin_data: Option, + #[serde(default)] + pub bound_variables: Option, + #[serde(default)] + pub explicit_variable_modes: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentNode { + #[serde(flatten)] + pub base: LayerBase, + pub children: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasNode { + #[serde(flatten)] + pub base: LayerBase, + pub children: Vec, + #[serde(rename = "backgroundColor")] + pub background_color: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FrameNode { + #[serde(flatten)] + pub base: LayerBase, + pub children: Vec, + #[serde(rename = "absoluteBoundingBox")] + pub absolute_bounding_box: Option, + #[serde(rename = "absoluteRenderBounds")] + pub absolute_render_bounds: Option, + pub preserve_ratio: Option, + pub constraints: Option, + #[serde(rename = "relativeTransform")] + pub relative_transform: Option>>, + pub size: Option, + pub layout_align: Option, + pub layout_grow: Option, + pub layout_positioning: Option, + pub min_width: Option, + pub max_width: Option, + pub min_height: Option, + pub max_height: Option, + pub layout_sizing_horizontal: Option, + pub layout_sizing_vertical: Option, + pub clips_content: Option, + pub layout_mode: Option, + pub primary_axis_sizing_mode: Option, + pub counter_axis_sizing_mode: Option, + pub primary_axis_align_items: Option, + pub counter_axis_align_items: Option, + pub padding_left: Option, + pub padding_right: Option, + pub padding_top: Option, + pub padding_bottom: Option, + pub item_spacing: Option, + pub item_reverse_z_index: Option, + pub strokes_included_in_layout: Option, + pub layout_wrap: Option, + pub counter_axis_spacing: Option, + pub counter_axis_align_content: Option, + pub layout_grids: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SectionNode { + #[serde(flatten)] + pub base: LayerBase, + pub children: Vec, + #[serde(rename = "sectionContentsHidden")] + pub section_contents_hidden: Option, + #[serde(rename = "absoluteBoundingBox")] + pub absolute_bounding_box: Option, + #[serde(rename = "absoluteRenderBounds")] + pub absolute_render_bounds: Option, + pub preserve_ratio: Option, + pub constraints: Option, + #[serde(rename = "relativeTransform")] + pub relative_transform: Option>>, + pub size: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ShapeNode { + #[serde(flatten)] + pub base: LayerBase, + #[serde(rename = "absoluteBoundingBox")] + pub absolute_bounding_box: Option, + #[serde(rename = "absoluteRenderBounds")] + pub absolute_render_bounds: Option, + pub preserve_ratio: Option, + pub constraints: Option, + #[serde(rename = "relativeTransform")] + pub relative_transform: Option>>, + pub size: Option, + pub strokes: Option>, + pub stroke_weight: Option, + pub stroke_align: Option, + pub stroke_join: Option, + pub stroke_dashes: Option>, + pub fills: Option>, + pub styles: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BooleanOperationNode { + #[serde(flatten)] + pub base: FrameNode, + #[serde(rename = "booleanOperation")] + pub boolean_operation: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ComponentNode { + #[serde(flatten)] + pub base: FrameNode, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ComponentSetNode { + #[serde(flatten)] + pub base: FrameNode, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstanceNode { + #[serde(flatten)] + pub base: FrameNode, + #[serde(rename = "componentId")] + pub component_id: Option, + #[serde(rename = "isExposedInstance")] + pub is_exposed_instance: Option, + #[serde(rename = "exposedInstances")] + pub exposed_instances: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkUnfurlNode { + #[serde(flatten)] + pub base: LayerBase, + pub size: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SliceNode { + #[serde(flatten)] + pub base: LayerBase, + pub size: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StarNode { + #[serde(flatten)] + pub base: ShapeNode, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegularPolygonNode { + #[serde(flatten)] + pub base: ShapeNode, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TextNode { + #[serde(flatten)] + pub base: LayerBase, + pub characters: String, + #[serde(rename = "style")] + pub style: Option, + #[serde(rename = "absoluteBoundingBox")] + pub absolute_bounding_box: Option, + pub character_style_overrides: Option>, + pub style_override_table: Option>, + pub line_types: Option>, + pub line_indentations: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TypeStyle { + pub font_family: Option, + pub font_post_script_name: Option, + pub font_weight: Option, + pub font_size: Option, + pub text_case: Option, + pub text_align_horizontal: Option, + pub text_align_vertical: Option, + pub letter_spacing: Option, + pub fills: Option>, + pub hyperlink: Option, + pub opentype_flags: Option>, + pub semantic_weight: Option, + pub semantic_italic: Option, + pub paragraph_spacing: Option, + pub paragraph_indent: Option, + pub list_spacing: Option, + pub text_decoration: Option, + pub text_auto_resize: Option, + pub text_truncation: Option, + pub max_lines: Option, + pub line_height_px: Option, + pub line_height_percent: Option, + pub line_height_percent_font_size: Option, + pub line_height_unit: Option, + pub is_override_over_text_style: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Paint { + SOLID { + color: RGBA, + }, + IMAGE { + image_ref: String, + scale_mode: Option, + }, + #[serde(other)] + Unknown, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum Node { + #[serde(rename = "DOCUMENT")] + Document(DocumentNode), + #[serde(rename = "CANVAS")] + Canvas(CanvasNode), + #[serde(rename = "BOOLEAN_OPERATION")] + BooleanOperation(BooleanOperationNode), + #[serde(rename = "FRAME")] + Frame(FrameNode), + #[serde(rename = "GROUP")] + Group(FrameNode), + #[serde(rename = "COMPONENT")] + Component(ComponentNode), + #[serde(rename = "COMPONENT_SET")] + ComponentSet(ComponentSetNode), + #[serde(rename = "INSTANCE")] + Instance(InstanceNode), + #[serde(rename = "RECTANGLE")] + Rectangle(ShapeNode), + #[serde(rename = "REGULAR_POLYGON")] + RegularPolygon(RegularPolygonNode), + #[serde(rename = "SECTION")] + Section(SectionNode), + #[serde(rename = "SLICE")] + Slice(SliceNode), + #[serde(rename = "STAR")] + Star(StarNode), + #[serde(rename = "VECTOR")] + Vector(ShapeNode), + #[serde(rename = "ELLIPSE")] + Ellipse(ShapeNode), + #[serde(rename = "LINE")] + Line(ShapeNode), + #[serde(rename = "LINK_UNFURL")] + LinkUnfurl(LinkUnfurlNode), + #[serde(rename = "TEXT")] + Text(TextNode), + #[serde(other)] + Unknown, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetFileResponse { + pub name: String, + pub role: Option, + #[serde(rename = "lastModified")] + pub last_modified: String, + #[serde(rename = "editorType")] + pub editor_type: String, + #[serde(rename = "thumbnailUrl")] + pub thumbnail_url: Option, + pub version: String, + pub document: DocumentNode, +} From e8915722508b038622c389c56e7d718bda50856f Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Jun 2025 18:18:30 +0900 Subject: [PATCH 065/262] parse --- .../io-figma/fixtures/766822741396935685.zip | Bin 0 -> 48387 bytes crates/io-figma/src/lib.rs | 23 ++++++ crates/io-figma/src/rest.rs | 68 +++++++++++++++--- 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 crates/io-figma/fixtures/766822741396935685.zip diff --git a/crates/io-figma/fixtures/766822741396935685.zip b/crates/io-figma/fixtures/766822741396935685.zip new file mode 100644 index 0000000000000000000000000000000000000000..33cdbb01085c36add8e2960c0acd43a13fd6433d GIT binary patch literal 48387 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 N3L3P)yZ?UszW_xWnj-)J literal 0 HcmV?d00001 diff --git a/crates/io-figma/src/lib.rs b/crates/io-figma/src/lib.rs index 2a7079e6c3..0451dea86a 100644 --- a/crates/io-figma/src/lib.rs +++ b/crates/io-figma/src/lib.rs @@ -1 +1,24 @@ pub mod rest; + +#[cfg(test)] +mod tests { + use super::rest::*; + use std::fs; + + #[test] + fn test_deserialize_figma_response() { + let json_str = fs::read_to_string("fixtures/766822741396935685.json") + .expect("Failed to read test fixture"); + + let response: GetFileResponse = + serde_json::from_str(&json_str).expect("Failed to deserialize Figma response"); + + // Test basic document structure + assert_eq!( + response.name, + "Investor Pitch Presentation Template (Community)" + ); + assert_eq!(response.document.base.name, "Document"); + assert_eq!(response.document.base.id, "0:0"); + } +} diff --git a/crates/io-figma/src/rest.rs b/crates/io-figma/src/rest.rs index c1790673d8..ffb6e07238 100644 --- a/crates/io-figma/src/rest.rs +++ b/crates/io-figma/src/rest.rs @@ -1,3 +1,4 @@ +use serde::de::{self, Deserializer}; use serde::Deserialize; use serde_json::Value; use std::collections::HashMap; @@ -11,7 +12,7 @@ pub struct RGBA { pub a: f32, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Rectangle { pub x: f32, @@ -115,9 +116,15 @@ pub struct FrameNode { #[serde(flatten)] pub base: LayerBase, pub children: Vec, - #[serde(rename = "absoluteBoundingBox")] + #[serde( + default, + deserialize_with = "crate::rest::deserialize_option_rectangle" + )] pub absolute_bounding_box: Option, - #[serde(rename = "absoluteRenderBounds")] + #[serde( + default, + deserialize_with = "crate::rest::deserialize_option_rectangle" + )] pub absolute_render_bounds: Option, pub preserve_ratio: Option, pub constraints: Option, @@ -160,9 +167,15 @@ pub struct SectionNode { pub children: Vec, #[serde(rename = "sectionContentsHidden")] pub section_contents_hidden: Option, - #[serde(rename = "absoluteBoundingBox")] + #[serde( + default, + deserialize_with = "crate::rest::deserialize_option_rectangle" + )] pub absolute_bounding_box: Option, - #[serde(rename = "absoluteRenderBounds")] + #[serde( + default, + deserialize_with = "crate::rest::deserialize_option_rectangle" + )] pub absolute_render_bounds: Option, pub preserve_ratio: Option, pub constraints: Option, @@ -176,9 +189,15 @@ pub struct SectionNode { pub struct ShapeNode { #[serde(flatten)] pub base: LayerBase, - #[serde(rename = "absoluteBoundingBox")] + #[serde( + default, + deserialize_with = "crate::rest::deserialize_option_rectangle" + )] pub absolute_bounding_box: Option, - #[serde(rename = "absoluteRenderBounds")] + #[serde( + default, + deserialize_with = "crate::rest::deserialize_option_rectangle" + )] pub absolute_render_bounds: Option, pub preserve_ratio: Option, pub constraints: Option, @@ -268,7 +287,10 @@ pub struct TextNode { pub characters: String, #[serde(rename = "style")] pub style: Option, - #[serde(rename = "absoluteBoundingBox")] + #[serde( + default, + deserialize_with = "crate::rest::deserialize_option_rectangle" + )] pub absolute_bounding_box: Option, pub character_style_overrides: Option>, pub style_override_table: Option>, @@ -377,3 +399,33 @@ pub struct GetFileResponse { pub version: String, pub document: DocumentNode, } + +// Custom deserializer for Option +pub fn deserialize_option_rectangle<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt = Option::::deserialize(deserializer)?; + match opt { + None | Some(Value::Null) => Ok(None), + Some(Value::Object(ref map)) => { + // If all fields are null, treat as None + let all_null = ["x", "y", "width", "height"] + .iter() + .all(|k| map.get(*k).map_or(true, |v| v.is_null())); + if all_null { + return Ok(None); + } + // Otherwise, try to deserialize as Rectangle + let rect: Rectangle = + serde_json::from_value(Value::Object(map.clone())).map_err(de::Error::custom)?; + Ok(Some(rect)) + } + Some(other) => { + // If it's not an object or null, error + Err(de::Error::custom(format!( + "Unexpected value for Rectangle: {other:?}" + ))) + } + } +} From 795fb90d081f6dfd39cf1e976c1e99ce97db4537 Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Jun 2025 19:01:00 +0900 Subject: [PATCH 066/262] add tests --- Cargo.lock | 331 ++++++++++++++++++ crates/io-figma/Cargo.toml | 6 +- .../io-figma/fixtures/767108022358877208.zip | Bin 0 -> 18924 bytes .../io-figma/fixtures/767127152320102433.zip | Bin 0 -> 335723 bytes .../io-figma/fixtures/767135333966182437.zip | Bin 0 -> 11557 bytes .../io-figma/fixtures/767544357629075510.zip | Bin 0 -> 8616 bytes .../io-figma/fixtures/768233500885703748.zip | Bin 0 -> 79860 bytes .../io-figma/fixtures/768273489419236430.zip | Bin 0 -> 2918891 bytes crates/io-figma/src/lib.rs | 71 +++- crates/io-figma/src/rest.rs | 36 +- 10 files changed, 426 insertions(+), 18 deletions(-) create mode 100644 crates/io-figma/fixtures/767108022358877208.zip create mode 100644 crates/io-figma/fixtures/767127152320102433.zip create mode 100644 crates/io-figma/fixtures/767135333966182437.zip create mode 100644 crates/io-figma/fixtures/767544357629075510.zip create mode 100644 crates/io-figma/fixtures/768233500885703748.zip create mode 100644 crates/io-figma/fixtures/768273489419236430.zip diff --git a/Cargo.lock b/Cargo.lock index 8d731cc6c6..089837257f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -138,6 +149,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -221,6 +241,15 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -248,6 +277,25 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "calloop" version = "0.13.0" @@ -375,6 +423,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -461,6 +519,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation" version = "0.9.4" @@ -501,6 +565,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -577,12 +650,59 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -675,6 +795,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "zip", ] [[package]] @@ -696,6 +817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -795,6 +917,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -823,9 +955,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -973,6 +1107,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.3.1" @@ -1208,6 +1351,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1335,6 +1487,26 @@ dependencies = [ "windows-targets 0.53.0", ] +[[package]] +name = "liblzma" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66352d7a8ac12d4877b6e6ea5a9b7650ee094257dc40889955bea5bc5b08c1d0" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "libredox" version = "0.1.3" @@ -1346,6 +1518,15 @@ dependencies = [ "redox_syscall 0.5.12", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1474,6 +1655,12 @@ dependencies = [ "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" @@ -1839,6 +2026,16 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1935,6 +2132,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "prettyplease" version = "0.2.33" @@ -2312,12 +2515,29 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "skia-bindings" version = "0.86.0" @@ -2525,6 +2745,25 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + [[package]] name = "tiny-skia" version = "0.11.4" @@ -2747,6 +2986,12 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -3577,6 +3822,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -3610,3 +3869,75 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zip" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.3", + "hmac", + "indexmap", + "liblzma", + "memchr", + "pbkdf2", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/crates/io-figma/Cargo.toml b/crates/io-figma/Cargo.toml index 94b3b1f8da..00e55a4c23 100644 --- a/crates/io-figma/Cargo.toml +++ b/crates/io-figma/Cargo.toml @@ -9,4 +9,8 @@ crate-type = ["cdylib", "rlib"] [dependencies] serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" -reqwest = "0.12.19" \ No newline at end of file +reqwest = "0.12.19" + +[dev-dependencies] +zip = {version = "4.0.0", features = ["deflate"]} + diff --git a/crates/io-figma/fixtures/767108022358877208.zip b/crates/io-figma/fixtures/767108022358877208.zip new file mode 100644 index 0000000000000000000000000000000000000000..a37d6fb23e350b70ff64a833ff2e73eab12bf2f4 GIT binary patch literal 18924 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 z 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

    pM9sZ5#^=&mV#0%>2yjb^pAi?~69jqcji&51a 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�<(EC+cL7c1x}6TEn?RhbB#KyHFW$D&Iq?CD2 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