app icon: regal engraved dragon (owner's ArtCraft drawing), Dock/taskbar/launcher wiring, palette docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-02 09:45:30 -04:00
parent dabd179d5a
commit 437320509d
29 changed files with 321 additions and 113 deletions

View File

@ -4,7 +4,7 @@ Every non-code asset in this repository (icons, images, fonts, example art, colo
**Policy (mandatory):** VectorCraft contains **no Adobe iconography, images, artwork, presets, swatch/brush/symbol/pattern libraries or ICC profiles**. Every asset is either original work by VectorCraft contributors, or third-party material under an open licence (OSI open source, public domain / CC0, or Creative Commons that allows redistribution), and it is attributed below. Screenshots of Adobe software are never committed.
Generated-in-code art is original and has no file to list. This covers the default swatches, brushes, symbols, patterns, graphic styles, image-trace presets, the app icon (`cargo xtask bundle`) and the vector tool cursors (`crates/ui-egui/src/cursors.rs`).
Generated-in-code art is original and has no file to list. This covers the default swatches, brushes, symbols, patterns, graphic styles, image-trace presets and the vector tool cursors (`crates/ui-egui/src/cursors.rs`).
| Asset | Author | Source | Licence | Notes |
|---|---|---|---|---|
@ -16,6 +16,23 @@ Generated-in-code art is original and has no file to list. This covers the defau
| `docs/brand/artcraft-mark-black.svg` | ArtCraft team (project owner) | https://getartcraft.com/ | ArtCraft brand mark, owned by the project (licence/trademark terms to be confirmed by the owner) | Used in README only; not shipped in the app |
| `docs/brand/artcraft-mark.png` | ArtCraft team (project owner) | https://getartcraft.com/ | ArtCraft brand mark, owned by the project (licence/trademark terms to be confirmed by the owner) | Used in README only; not shipped in the app |
| `docs/brand/artcraft-mark.svg` | ArtCraft team (project owner) | https://getartcraft.com/ | ArtCraft brand mark, owned by the project (licence/trademark terms to be confirmed by the owner) | Used in README only; not shipped in the app |
| `assets/app-icon/LICENSE.txt` | (licence/readme text) | VectorCraft | MIT OR Apache-2.0 | |
| `assets/app-icon/README.md` | (licence/readme text) | VectorCraft | MIT OR Apache-2.0 | |
| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.vectorcraft.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.vectorcraft.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.vectorcraft.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.vectorcraft.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.vectorcraft.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.vectorcraft.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.vectorcraft.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.vectorcraft.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.vectorcraft.svg` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/vectorcraft-1024.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/vectorcraft-macos-512.png` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/vectorcraft-small.svg` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/vectorcraft.icns` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/vectorcraft.ico` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/app-icon/vectorcraft.svg` | Project owner (ArtCraft team) | Original artwork drawn in ArtCraft for VectorCraft, vectorised and rendered by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon (engraved dragon) |
| `assets/fonts/Inter-Medium.ttf` | Rasmus Andersson / The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) | Open-source typeface, not product iconography or artwork |
| `assets/fonts/Inter-Regular.ttf` | Rasmus Andersson / The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) | Open-source typeface, not product iconography or artwork |
| `assets/fonts/Inter-SemiBold.ttf` | Rasmus Andersson / The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) | Open-source typeface, not product iconography or artwork |

45
Cargo.lock generated
View File

