M14.3: the dragon app icon is the brand mark

The mark left of Home (and in the About box) is now the VectorCraft app icon,
the engraved dragon from assets/app-icon/ (128 px PNG, include_bytes!), decoded
once per context into a mipmapped texture and drawn at 22 pt with the icon's own
rounded corners. The old code-drawn gradient square with a pen nib is removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-05 02:00:05 +07:00
1 parent 3c856caa65
commit 512e96e7c8
6 files changed
+75 -45

No files matched your search

Generated
+1
View File
@@ -4330,6 +4330,7 @@ version = "0.1.1"
dependencies = [ dependencies = [
"egui", "egui",
"egui_extras", "egui_extras",
"image",
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
+1 -1
View File
@@ -31,7 +31,7 @@ artwork. Licence: see `LICENSE.txt`.
| `vectorcraft-macos-512.png` | tile with Apple margin, 512 px | runtime Dock icon (`apps/vectorcraft/src/main.rs`) | | `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.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`) | | `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/<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, the 128 one the brand mark in the app bar and About box (`crates/ui-egui/src/brand.rs`) | `packaging/linux/ai.storyteller.vectorcraft.desktop` |
| `hicolor/scalable/apps/ai.storyteller.vectorcraft.svg` | Linux scalable icon | as above | | `hicolor/scalable/apps/ai.storyteller.vectorcraft.svg` | Linux scalable icon | as above |
## How it reaches each OS ## How it reaches each OS
+1
View File
@@ -22,6 +22,7 @@ vectorcraft-pdf = { workspace = true }
vectorcraft-format = { workspace = true } vectorcraft-format = { workspace = true }
egui = { workspace = true } egui = { workspace = true }
egui_extras = { workspace = true } egui_extras = { workspace = true }
image = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
log = { workspace = true } log = { workspace = true }
+69 -41
View File
@@ -1,52 +1,80 @@
//! The VectorCraft brand mark (original artwork, drawn in code): a warm diagonal-gradient rounded //! The VectorCraft brand mark: the app icon (the engraved dragon, `assets/app-icon/`, see its
//! square with a white pen nib — the same design as the app icon rendered by `cargo xtask bundle`. //! README). The PNG carries the icon's rounded corners; it is decoded once per context into a
//! mipmapped texture, so it stays crisp from the 22 pt app-bar mark to the About box.
use egui::epaint::{Mesh, Vertex, WHITE_UV}; use egui::{Color32, Context, Id, Rect, TextureHandle, TextureOptions, Ui, pos2};
use egui::{Color32, Painter, Pos2, Rect, Shape, pos2};
const TOP: Color32 = Color32::from_rgb(0xff, 0x9a, 0x3c); /// 128 px: sharp at 44 pt on a 2× display, and the mipmaps keep 22 pt clean.
const BOTTOM: Color32 = Color32::from_rgb(0xd4, 0x14, 0x5a); const ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/128x128/apps/ai.storyteller.vectorcraft.png");
fn lerp(a: Color32, b: Color32, t: f32) -> Color32 { /// The icon's pixels (one transparent pixel if it couldn't be decoded, which a test rules out).
let m = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t).round() as u8; fn decode() -> egui::ColorImage {
Color32::from_rgb(m(a.r(), b.r()), m(a.g(), b.g()), m(a.b(), b.b())) match image::load_from_memory_with_format(ICON_PNG, image::ImageFormat::Png) {
Ok(img) => {
let img = img.to_rgba8();
egui::ColorImage::from_rgba_unmultiplied([img.width() as usize, img.height() as usize], img.as_raw())
}
Err(_) => egui::ColorImage::filled([1, 1], Color32::TRANSPARENT),
}
}
/// The icon texture, uploaded on first use and kept in the context.
fn texture(ctx: &Context) -> TextureHandle {
let id = Id::new("vectorcraft-brand-mark");
if let Some(tex) = ctx.data(|d| d.get_temp::<TextureHandle>(id)) {
return tex;
}
let options = TextureOptions { mipmap_mode: Some(egui::TextureFilter::Linear), ..TextureOptions::LINEAR };
let tex = ctx.load_texture("vectorcraft-brand-mark", decode(), options);
ctx.data_mut(|d| d.insert_temp(id, tex.clone()));
tex
} }
/// Paint the mark into `r` (square). /// Paint the mark into `r` (square).
pub fn paint_mark(p: &Painter, r: Rect) { pub fn paint_mark(ui: &Ui, r: Rect) {
let rad = r.width() * 0.22; let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
// Rounded-square outline as a fan so each vertex gets its diagonal-gradient colour. ui.painter().image(texture(ui.ctx()).id(), r, uv, Color32::WHITE);
let mut outline = vec![];
for (c, a0) in [
(pos2(r.max.x - rad, r.min.y + rad), -90.0f32),
(pos2(r.max.x - rad, r.max.y - rad), 0.0),
(pos2(r.min.x + rad, r.max.y - rad), 90.0),
(pos2(r.min.x + rad, r.min.y + rad), 180.0),
] {
for i in 0..=6 {
let a = (a0 + 90.0 * i as f32 / 6.0).to_radians();
outline.push(c + egui::vec2(a.cos(), a.sin()) * rad);
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::VectorcraftApp;
#[test]
fn the_icon_decodes_to_a_rounded_128_px_tile() {
let img = decode();
assert_eq!(img.size, [128, 128]);
// Rounded corners: the corner pixel is transparent, the centre opaque.
assert_eq!(img.pixels[0].a(), 0);
assert_eq!(img.pixels[64 * 128 + 64].a(), 255);
} }
let colour = |q: Pos2| {
let t = ((q.x - r.min.x) + (q.y - r.min.y)) / (r.width() + r.height()); #[test]
lerp(TOP, BOTTOM, t.clamp(0.0, 1.0)) fn the_app_bar_paints_the_icon_from_one_cached_texture() {
let ctx = Context::default();
crate::theme::install_fonts(&ctx);
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
let mut frame = || {
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| crate::chrome::app_bar(&mut app, ui));
let uploads = out.textures_delta.set.values().flat_map(|d| d.iter()).filter(|d| d.image.size() == [128, 128]).count();
out.textures_delta.clear();
let marks: Vec<Rect> = out
.shapes
.iter()
.filter_map(|c| match &c.shape {
egui::Shape::Mesh(m) if m.texture_id == texture(&ctx).id() => Some(m.calc_bounds()),
_ => None,
})
.collect();
(uploads, marks)
}; };
let mut mesh = Mesh::default(); let (uploads, marks) = frame();
mesh.vertices.push(Vertex { pos: r.center(), uv: WHITE_UV, color: colour(r.center()) }); assert_eq!(uploads, 1);
for q in &outline { assert_eq!(marks.len(), 1);
mesh.vertices.push(Vertex { pos: *q, uv: WHITE_UV, color: colour(*q) }); assert_eq!(marks[0].size(), egui::vec2(22.0, 22.0));
// Later frames reuse the texture.
let (uploads, marks) = frame();
assert_eq!((uploads, marks.len()), (0, 1));
} }
let n = outline.len() as u32;
for i in 0..n {
mesh.add_triangle(0, 1 + i, 1 + (i + 1) % n);
}
p.add(Shape::mesh(mesh));
// Pen nib (same proportions as the app icon, 1024-unit design space).
let s = r.width() / 1024.0;
let at = |x: f32, y: f32| pos2(r.min.x + x * s, r.min.y + y * s);
let nib = vec![at(512.0, 770.0), at(352.0, 540.0), at(417.0, 290.0), at(607.0, 290.0), at(672.0, 540.0)];
p.add(Shape::convex_polygon(nib, Color32::WHITE, egui::Stroke::NONE));
p.circle_filled(at(512.0, 490.0), 40.0 * s, colour(at(512.0, 490.0)));
p.line_segment([at(512.0, 525.0), at(512.0, 765.0)], egui::Stroke::new((18.0 * s).max(1.0), colour(at(512.0, 650.0))));
} }
+2 -2
View File
@@ -23,9 +23,9 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
titlebar::drag_area(ui, ui.max_rect()); titlebar::drag_area(ui, ui.max_rect());
} }
ui.horizontal_centered(|ui| { ui.horizontal_centered(|ui| {
// Brand mark (our own artwork, matches the app icon). // Brand mark: the app icon.
let (r, _) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::hover()); let (r, _) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::hover());
crate::brand::paint_mark(ui.painter(), r); crate::brand::paint_mark(ui, r);
ui.add_space(4.0); ui.add_space(4.0);
if widgets::icon_button(ui, "house", "Home", false, 24.0).clicked() { if widgets::icon_button(ui, "house", "Home", false, 24.0).clicked() {
app.ui.dialog = Some(crate::state::Dialog::new("newDocument", json!({"preset": "Letter", "width": "612 pt", "height": "792 pt", "units": "Points", "artboards": 1, "colorMode": "RGB", "name": "Untitled-1"}))); app.ui.dialog = Some(crate::state::Dialog::new("newDocument", json!({"preset": "Letter", "width": "612 pt", "height": "792 pt", "units": "Points", "artboards": 1, "colorMode": "RGB", "name": "Untitled-1"})));
+1 -1
View File
@@ -19,7 +19,7 @@ pub(super) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
ui.set_width(380.0); ui.set_width(380.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
let (r, _) = ui.allocate_exact_size(egui::vec2(44.0, 44.0), egui::Sense::hover()); let (r, _) = ui.allocate_exact_size(egui::vec2(44.0, 44.0), egui::Sense::hover());
crate::brand::paint_mark(ui.painter(), r); crate::brand::paint_mark(ui, r);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.label(egui::RichText::new("VectorCraft").font(theme::semibold(22.0))); ui.label(egui::RichText::new("VectorCraft").font(theme::semibold(22.0)));
ui.label(format!("Version {} — open-source vector illustration in pure Rust.", env!("CARGO_PKG_VERSION"))); ui.label(format!("Version {} — open-source vector illustration in pure Rust.", env!("CARGO_PKG_VERSION")));