mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
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:
1 parent
3c856caa65
commit
512e96e7c8
6 files changed
+76
-46
No files matched your search
Generated
+1
@@ -4330,6 +4330,7 @@ version = "0.1.1"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_extras",
|
||||
"image",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -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.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/<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 |
|
||||
|
||||
## How it reaches each OS
|
||||
|
||||
@@ -22,6 +22,7 @@ vectorcraft-pdf = { workspace = true }
|
||||
vectorcraft-format = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
image = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
+70
-42
@@ -1,52 +1,80 @@
|
||||
//! The VectorCraft brand mark (original artwork, drawn in code): a warm diagonal-gradient rounded
|
||||
//! square with a white pen nib — the same design as the app icon rendered by `cargo xtask bundle`.
|
||||
//! The VectorCraft brand mark: the app icon (the engraved dragon, `assets/app-icon/`, see its
|
||||
//! 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, Painter, Pos2, Rect, Shape, pos2};
|
||||
use egui::{Color32, Context, Id, Rect, TextureHandle, TextureOptions, Ui, pos2};
|
||||
|
||||
const TOP: Color32 = Color32::from_rgb(0xff, 0x9a, 0x3c);
|
||||
const BOTTOM: Color32 = Color32::from_rgb(0xd4, 0x14, 0x5a);
|
||||
/// 128 px: sharp at 44 pt on a 2× display, and the mipmaps keep 22 pt clean.
|
||||
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 {
|
||||
let m = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t).round() as u8;
|
||||
Color32::from_rgb(m(a.r(), b.r()), m(a.g(), b.g()), m(a.b(), b.b()))
|
||||
/// The icon's pixels (one transparent pixel if it couldn't be decoded, which a test rules out).
|
||||
fn decode() -> egui::ColorImage {
|
||||
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).
|
||||
pub fn paint_mark(p: &Painter, r: Rect) {
|
||||
let rad = r.width() * 0.22;
|
||||
// Rounded-square outline as a fan so each vertex gets its diagonal-gradient colour.
|
||||
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);
|
||||
pub fn paint_mark(ui: &Ui, r: Rect) {
|
||||
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
||||
ui.painter().image(texture(ui.ctx()).id(), r, uv, Color32::WHITE);
|
||||
}
|
||||
|
||||
#[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());
|
||||
lerp(TOP, BOTTOM, t.clamp(0.0, 1.0))
|
||||
|
||||
#[test]
|
||||
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();
|
||||
mesh.vertices.push(Vertex { pos: r.center(), uv: WHITE_UV, color: colour(r.center()) });
|
||||
for q in &outline {
|
||||
mesh.vertices.push(Vertex { pos: *q, uv: WHITE_UV, color: colour(*q) });
|
||||
}
|
||||
let n = outline.len() as u32;
|
||||
for i in 0..n {
|
||||
mesh.add_triangle(0, 1 + i, 1 + (i + 1) % n);
|
||||
let (uploads, marks) = frame();
|
||||
assert_eq!(uploads, 1);
|
||||
assert_eq!(marks.len(), 1);
|
||||
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));
|
||||
}
|
||||
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))));
|
||||
}
|
||||
@@ -23,9 +23,9 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
titlebar::drag_area(ui, ui.max_rect());
|
||||
}
|
||||
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());
|
||||
crate::brand::paint_mark(ui.painter(), r);
|
||||
crate::brand::paint_mark(ui, r);
|
||||
ui.add_space(4.0);
|
||||
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"})));
|
||||
|
||||
@@ -19,7 +19,7 @@ pub(super) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
ui.set_width(380.0);
|
||||
ui.horizontal(|ui| {
|
||||
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.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")));
|
||||
|
||||
Reference in new issue
Block a user