diff --git a/CLAUDE.md b/CLAUDE.md index 4ebd1f9..7f43886 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,26 @@ -# SP-404MK2 layers +# 404 Layers -A combo trainer for the Roland SP-404MK2, wrapped as a sideloaded Android APK. +A combo trainer for the Roland SP-404MK2, sideloaded to an Android phone. + +**The name is set in five places** and `cap sync` regenerates one of them: +`capacitor.config.json` (`appName`, which Capacitor writes into `strings.xml`), +`android/.../values/strings.xml` (`app_name`, `title_activity_main`), +`index.html` ``, `package.json`, and this file. `reference/` keeps the old +name on purpose — it is frozen. + +**`appId` must stay `dev.ncode.sp404layers`.** Changing it makes a different app +as far as Android is concerned: a fresh install, and the drill progress on the +phone orphaned. + +**The launcher icon is generated, not hand-drawn:** `npm run icon` runs +`scripts/make-icon.mjs`, which draws a 3×3 pad grid with the centre pad lit using +ImageMagick primitives — there is no SVG rasteriser on this machine. It writes +the five adaptive foregrounds (art inside the inner 66%, because the launcher +masks the rest), the five legacy square/round pairs, and previews to +`design/icon/` since a PNG on disk is the only way to actually look at it. The +convert calls pass `-strip`: without it the pixels are identical between runs but +the bytes are not, so the committed PNGs churn and the script cannot be shown to +be their source. **Phase 1 (current):** the original single-file web app, wrapped in Capacitor as-is. **Phase 2 (next):** port to React + TypeScript via Vite — data and pure logic with diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index c7bd21d..0000000 --- a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:aapt="http://schemas.android.com/aapt" - android:width="108dp" - android:height="108dp" - android:viewportHeight="108" - android:viewportWidth="108"> - <path - android:fillType="evenOdd" - android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z" - android:strokeColor="#00000000" - android:strokeWidth="1"> - <aapt:attr name="android:fillColor"> - <gradient - android:endX="78.5885" - android:endY="90.9159" - android:startX="48.7653" - android:startY="61.0927" - android:type="linear"> - <item - android:color="#44000000" - android:offset="0.0" /> - <item - android:color="#00000000" - android:offset="1.0" /> - </gradient> - </aapt:attr> - </path> - <path - android:fillColor="#FFFFFF" - android:fillType="nonZero" - android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z" - android:strokeColor="#00000000" - android:strokeWidth="1" /> -</vector> diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index d5fccc5..0000000 --- a/android/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="108dp" - android:height="108dp" - android:viewportHeight="108" - android:viewportWidth="108"> - <path - android:fillColor="#26A69A" - android:pathData="M0,0h108v108h-108z" /> - <path - android:fillColor="#00000000" - android:pathData="M9,0L9,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,0L19,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M29,0L29,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M39,0L39,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M49,0L49,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M59,0L59,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M69,0L69,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M79,0L79,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M89,0L89,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M99,0L99,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,9L108,9" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,19L108,19" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,29L108,29" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,39L108,39" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,49L108,49" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,59L108,59" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,69L108,69" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,79L108,79" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,89L108,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,99L108,99" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,29L89,29" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,39L89,39" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,49L89,49" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,59L89,59" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,69L89,69" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,79L89,79" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M29,19L29,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M39,19L39,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M49,19L49,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M59,19L59,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M69,19L69,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M79,19L79,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> -</vector> diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index c023e50..31bdaa6 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index 2127973..6384b4b 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index b441f37..7c17eae 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 72905b8..6fbee68 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index 8ed0605..4bb2b37 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index 9502e47..b00a0f4 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 4d1e077..7e2e75d 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index df0f158..4eff8f7 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 853db04..0bc4ef6 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 6cdf97c..7201136 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index 2960cbb..32537d6 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 8e3093a..9c0dbb9 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 46de6e2..34f0d7e 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index d2ea9ab..2830aa6 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index a40d73e..2bec95d 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml index c5d5899..fc35a53 100644 --- a/android/app/src/main/res/values/ic_launcher_background.xml +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <color name="ic_launcher_background">#FFFFFF</color> -</resources> \ No newline at end of file + <color name="ic_launcher_background">#e9e9e6</color> +</resources> diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 32173ea..e868774 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ <?xml version='1.0' encoding='utf-8'?> <resources> - <string name="app_name">SP-404MK2 layers</string> - <string name="title_activity_main">SP-404MK2 layers</string> + <string name="app_name">404 Layers</string> + <string name="title_activity_main">404 Layers</string> <string name="package_name">dev.ncode.sp404layers</string> <string name="custom_url_scheme">dev.ncode.sp404layers</string> </resources> diff --git a/capacitor.config.json b/capacitor.config.json index 7735185..ab3dee3 100644 --- a/capacitor.config.json +++ b/capacitor.config.json @@ -1,6 +1,6 @@ { "appId": "dev.ncode.sp404layers", - "appName": "SP-404MK2 layers", + "appName": "404 Layers", "webDir": "dist", "android": { "backgroundColor": "#151719", diff --git a/design/icon/preview-as-launcher-shows-it.png b/design/icon/preview-as-launcher-shows-it.png new file mode 100644 index 0000000..0b141d8 Binary files /dev/null and b/design/icon/preview-as-launcher-shows-it.png differ diff --git a/design/icon/preview-round.png b/design/icon/preview-round.png new file mode 100644 index 0000000..23668b3 Binary files /dev/null and b/design/icon/preview-round.png differ diff --git a/design/icon/preview-square.png b/design/icon/preview-square.png new file mode 100644 index 0000000..cc38281 Binary files /dev/null and b/design/icon/preview-square.png differ diff --git a/index.html b/index.html index cbac5ee..52a1f0e 100644 --- a/index.html +++ b/index.html @@ -14,7 +14,7 @@ --> <meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="color-scheme" content="light dark" /> - <title>SP-404MK2 layers + 404 Layers
diff --git a/package.json b/package.json index 3ad17a0..a4f4fe9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "type": "module", - "description": "SP-404MK2 combo trainer, wrapped as an Android app", + "description": "404 Layers — an SP-404MK2 combo trainer for Android", "scripts": { "sync:www": "bash scripts/sync-www.sh", "sync": "npm run build && npx cap sync android", @@ -20,6 +20,7 @@ "test:watch": "vitest", "typecheck": "tsc --noEmit", "extract:data": "node scripts/extract-reference-data.mjs", + "icon": "node scripts/make-icon.mjs", "dev": "vite", "build": "vite build", "preview": "vite preview" diff --git a/scripts/make-icon.mjs b/scripts/make-icon.mjs new file mode 100644 index 0000000..02b8759 --- /dev/null +++ b/scripts/make-icon.mjs @@ -0,0 +1,114 @@ +/** + * Draws the launcher icon: a 3x3 pad grid with the centre pad lit. + * + * Kept in the repo because the icon is otherwise only 15 binary PNGs with no + * record of how they were made — changing a colour or a gap would mean redrawing + * it from scratch. Re-run with: npm run icon + * + * There is no SVG rasteriser on this machine (no Inkscape, no rsvg-convert, and + * ImageMagick 6 here has no SVG delegate), so the artwork is ImageMagick + * primitives. The command is built as a real array: three attempts at this in + * bash collapsed the nine draw commands into one argument, which ImageMagick + * then read as a filename. + */ +import { execFileSync } from "node:child_process"; +import { mkdirSync, rmSync } from "node:fs"; + +const BODY = "#e9e9e6"; +const PAD = "#1b1b1b"; +const ORANGE = "#ff4a00"; +const INK = "#111111"; + +const RES = "android/app/src/main/res"; + +/** Adaptive foregrounds: 108dp canvas, art confined to the inner 66%. */ +const FOREGROUND = [ + ["mdpi", 108], + ["hdpi", 162], + ["xhdpi", 216], + ["xxhdpi", 324], + ["xxxhdpi", 432], +]; + +/** Legacy icons, for launchers older than adaptive support. */ +const LEGACY = [ + ["mdpi", 48], + ["hdpi", 72], + ["xhdpi", 96], + ["xxhdpi", 144], + ["xxxhdpi", 192], +]; + +/** + * @param pct how much of the canvas the grid may use. 66 for adaptive + * foregrounds, because the launcher masks everything outside that; 82 for + * legacy icons, which are shown whole. + */ +function grid(size, out, pct, bg) { + const inner = Math.floor((size * pct) / 100); + const off = Math.floor((size - inner) / 2); + const cell = Math.floor((inner * 26) / 100); // 3 cells + 2 gaps of 11% + const gap = Math.floor((inner * 11) / 100); + const rad = Math.max(1, Math.floor((cell * 18) / 100)); // as on the pads + const stroke = Math.max(1, Math.round(size / 110)); + + const args = ["-size", `${size}x${size}`, `xc:${bg}`]; + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 3; col++) { + const x = off + col * (cell + gap); + const y = off + row * (cell + gap); + const lit = row === 1 && col === 1; + args.push( + "-fill", lit ? ORANGE : PAD, + "-stroke", INK, + "-strokewidth", String(stroke), + "-draw", `roundrectangle ${x},${y} ${x + cell},${y + cell} ${rad},${rad}`, + ); + } + } + // -strip removes the tIME chunk. Without it the pixels are identical between + // runs but the bytes are not, so the committed PNGs churn on every re-run and + // this script cannot be shown to be their source. + args.push("-strip", out); + execFileSync("convert", args); +} + +function circleMask(src, size, out) { + const half = Math.floor(size / 2); + execFileSync("convert", [ + src, + "(", "-size", `${size}x${size}`, "xc:none", + "-fill", "white", "-draw", `circle ${half},${half} ${half},0`, ")", + "-alpha", "set", "-compose", "DstIn", "-composite", "-strip", out, + ]); +} + +for (const [density, px] of FOREGROUND) { + grid(px, `${RES}/mipmap-${density}/ic_launcher_foreground.png`, 66, "none"); +} + +for (const [density, px] of LEGACY) { + const square = `${RES}/mipmap-${density}/ic_launcher.png`; + grid(px, square, 82, BODY); + circleMask(square, px, `${RES}/mipmap-${density}/ic_launcher_round.png`); +} + +// Previews, since a PNG on disk is the only way to actually look at this. +mkdirSync("design/icon", { recursive: true }); +grid(512, "design/icon/preview-square.png", 82, BODY); +circleMask("design/icon/preview-square.png", 512, "design/icon/preview-round.png"); + +grid(432, "design/icon/.fg.png", 66, "none"); +execFileSync("convert", [ + "-size", "432x432", `xc:${BODY}`, "design/icon/.fg.png", "-composite", + "(", "-size", "432x432", "xc:none", "-fill", "white", + "-draw", "roundrectangle 0,0 431,431 108,108", ")", + "-alpha", "set", "-compose", "DstIn", "-composite", + "design/icon/preview-as-launcher-shows-it.png", +]); +rmSync("design/icon/.fg.png", { force: true }); + +console.log( + `icon drawn: ${FOREGROUND.length} adaptive foregrounds, ` + + `${LEGACY.length} legacy pairs, 3 previews in design/icon/`, +);