From 759b59d61d5c3040b007f48ffab10175aab58a2a Mon Sep 17 00:00:00 2001 From: Kawa Date: Fri, 28 Aug 2026 13:53:23 +0200 Subject: [PATCH] feat: rebrand app and packaging icons to new Motionity logo Swap the small icon mark, README banner, packaged app icon, and favicon over to the new brand assets. make-icon.cjs and the new make-favicon.cjs re-derive their bar geometry/colours from src/assets/logo.svg instead of the old white-bars mark. Co-Authored-By: Claude Sonnet 5 --- README.md | 4 +- package.json | 1 + scripts/make-favicon.cjs | 109 +++++++++++++++++++++++++++++++++++++++ scripts/make-icon.cjs | 23 ++++----- src/assets/logo-full.svg | 1 + src/assets/logo.svg | 6 +-- src/favicon.ico | Bin 4286 -> 4286 bytes 7 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 scripts/make-favicon.cjs create mode 100644 src/assets/logo-full.svg diff --git a/README.md b/README.md index 98ab685..b7c71d9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@
-Motionity - -# Motionity +Motionity **Web-based motion graphics editor** — keyframing, masking, filters, text animations. A free alternative to After Effects and Canva, running entirely in the browser. diff --git a/package.json b/package.json index 6dbacc1..54f1f3a 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "scripts": { "vendor": "node scripts/vendor.mjs", "icons": "node scripts/make-icon.cjs", + "favicon": "node scripts/make-favicon.cjs", "start": "node scripts/server.cjs", "dev": "electron .", "dist:win": "npm run vendor && npm run icons && electron-builder --win", diff --git a/scripts/make-favicon.cjs b/scripts/make-favicon.cjs new file mode 100644 index 0000000..d358b0d --- /dev/null +++ b/scripts/make-favicon.cjs @@ -0,0 +1,109 @@ +#!/usr/bin/env node +// Renders src/favicon.ico (32x32) from the same geometry as +// src/assets/logo.svg: three coloured rounded bars on the app's dark background. +// +// Hand-rolled so packaging needs no image toolchain (no ImageMagick, no sharp). + +const { writeFileSync } = require('node:fs'); +const { resolve } = require('node:path'); + +const SIZE = 32; +const SS = 8; // supersampling factor, for antialiased edges at small size +const BG = [0x14, 0x16, 0x29]; +const BG_RADIUS = 6; // squircle-ish corner on the icon plate + +// viewBox="0 0 157 183" in src/assets/logo.svg +const VIEW = { w: 157, h: 183 }; +const BARS = [ + { x: 79, y: 0, w: 79, h: 47, r: 23.5, fg: [0x75, 0x4b, 0xff] }, + { x: 0, y: 68, w: 157, h: 47, r: 23.5, fg: [0x60, 0x2f, 0xff] }, + { x: 0, y: 136, w: 79, h: 47, r: 23.5, fg: [0x06, 0x93, 0xc0] }, +]; + +function insideRoundedRect(px, py, { x, y, w, h, r }) { + const dx = Math.max(x + r - px, 0, px - (x + w - r)); + const dy = Math.max(y + r - py, 0, py - (y + h - r)); + return dx * dx + dy * dy <= r * r; +} + +// The logo occupies 62% of the plate, centred. +const scale = (SIZE * 0.62) / VIEW.h; +const offsetX = (SIZE - VIEW.w * scale) / 2; +const offsetY = (SIZE - VIEW.h * scale) / 2; + +const plate = { x: 0, y: 0, w: SIZE, h: SIZE, r: BG_RADIUS }; + +// BGRA pixels, row-major, top-to-bottom. +const pixels = Buffer.alloc(SIZE * SIZE * 4); +for (let py = 0; py < SIZE; py++) { + for (let px = 0; px < SIZE; px++) { + let plateHits = 0; + const colorSum = [0, 0, 0]; + for (let sy = 0; sy < SS; sy++) { + for (let sx = 0; sx < SS; sx++) { + const fx = px + (sx + 0.5) / SS; + const fy = py + (sy + 0.5) / SS; + if (!insideRoundedRect(fx, fy, plate)) continue; + plateHits++; + const lx = (fx - offsetX) / scale; + const ly = (fy - offsetY) / scale; + const bar = BARS.find((b) => insideRoundedRect(lx, ly, b)); + const pixel = bar ? bar.fg : BG; + for (let c = 0; c < 3; c++) colorSum[c] += pixel[c]; + } + } + const total = SS * SS; + const alpha = Math.round((plateHits / total) * 255); + const o = (py * SIZE + px) * 4; + const r = plateHits ? Math.round(colorSum[0] / plateHits) : 0; + const g = plateHits ? Math.round(colorSum[1] / plateHits) : 0; + const b = plateHits ? Math.round(colorSum[2] / plateHits) : 0; + pixels[o] = b; + pixels[o + 1] = g; + pixels[o + 2] = r; + pixels[o + 3] = alpha; + } +} + +// Pack into a single-image .ico: ICONDIR + ICONDIRENTRY + BITMAPINFOHEADER +// + 32bpp XOR data (bottom-up) + 1bpp AND mask (bottom-up, all-zero since +// the XOR data already carries a full alpha channel). +const xor = Buffer.alloc(SIZE * SIZE * 4); +for (let py = 0; py < SIZE; py++) { + const srcRow = py * SIZE * 4; + const dstRow = (SIZE - 1 - py) * SIZE * 4; // bottom-up + pixels.copy(xor, dstRow, srcRow, srcRow + SIZE * 4); +} +const andMaskRowBytes = Math.ceil(SIZE / 32) * 4; // 1bpp, padded to 4 bytes +const and = Buffer.alloc(andMaskRowBytes * SIZE, 0); + +const dib = Buffer.alloc(40); +dib.writeUInt32LE(40, 0); // biSize +dib.writeInt32LE(SIZE, 4); // biWidth +dib.writeInt32LE(SIZE * 2, 8); // biHeight (XOR + AND) +dib.writeUInt16LE(1, 12); // biPlanes +dib.writeUInt16LE(32, 14); // biBitCount +dib.writeUInt32LE(0, 16); // biCompression: BI_RGB + +const imageData = Buffer.concat([dib, xor, and]); + +const iconDir = Buffer.alloc(6); +iconDir.writeUInt16LE(0, 0); // reserved +iconDir.writeUInt16LE(1, 2); // type: icon +iconDir.writeUInt16LE(1, 4); // image count + +const entry = Buffer.alloc(16); +entry[0] = SIZE; // width +entry[1] = SIZE; // height +entry[2] = 0; // color count +entry[3] = 0; // reserved +entry.writeUInt16LE(1, 4); // planes +entry.writeUInt16LE(32, 6); // bit count +entry.writeUInt32LE(imageData.length, 8); // bytes in resource +entry.writeUInt32LE(6 + 16, 12); // image offset + +const ico = Buffer.concat([iconDir, entry, imageData]); + +const dest = resolve(__dirname, '..', 'src', 'favicon.ico'); +writeFileSync(dest, ico); +console.log(`Wrote ${dest} (${SIZE}x${SIZE}, ${Math.round(ico.length / 1024)} KB)`); diff --git a/scripts/make-icon.cjs b/scripts/make-icon.cjs index ae2f174..baa90ee 100644 --- a/scripts/make-icon.cjs +++ b/scripts/make-icon.cjs @@ -1,6 +1,6 @@ #!/usr/bin/env node // Renders build/icon.png (512x512) from the same geometry as -// src/assets/logo.svg: three white rounded bars on the app's dark background. +// src/assets/logo.svg: three coloured rounded bars on the app's dark background. // // Hand-rolled so packaging needs no image toolchain (no ImageMagick, no sharp). // electron-builder derives the Windows .ico and the Linux icon set from it. @@ -12,15 +12,14 @@ const { join, resolve } = require('node:path'); const SIZE = 512; const SS = 4; // supersampling factor, for antialiased edges const BG = [0x14, 0x16, 0x29]; -const FG = [0xff, 0xff, 0xff]; const BG_RADIUS = 96; // squircle-ish corner on the icon plate -// viewBox="0 0 24 19" in src/assets/logo.svg -const VIEW = { w: 24, h: 19 }; +// viewBox="0 0 157 183" in src/assets/logo.svg +const VIEW = { w: 157, h: 183 }; const BARS = [ - { x: 17.3193, y: 8.36011, w: 6.08, h: 10.64, r: 3.04 }, - { x: 8.95996, y: 0, w: 6.08, h: 19, r: 3.04 }, - { x: 0.599609, y: 0, w: 6.08, h: 19, r: 3.04 }, + { x: 79, y: 0, w: 79, h: 47, r: 23.5, fg: [0x75, 0x4b, 0xff] }, + { x: 0, y: 68, w: 157, h: 47, r: 23.5, fg: [0x60, 0x2f, 0xff] }, + { x: 0, y: 136, w: 79, h: 47, r: 23.5, fg: [0x06, 0x93, 0xc0] }, ]; function insideRoundedRect(px, py, { x, y, w, h, r }) { @@ -43,7 +42,7 @@ for (let py = 0; py < SIZE; py++) { raw[rowStart] = 0; for (let px = 0; px < SIZE; px++) { let plateHits = 0; - let barHits = 0; + const colorSum = [0, 0, 0]; for (let sy = 0; sy < SS; sy++) { for (let sx = 0; sx < SS; sx++) { const fx = px + (sx + 0.5) / SS; @@ -52,16 +51,16 @@ for (let py = 0; py < SIZE; py++) { plateHits++; const lx = (fx - offsetX) / scale; const ly = (fy - offsetY) / scale; - if (BARS.some((bar) => insideRoundedRect(lx, ly, bar))) barHits++; + const bar = BARS.find((b) => insideRoundedRect(lx, ly, b)); + const pixel = bar ? bar.fg : BG; + for (let c = 0; c < 3; c++) colorSum[c] += pixel[c]; } } const total = SS * SS; const alpha = Math.round((plateHits / total) * 255); - // Blend bar coverage over the plate colour; alpha carries the plate edge. - const mix = plateHits ? barHits / plateHits : 0; const o = rowStart + 1 + px * 4; for (let c = 0; c < 3; c++) { - raw[o + c] = Math.round(BG[c] + (FG[c] - BG[c]) * mix); + raw[o + c] = plateHits ? Math.round(colorSum[c] / plateHits) : 0; } raw[o + 3] = alpha; } diff --git a/src/assets/logo-full.svg b/src/assets/logo-full.svg new file mode 100644 index 0000000..3b3956c --- /dev/null +++ b/src/assets/logo-full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/logo.svg b/src/assets/logo.svg index c22b359..d324643 100644 --- a/src/assets/logo.svg +++ b/src/assets/logo.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/favicon.ico b/src/favicon.ico index 55c1f42118d97f0e198788b66510ce1fec705896..1592f5a328c4cdf80cb504c4911e8f2cac2ac11e 100644 GIT binary patch literal 4286 zcmZQzU<5)11qKkwutI==L5zWcK?8_^LJST-3=$^_=Ca?5C$IYdg=^OT z|K6n-_^?gXe*+0|n!4kHJ+N%RVmC~zOV5F(_J8uo$7}y;Gk==eztu7Xul>aurZlzR zSVHnY$PG_iGEwk7=d}O5`i{idO|T6}N_r$wEv!#Oj5}$e2G$>=h1tZI3GNF5!)(C% gU(i0_fb~CN{a|1i!1{u;>J!1z8b<#c=7xcw0ZoDCE&u=k literal 4286 zcmeH}%}Z2K7>Ca|=Hy3en6@d$RaT2Ik{Ajq0$l{6Mhk1#wvDzDqJJQ2S<%i-3qy+f z4~(EiMIU4;K{#z(6j~9APi&zy7K`JMMY_uM%j#$@<(bQpb` z@n&P%j4}HGmpKLUhsJL$_3NxV((iw127(|k5gAI0cZi$Rh4Fm>qj)uxLvSA+!%Zka z7Q${Sp$;y>1U!K|&<%F{wrs$69v*<=-GHN@XN!lSI#j_Zd;)7fsLaA~uraq$zXD&t z+Ak_k;TTw5j{0Tz467hhyytKNLY)Q?)sIlV1fSi;*$C3=Ip2WK?&53%=?=kL@YyY# znj3dc&(U_D-R(C;V~_N37M8(h8doPscN%_z&ooyD!OgGtwBQrt`v`eZ+(B3ZpH1UD z0MhmN1d;DGWfvsPUqqXi9=0O?E8?X2|KIt4!TZ~VBko?o$(7-Oo)^C!tIO`89&^Plwmn_(`)^DpV#JM({mzKh^9-GTOk-qiqn2cPL& z+Xv;Ox9?wfBIWVfBb>WH+5+gD_t_ZE7T8RF-Pf*xd$-WNZ65j|T(^{Ua2-~_HSKF> zAbg&(27ox~x07-Nv|r7^L+AzV8(~P5&P!OA)j%!>!NxQN#X?i? K^=dxOWh-ln@