Rename to 404 Layers, and give it a real icon
The name was SP-404MK2 layers, which is 16 characters and truncates in the launcher. It is set in five places, one of which cap sync regenerates from appName, so all five move together: capacitor.config, strings.xml, index.html, package.json and CLAUDE.md. reference/ keeps the old name because it is frozen. appId is untouched - changing it would make this a different app to Android, meaning a fresh install and the drill progress on the phone orphaned. The icon is a 3x3 pad grid with the centre pad lit: the app's own subject, legible when a launcher masks it to a circle, and in the settled palette. Three columns rather than the hardware's four so the cells survive 48px, which I checked at 800% magnification. scripts/make-icon.mjs draws it with ImageMagick primitives, because there is no SVG rasteriser here - no Inkscape, no rsvg-convert, and no SVG delegate in ImageMagick 6. It is in the repo because the icon would otherwise be 15 binary PNGs with no record of how they were made. Two things that took a few attempts. The draw commands have to be built as a real array: three tries in bash collapsed all nine into one argument, which ImageMagick read as a filename. And the convert calls need -strip, because without it the pixels are identical between runs but the bytes are not, so the PNGs churned and the script could not be shown to be their source. Two consecutive runs are now byte-identical. Also removed two orphaned Capacitor vectors, 204 lines of gradient swoosh and grid pattern that nothing referenced once the adaptive icon pointed at @mipmap and @color. Icons total 52KB. Built and installed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
24
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` `<title>`, `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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 850 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 536 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 992 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 940 B |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 8.1 KiB |
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
<color name="ic_launcher_background">#e9e9e6</color>
|
||||
</resources>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appId": "dev.ncode.sp404layers",
|
||||
"appName": "SP-404MK2 layers",
|
||||
"appName": "404 Layers",
|
||||
"webDir": "dist",
|
||||
"android": {
|
||||
"backgroundColor": "#151719",
|
||||
|
||||
BIN
design/icon/preview-as-launcher-shows-it.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
design/icon/preview-round.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
design/icon/preview-square.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -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</title>
|
||||
<title>404 Layers</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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"
|
||||
|
||||
114
scripts/make-icon.mjs
Normal file
@@ -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/`,
|
||||
);
|
||||