Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified OpenBitFun-Installer/src-tauri/icons/openbitfun-app-icon.icns
Binary file not shown.
Binary file modified OpenBitFun-Installer/src-tauri/icons/openbitfun-app-icon.ico
Binary file not shown.
Binary file modified OpenBitFun-Installer/src-tauri/icons/openbitfun-app-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified OpenBitFun-Installer/src/assets/openbitfun-app-icon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified OpenBitFun-Installer/src/assets/openbitfun-app-icon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified OpenBitFun-Installer/src/assets/openbitfun-app-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 21 additions & 24 deletions assets/brand/README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,35 @@
# OpenBitFun application brand assets

`source/openbitfun-mark.svg` is the transparent vector master for the fine-line
Logo. Its fifteen rounded hexagonal contours match the About dialog's static
geometry. Motion and moving highlights are intentionally absent from icon files.
The SVG uses `currentColor`, with a light default for dark backgrounds.
The outer contour is opaque and heavier than the interior filaments so the
white rim remains legible when the master is reduced by an application host.
`source/openbitfun-mark.svg` is the transparent vector master for the current
fine-line Logo used by startup and in-product brand surfaces. Its fifteen rounded
hexagonal contours match the About dialog's static geometry. Motion and moving
highlights are intentionally absent from static files. The SVG uses
`currentColor`, with a light default for dark backgrounds.

The generator also maintains the existing transparent PNG paths:

- `openbitfun-mark-dark.png` is the dark mark for light surfaces.
- `openbitfun-mark-light.png` is the light mark for dark surfaces.

The application icon uses the light mark on the existing black rounded-square
background, with transparent corners. The 1024 px master is rendered directly
from the vector before generating Windows ICO and macOS ICNS containers.
`source/openbitfun-app-mark.png` preserves the originally submitted silver
hexagonal application mark. Application, window, taskbar, Dock, tray, browser,
installer, and mobile launcher icons use this artwork on the black rounded-square
background with transparent corners. It is intentionally independent from the
current startup Logo, so regenerating either family cannot replace the other.

`exports/` contains the SVG, ICO, ICNS, and PNGs at 16, 24, 32, 48, 64, 96,
128, 192, 256, 512, 1024, and 2048 px. Each PNG size includes a dark transparent
mark, a light transparent mark, and the application icon. PNG exports are
rendered directly from the SVG rather than enlarged from a small bitmap.
At 16–24 px, 32–48 px, and 64–96 px, PNGs use 3, 5, and 7 contours respectively
with a minimum 1.35 px opaque outer rim, a 0.85 px inner contour, and 0.65 px
interior filaments. Larger PNGs and the SVG retain all 15 contours.
Windows ICO frames and Linux icons use the same size-specific renders; macOS
PNG representations do too. Tauri encodes the legacy 16/32 px ICNS representations
from their corresponding optical renders.

Browser entry points select explicit 16/32 px favicons. The macOS menu bar uses
`src/apps/desktop/icons/openbitfun-tray-template.png`, a transparent 32 px template
drawn for a 16 pt display, so the system can tint it for light and dark menus.
Windows and Linux trays use the 32 px application icon.

Run `pnpm run generate-brand-assets` after changing the SVG master. The
mark, a light transparent mark, and the application icon. Fine-line mark exports
are rendered directly from the SVG with size-specific optical treatments at
small sizes. Application icons are resized from the preserved application artwork.
Windows ICO frames, Linux icons, and macOS PNG representations use the matching
size-specific application exports. Tauri encodes the legacy 16/32 px ICNS
representations from the same artwork.

Browser entry points select explicit 16/32 px favicons. The system tray reuses
the configured application/window icon on every desktop platform.

Run `pnpm run generate-brand-assets` after changing either source master. The
generator is the single owner of the derived desktop, web, installer, Android,
iOS, and HarmonyOS files.

