Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
160 lines
7.4 KiB
JavaScript
160 lines
7.4 KiB
JavaScript
// Generates icon-mock.html from the REAL SoulStream brand-kit sources.
|
|
// Reads only; never writes into F:/Coding/SoulStream.
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const KIT = 'F:/Coding/SoulStream/branding/soulstream-logo-kit';
|
|
const ADP = KIT + '/android/adaptive';
|
|
const OUT = path.dirname(new URL(import.meta.url).pathname.replace(/^\//, ''));
|
|
|
|
const read = p => fs.readFileSync(p, 'utf8');
|
|
|
|
/* ---------- helpers ---------------------------------------------------- */
|
|
function attrs(tag) {
|
|
const o = {};
|
|
for (const m of tag.matchAll(/([\w:-]+)="([^"]*)"/g)) o[m[1]] = m[2];
|
|
return o;
|
|
}
|
|
function gradBlock(svg, id) {
|
|
const i = svg.indexOf('id="' + id + '"');
|
|
if (i < 0) throw new Error('gradient not found: ' + id);
|
|
const start = svg.lastIndexOf('<', i);
|
|
const isRadial = svg.slice(start, i).includes('radialGradient');
|
|
const close = isRadial ? '</radialGradient>' : '</linearGradient>';
|
|
const end = svg.indexOf(close, i) + close.length;
|
|
return { block: svg.slice(start, end), radial: isRadial };
|
|
}
|
|
function stopsOf(block) {
|
|
return [...block.matchAll(/<stop\b[^>]*\/?>/g)].map(m => {
|
|
const a = attrs(m[0]);
|
|
const s = { o: Number(a.offset), c: a['stop-color'] };
|
|
if (a['stop-opacity'] != null) s.a = Number(a['stop-opacity']);
|
|
return s;
|
|
});
|
|
}
|
|
function linOf(svg, id) {
|
|
const { block } = gradBlock(svg, id);
|
|
const a = attrs(block.slice(0, block.indexOf('>')));
|
|
return { x1: +a.x1, y1: +a.y1, x2: +a.x2, y2: +a.y2, stops: stopsOf(block) };
|
|
}
|
|
function radOf(svg, id) {
|
|
const { block } = gradBlock(svg, id);
|
|
const a = attrs(block.slice(0, block.indexOf('>')));
|
|
const gt = a.gradientTransform || '';
|
|
const tr = /translate\(\s*([-\d.]+)[ ,]+([-\d.]+)\s*\)/.exec(gt);
|
|
const ro = /rotate\(\s*([-\d.]+)\s*\)/.exec(gt);
|
|
const sc = /scale\(\s*([-\d.]+)\s*\)/.exec(gt);
|
|
if (!tr || !sc) throw new Error('unexpected gradientTransform: ' + gt);
|
|
return { cx: +tr[1], cy: +tr[2], rot: ro ? +ro[1] : 0, r: +sc[1], stops: stopsOf(block) };
|
|
}
|
|
const r6 = n => Math.round(n * 1e6) / 1e6;
|
|
function scaleLin(g, s, off) {
|
|
return { x1: r6(g.x1 * s + off), y1: r6(g.y1 * s + off),
|
|
x2: r6(g.x2 * s + off), y2: r6(g.y2 * s + off), stops: g.stops };
|
|
}
|
|
function scaleRad(g, s, off) {
|
|
return { cx: r6(g.cx * s + off), cy: r6(g.cy * s + off), rot: g.rot, r: r6(g.r * s), stops: g.stops };
|
|
}
|
|
|
|
/* ---------- read the real layers --------------------------------------- */
|
|
const fgSvg = read(ADP + '/ic_launcher_foreground.svg');
|
|
const bgSvg = read(ADP + '/ic_launcher_background.svg');
|
|
const monoSvg = read(ADP + '/ic_launcher_monochrome.svg');
|
|
const conSvg = read('F:/Coding/SoulStream/src-tauri/icons/icon.svg');
|
|
|
|
const fgTag = /<path\b[^>]*>/.exec(fgSvg)[0];
|
|
const monoTag = /<path\b[^>]*>/.exec(monoSvg)[0];
|
|
const fgA = attrs(fgTag), monoA = attrs(monoTag);
|
|
|
|
if (fgA.d !== monoA.d) console.warn('WARN: monochrome path d differs from foreground');
|
|
else console.log('ok monochrome path d is byte-identical to the foreground path');
|
|
if (fgA.transform !== monoA.transform) console.warn('WARN: transforms differ');
|
|
else console.log('ok both layers share transform:', fgA.transform);
|
|
|
|
const flat = attrs(/<rect\b[^>]*>/.exec(bgSvg)[0]).fill;
|
|
console.log('ok kit adaptive background =', flat);
|
|
|
|
// transform: translate(a b) scale(s) translate(c d)
|
|
const tm = /translate\(([-\d.]+)\s+([-\d.]+)\)\s*scale\(([-\d.]+)\)\s*translate\(([-\d.]+)\s+([-\d.]+)\)/
|
|
.exec(fgA.transform);
|
|
if (!tm) throw new Error('unexpected foreground transform');
|
|
const tParsed = [ +tm[1], +tm[2], +tm[3], +tm[4], +tm[5] ];
|
|
console.log('ok optical offset preserved verbatim: translate(' + tm[4] + ' ' + tm[5] + ')');
|
|
|
|
const fgGrad = linOf(fgSvg, 'adaptive-gradient');
|
|
const surf1024 = linOf(conSvg, 'rounded-4-surface-gradient');
|
|
const glow1024 = radOf(conSvg, 'rounded-4-glow-gradient');
|
|
console.log('ok concept 04 surface axis :', JSON.stringify(surf1024).slice(0, 120));
|
|
console.log('ok concept 04 glow : centre', glow1024.cx + ',' + glow1024.cy,
|
|
'r', glow1024.r, 'peak alpha', glow1024.stops[0].a, glow1024.stops[0].c);
|
|
|
|
const S_LAYER = 108 / 1024; // B: 1024 artwork spans the whole 108 layer
|
|
const S_MASK = 72 / 1024; // C: 1024 artwork spans only the visible 72 window
|
|
|
|
const gradL = { surf: scaleLin(surf1024, S_LAYER, 0), glow: scaleRad(glow1024, S_LAYER, 0) };
|
|
const gradM = { surf: scaleLin(surf1024, S_MASK, 18), glow: scaleRad(glow1024, S_MASK, 18) };
|
|
|
|
/* ---------- composite sampling ----------------------------------------- */
|
|
const hex2rgb = h => [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16));
|
|
const rgb2hex = c => '#' + c.map(v => Math.round(Math.max(0, Math.min(255, v)))
|
|
.toString(16).padStart(2, '0')).join('').toUpperCase();
|
|
function sampleStops(stops, t) {
|
|
t = Math.max(0, Math.min(1, t));
|
|
let a = stops[0], b = stops[stops.length - 1];
|
|
for (let i = 0; i < stops.length - 1; i++)
|
|
if (t >= stops[i].o && t <= stops[i + 1].o) { a = stops[i]; b = stops[i + 1]; break; }
|
|
const f = b.o === a.o ? 0 : (t - a.o) / (b.o - a.o);
|
|
const ca = hex2rgb(a.c), cb = hex2rgb(b.c);
|
|
const al = (a.a == null ? 1 : a.a), bl = (b.a == null ? 1 : b.a);
|
|
return { rgb: ca.map((v, i) => v + f * (cb[i] - v)), a: al + f * (bl - al) };
|
|
}
|
|
function linT(g, p) {
|
|
const vx = g.x2 - g.x1, vy = g.y2 - g.y1;
|
|
return ((p[0] - g.x1) * vx + (p[1] - g.y1) * vy) / (vx * vx + vy * vy);
|
|
}
|
|
function composite(v, p) {
|
|
if (v === 'flat') return rgb2hex(hex2rgb(flat));
|
|
const g = v === 'gradL' ? gradL : gradM;
|
|
const s = sampleStops(g.surf.stops, linT(g.surf, p)).rgb;
|
|
const d = Math.hypot(p[0] - g.glow.cx, p[1] - g.glow.cy);
|
|
const gl = sampleStops(g.glow.stops, d / g.glow.r);
|
|
return rgb2hex(s.map((v2, i) => gl.rgb[i] * gl.a + v2 * (1 - gl.a)));
|
|
}
|
|
const R = 36 / Math.SQRT2;
|
|
const POINTS = [
|
|
['visible NW corner (18, 18)', [18, 18]],
|
|
['circle NW rim (' + r6(54 - R).toFixed(1) + ', ' + r6(54 - R).toFixed(1) + ')', [54 - R, 54 - R]],
|
|
['mask centre (54, 54)', [54, 54]],
|
|
['circle SE rim (' + r6(54 + R).toFixed(1) + ', ' + r6(54 + R).toFixed(1) + ')', [54 + R, 54 + R]],
|
|
['visible SE corner (90, 90)', [90, 90]]
|
|
];
|
|
const samples = POINTS.map(([where, p]) => ({
|
|
where, v: ['flat', 'gradL', 'gradM'].map(v => composite(v, p))
|
|
}));
|
|
console.log('\n point A flat B layer C mask');
|
|
for (const s of samples) console.log(' ' + s.where.padEnd(34) + s.v.join(' '));
|
|
|
|
/* ---------- shipping raster -------------------------------------------- */
|
|
const pngPath = 'F:/Coding/SoulStream/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png';
|
|
const todayPng = 'data:image/png;base64,' + fs.readFileSync(pngPath).toString('base64');
|
|
console.log('\nok shipping raster embedded:', pngPath);
|
|
|
|
/* ---------- emit -------------------------------------------------------- */
|
|
const DATA = {
|
|
flat,
|
|
fgPath: { d: fgA.d, t: fgA.transform, tParsed },
|
|
monoPath: { d: monoA.d, t: monoA.transform },
|
|
fgGrad, gradL, gradM, samples, todayPng,
|
|
themed: {
|
|
light: { ground: '#DCE3E9', ink: '#44515E' },
|
|
dark: { ground: '#2A3138', ink: '#C6D3DF' }
|
|
}
|
|
};
|
|
|
|
const tpl = read(path.join(OUT, 'template.html'));
|
|
if (!tpl.includes('/*__DATA__*/null')) throw new Error('template marker missing');
|
|
const html = tpl.replace('/*__DATA__*/null', JSON.stringify(DATA));
|
|
const dest = path.join(OUT, 'icon-mock.html');
|
|
fs.writeFileSync(dest, html, 'utf8');
|
|
console.log('ok wrote', dest, (html.length / 1024).toFixed(1) + ' KB');
|