@ -3355,6 +3355,15 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "shlex"
version = "2.0.1"
@ -3776,6 +3785,21 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "toml"
version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
@ -3806,6 +3830,12 @@ dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
name = "tracing"
version = "0.1.44"
@ -4026,6 +4056,7 @@ dependencies = [
"vectorcraft-engine",
"vectorcraft-ui-egui",
"webbrowser",
"winresource",
]
[[package]]
@ -5177,6 +5208,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winresource"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087"
dependencies = [
"toml",
"version_check",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
@ -5282,10 +5323,6 @@ name = "xtask"
version = "0.1.0"
dependencies = [
"serde_json",
"vectorcraft-color",
"vectorcraft-doc",
"vectorcraft-geom",
"vectorcraft-render",
]
[[package]]

View File

@ -7,6 +7,10 @@
</a>
</p>
<p align="center">
<img alt="VectorCraft app icon: an engraved dragon on VectorCraft red" src="assets/app-icon/vectorcraft-1024.png" width="128">
</p>
<h1 align="center">VectorCraft</h1>
<p align="center">
@ -220,6 +224,8 @@ And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video stud
MIT OR Apache-2.0. Bundled fonts are OFL, Lucide icons are ISC, and all other icons and art are
original. Per-asset attribution is in [`ASSETS.md`](ASSETS.md) (see also [`NOTICE`](NOTICE)).
The app icon (an engraved dragon on VectorCraft red, `#e8573f`) is the owner's original artwork; its
palette and files are in [`assets/app-icon/`](assets/app-icon/README.md).
<sub>VectorCraft is an independent project and is not affiliated with or endorsed by Adobe. "Adobe", "Illustrator", "Photoshop", "Premiere Pro", "Lightroom" and "Acrobat" are trademarks of Adobe Inc., used here only to describe compatibility and workflow familiarity.</sub>

View File

@ -24,3 +24,7 @@ webbrowser = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
muda = { version = "0.21", default-features = false }
[build-dependencies]
# Icon + VERSIONINFO for vectorcraft.exe; build.rs only uses it when targeting Windows.
winresource = "0.1"

28
apps/vectorcraft/build.rs Normal file
View File

@ -0,0 +1,28 @@
//! Windows only: embed the app icon and version info (VERSIONINFO) into `vectorcraft.exe`.
//!
//! On every other target this does nothing. A missing resource compiler is a warning, so a
//! cross-compile from macOS or Linux still links, unless `VECTORCRAFT_REQUIRE_WINRES=1` turns it into
//! an error (for release builds).
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=../../assets/app-icon/vectorcraft.ico");
println!("cargo:rerun-if-env-changed=VECTORCRAFT_REQUIRE_WINRES");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
let mut res = winresource::WindowsResource::new();
res.set_icon("../../assets/app-icon/vectorcraft.ico")
.set("ProductName", "VectorCraft")
.set("FileDescription", "VectorCraft vector illustration editor")
.set("CompanyName", "Learning Machines LLC")
.set("LegalCopyright", "Copyright (c) the VectorCraft authors. MIT OR Apache-2.0.")
.set("OriginalFilename", "vectorcraft.exe")
.set("InternalName", "vectorcraft");
if let Err(e) = res.compile() {
if std::env::var_os("VECTORCRAFT_REQUIRE_WINRES").is_some() {
panic!("embedding Windows resources failed: {e}");
}
println!("cargo:warning=vectorcraft.exe built without icon/version resources: {e}");
}
}

View File