Expand Down
Binary file modified assets/brand/exports/openbitfun-app-icon-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-2048.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon-96.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/brand/exports/openbitfun-app-icon.icns
Binary file not shown.
Binary file modified assets/brand/exports/openbitfun-app-icon.ico
Binary file not shown.
Binary file added assets/brand/source/openbitfun-app-mark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 18 additions & 14 deletions scripts/generate-brand-assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.resolve(SCRIPT_DIR, '..');
const SOURCE_DIR = path.join(ROOT_DIR, 'assets', 'brand', 'source');
const SOURCE_SVG = path.join(SOURCE_DIR, 'openbitfun-mark.svg');
const SOURCE_APP_MARK = path.join(SOURCE_DIR, 'openbitfun-app-mark.png');
const SOURCE_MARKS = {
dark: path.join(SOURCE_DIR, 'openbitfun-mark-dark.png'),
light: path.join(SOURCE_DIR, 'openbitfun-mark-light.png'),
Expand Down Expand Up @@ -37,6 +38,7 @@ const LEGACY_APPLICATION_ASSETS = [
'src/apps/desktop/icons/icon.png',
'src/apps/desktop/icons/icon.ico',
'src/apps/desktop/icons/icon.icns',
'src/apps/desktop/icons/openbitfun-tray-template.png',
'src/apps/desktop/icons/Square30x30Logo.png',
'src/apps/desktop/icons/Square44x44Logo.png',
'src/apps/desktop/icons/Square71x71Logo.png',
Expand Down Expand Up @@ -79,6 +81,14 @@ async function writePng(filePath, buffer) {
await writeFile(filePath, buffer);
}

async function normalizePng(input) {
return sharp(input)
.ensureAlpha()
.resize({ width: BRAND_SIZE, height: BRAND_SIZE, fit: 'contain', kernel: 'lanczos3' })
.png({ compressionLevel: 9, adaptiveFiltering: true })
.toBuffer();
}

async function renderMark(svg, size, tone, opticalSize = size) {
// At favicon sizes, fifteen subpixel strokes disappear. Keep the same
// silhouette with fewer filaments. The opaque outer rim must survive
Expand Down Expand Up @@ -194,7 +204,7 @@ async function generateTauriContainers(applicationIcon, renderIcon) {
);

// Keep Tauri's directory metadata, but rasterize each Windows size from
// the vector with its optical treatment instead of shrinking one bitmap.
// the canonical application artwork so the container matches the exports.
const icoTemplate = await readFile(path.join(tempDir, 'icon.ico'));
const frameCount = icoTemplate.readUInt16LE(4);
const icoDirectory = Buffer.from(icoTemplate.subarray(0, 6 + frameCount * 16));
Expand All @@ -210,8 +220,8 @@ async function generateTauriContainers(applicationIcon, renderIcon) {
icoOffset += png.length;
}

// Let Tauri encode legacy RGB/mask chunks from each optical render;
// modern PNG representations also use direct vector renders.
// Let Tauri encode legacy RGB/mask chunks from the same application
// artwork; modern PNG representations use the matching exported sizes.
const icnsTemplate = await readFile(path.join(tempDir, 'icon.icns'));
const legacyChunks = new Map();
for (const [size, types] of [[16, ['is32', 's8mk']], [32, ['il32', 'l8mk']]]) {
Expand Down Expand Up @@ -264,17 +274,16 @@ async function removeLegacyApplicationAssets() {

async function generateBrandAssets() {
const svg = await readFile(SOURCE_SVG, 'utf8');
const [darkMark, lightMark] = await Promise.all([
const [darkMark, lightMark, applicationSourceMark] = await Promise.all([
renderMark(svg, BRAND_SIZE, '#202020'),
renderMark(svg, BRAND_SIZE, '#e8e8e8'),
normalizePng(SOURCE_APP_MARK),
]);
await writePng(SOURCE_MARKS.dark, darkMark);
await writePng(SOURCE_MARKS.light, lightMark);
const applicationMark = await createApplicationMark(lightMark);
const applicationMark = await createApplicationMark(applicationSourceMark);
const applicationIcon = await createApplicationIcon(applicationMark);
const applicationIconLarge = await createApplicationIcon(
await createApplicationMark(await renderMark(svg, APP_ICON_SIZE, '#e8e8e8')),
);
const applicationIconLarge = await resizePng(applicationIcon, APP_ICON_SIZE);
const darkMarkSmall = await renderMark(svg, 128, '#202020');
const lightMarkSmall = await renderMark(svg, 128, '#e8e8e8');
const iconCache = new Map([
Expand All @@ -283,8 +292,7 @@ async function generateBrandAssets() {
]);
const renderIcon = size => {
if (!iconCache.has(size)) {
iconCache.set(size, renderMark(svg, size, '#e8e8e8')
.then(createApplicationMark).then(createApplicationIcon));
iconCache.set(size, resizePng(applicationIcon, size));
}
return iconCache.get(size);
};
Expand Down Expand Up @@ -315,10 +323,6 @@ async function generateBrandAssets() {

const desktopIconDir = outputPath('src', 'apps', 'desktop', 'icons');
await writePng(path.join(desktopIconDir, 'openbitfun-app-icon.png'), applicationIconLarge);
// A 32 px Retina template represents a 16 pt menu-bar mark. Its alpha
// silhouette is tinted by macOS, including light menus and selected states.
await writePng(path.join(desktopIconDir, 'openbitfun-tray-template.png'),
await renderMark(svg, 32, '#000000', 16));
await writePng(path.join(desktopIconDir, 'openbitfun-app-icon.ico'), tauriContainers.ico);
await writePng(path.join(desktopIconDir, 'openbitfun-app-icon.icns'), tauriContainers.icns);
for (const size of DESKTOP_HICOLOR_SIZES) {
Expand Down
40 changes: 28 additions & 12 deletions scripts/generate-brand-assets.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readFileSync, readdirSync } from 'node:fs';
import test from 'node:test';
import sharp from 'sharp';
Expand Down Expand Up @@ -51,6 +52,27 @@ test('generated macOS icons use the canonical ICNS layout', () => {
assert.ok(desktop.equals(installer), 'desktop and installer ICNS files differ');
});

test('application icons preserve the submitted artwork independently from the startup Logo', async () => {
const applicationMark = readFileSync('assets/brand/source/openbitfun-app-mark.png');
const startupMark = readFileSync('assets/brand/source/openbitfun-mark-light.png');
const generatedIcon = readFileSync('assets/brand/exports/openbitfun-app-icon-512.png');

assert.equal(
createHash('sha256').update(applicationMark).digest('hex'),
'6cda0b01d037ef552690d210a1e5adfa807a806d685a49f46d110b1e3765b5a0',
);
assert.equal(
createHash('sha256').update(generatedIcon).digest('hex'),
'df460c4a43d9a68e15bee7b23b0bb68f7b9e70c26c68158ef82e2d4fd9e6288f',
);
assert.notDeepEqual(applicationMark, startupMark);

const metadata = await sharp(applicationMark).metadata();
assert.equal(metadata.width, 512);
assert.equal(metadata.height, 512);
assert.equal(metadata.hasAlpha, true);
});

test('brand exports provide decodable transparent PNGs at every advertised size', async () => {
const sizes = [16, 24, 32, 48, 64, 96, 128, 192, 256, 512, 1024, 2048];
for (const size of sizes) {
Expand Down Expand Up @@ -112,20 +134,14 @@ test('small icons retain a bright rim around the entire silhouette', async () =>
}
});

test('menu-bar template remains transparent and visible at its logical size', async () => {
const { data, info } = await sharp('src/apps/desktop/icons/openbitfun-tray-template.png')
.resize(16, 16).raw().toBuffer({ resolveWithObject: true });
assert.equal(info.channels, 4);
assert.equal(data[3], 0, 'template must not include the app background');
assert.equal(data[(8 * 16 + 8) * 4 + 3], 0, 'center must remain open');
let opaquePixels = 0;
for (let offset = 3; offset < data.length; offset += 4) {
if (data[offset] >= 200) opaquePixels++;
}
assert.ok(opaquePixels >= 24, 'template loses its silhouette at menu-bar size');
test('desktop tray reuses the configured application icon', () => {
const source = readFileSync('src/apps/desktop/src/tray.rs', 'utf8');
assert.match(source, /default_window_icon\(\)/);
assert.doesNotMatch(source, /openbitfun-tray-template/);
assert.doesNotMatch(source, /icon_as_template/);
});

test('browser entry points reference generated optical favicons', () => {
test('browser entry points reference generated application favicons', () => {
for (const [htmlPath, assetDir] of [
['src/web-ui/index.html', 'src/web-ui/public/brand'],
['src/mobile-web/index.html', 'src/mobile-web/public/brand'],
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/apps/desktop/icons/hicolor/16x16/apps/openbitfun-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/apps/desktop/icons/hicolor/32x32/apps/openbitfun-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/apps/desktop/icons/hicolor/48x48/apps/openbitfun-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/apps/desktop/icons/hicolor/64x64/apps/openbitfun-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/apps/desktop/icons/openbitfun-app-icon.icns
Binary file not shown.
Binary file modified src/apps/desktop/icons/openbitfun-app-icon.ico
Binary file not shown.
Binary file modified src/apps/desktop/icons/openbitfun-app-icon.png
Binary file removed src/apps/desktop/icons/openbitfun-tray-template.png
Diff not rendered.
9 changes: 4 additions & 5 deletions src/apps/desktop/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,15 @@ pub fn setup_tray(
startup_trace.record_elapsed_step(TRAY_TRACE_CATEGORY, "setup_tray.menu", step_started);

let step_started = Instant::now();
#[cfg(target_os = "macos")]
let icon = tauri::include_image!("icons/openbitfun-tray-template.png");
#[cfg(not(target_os = "macos"))]
let icon = tauri::include_image!("icons/hicolor/32x32/apps/openbitfun-desktop.png");
let icon = app
.default_window_icon()
.ok_or("No default window icon")?
.clone();
startup_trace.record_elapsed_step(TRAY_TRACE_CATEGORY, "setup_tray.icon", step_started);

let step_started = Instant::now();
let tray = TrayIconBuilder::new()
.icon(icon)
.icon_as_template(cfg!(target_os = "macos"))
.menu(&initial_menu)
.tooltip("OpenBitFun")
.on_menu_event(|app, event| {
Expand Down
Binary file modified src/apps/relay-server/static/brand/openbitfun-app-icon-16.png
Binary file modified src/apps/relay-server/static/brand/openbitfun-app-icon-32.png
Binary file modified src/apps/relay-server/static/brand/openbitfun-app-icon.png
Binary file modified src/mobile-web/public/brand/openbitfun-app-icon-16.png
Binary file modified src/mobile-web/public/brand/openbitfun-app-icon-32.png
Binary file modified src/mobile-web/public/brand/openbitfun-app-icon.png
Binary file modified src/web-ui/public/brand/openbitfun-app-icon-16.png
Binary file modified src/web-ui/public/brand/openbitfun-app-icon-32.png
Binary file modified src/web-ui/public/brand/openbitfun-app-icon.png
Loading