App icon: the chameleon, vectorised like the other Crafting Apps (#156)
The owner's engraving-style chameleon drawing, traced to three colours (ink, paper, FigCraft green #02da96) on the shared 512-unit tile. assets/app-icon/figcraft.svg is the master; cargo xtask icon now renders the PNGs (macOS grid), the hicolor theme and the .ico (tight crop, 16-256 px) from it with resvg instead of drawing the old geometric mark. Off macOS the window icon uses the tight 256 px render.
|
|
@ -46,7 +46,8 @@ code.
|
|||
`assets/`, `docs/brand/` or `docs/images/`.
|
||||
- FigCraft's UI icons are drawn in code (`crates/ui-egui/src/icons.rs`) from simple primitives and
|
||||
are original. Keep it that way: new icons are new code, designed from scratch.
|
||||
- The app icon is generated in code (`cargo xtask icon`, `xtask/src/icon.rs`).
|
||||
- The app icon is the owner's chameleon drawing, vectorised (`assets/app-icon/README.md`); `cargo xtask icon`
|
||||
renders the PNGs and `.ico` from `assets/app-icon/figcraft.svg`.
|
||||
- **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this
|
||||
repo.** The app uses it only through the optional `CRAFT_FONTS_DIR` build input
|
||||
(`.cargo/config.toml` points it at `../craft-fonts` for local builds; `crates/text/build.rs`
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ MIT OR Apache-2.0. No third-party icon sets are used.
|
|||
|
||||
| Path | Author | Source | Licence | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `assets/app-icon/` (all files: `figcraft-*.png`, `figcraft.ico`, `hicolor/*/apps/ai.storyteller.figcraft.png`) | FigCraft contributors | Generated by `cargo xtask icon` (`xtask/src/icon.rs`) with FigCraft's own renderer | MIT OR Apache-2.0 | Original geometric mark (stacked layer cards and a cursor) |
|
||||
| `assets/app-icon/` (all files: `figcraft.svg`, `figcraft-small.svg`, `figcraft-*.png`, `figcraft.ico`, `hicolor/`) | The project owner (engraving-style drawing, vectorised) | Original work, see [`assets/app-icon/README.md`](assets/app-icon/README.md); derived files rendered by `cargo xtask icon` | MIT OR Apache-2.0, [`assets/app-icon/LICENSE.txt`](assets/app-icon/LICENSE.txt) | FigCraft app icon (chameleon) |
|
||||
| `docs/brand/artcraft-logo.svg`, `docs/brand/artcraft-logo.png`, `docs/brand/artcraft-logo-white.svg`, `docs/brand/artcraft-logo-white.png`, `docs/brand/artcraft-mark.svg`, `docs/brand/artcraft-mark.png`, `docs/brand/artcraft-mark-black.svg`, `docs/brand/artcraft-mark-black.png` | ArtCraft Team | storytold/craftrules `assets/brand/` | ArtCraft brand terms (`docs/brand/LICENSE-brand.txt`), not open source | Trademarks; unmodified use within FigCraft only |
|
||||
| `docs/images/` (all screenshots and exports: `shot-*.png`, `art-*.png`) | FigCraft contributors | Screenshots of FigCraft showing synthetic demo documents made in FigCraft, and PNG exports of those documents by FigCraft's renderer | MIT OR Apache-2.0 | No third-party artwork |
|
||||
| `examples/*.figcraft` | FigCraft contributors | Synthetic demo documents built through FigCraft's command API; invented copy, shapes and vectors drawn from scratch, no images or embedded fonts | MIT OR Apache-2.0 | Text uses Inter (OFL, from craft-fonts or the system); no third-party artwork |
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ code.
|
|||
`assets/`, `docs/brand/` or `docs/images/`.
|
||||
- FigCraft's UI icons are drawn in code (`crates/ui-egui/src/icons.rs`) from simple primitives and
|
||||
are original. Keep it that way: new icons are new code, designed from scratch.
|
||||
- The app icon is generated in code (`cargo xtask icon`, `xtask/src/icon.rs`).
|
||||
- The app icon is the owner's chameleon drawing, vectorised (`assets/app-icon/README.md`); `cargo xtask icon`
|
||||
renders the PNGs and `.ico` from `assets/app-icon/figcraft.svg`.
|
||||
- **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this
|
||||
repo.** The app uses it only through the optional `CRAFT_FONTS_DIR` build input
|
||||
(`.cargo/config.toml` points it at `../craft-fonts` for local builds; `crates/text/build.rs`
|
||||
|
|
|
|||
|
|
@ -5805,12 +5805,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
|||
name = "xtask"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"figcraft-color",
|
||||
"figcraft-doc",
|
||||
"figcraft-engine",
|
||||
"figcraft-render",
|
||||
"figcraft-svg",
|
||||
"image",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -210,7 +210,11 @@ impl Services for Desktop {
|
|||
|
||||
/// The window icon from the bundled app icon (if present).
|
||||
pub fn window_icon() -> Option<egui::IconData> {
|
||||
// macOS wants Apple's padded grid; the other platforms the tile edge to edge.
|
||||
#[cfg(target_os = "macos")]
|
||||
let bytes = include_bytes!("../../../assets/app-icon/figcraft-256.png");
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let bytes = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.figcraft.png");
|
||||
let img = image::load_from_memory(bytes).ok()?.to_rgba8();
|
||||
let (w, h) = img.dimensions();
|
||||
Some(egui::IconData { rgba: img.into_raw(), width: w, height: h })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
FigCraft app icon (figcraft.svg, figcraft-small.svg and every file derived from them in this
|
||||
directory: figcraft-<size>.png, figcraft.ico, hicolor/).
|
||||
|
||||
Original artwork made for this project by the project owner: an engraving-style drawing, then
|
||||
vectorised. No third-party material. Licensed like the rest of FigCraft, 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 `cargo xtask icon`.
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# FigCraft app icon
|
||||
|
||||
A **chameleon**: an engraving portrait in profile with a curled tail, in the Crafting Apps'
|
||||
owl-template framing (full-bleed field, head and body filling the tile, body running off the edges).
|
||||
|
||||
## Palette
|
||||
|
||||
Exactly three colours:
|
||||
|
||||
| Colour | Hex | Used for |
|
||||
|---|---|---|
|
||||
| Ink | `#0b0b0c` | line work and contours |
|
||||
| Paper | `#efe9dc` | the figure |
|
||||
| FigCraft green (app colour) | `#02da96` | the full-bleed field |
|
||||
|
||||
## Geometry
|
||||
|
||||
A 512-unit tile (`viewBox="0 0 512 512"`), rounded square with `rx=112`, no border. The macOS
|
||||
renders pad it onto Apple's 824/1024 icon grid; the Windows and Linux renders crop 22 units off
|
||||
each side so the figure reads at 16–48 px.
|
||||
|
||||
## Provenance
|
||||
|
||||
The owner's original engraving-style drawing (1500 px, keyed to the palette), supplied for
|
||||
FigCraft and vectorised with craftrules `assets/logo-options/_tools/vectorize_tile.py`. The source
|
||||
image is not kept in this repo. License: see `LICENSE.txt`.
|
||||
|
||||
## Files
|
||||
|
||||
- `figcraft.svg`: canonical master (traced at 1500 px, the source's size).
|
||||
- `figcraft-small.svg`: lighter trace (traced at 1024 px); also the hicolor scalable icon.
|
||||
- `figcraft-<size>.png` (16–1024): renders on the macOS grid; `packaging/macos/package.sh` builds
|
||||
the `.icns` from them.
|
||||
- `figcraft.ico`: Windows icon, 16–256 px, embedded in the `.exe` by `apps/figcraft/build.rs`.
|
||||
- `hicolor/<size>/apps/ai.storyteller.figcraft.png` (16–512) and `hicolor/scalable/...svg`:
|
||||
Linux icon theme; the 256 px one is also the runtime window icon off macOS.
|
||||
|
||||
## Regenerate
|
||||
|
||||
Replace `figcraft.svg` (and `figcraft-small.svg`), then run `cargo xtask icon`. It needs `resvg`
|
||||
(`cargo install resvg`).
|
||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 559 B After Width: | Height: | Size: 678 B |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 453 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 559 B After Width: | Height: | Size: 779 B |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 453 KiB |
|
|
@ -7,9 +7,4 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
serde_json = { workspace = true }
|
||||
figcraft-doc = { workspace = true }
|
||||
figcraft-color = { workspace = true }
|
||||
figcraft-render = { workspace = true }
|
||||
figcraft-engine = { workspace = true }
|
||||
figcraft-svg = { workspace = true }
|
||||
image = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,102 +1,64 @@
|
|||
//! The app icon, generated from code with FigCraft's own renderer (original artwork,
|
||||
//! MIT OR Apache-2.0): a rounded tile in the app colour with a paper "frame" mark — three stacked
|
||||
//! layer cards and a cursor. Writes assets/app-icon/*.png, the hicolor theme and figcraft.ico.
|
||||
//! The app icon: rasterises the master `assets/app-icon/figcraft.svg` (the chameleon, see
|
||||
//! `assets/app-icon/README.md`) with the `resvg` CLI (`cargo install resvg`). Writes
|
||||
//! figcraft-<size>.png (macOS grid), the hicolor theme and figcraft.ico. The outputs are
|
||||
//! committed, so building and packaging never need `resvg`.
|
||||
|
||||
use figcraft_color::{Color, Paint};
|
||||
use figcraft_doc::{Document, NodeKind};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
pub const APP_COLOR: &str = "0EA5C6";
|
||||
const INK: &str = "0B0B0C";
|
||||
const PAPER: &str = "EFE9DC";
|
||||
const VIEW_BOX: &str = r#"viewBox="0 0 512 512""#;
|
||||
/// macOS: the 512-unit tile padded onto Apple's 824/1024 icon grid (transparent margin).
|
||||
const MAC_VIEW_BOX: &str = r#"viewBox="-62 -62 636 636""#;
|
||||
/// Windows and Linux: 22 units cropped off each side so the portrait reads at 16-48 px.
|
||||
const TIGHT_VIEW_BOX: &str = r#"viewBox="22 22 468 468""#;
|
||||
|
||||
fn build() -> Option<Document> {
|
||||
let mut d = Document::new("icon");
|
||||
let p = d.first_page()?;
|
||||
let bg = d.create(p, NodeKind::Rectangle, "tile", 0.0, 0.0, 512.0, 512.0).ok()?;
|
||||
let n = d.get_mut(bg).ok()?;
|
||||
n.corners.radii = [112.0; 4];
|
||||
n.corners.smoothing = 0.6;
|
||||
n.fills = vec![Paint::solid(Color::from_hex(APP_COLOR)?)];
|
||||
// Three stacked cards (layers), back to front.
|
||||
for (i, (x, y, w, h)) in [(150.0, 96.0, 250.0, 200.0), (122.0, 150.0, 250.0, 200.0), (94.0, 204.0, 250.0, 200.0)].iter().enumerate() {
|
||||
let c = d.create(p, NodeKind::Rectangle, "card", *x, *y, *w, *h).ok()?;
|
||||
let n = d.get_mut(c).ok()?;
|
||||
n.corners.radii = [28.0; 4];
|
||||
n.corners.smoothing = 0.6;
|
||||
n.fills = vec![Paint::solid(Color::from_hex(PAPER)?)];
|
||||
n.strokes = vec![Paint::solid(Color::from_hex(INK)?)];
|
||||
n.stroke.weight = 14.0;
|
||||
n.stroke.align = figcraft_doc::StrokeAlign::Inside;
|
||||
if i == 2 {
|
||||
// Front card: a frame grid "#".
|
||||
for (gx, gy, gw, gh) in
|
||||
[(150.0, 228.0, 14.0, 152.0), (250.0, 228.0, 14.0, 152.0), (118.0, 260.0, 178.0, 14.0), (118.0, 330.0, 178.0, 14.0)]
|
||||
{
|
||||
let g = d.create(p, NodeKind::Rectangle, "grid", gx, gy, gw, gh).ok()?;
|
||||
let gn = d.get_mut(g).ok()?;
|
||||
gn.fills = vec![Paint::solid(Color::from_hex(INK)?)];
|
||||
gn.corners.radii = [7.0; 4];
|
||||
fn render(svg: &Path, size: u32, out: &Path) -> bool {
|
||||
let s = size.to_string();
|
||||
let ok = Command::new("resvg").args(["-w", &s, "-h", &s]).arg(svg).arg(out).status().is_ok_and(|st| st.success());
|
||||
if !ok {
|
||||
eprintln!("icon: resvg failed for {} (is it installed? `cargo install resvg`)", out.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cursor.
|
||||
let cur = figcraft_doc::VectorNetwork::from_points(
|
||||
&[
|
||||
(0.0, 0.0).into(),
|
||||
(0.0, 120.0).into(),
|
||||
(32.0, 92.0).into(),
|
||||
(52.0, 136.0).into(),
|
||||
(72.0, 128.0).into(),
|
||||
(52.0, 84.0).into(),
|
||||
(92.0, 82.0).into(),
|
||||
],
|
||||
true,
|
||||
);
|
||||
let c = d.create(p, NodeKind::Vector { network: cur }, "cursor", 318.0, 300.0, 92.0, 136.0).ok()?;
|
||||
let n = d.get_mut(c).ok()?;
|
||||
n.fills = vec![Paint::solid(Color::from_hex(INK)?)];
|
||||
n.strokes = vec![Paint::solid(Color::from_hex(PAPER)?)];
|
||||
n.stroke.weight = 10.0;
|
||||
n.stroke.align = figcraft_doc::StrokeAlign::Outside;
|
||||
n.stroke.join = figcraft_doc::StrokeJoin::Round;
|
||||
Some(d)
|
||||
ok
|
||||
}
|
||||
|
||||
pub fn generate(root: &std::path::Path) -> bool {
|
||||
let Some(doc) = build() else { return false };
|
||||
let Some(page) = doc.first_page() else { return false };
|
||||
pub fn generate(root: &Path) -> bool {
|
||||
let dir = root.join("assets/app-icon");
|
||||
let _ = std::fs::create_dir_all(dir.join("hicolor"));
|
||||
let render = |size: u32| -> Option<image::RgbaImage> {
|
||||
let view = figcraft_render::View { origin: (0.0, 0.0), scale: f64::from(size) / 512.0, width: size, height: size };
|
||||
let opts = figcraft_render::Options { background: Some(Color::TRANSPARENT), ..Default::default() };
|
||||
let pix = figcraft_render::render_view(&doc, page, &view, &opts)?;
|
||||
image::RgbaImage::from_raw(size, size, figcraft_render::to_rgba8(&pix))
|
||||
let Ok(master) = std::fs::read_to_string(dir.join("figcraft.svg")) else {
|
||||
eprintln!("icon: cannot read {}", dir.join("figcraft.svg").display());
|
||||
return false;
|
||||
};
|
||||
let mut ok = true;
|
||||
if !master.contains(VIEW_BOX) {
|
||||
eprintln!("icon: expected {VIEW_BOX} in figcraft.svg");
|
||||
return false;
|
||||
}
|
||||
let tmp = root.join("target/icon");
|
||||
let (mac, tight) = (tmp.join("mac.svg"), tmp.join("tight.svg"));
|
||||
let mut ok = std::fs::create_dir_all(&tmp).is_ok()
|
||||
&& std::fs::write(&mac, master.replacen(VIEW_BOX, MAC_VIEW_BOX, 1)).is_ok()
|
||||
&& std::fs::write(&tight, master.replacen(VIEW_BOX, TIGHT_VIEW_BOX, 1)).is_ok();
|
||||
if !ok {
|
||||
eprintln!("icon: cannot write {}", tmp.display());
|
||||
return false;
|
||||
}
|
||||
for size in [16u32, 32, 48, 64, 128, 256, 512, 1024] {
|
||||
let Some(img) = render(size) else { return false };
|
||||
let name = if size == 1024 { "figcraft-1024.png".to_string() } else { format!("figcraft-{size}.png") };
|
||||
ok &= img.save(dir.join(&name)).is_ok();
|
||||
ok &= render(&mac, size, &dir.join(format!("figcraft-{size}.png")));
|
||||
if size <= 512 {
|
||||
let hd = dir.join(format!("hicolor/{size}x{size}/apps"));
|
||||
let _ = std::fs::create_dir_all(&hd);
|
||||
ok &= img.save(hd.join("ai.storyteller.figcraft.png")).is_ok();
|
||||
ok &= render(&tight, size, &hd.join("ai.storyteller.figcraft.png"));
|
||||
}
|
||||
}
|
||||
// Windows .ico (PNG-compressed entries).
|
||||
let mut ico = Vec::new();
|
||||
let sizes = [16u32, 32, 48, 256];
|
||||
let sizes = [16u32, 20, 24, 32, 40, 48, 64, 128, 256];
|
||||
let pngs: Vec<Vec<u8>> = sizes
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
let img = render(*s)?;
|
||||
let mut out = Vec::new();
|
||||
img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?;
|
||||
Some(out)
|
||||
let png = tmp.join(format!("ico-{s}.png"));
|
||||
render(&tight, *s, &png).then(|| std::fs::read(png).ok()).flatten()
|
||||
})
|
||||
.collect();
|
||||
ico.extend_from_slice(&[0, 0, 1, 0, pngs.len() as u8, 0]);
|
||||
ok &= pngs.len() == sizes.len();
|
||||
let mut ico = vec![0, 0, 1, 0, pngs.len() as u8, 0];
|
||||
let mut offset = 6 + 16 * pngs.len() as u32;
|
||||
for (s, png) in sizes.iter().zip(&pngs) {
|
||||
let b = if *s >= 256 { 0 } else { *s as u8 };
|
||||
|
|
@ -109,17 +71,10 @@ pub fn generate(root: &std::path::Path) -> bool {
|
|||
ico.extend_from_slice(png);
|
||||
}
|
||||
ok &= std::fs::write(dir.join("figcraft.ico"), ico).is_ok();
|
||||
// Scalable SVG (same artwork, written by FigCraft's SVG exporter).
|
||||
let tops: Vec<_> = doc.node(page).map(|p| p.children.clone()).unwrap_or_default();
|
||||
match figcraft_svg::export_svg(&doc, &tops, &Default::default()) {
|
||||
Some(svg) => {
|
||||
// The lighter trace keeps the scalable theme icon cheap to render.
|
||||
let sd = dir.join("hicolor/scalable/apps");
|
||||
let _ = std::fs::create_dir_all(&sd);
|
||||
ok &= std::fs::write(dir.join("figcraft.svg"), &svg).is_ok();
|
||||
ok &= std::fs::write(sd.join("ai.storyteller.figcraft.svg"), &svg).is_ok();
|
||||
}
|
||||
None => ok = false,
|
||||
}
|
||||
ok &= std::fs::copy(dir.join("figcraft-small.svg"), sd.join("ai.storyteller.figcraft.svg")).is_ok();
|
||||
eprintln!("icon: wrote {}", dir.display());
|
||||
ok
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
//! - `assets` every file under assets/ and docs/ has a row in ATTRIBUTION.md
|
||||
//! - `parity` menu-catalog coverage → docs/parity-checklist.md
|
||||
//! - `version` print the version, or `version set X.Y.Z`
|
||||
//! - `icon` regenerate the app icon PNGs/ICO from code (original artwork)
|
||||
//! - `icon` regenerate the app icon PNGs/ICO from assets/app-icon/figcraft.svg (needs resvg)
|
||||
//! - `wasm` check the core crates and web app for wasm32, and build the plugin SDK and examples
|
||||
//! - `plugins` format-check, lint, test and build the plugin SDK and example plugins (`plugins/`, a
|
||||
//! workspace of its own) for wasm32: `<target>/plugins/dist/<example>/` holds each manifest and
|
||||
|
|
|
|||