@ -116,6 +116,17 @@ fn services() -> Services {
}
}
/// The window, Dock, taskbar and app-switcher icon (`assets/app-icon/`, see its README). macOS gets
/// the version with Apple's transparent margin; elsewhere the full-bleed tile. The app ID matches
/// `packaging/linux/ai.storyteller.vectorcraft.desktop` so Wayland docks find the launcher icon.
fn app_icon() -> egui::IconData {
#[cfg(target_os = "macos")]
let png: &[u8] = include_bytes!("../../../assets/app-icon/vectorcraft-macos-512.png");
#[cfg(not(target_os = "macos"))]
let png: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.vectorcraft.png");
eframe::icon_data::from_png_bytes(png).unwrap_or_default()
}
fn main() -> eframe::Result {
let mut control_port: Option<u16> = std::env::var("VECTORCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
let mut files = Vec::new();
@ -138,7 +149,9 @@ fn main() -> eframe::Result {
.with_drag_and_drop(true)
.with_fullsize_content_view(true)
.with_titlebar_shown(false)
.with_title_shown(false),
.with_title_shown(false)
.with_icon(app_icon())
.with_app_id("ai.storyteller.vectorcraft"),
..Default::default()
};
eframe::run_native(

View File

@ -0,0 +1,12 @@
VectorCraft app icon (vectorcraft.svg, vectorcraft-small.svg and every file derived from them in this
directory: vectorcraft-1024.png, vectorcraft-macos-512.png, vectorcraft.icns, vectorcraft.ico, hicolor/).
Original artwork made for this project by the project owner, drawn in ArtCraft and vectorised for
VectorCraft (no third-party material). Licensed like the rest of VectorCraft, under either of
* Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
* MIT license (https://opensource.org/licenses/MIT)
at your option.
Regenerate the derived files with packaging/icons.sh.

51
assets/app-icon/README.md Normal file
View File

@ -0,0 +1,51 @@
# VectorCraft app icon
**Creature:** a regal engraved dragon, VectorCraft's mascot.
**Style:** an engraving-style portrait in the Crafting Apps owl template (the FilmCraft owl): a tight
head-and-shoulders portrait on a full-bleed field, no frame or roundel, with the body running off the tile.
**Palette** (exactly three colours):
| Colour | Hex | Used for |
|---|---|---|
| Ink | `#0b0b0c` | line work and contour |
| Paper | `#efe9dc` | the figure |
| VectorCraft colour | `#e8573f` | the full-bleed field |
**Tile:** `viewBox="0 0 512 512"`, a rounded square with `rx=112`, clipped. The macOS files add Apple's
transparent margin (tile = 824/1024 of the canvas); Windows and Linux use the tile edge to edge.
**Provenance:** the owner's original ArtCraft drawing (2880 px, keyed to the palette), vectorised with
craftrules `assets/logo-options/_tools/vectorize_tile.py`. The drawing is kept in craftrules at
`craftrules/assets/app-icons/vectorcraft/source.png`, not here. `vectorcraft.svg` is the canonical
artwork. Licence: see `LICENSE.txt`.
## Files
| File | What | Used by |
|---|---|---|
| `vectorcraft.svg` | master vector (traced at 2048 px, about 700 KB) | source for everything below |
| `vectorcraft-small.svg` | lighter vector (traced at 1024 px, about 370 KB) | `hicolor/scalable` |
| `vectorcraft-1024.png` | full tile, 1024 px | docs, store listings |
| `vectorcraft-macos-512.png` | tile with Apple margin, 512 px | runtime Dock icon (`apps/vectorcraft/src/main.rs`) |
| `vectorcraft.icns` | macOS icon set, 16 to 1024 px | `cargo xtask bundle` (`CFBundleIconFile`) |
| `vectorcraft.ico` | Windows icon, 16 to 256 px | `apps/vectorcraft/build.rs` (embedded in the `.exe`) |
| `hicolor/<size>/apps/ai.storyteller.vectorcraft.png` | Linux theme icons, 16 to 512 px; the 256 one is also the runtime icon on Windows and Linux | `packaging/linux/ai.storyteller.vectorcraft.desktop` |
| `hicolor/scalable/apps/ai.storyteller.vectorcraft.svg` | Linux scalable icon | as above |
## How it reaches each OS
- **macOS:** the app sets the Dock / app-switcher icon at runtime (`ViewportBuilder::with_icon`); the
`.app` from `cargo xtask bundle` carries `vectorcraft.icns`.
- **Windows:** `build.rs` embeds `vectorcraft.ico` and VERSIONINFO with `winresource` (taskbar, Start
menu, Explorer, Alt-Tab); the runtime icon covers the title bar.
- **Linux:** install `packaging/linux/ai.storyteller.vectorcraft.desktop` to `share/applications/` and
`hicolor/` to `share/icons/`. The app sets its Wayland app ID / X11 class to
`ai.storyteller.vectorcraft`, so the dock matches the window to the launcher.
## Regenerate
Edit or replace `vectorcraft.svg` (and `vectorcraft-small.svg`), then run `packaging/icons.sh`
(needs `resvg`; `iconutil` on macOS for the `.icns`). It writes every PNG, the `.ico` (via
`cargo xtask ico`) and the `.icns`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 708 KiB

58
packaging/icons.sh Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Regenerate every app icon from assets/app-icon/vectorcraft.svg (the canonical artwork).
#
# Needs: resvg (brew install resvg / cargo install resvg). On macOS, iconutil also writes the
# .icns. The outputs are committed, so building and packaging never need these tools.
#
# packaging/icons.sh
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIR="$ROOT/assets/app-icon"
SVG="$DIR/vectorcraft.svg"
ID="ai.storyteller.vectorcraft"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
command -v resvg >/dev/null || { echo "error: resvg not found (brew install resvg)" >&2; exit 1; }
# The SVG is a full-bleed 512 tile, used as-is on Windows and Linux. macOS icons follow Apple's grid:
# the tile is 824/1024 of the canvas with a transparent margin, so widen the viewBox around it.
MAC="$TMP/mac.svg"
sed 's/viewBox="0 0 512 512"/viewBox="-62.136 -62.136 636.272 636.272"/' "$SVG" >"$MAC"
grep -q 'viewBox="-62.136' "$MAC" || { echo "error: $SVG has no 512 viewBox" >&2; exit 1; }
render() { resvg -w "$2" -h "$2" "$1" "$3" </dev/null; }
render "$SVG" 1024 "$DIR/vectorcraft-1024.png"
# Runtime window/Dock icon on macOS (embedded by apps/vectorcraft/src/main.rs).
render "$MAC" 512 "$DIR/vectorcraft-macos-512.png"
# Linux hicolor theme (the 256 one is also the runtime icon on Windows and Linux).
for s in 16 24 32 48 64 128 256 512; do
mkdir -p "$DIR/hicolor/${s}x${s}/apps"
render "$SVG" "$s" "$DIR/hicolor/${s}x${s}/apps/$ID.png"
done
mkdir -p "$DIR/hicolor/scalable/apps"
cp "$DIR/vectorcraft-small.svg" "$DIR/hicolor/scalable/apps/$ID.svg"
# Windows .ico.
ICO_PNGS=()
for s in 16 20 24 32 40 48 64 128 256; do
render "$SVG" "$s" "$TMP/ico-$s.png"
ICO_PNGS+=("$TMP/ico-$s.png")
done
(cd "$ROOT" && cargo run -q -p xtask -- ico "$DIR/vectorcraft.ico" "${ICO_PNGS[@]}")
# macOS .icns.
if command -v iconutil >/dev/null; then
SET="$TMP/vectorcraft.iconset"
mkdir -p "$SET"
for s in 16 32 128 256 512; do
render "$MAC" "$s" "$SET/icon_${s}x${s}.png"
render "$MAC" $((s * 2)) "$SET/icon_${s}x${s}@2x.png"
done
iconutil -c icns -o "$DIR/vectorcraft.icns" "$SET"
else
echo "warning: iconutil not found (macOS only); vectorcraft.icns not regenerated" >&2
fi
echo "icons written to $DIR"

View File

@ -0,0 +1,14 @@
[Desktop Entry]
Type=Application
Name=VectorCraft
GenericName=Vector Graphics Editor
Comment=Draw and edit vector illustrations, SVG and PDF
Exec=vectorcraft %F
TryExec=vectorcraft
Icon=ai.storyteller.vectorcraft
Terminal=false
StartupNotify=true
StartupWMClass=ai.storyteller.vectorcraft
Categories=Graphics;2DGraphics;VectorGraphics;
Keywords=vector;illustration;svg;pdf;drawing;bezier;illustrator;
MimeType=image/svg+xml;application/pdf;

View File

@ -7,7 +7,3 @@ publish = false
[dependencies]
serde_json = { workspace = true }
vectorcraft-doc = { workspace = true }
vectorcraft-geom = { workspace = true }
vectorcraft-color = { workspace = true }
vectorcraft-render = { workspace = true }

View File

@ -1,79 +1,9 @@
//! `cargo xtask bundle`: build the release app and wrap it as `dist/VectorCraft.app` (macOS), with an
//! icon rendered by VectorCraft's own renderer.
//! `cargo xtask bundle`: build the release app and wrap it as `dist/VectorCraft.app` (macOS), with the
//! committed app icon `assets/app-icon/vectorcraft.icns` (regenerate it with `packaging/icons.sh`).
use std::path::Path;
use std::process::Command;
use vectorcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint};
use vectorcraft_doc::{Appearance, Document, Node};
use vectorcraft_geom::{Anchor, PathData, Point, Rect, SubPath, shapes};
use vectorcraft_render::Renderer;
/// The app icon: a warm-gradient rounded square with a white pen nib (our own artwork).
pub fn icon_png(size: u32) -> Vec<u8> {
let s = 1024.0;
let mut d = Document::new(s, s);
let l = d.layers[0].id;
let grad = GradientPaint::new(Gradient {
kind: GradientKind::Linear,
stops: vec![
GradientStop { offset: 0.0, color: Color::from_hex("#ff9a3c").unwrap(), opacity: 1.0, midpoint: 0.5 },
GradientStop { offset: 1.0, color: Color::from_hex("#d4145a").unwrap(), opacity: 1.0, midpoint: 0.5 },
],
});
let mut gp = grad;
gp.angle = -45.0;
let bg = d.alloc_id();
d.insert(
Some(l),
9,
Node::path(
bg,
shapes::rounded_rectangle(Rect::new(100.0, 100.0, 924.0, 924.0), 185.0),
Appearance::basic(Paint::Gradient(Box::new(gp)), Paint::None, 0.0),
),
)
.unwrap();
// Pen nib.
let c = Point::new(512.0, 520.0);
let nib = PathData::single(SubPath::new(
vec![
Anchor::corner(Point::new(c.x, c.y + 250.0)),
Anchor::with_handles(Point::new(c.x - 170.0, c.y - 20.0), Point::new(c.x - 150.0, c.y + 80.0), Point::new(c.x - 170.0, c.y - 90.0)),
Anchor::corner(Point::new(c.x - 95.0, c.y - 230.0)),
Anchor::corner(Point::new(c.x + 95.0, c.y - 230.0)),
Anchor::with_handles(Point::new(c.x + 170.0, c.y - 20.0), Point::new(c.x + 170.0, c.y - 90.0), Point::new(c.x + 150.0, c.y + 80.0)),
],
true,
));
let n = d.alloc_id();
d.insert(Some(l), 9, Node::path(n, nib, Appearance::basic(Paint::solid(Color::WHITE), Paint::None, 0.0))).unwrap();
let hole = d.alloc_id();
d.insert(
Some(l),
9,
Node::path(
hole,
shapes::ellipse(Rect::from_center_size(Point::new(c.x, c.y - 30.0), (74.0, 74.0))),
Appearance::basic(Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))), Paint::None, 0.0),
),
)
.unwrap();
d.node_mut(hole).unwrap().appearance = Appearance::basic(Paint::solid(Color::from_hex("#e8573f").unwrap()), Paint::None, 0.0);
let slit = d.alloc_id();
d.insert(
Some(l),
9,
Node::path(
slit,
shapes::rectangle(Rect::new(c.x - 9.0, c.y + 5.0, c.x + 9.0, c.y + 245.0)),
Appearance::basic(Paint::solid(Color::from_hex("#e0457a").unwrap()), Paint::None, 0.0),
),
)
.unwrap();
Renderer::new().render_region(&d, Rect::new(0.0, 0.0, s, s), size as f64 / s, false).to_png()
}
pub fn run(root: &Path) -> Result<(), String> {
let status = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
.current_dir(root)
@ -90,36 +20,7 @@ pub fn run(root: &Path) -> Result<(), String> {
std::fs::create_dir_all(app.join("Resources")).map_err(|e| e.to_string())?;
std::fs::copy(target.join("release/vectorcraft"), app.join("MacOS/VectorCraft")).map_err(|e| format!("copy app: {e}"))?;
std::fs::copy(target.join("release/vectorcraft-cli"), app.join("MacOS/vectorcraft-cli")).map_err(|e| format!("copy cli: {e}"))?;
// Icon set → .icns via iconutil (macOS build tool).
let iconset = root.join("dist/VectorCraft.iconset");
let _ = std::fs::remove_dir_all(&iconset);
std::fs::create_dir_all(&iconset).map_err(|e| e.to_string())?;
for (sz, name) in [
(16, "16x16"),
(32, "16x16@2x"),
(32, "32x32"),
(64, "32x32@2x"),
(128, "128x128"),
(256, "128x128@2x"),
(256, "256x256"),
(512, "256x256@2x"),
(512, "512x512"),
(1024, "512x512@2x"),
] {
std::fs::write(iconset.join(format!("icon_{name}.png")), icon_png(sz)).map_err(|e| e.to_string())?;
}
std::fs::write(root.join("dist/VectorCraft-icon.png"), icon_png(1024)).map_err(|e| e.to_string())?;
let ok = Command::new("iconutil")
.args(["-c", "icns"])
.arg(&iconset)
.arg("-o")
.arg(app.join("Resources/VectorCraft.icns"))
.status()
.map(|s| s.success())
.unwrap_or(false);
if !ok {
eprintln!("warning: iconutil failed or missing; bundle has no icon");
}
std::fs::copy(root.join("assets/app-icon/vectorcraft.icns"), app.join("Resources/VectorCraft.icns")).map_err(|e| format!("copy icon: {e}"))?;
let version = env!("CARGO_PKG_VERSION");
let plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>

64
xtask/src/ico.rs Normal file
View File

@ -0,0 +1,64 @@
//! `cargo xtask ico <out.ico> <png>...`: pack square PNGs (each at most 256 px) into a Windows `.ico`.
//!
//! Every entry is stored as PNG data, which Windows Vista and later read at every size. Used by
//! `packaging/icons.sh`.
use std::path::Path;
/// Width and height from a PNG's IHDR chunk.
fn png_size(png: &[u8]) -> Result<(u32, u32), String> {
if png.len() < 24 || &png[..8] != b"\x89PNG\r\n\x1a\n" || &png[12..16] != b"IHDR" {
return Err("not a PNG".into());
}
let be = |i: usize| u32::from_be_bytes([png[i], png[i + 1], png[i + 2], png[i + 3]]);
Ok((be(16), be(20)))
}
/// Build the `.ico` bytes from PNG images.
pub fn pack(pngs: &[Vec<u8>]) -> Result<Vec<u8>, String> {
let count = u16::try_from(pngs.len()).map_err(|_| "too many images")?;
let mut out = Vec::new();
out.extend_from_slice(&[0, 0, 1, 0]);
out.extend_from_slice(&count.to_le_bytes());
let mut offset = 6 + 16 * pngs.len() as u32;
for png in pngs {
let (w, h) = png_size(png)?;
if w > 256 || h > 256 {
return Err(format!("{w}x{h} is larger than 256 px"));
}
// 256 is stored as 0.
out.push((w % 256) as u8);
out.push((h % 256) as u8);
out.extend_from_slice(&[0, 0]); // palette size, reserved
out.extend_from_slice(&1u16.to_le_bytes()); // colour planes
out.extend_from_slice(&32u16.to_le_bytes()); // bits per pixel
out.extend_from_slice(&(png.len() as u32).to_le_bytes());
out.extend_from_slice(&offset.to_le_bytes());
offset += png.len() as u32;
}
for png in pngs {
out.extend_from_slice(png);
}
Ok(out)
}
pub fn run(args: &[&str]) -> Result<(), String> {
let [out, inputs @ ..] = args else {
return Err("usage: cargo xtask ico <out.ico> <png>...".into());
};
if inputs.is_empty() {
return Err("usage: cargo xtask ico <out.ico> <png>...".into());
}
let pngs = inputs.iter().map(|p| std::fs::read(p).map_err(|e| format!("{p}: {e}"))).collect::<Result<Vec<_>, _>>()?;
std::fs::write(Path::new(out), pack(&pngs)?).map_err(|e| format!("{out}: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_non_png() {
assert!(pack(&[b"nope".to_vec()]).is_err());
}
}

View File

@ -5,6 +5,7 @@
mod assets;
mod bundle;
mod ico;
mod layers;
mod stats;
@ -21,7 +22,9 @@ commands:
ci fmt --check, clippy -D warnings, test, assets, layers, wasm (stops at first failure)
corpus [--download]
show where test corpora live; --download fetches PngSuite into corpus/pngsuite
bundle build dist/VectorCraft.app (macOS) with an icon rendered by VectorCraft
bundle build dist/VectorCraft.app (macOS) with assets/app-icon/vectorcraft.icns
ico <out.ico> <png>...
pack PNGs into a Windows .ico (used by packaging/icons.sh)
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
";
@ -35,6 +38,7 @@ fn main() -> ExitCode {
Some("ci") => cmd_ci(),
Some("corpus") => cmd_corpus(rest.contains(&"--download")),
Some("bundle") => bundle::run(&root()),
Some("ico") => ico::run(&rest),
Some("stats") => stats::run(&root(), rest.contains(&"--exact")),
Some("-h" | "--help" | "help") | None => {
print!("{USAGE}");