diff --git a/README.md b/README.md index 493b5b5..0c655de 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,14 @@ uv run python examples/hello.py # Python: same, plus hello.draw.js Both build a small document programmatically and export it. Source: `crates/draw-core/examples/hello.rs`, `examples/hello.py`. +### Sample gallery + +[examples/gallery/](examples/gallery/) contains five canonical drawings — flowchart, sticky-notes, wireframe, sketch, patterns — each committed as `.draw.json` + `.svg` + `.png`. All five are produced by a single Rust example that exercises the full API surface (every element type, every fill pattern, stroke/dash styles, freedraw curves, arrows with heads): + +```bash +cargo run --example gallery -p dkdc-draw-core +``` + ### Keyboard shortcuts | Key | Action | diff --git a/crates/draw-core/examples/gallery.rs b/crates/draw-core/examples/gallery.rs new file mode 100644 index 0000000..ef00f9c --- /dev/null +++ b/crates/draw-core/examples/gallery.rs @@ -0,0 +1,655 @@ +//! Sample gallery: builds 5 canonical drawings demonstrating the full +//! `draw-core` API surface and exports each as `.draw.json`, `.svg`, and +//! `.png` under `examples/gallery/`. +//! +//! Run with: `cargo run --example gallery -p dkdc-draw-core` +//! +//! Each drawing here targets a different feature slice: +//! - flowchart: rectangles, diamond, arrows with arrowheads, text +//! - sticky: solid-fill rectangles with text labels (no hachure) +//! - wireframe: ellipses, dashed strokes, bordered layout +//! - sketch: freedraw curves + text annotations +//! - patterns: every FillType side by side (Solid, Hachure, CrossHatch, None) + +use std::fs; +use std::path::{Path, PathBuf}; + +use draw_core::style::{Arrowhead, FillStyle, FillType, FontStyle, StrokeStyle, TextAlign}; +use draw_core::{ + Document, Element, FreeDrawElement, LineElement, Point, ShapeElement, TextElement, + export_png_with_scale, export_svg, storage, +}; + +fn main() -> anyhow::Result<()> { + let out_dir: PathBuf = std::env::var("DRAW_GALLERY_OUT") + .map(PathBuf::from) + .unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("examples") + .join("gallery") + }); + fs::create_dir_all(&out_dir)?; + + let drawings = [ + ("flowchart", flowchart()), + ("sticky", sticky_notes()), + ("wireframe", wireframe()), + ("sketch", sketch()), + ("patterns", patterns()), + ]; + + for (slug, mut doc) in drawings.iter().cloned() { + // Pin id and timestamps so regenerated artifacts are byte-identical to + // the committed ones — reviewers see a clean no-op diff unless content + // actually changed. + pin_metadata(&mut doc, slug); + write_drawing(&out_dir, slug, &doc)?; + } + + println!("wrote {} drawings to {}", drawings.len(), out_dir.display()); + Ok(()) +} + +fn pin_metadata(doc: &mut Document, slug: &str) { + doc.id = format!("gallery-{slug}"); + let pinned = "2026-01-01T00:00:00+00:00".to_string(); + doc.created_at = pinned.clone(); + doc.modified_at = pinned; +} + +fn write_drawing(out_dir: &Path, slug: &str, doc: &Document) -> anyhow::Result<()> { + let json_path = out_dir.join(format!("{slug}.draw.json")); + storage::save(doc, &json_path)?; + + let svg_path = out_dir.join(format!("{slug}.svg")); + fs::write(&svg_path, export_svg(doc))?; + + // 2x scale for retina-quality gallery assets. + let png_path = out_dir.join(format!("{slug}.png")); + fs::write(&png_path, export_png_with_scale(doc, 2.0)?)?; + + println!("wrote {slug}: json + svg + png"); + Ok(()) +} + +// ── Drawing builders ────────────────────────────────────────────────── + +fn flowchart() -> Document { + let mut doc = Document::new("flowchart".into()); + + // Start ellipse + doc.add_element(Element::Ellipse(shape_with_fill( + "start", + 40.0, + 20.0, + 140.0, + 60.0, + FillType::Hachure, + "#10b981", + ))); + doc.add_element(Element::Text(centered_label( + "start-label", + 70.0, + 42.0, + "start", + 18.0, + "#064e3b", + ))); + + // Decision diamond + doc.add_element(Element::Diamond(shape_with_fill( + "decide", + 40.0, + 140.0, + 140.0, + 100.0, + FillType::Hachure, + "#f59e0b", + ))); + doc.add_element(Element::Text(centered_label( + "decide-label", + 75.0, + 180.0, + "valid?", + 16.0, + "#78350f", + ))); + + // Accept rectangle + doc.add_element(Element::Rectangle(shape_with_fill( + "accept", + 240.0, + 150.0, + 140.0, + 80.0, + FillType::Hachure, + "#3b82f6", + ))); + doc.add_element(Element::Text(centered_label( + "accept-label", + 270.0, + 180.0, + "process", + 16.0, + "#1e3a8a", + ))); + + // Reject rectangle + doc.add_element(Element::Rectangle(shape_with_fill( + "reject", + 40.0, + 300.0, + 140.0, + 80.0, + FillType::Hachure, + "#ef4444", + ))); + doc.add_element(Element::Text(centered_label( + "reject-label", + 80.0, + 330.0, + "reject", + 16.0, + "#7f1d1d", + ))); + + // Arrows + doc.add_element(Element::Arrow(arrow_with_head( + "a1", + 110.0, + 80.0, + vec![Point::new(0.0, 0.0), Point::new(0.0, 60.0)], + ))); + doc.add_element(Element::Arrow(arrow_with_head( + "a2", + 180.0, + 190.0, + vec![Point::new(0.0, 0.0), Point::new(60.0, 0.0)], + ))); + doc.add_element(Element::Arrow(arrow_with_head( + "a3", + 110.0, + 240.0, + vec![Point::new(0.0, 0.0), Point::new(0.0, 60.0)], + ))); + + // Edge labels + doc.add_element(Element::Text(centered_label( + "yes-label", + 190.0, + 170.0, + "yes", + 14.0, + "#52525b", + ))); + doc.add_element(Element::Text(centered_label( + "no-label", 120.0, 260.0, "no", 14.0, "#52525b", + ))); + + doc +} + +fn sticky_notes() -> Document { + let mut doc = Document::new("sticky-notes".into()); + + let notes = [ + ( + "note1", + 20.0, + 20.0, + "#fef3c7", + "#78350f", + "shopping\n bread\n butter", + ), + ( + "note2", + 200.0, + 20.0, + "#dbeafe", + "#1e3a8a", + "read list\n Rust book\n SICP", + ), + ( + "note3", + 380.0, + 20.0, + "#fce7f3", + "#831843", + "weekend\n hike\n cook", + ), + ( + "note4", + 110.0, + 220.0, + "#d1fae5", + "#064e3b", + "ideas\n draw gallery\n lint pass", + ), + ( + "note5", + 290.0, + 220.0, + "#fee2e2", + "#7f1d1d", + "blockers\n pixmap panic\n hachure opacity", + ), + ]; + + for (i, (slug, x, y, fill_hex, text_hex, text)) in notes.iter().enumerate() { + doc.add_element(Element::Rectangle(ShapeElement { + id: format!("{slug}-bg"), + x: *x, + y: *y, + width: 160.0, + height: 160.0, + angle: if i % 2 == 0 { -0.03 } else { 0.03 }, + stroke: StrokeStyle { + color: (*text_hex).to_string(), + width: 1.5, + dash: vec![], + }, + fill: FillStyle { + color: (*fill_hex).to_string(), + style: FillType::Solid, + gap: 10.0, + angle: 0.0, + }, + opacity: 1.0, + locked: false, + group_id: None, + })); + doc.add_element(Element::Text(TextElement { + id: format!("{slug}-text"), + x: x + 12.0, + y: y + 16.0, + text: (*text).to_string(), + font: FontStyle { + family: "Inter, sans-serif".to_string(), + size: 14.0, + align: TextAlign::Left, + }, + stroke: StrokeStyle { + color: (*text_hex).to_string(), + width: 1.0, + dash: vec![], + }, + opacity: 1.0, + angle: 0.0, + locked: false, + group_id: None, + })); + } + + doc +} + +fn wireframe() -> Document { + let mut doc = Document::new("wireframe".into()); + + // Window frame + doc.add_element(Element::Rectangle(shape_with_stroke( + "frame", + 20.0, + 20.0, + 440.0, + 320.0, + "#334155", + 2.5, + vec![], + ))); + // Title bar + doc.add_element(Element::Rectangle(shape_with_stroke( + "titlebar", + 20.0, + 20.0, + 440.0, + 40.0, + "#334155", + 1.5, + vec![], + ))); + doc.add_element(Element::Text(centered_label( + "title", 34.0, 34.0, "~/draw", 14.0, "#334155", + ))); + + // Dashed content area + doc.add_element(Element::Rectangle(shape_with_stroke( + "content", + 40.0, + 80.0, + 400.0, + 240.0, + "#64748b", + 1.0, + vec![6.0, 4.0], + ))); + + // Inner buttons (ellipses) + doc.add_element(Element::Ellipse(shape_with_stroke( + "btn1", + 60.0, + 100.0, + 80.0, + 32.0, + "#0ea5e9", + 1.5, + vec![], + ))); + doc.add_element(Element::Text(centered_label( + "btn1-label", + 80.0, + 110.0, + "open", + 12.0, + "#0c4a6e", + ))); + + doc.add_element(Element::Ellipse(shape_with_stroke( + "btn2", + 160.0, + 100.0, + 80.0, + 32.0, + "#0ea5e9", + 1.5, + vec![], + ))); + doc.add_element(Element::Text(centered_label( + "btn2-label", + 180.0, + 110.0, + "save", + 12.0, + "#0c4a6e", + ))); + + doc.add_element(Element::Ellipse(shape_with_stroke( + "btn3", + 260.0, + 100.0, + 80.0, + 32.0, + "#0ea5e9", + 1.5, + vec![], + ))); + doc.add_element(Element::Text(centered_label( + "btn3-label", + 280.0, + 110.0, + "export", + 12.0, + "#0c4a6e", + ))); + + // Canvas placeholder with crossed diagonals + doc.add_element(Element::Rectangle(shape_with_stroke( + "canvas", + 60.0, + 160.0, + 380.0, + 140.0, + "#94a3b8", + 1.0, + vec![], + ))); + doc.add_element(Element::Line(LineElement::new( + "d1".into(), + 60.0, + 160.0, + vec![Point::new(0.0, 0.0), Point::new(380.0, 140.0)], + ))); + doc.add_element(Element::Line(LineElement::new( + "d2".into(), + 440.0, + 160.0, + vec![Point::new(0.0, 0.0), Point::new(-380.0, 140.0)], + ))); + + doc +} + +fn sketch() -> Document { + let mut doc = Document::new("sketch".into()); + + // A squiggly curve + let squiggle: Vec = (0..60) + .map(|i| { + let t = i as f64; + Point::new(t * 6.0, (t * 0.3).sin() * 40.0) + }) + .collect(); + doc.add_element(Element::FreeDraw(FreeDrawElement { + id: "wave".into(), + x: 30.0, + y: 80.0, + points: squiggle, + stroke: StrokeStyle { + color: "#8b5cf6".to_string(), + width: 2.5, + dash: vec![], + }, + opacity: 1.0, + locked: false, + group_id: None, + })); + + // A heart-ish freedraw + let heart: Vec = (0..64) + .map(|i| { + let t = i as f64 / 64.0 * std::f64::consts::TAU; + let x = 16.0 * t.sin().powi(3); + let y = + -(13.0 * t.cos() - 5.0 * (2.0 * t).cos() - 2.0 * (3.0 * t).cos() - (4.0 * t).cos()); + Point::new(x * 3.0, y * 3.0) + }) + .collect(); + doc.add_element(Element::FreeDraw(FreeDrawElement { + id: "heart".into(), + x: 250.0, + y: 180.0, + points: heart, + stroke: StrokeStyle { + color: "#e11d48".to_string(), + width: 3.0, + dash: vec![], + }, + opacity: 1.0, + locked: false, + group_id: None, + })); + + // Annotation arrow pointing at the heart + doc.add_element(Element::Arrow(arrow_with_head( + "ann-arrow", + 100.0, + 220.0, + vec![Point::new(0.0, 0.0), Point::new(120.0, -20.0)], + ))); + doc.add_element(Element::Text(centered_label( + "ann-text", + 20.0, + 220.0, + "freedraw!", + 16.0, + "#334155", + ))); + + doc.add_element(Element::Text(centered_label( + "wave-text", + 30.0, + 40.0, + "sin wave via FreeDraw", + 16.0, + "#334155", + ))); + + doc +} + +fn patterns() -> Document { + let mut doc = Document::new("fill-patterns".into()); + + let tiles = [ + ("solid", 20.0, "Solid", FillType::Solid, "#ec4899"), + ("hachure", 200.0, "Hachure", FillType::Hachure, "#ec4899"), + ( + "crosshatch", + 380.0, + "CrossHatch", + FillType::CrossHatch, + "#ec4899", + ), + ("none", 560.0, "None", FillType::None, "#ec4899"), + ]; + + for (slug, x, label, style, color) in tiles { + doc.add_element(Element::Rectangle(ShapeElement { + id: format!("{slug}-rect"), + x, + y: 40.0, + width: 160.0, + height: 120.0, + angle: 0.0, + stroke: StrokeStyle { + color: color.to_string(), + width: 2.0, + dash: vec![], + }, + fill: FillStyle { + color: color.to_string(), + style, + gap: 8.0, + angle: -0.785, + }, + opacity: 1.0, + locked: false, + group_id: None, + })); + doc.add_element(Element::Text(centered_label( + &format!("{slug}-label"), + x + 10.0, + 180.0, + label, + 14.0, + "#1f2937", + ))); + } + + doc +} + +// ── Small builders ──────────────────────────────────────────────────── + +fn shape_with_fill( + id: &str, + x: f64, + y: f64, + w: f64, + h: f64, + style: FillType, + color: &str, +) -> ShapeElement { + ShapeElement { + id: id.to_string(), + x, + y, + width: w, + height: h, + angle: 0.0, + stroke: StrokeStyle { + color: color.to_string(), + width: 2.0, + dash: vec![], + }, + fill: FillStyle { + color: color.to_string(), + style, + gap: 10.0, + angle: -0.785, + }, + opacity: 1.0, + locked: false, + group_id: None, + } +} + +fn shape_with_stroke( + id: &str, + x: f64, + y: f64, + w: f64, + h: f64, + color: &str, + stroke_w: f64, + dash: Vec, +) -> ShapeElement { + ShapeElement { + id: id.to_string(), + x, + y, + width: w, + height: h, + angle: 0.0, + stroke: StrokeStyle { + color: color.to_string(), + width: stroke_w, + dash, + }, + fill: FillStyle { + color: color.to_string(), + style: FillType::None, + gap: 10.0, + angle: 0.0, + }, + opacity: 1.0, + locked: false, + group_id: None, + } +} + +fn arrow_with_head(id: &str, x: f64, y: f64, points: Vec) -> LineElement { + LineElement { + id: id.to_string(), + x, + y, + points, + stroke: StrokeStyle { + color: "#334155".to_string(), + width: 2.0, + dash: vec![], + }, + start_arrowhead: None, + end_arrowhead: Some(Arrowhead::Arrow), + opacity: 1.0, + locked: false, + group_id: None, + start_binding: None, + end_binding: None, + } +} + +fn centered_label(id: &str, x: f64, y: f64, text: &str, size: f64, color: &str) -> TextElement { + TextElement { + id: id.to_string(), + x, + y, + text: text.to_string(), + font: FontStyle { + family: "Inter, sans-serif".to_string(), + size, + align: TextAlign::Left, + }, + stroke: StrokeStyle { + color: color.to_string(), + width: 1.0, + dash: vec![], + }, + opacity: 1.0, + angle: 0.0, + locked: false, + group_id: None, + } +} diff --git a/examples/gallery/README.md b/examples/gallery/README.md new file mode 100644 index 0000000..5b0a85b --- /dev/null +++ b/examples/gallery/README.md @@ -0,0 +1,47 @@ +# gallery + +Canonical sample drawings showing off the full `draw-core` API surface. Each +drawing is committed as three artifacts: + +- `.draw.json` — the document, in the native `.draw.json` format (this + is what you'd open in the app or CLI). +- `.svg` — exported via `export_svg`. +- `.png` — exported via `export_png_with_scale(&doc, 2.0)` (retina 2x). + +## index + +| drawing | features exercised | +| ----------- | ------------------------------------------------------- | +| flowchart | rectangles, diamond, ellipse, arrows with arrowheads, text labels | +| sticky | solid-fill rectangles with rotation, multiline text labels | +| wireframe | ellipses, dashed strokes, nested frames, diagonal lines | +| sketch | freedraw curves (sine wave + parametric heart), arrow annotation | +| patterns | every `FillType` side by side (Solid, Hachure, CrossHatch, None) | + +## regenerating + +All artifacts are produced by a single Rust example: + +```bash +cargo run --example gallery -p dkdc-draw-core +``` + +It writes into `examples/gallery/` relative to the workspace root. Override +the output directory with `DRAW_GALLERY_OUT=`. + +The Python bindings can round-trip any `.draw.json` in this directory: + +```bash +uv run python -c ' +import json, pathlib, dkdc_draw +doc = pathlib.Path("examples/gallery/flowchart.draw.json").read_text() +pathlib.Path("/tmp/flowchart.svg").write_text(dkdc_draw.export_svg(doc)) +' +``` + +## convention + +If you change any drawing in `crates/draw-core/examples/gallery.rs`, +regenerate the artifacts and commit them together. The committed assets are +the gallery — reviewers use the diff on the `.svg` / `.png` to see exactly +how rendering changed. diff --git a/examples/gallery/flowchart.draw.json b/examples/gallery/flowchart.draw.json new file mode 100644 index 0000000..6212751 --- /dev/null +++ b/examples/gallery/flowchart.draw.json @@ -0,0 +1,316 @@ +{ + "id": "gallery-flowchart", + "version": 1, + "name": "flowchart", + "elements": [ + { + "type": "Ellipse", + "id": "start", + "x": 40.0, + "y": 20.0, + "width": 140.0, + "height": 60.0, + "angle": 0.0, + "stroke": { + "color": "#10b981", + "width": 2.0, + "dash": [] + }, + "fill": { + "color": "#10b981", + "style": "hachure", + "gap": 10.0, + "angle": -0.785 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "start-label", + "x": 70.0, + "y": 42.0, + "text": "start", + "font": { + "family": "Inter, sans-serif", + "size": 18.0, + "align": "left" + }, + "stroke": { + "color": "#064e3b", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Diamond", + "id": "decide", + "x": 40.0, + "y": 140.0, + "width": 140.0, + "height": 100.0, + "angle": 0.0, + "stroke": { + "color": "#f59e0b", + "width": 2.0, + "dash": [] + }, + "fill": { + "color": "#f59e0b", + "style": "hachure", + "gap": 10.0, + "angle": -0.785 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "decide-label", + "x": 75.0, + "y": 180.0, + "text": "valid?", + "font": { + "family": "Inter, sans-serif", + "size": 16.0, + "align": "left" + }, + "stroke": { + "color": "#78350f", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "accept", + "x": 240.0, + "y": 150.0, + "width": 140.0, + "height": 80.0, + "angle": 0.0, + "stroke": { + "color": "#3b82f6", + "width": 2.0, + "dash": [] + }, + "fill": { + "color": "#3b82f6", + "style": "hachure", + "gap": 10.0, + "angle": -0.785 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "accept-label", + "x": 270.0, + "y": 180.0, + "text": "process", + "font": { + "family": "Inter, sans-serif", + "size": 16.0, + "align": "left" + }, + "stroke": { + "color": "#1e3a8a", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "reject", + "x": 40.0, + "y": 300.0, + "width": 140.0, + "height": 80.0, + "angle": 0.0, + "stroke": { + "color": "#ef4444", + "width": 2.0, + "dash": [] + }, + "fill": { + "color": "#ef4444", + "style": "hachure", + "gap": 10.0, + "angle": -0.785 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "reject-label", + "x": 80.0, + "y": 330.0, + "text": "reject", + "font": { + "family": "Inter, sans-serif", + "size": 16.0, + "align": "left" + }, + "stroke": { + "color": "#7f1d1d", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Arrow", + "id": "a1", + "x": 110.0, + "y": 80.0, + "points": [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 0.0, + "y": 60.0 + } + ], + "stroke": { + "color": "#334155", + "width": 2.0, + "dash": [] + }, + "start_arrowhead": null, + "end_arrowhead": "arrow", + "opacity": 1.0, + "locked": false, + "group_id": null, + "start_binding": null, + "end_binding": null + }, + { + "type": "Arrow", + "id": "a2", + "x": 180.0, + "y": 190.0, + "points": [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 60.0, + "y": 0.0 + } + ], + "stroke": { + "color": "#334155", + "width": 2.0, + "dash": [] + }, + "start_arrowhead": null, + "end_arrowhead": "arrow", + "opacity": 1.0, + "locked": false, + "group_id": null, + "start_binding": null, + "end_binding": null + }, + { + "type": "Arrow", + "id": "a3", + "x": 110.0, + "y": 240.0, + "points": [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 0.0, + "y": 60.0 + } + ], + "stroke": { + "color": "#334155", + "width": 2.0, + "dash": [] + }, + "start_arrowhead": null, + "end_arrowhead": "arrow", + "opacity": 1.0, + "locked": false, + "group_id": null, + "start_binding": null, + "end_binding": null + }, + { + "type": "Text", + "id": "yes-label", + "x": 190.0, + "y": 170.0, + "text": "yes", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#52525b", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "no-label", + "x": 120.0, + "y": 260.0, + "text": "no", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#52525b", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + } + ], + "view": { + "scroll_x": 0.0, + "scroll_y": 0.0, + "zoom": 1.0 + }, + "created_at": "2026-01-01T00:00:00+00:00", + "modified_at": "2026-01-01T00:00:00+00:00" +} \ No newline at end of file diff --git a/examples/gallery/flowchart.png b/examples/gallery/flowchart.png new file mode 100644 index 0000000..384aeec Binary files /dev/null and b/examples/gallery/flowchart.png differ diff --git a/examples/gallery/flowchart.svg b/examples/gallery/flowchart.svg new file mode 100644 index 0000000..762ef0c --- /dev/null +++ b/examples/gallery/flowchart.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + start + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + valid? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + process + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + reject + + + + yes + no + \ No newline at end of file diff --git a/examples/gallery/patterns.draw.json b/examples/gallery/patterns.draw.json new file mode 100644 index 0000000..9b1d671 --- /dev/null +++ b/examples/gallery/patterns.draw.json @@ -0,0 +1,190 @@ +{ + "id": "gallery-patterns", + "version": 1, + "name": "fill-patterns", + "elements": [ + { + "type": "Rectangle", + "id": "solid-rect", + "x": 20.0, + "y": 40.0, + "width": 160.0, + "height": 120.0, + "angle": 0.0, + "stroke": { + "color": "#ec4899", + "width": 2.0, + "dash": [] + }, + "fill": { + "color": "#ec4899", + "style": "solid", + "gap": 8.0, + "angle": -0.785 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "solid-label", + "x": 30.0, + "y": 180.0, + "text": "Solid", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#1f2937", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "hachure-rect", + "x": 200.0, + "y": 40.0, + "width": 160.0, + "height": 120.0, + "angle": 0.0, + "stroke": { + "color": "#ec4899", + "width": 2.0, + "dash": [] + }, + "fill": { + "color": "#ec4899", + "style": "hachure", + "gap": 8.0, + "angle": -0.785 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "hachure-label", + "x": 210.0, + "y": 180.0, + "text": "Hachure", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#1f2937", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "crosshatch-rect", + "x": 380.0, + "y": 40.0, + "width": 160.0, + "height": 120.0, + "angle": 0.0, + "stroke": { + "color": "#ec4899", + "width": 2.0, + "dash": [] + }, + "fill": { + "color": "#ec4899", + "style": "crosshatch", + "gap": 8.0, + "angle": -0.785 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "crosshatch-label", + "x": 390.0, + "y": 180.0, + "text": "CrossHatch", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#1f2937", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "none-rect", + "x": 560.0, + "y": 40.0, + "width": 160.0, + "height": 120.0, + "angle": 0.0, + "stroke": { + "color": "#ec4899", + "width": 2.0, + "dash": [] + }, + "fill": { + "color": "#ec4899", + "style": "none", + "gap": 8.0, + "angle": -0.785 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "none-label", + "x": 570.0, + "y": 180.0, + "text": "None", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#1f2937", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + } + ], + "view": { + "scroll_x": 0.0, + "scroll_y": 0.0, + "zoom": 1.0 + }, + "created_at": "2026-01-01T00:00:00+00:00", + "modified_at": "2026-01-01T00:00:00+00:00" +} \ No newline at end of file diff --git a/examples/gallery/patterns.png b/examples/gallery/patterns.png new file mode 100644 index 0000000..3187862 Binary files /dev/null and b/examples/gallery/patterns.png differ diff --git a/examples/gallery/patterns.svg b/examples/gallery/patterns.svg new file mode 100644 index 0000000..baef53f --- /dev/null +++ b/examples/gallery/patterns.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + Solid + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hachure + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CrossHatch + + None + \ No newline at end of file diff --git a/examples/gallery/sketch.draw.json b/examples/gallery/sketch.draw.json new file mode 100644 index 0000000..020f9ac --- /dev/null +++ b/examples/gallery/sketch.draw.json @@ -0,0 +1,612 @@ +{ + "id": "gallery-sketch", + "version": 1, + "name": "sketch", + "elements": [ + { + "type": "FreeDraw", + "id": "wave", + "x": 30.0, + "y": 80.0, + "points": [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 6.0, + "y": 11.820808266453582 + }, + { + "x": 12.0, + "y": 22.585698935801414 + }, + { + "x": 18.0, + "y": 31.333076385099332 + }, + { + "x": 24.0, + "y": 37.28156343868905 + }, + { + "x": 30.0, + "y": 39.89979946416218 + }, + { + "x": 36.0, + "y": 38.95390523512781 + }, + { + "x": 42.0, + "y": 34.52837466595495 + }, + { + "x": 48.0, + "y": 27.018527222046043 + }, + { + "x": 54.0, + "y": 17.095195209353207 + }, + { + "x": 60.0, + "y": 5.644800322394689 + }, + { + "x": 66.0, + "y": -6.309827765729929 + }, + { + "x": 72.0, + "y": -17.700817731794082 + }, + { + "x": 78.0, + "y": -27.51064636735895 + }, + { + "x": 84.0, + "y": -34.86303089654353 + }, + { + "x": 90.0, + "y": -39.10120470660388 + }, + { + "x": 96.0, + "y": -39.84658435343363 + }, + { + "x": 102.0, + "y": -37.0325872931093 + }, + { + "x": 108.0, + "y": -30.910579502239507 + }, + { + "x": 114.0, + "y": -22.027421703905503 + }, + { + "x": 120.0, + "y": -11.176619927957034 + }, + { + "x": 126.0, + "y": 0.6725560193739886 + }, + { + "x": 132.0, + "y": 12.461654540535115 + }, + { + "x": 138.0, + "y": 23.137590575527977 + }, + { + "x": 144.0, + "y": 31.746714553966108 + }, + { + "x": 150.0, + "y": 37.519999070989556 + }, + { + "x": 156.0, + "y": 39.9417338149842 + }, + { + "x": 162.0, + "y": 38.79559243380345 + }, + { + "x": 168.0, + "y": 34.18395632353122 + }, + { + "x": 174.0, + "y": 26.518769203287334 + }, + { + "x": 180.0, + "y": 16.484739409670265 + }, + { + "x": 186.0, + "y": 4.978176940282538 + }, + { + "x": 192.0, + "y": -6.973071248919186 + }, + { + "x": 198.0, + "y": -18.301435751012857 + }, + { + "x": 204.0, + "y": -27.994987503741694 + }, + { + "x": 210.0, + "y": -35.18783039886681 + }, + { + "x": 216.0, + "y": -39.23744920265965 + }, + { + "x": 222.0, + "y": -39.78210352815957 + }, + { + "x": 228.0, + "y": -36.77314102658703 + }, + { + "x": 234.0, + "y": -30.479343356761333 + }, + { + "x": 240.0, + "y": -21.462916720017397 + }, + { + "x": 246.0, + "y": -10.529271654632106 + }, + { + "x": 252.0, + "y": 1.3449218888454677 + }, + { + "x": 258.0, + "y": 13.09897756550772 + }, + { + "x": 264.0, + "y": 23.682940588288922 + }, + { + "x": 270.0, + "y": 32.15137706206483 + }, + { + "x": 276.0, + "y": 37.74782677776417 + }, + { + "x": 282.0, + "y": 39.972375549916705 + }, + { + "x": 288.0, + "y": 38.62631106197112 + }, + { + "x": 294.0, + "y": 33.82987324571737 + }, + { + "x": 300.0, + "y": 26.011513606284673 + }, + { + "x": 306.0, + "y": 15.869622925224547 + }, + { + "x": 312.0, + "y": 4.310146091977762 + }, + { + "x": 318.0, + "y": -7.634343254967506 + }, + { + "x": 324.0, + "y": -18.896879455938645 + }, + { + "x": 330.0, + "y": -28.471413694764923 + }, + { + "x": 336.0, + "y": -35.502681343260186 + }, + { + "x": 342.0, + "y": -39.36260020326571 + }, + { + "x": 348.0, + "y": -39.70637521882533 + }, + { + "x": 354.0, + "y": -36.50329799164738 + } + ], + "stroke": { + "color": "#8b5cf6", + "width": 2.5, + "dash": [] + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "FreeDraw", + "id": "heart", + "x": 250.0, + "y": 180.0, + "points": [ + { + "x": 0.0, + "y": -15.0 + }, + { + "x": 0.045200924810633265, + "y": -15.587144522240113 + }, + { + "x": 0.3564087963453909, + "y": -17.28229493068177 + }, + { + "x": 1.174122940807801, + "y": -19.89421890694082 + }, + { + "x": 2.690049175007791, + "y": -23.12859945595143 + }, + { + "x": 5.028065805669552, + "y": -26.621323269409714 + }, + { + "x": 8.231105023866913, + "y": -29.978925669979326 + }, + { + "x": 12.255103057710976, + "y": -32.821071869394665 + }, + { + "x": 16.970562748477136, + "y": -34.81980515339464 + }, + { + "x": 22.171615479146563, + "y": -35.73085909624809 + }, + { + "x": 27.59182217869809, + "y": -35.41352259921986 + }, + { + "x": 32.9253712004955, + "y": -33.83718488863639 + }, + { + "x": 37.8518643587874, + "y": -31.074532775104437 + }, + { + "x": 42.06257149632326, + "y": -27.283159020543362 + }, + { + "x": 45.28590544214684, + "y": -22.67881660085628 + }, + { + "x": 47.309934188985586, + "y": -17.504517144894248 + }, + { + "x": 48.0, + "y": -12.000000000000004 + }, + { + "x": 47.3099341889856, + "y": -6.375764072134954 + }, + { + "x": 45.28590544214684, + "y": -0.7949286873630488 + }, + { + "x": 42.06257149632327, + "y": 4.635171245657534 + }, + { + "x": 37.8518643587874, + "y": 9.861329339508005 + }, + { + "x": 32.92537120049551, + "y": 14.873977303857792 + }, + { + "x": 27.591822178698095, + "y": 19.690378941147863 + }, + { + "x": 22.171615479146567, + "y": 24.334872240696512 + }, + { + "x": 16.970562748477143, + "y": 28.819805153394633 + }, + { + "x": 12.255103057710976, + "y": 33.130504334810794 + }, + { + "x": 8.231105023866913, + "y": 37.21678795381274 + }, + { + "x": 5.028065805669559, + "y": 40.99232966580723 + }, + { + "x": 2.6900491750077933, + "y": 44.34180289154786 + }, + { + "x": 1.174122940807802, + "y": 47.134407870207724 + }, + { + "x": 0.35640879634539285, + "y": 49.24132159313966 + }, + { + "x": 0.04520092481063357, + "y": 50.553980129404735 + }, + { + "x": 8.816044883168516e-47, + "y": 51.0 + }, + { + "x": -0.045200924810633245, + "y": 50.55398012940475 + }, + { + "x": -0.35640879634539147, + "y": 49.24132159313965 + }, + { + "x": -1.1741229408077984, + "y": 47.13440787020773 + }, + { + "x": -2.690049175007789, + "y": 44.34180289154786 + }, + { + "x": -5.028065805669552, + "y": 40.99232966580725 + }, + { + "x": -8.231105023866903, + "y": 37.216787953812755 + }, + { + "x": -12.255103057710965, + "y": 33.1305043348108 + }, + { + "x": -16.970562748477136, + "y": 28.81980515339464 + }, + { + "x": -22.17161547914653, + "y": 24.334872240696537 + }, + { + "x": -27.59182217869809, + "y": 19.69037894114788 + }, + { + "x": -32.9253712004955, + "y": 14.873977303857803 + }, + { + "x": -37.85186435878737, + "y": 9.86132933950804 + }, + { + "x": -42.06257149632326, + "y": 4.6351712456575465 + }, + { + "x": -45.28590544214683, + "y": -0.7949286873630245 + }, + { + "x": -47.3099341889856, + "y": -6.3757640721349675 + }, + { + "x": -48.0, + "y": -11.99999999999999 + }, + { + "x": -47.3099341889856, + "y": -17.5045171448942 + }, + { + "x": -45.28590544214684, + "y": -22.678816600856273 + }, + { + "x": -42.06257149632327, + "y": -27.283159020543344 + }, + { + "x": -37.85186435878738, + "y": -31.074532775104444 + }, + { + "x": -32.92537120049551, + "y": -33.837184888636386 + }, + { + "x": -27.591822178698113, + "y": -35.41352259921985 + }, + { + "x": -22.17161547914655, + "y": -35.73085909624808 + }, + { + "x": -16.970562748477153, + "y": -34.81980515339463 + }, + { + "x": -12.255103057711002, + "y": -32.82107186939467 + }, + { + "x": -8.231105023866913, + "y": -29.978925669979333 + }, + { + "x": -5.028065805669563, + "y": -26.621323269409714 + }, + { + "x": -2.690049175007804, + "y": -23.128599455951466 + }, + { + "x": -1.174122940807803, + "y": -19.89421890694083 + }, + { + "x": -0.3564087963453934, + "y": -17.282294930681783 + }, + { + "x": -0.04520092481063313, + "y": -15.58714452224011 + } + ], + "stroke": { + "color": "#e11d48", + "width": 3.0, + "dash": [] + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Arrow", + "id": "ann-arrow", + "x": 100.0, + "y": 220.0, + "points": [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 120.0, + "y": -20.0 + } + ], + "stroke": { + "color": "#334155", + "width": 2.0, + "dash": [] + }, + "start_arrowhead": null, + "end_arrowhead": "arrow", + "opacity": 1.0, + "locked": false, + "group_id": null, + "start_binding": null, + "end_binding": null + }, + { + "type": "Text", + "id": "ann-text", + "x": 20.0, + "y": 220.0, + "text": "freedraw!", + "font": { + "family": "Inter, sans-serif", + "size": 16.0, + "align": "left" + }, + "stroke": { + "color": "#334155", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "wave-text", + "x": 30.0, + "y": 40.0, + "text": "sin wave via FreeDraw", + "font": { + "family": "Inter, sans-serif", + "size": 16.0, + "align": "left" + }, + "stroke": { + "color": "#334155", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + } + ], + "view": { + "scroll_x": 0.0, + "scroll_y": 0.0, + "zoom": 1.0 + }, + "created_at": "2026-01-01T00:00:00+00:00", + "modified_at": "2026-01-01T00:00:00+00:00" +} \ No newline at end of file diff --git a/examples/gallery/sketch.png b/examples/gallery/sketch.png new file mode 100644 index 0000000..c66e26f Binary files /dev/null and b/examples/gallery/sketch.png differ diff --git a/examples/gallery/sketch.svg b/examples/gallery/sketch.svg new file mode 100644 index 0000000..f6167fa --- /dev/null +++ b/examples/gallery/sketch.svg @@ -0,0 +1,7 @@ + + + + + freedraw! + sin wave via FreeDraw + \ No newline at end of file diff --git a/examples/gallery/sticky.draw.json b/examples/gallery/sticky.draw.json new file mode 100644 index 0000000..b068cc0 --- /dev/null +++ b/examples/gallery/sticky.draw.json @@ -0,0 +1,234 @@ +{ + "id": "gallery-sticky", + "version": 1, + "name": "sticky-notes", + "elements": [ + { + "type": "Rectangle", + "id": "note1-bg", + "x": 20.0, + "y": 20.0, + "width": 160.0, + "height": 160.0, + "angle": -0.03, + "stroke": { + "color": "#78350f", + "width": 1.5, + "dash": [] + }, + "fill": { + "color": "#fef3c7", + "style": "solid", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "note1-text", + "x": 32.0, + "y": 36.0, + "text": "shopping\n bread\n butter", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#78350f", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "note2-bg", + "x": 200.0, + "y": 20.0, + "width": 160.0, + "height": 160.0, + "angle": 0.03, + "stroke": { + "color": "#1e3a8a", + "width": 1.5, + "dash": [] + }, + "fill": { + "color": "#dbeafe", + "style": "solid", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "note2-text", + "x": 212.0, + "y": 36.0, + "text": "read list\n Rust book\n SICP", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#1e3a8a", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "note3-bg", + "x": 380.0, + "y": 20.0, + "width": 160.0, + "height": 160.0, + "angle": -0.03, + "stroke": { + "color": "#831843", + "width": 1.5, + "dash": [] + }, + "fill": { + "color": "#fce7f3", + "style": "solid", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "note3-text", + "x": 392.0, + "y": 36.0, + "text": "weekend\n hike\n cook", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#831843", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "note4-bg", + "x": 110.0, + "y": 220.0, + "width": 160.0, + "height": 160.0, + "angle": 0.03, + "stroke": { + "color": "#064e3b", + "width": 1.5, + "dash": [] + }, + "fill": { + "color": "#d1fae5", + "style": "solid", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "note4-text", + "x": 122.0, + "y": 236.0, + "text": "ideas\n draw gallery\n lint pass", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#064e3b", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "note5-bg", + "x": 290.0, + "y": 220.0, + "width": 160.0, + "height": 160.0, + "angle": -0.03, + "stroke": { + "color": "#7f1d1d", + "width": 1.5, + "dash": [] + }, + "fill": { + "color": "#fee2e2", + "style": "solid", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "note5-text", + "x": 302.0, + "y": 236.0, + "text": "blockers\n pixmap panic\n hachure opacity", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#7f1d1d", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + } + ], + "view": { + "scroll_x": 0.0, + "scroll_y": 0.0, + "zoom": 1.0 + }, + "created_at": "2026-01-01T00:00:00+00:00", + "modified_at": "2026-01-01T00:00:00+00:00" +} \ No newline at end of file diff --git a/examples/gallery/sticky.png b/examples/gallery/sticky.png new file mode 100644 index 0000000..1323652 Binary files /dev/null and b/examples/gallery/sticky.png differ diff --git a/examples/gallery/sticky.svg b/examples/gallery/sticky.svg new file mode 100644 index 0000000..e44fedb --- /dev/null +++ b/examples/gallery/sticky.svg @@ -0,0 +1,22 @@ + + + shopping + bread + butter + + read list + Rust book + SICP + + weekend + hike + cook + + ideas + draw gallery + lint pass + + blockers + pixmap panic + hachure opacity + \ No newline at end of file diff --git a/examples/gallery/wireframe.draw.json b/examples/gallery/wireframe.draw.json new file mode 100644 index 0000000..4a53829 --- /dev/null +++ b/examples/gallery/wireframe.draw.json @@ -0,0 +1,318 @@ +{ + "id": "gallery-wireframe", + "version": 1, + "name": "wireframe", + "elements": [ + { + "type": "Rectangle", + "id": "frame", + "x": 20.0, + "y": 20.0, + "width": 440.0, + "height": 320.0, + "angle": 0.0, + "stroke": { + "color": "#334155", + "width": 2.5, + "dash": [] + }, + "fill": { + "color": "#334155", + "style": "none", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "titlebar", + "x": 20.0, + "y": 20.0, + "width": 440.0, + "height": 40.0, + "angle": 0.0, + "stroke": { + "color": "#334155", + "width": 1.5, + "dash": [] + }, + "fill": { + "color": "#334155", + "style": "none", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "title", + "x": 34.0, + "y": 34.0, + "text": "~/draw", + "font": { + "family": "Inter, sans-serif", + "size": 14.0, + "align": "left" + }, + "stroke": { + "color": "#334155", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "content", + "x": 40.0, + "y": 80.0, + "width": 400.0, + "height": 240.0, + "angle": 0.0, + "stroke": { + "color": "#64748b", + "width": 1.0, + "dash": [ + 6.0, + 4.0 + ] + }, + "fill": { + "color": "#64748b", + "style": "none", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Ellipse", + "id": "btn1", + "x": 60.0, + "y": 100.0, + "width": 80.0, + "height": 32.0, + "angle": 0.0, + "stroke": { + "color": "#0ea5e9", + "width": 1.5, + "dash": [] + }, + "fill": { + "color": "#0ea5e9", + "style": "none", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "btn1-label", + "x": 80.0, + "y": 110.0, + "text": "open", + "font": { + "family": "Inter, sans-serif", + "size": 12.0, + "align": "left" + }, + "stroke": { + "color": "#0c4a6e", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Ellipse", + "id": "btn2", + "x": 160.0, + "y": 100.0, + "width": 80.0, + "height": 32.0, + "angle": 0.0, + "stroke": { + "color": "#0ea5e9", + "width": 1.5, + "dash": [] + }, + "fill": { + "color": "#0ea5e9", + "style": "none", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "btn2-label", + "x": 180.0, + "y": 110.0, + "text": "save", + "font": { + "family": "Inter, sans-serif", + "size": 12.0, + "align": "left" + }, + "stroke": { + "color": "#0c4a6e", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Ellipse", + "id": "btn3", + "x": 260.0, + "y": 100.0, + "width": 80.0, + "height": 32.0, + "angle": 0.0, + "stroke": { + "color": "#0ea5e9", + "width": 1.5, + "dash": [] + }, + "fill": { + "color": "#0ea5e9", + "style": "none", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Text", + "id": "btn3-label", + "x": 280.0, + "y": 110.0, + "text": "export", + "font": { + "family": "Inter, sans-serif", + "size": 12.0, + "align": "left" + }, + "stroke": { + "color": "#0c4a6e", + "width": 1.0, + "dash": [] + }, + "opacity": 1.0, + "angle": 0.0, + "locked": false, + "group_id": null + }, + { + "type": "Rectangle", + "id": "canvas", + "x": 60.0, + "y": 160.0, + "width": 380.0, + "height": 140.0, + "angle": 0.0, + "stroke": { + "color": "#94a3b8", + "width": 1.0, + "dash": [] + }, + "fill": { + "color": "#94a3b8", + "style": "none", + "gap": 10.0, + "angle": 0.0 + }, + "opacity": 1.0, + "locked": false, + "group_id": null + }, + { + "type": "Line", + "id": "d1", + "x": 60.0, + "y": 160.0, + "points": [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 380.0, + "y": 140.0 + } + ], + "stroke": { + "color": "#a855f7", + "width": 2.0, + "dash": [] + }, + "start_arrowhead": null, + "end_arrowhead": null, + "opacity": 1.0, + "locked": false, + "group_id": null, + "start_binding": null, + "end_binding": null + }, + { + "type": "Line", + "id": "d2", + "x": 440.0, + "y": 160.0, + "points": [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": -380.0, + "y": 140.0 + } + ], + "stroke": { + "color": "#a855f7", + "width": 2.0, + "dash": [] + }, + "start_arrowhead": null, + "end_arrowhead": null, + "opacity": 1.0, + "locked": false, + "group_id": null, + "start_binding": null, + "end_binding": null + } + ], + "view": { + "scroll_x": 0.0, + "scroll_y": 0.0, + "zoom": 1.0 + }, + "created_at": "2026-01-01T00:00:00+00:00", + "modified_at": "2026-01-01T00:00:00+00:00" +} \ No newline at end of file diff --git a/examples/gallery/wireframe.png b/examples/gallery/wireframe.png new file mode 100644 index 0000000..a00ada7 Binary files /dev/null and b/examples/gallery/wireframe.png differ diff --git a/examples/gallery/wireframe.svg b/examples/gallery/wireframe.svg new file mode 100644 index 0000000..3c302c0 --- /dev/null +++ b/examples/gallery/wireframe.svg @@ -0,0 +1,15 @@ + + + + ~/draw + + + open + + save + + export + + + + \ No newline at end of file