0.2.3: the install icon keeps the 0.2.x mint rook (the owner's choice), refined: a narrower tower, lighter base and more padding so it sits well among launcher icons; manifest sizes, maskable and favicons re-exported (the ICO keeps its transparent mask)
This commit is contained in:
1 parent
9042125430
commit
9ec84f36a0
16 files changed
+95
-31
No files matched your search
@@ -43,7 +43,7 @@ test('installed / standalone hides stale offers; disposing removes listeners', a
|
||||
assert.equal(offer().e.defaultPrevented, false);
|
||||
const native = rig(true); native.offer(); assert.equal(native.pwa.available(), false); native.pwa.dispose();
|
||||
});
|
||||
test('manifest provides opaque 192/512 icons for both purposes with a safe maskable mark', () => {
|
||||
test('manifest provides opaque mint-rook 192/512 icons for both purposes with a safe maskable mark', () => {
|
||||
const m = JSON.parse(readFileSync(new URL('../../public/manifest.json', import.meta.url)));
|
||||
assert.equal(m.start_url, '/'); assert.equal(m.scope, '/');
|
||||
assert.deepEqual(m.icons.map((i) => `${i.purpose}:${i.sizes}`).sort(),
|
||||
@@ -55,13 +55,14 @@ test('manifest provides opaque 192/512 icons for both purposes with a safe maska
|
||||
const size = Number(icon.sizes.split('x')[0]);
|
||||
const { w, h, rgba } = decodePng(bytes);
|
||||
assert.equal(w, size); assert.equal(h, size);
|
||||
let foreground = 0;
|
||||
let foreground = 0, solidMint = 0;
|
||||
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
|
||||
const i = (y * w + x) * 4;
|
||||
assert.equal(rgba[i + 3], 255, 'opaque edge-to-edge');
|
||||
assert.equal(rgba[i], rgba[i + 1], 'season-independent neutral palette');
|
||||
assert.equal(rgba[i], rgba[i + 2]);
|
||||
if (rgba[i] === 36) continue; // #242424 background
|
||||
const [r, g, b] = rgba.subarray(i, i + 3);
|
||||
if (r === 12 && g === 15 && b === 14) continue; // Original #0c0f0e background
|
||||
assert.ok(g >= b && b > r, 'mint palette, including antialiased edges');
|
||||
if (r === 78 && g === 216 && b === 175) solidMint++; // Original #4ed8af
|
||||
foreground++;
|
||||
if (icon.purpose === 'maskable') {
|
||||
// W3C safe zone is a circle, not merely a centred 80% square.
|
||||
@@ -70,6 +71,7 @@ test('manifest provides opaque 192/512 icons for both purposes with a safe maska
|
||||
}
|
||||
}
|
||||
assert.ok(foreground > size * size * 0.12, 'the exported mark is not empty or tiny');
|
||||
assert.ok(solidMint > size * size * 0.12, 'the original flat mint identity is retained');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,7 +88,7 @@ test('favicon and Apple links resolve to real sizes, including all legacy ICO fr
|
||||
assert.ok(link.includes(`sizes="${w}x${h}"`), href);
|
||||
} else if (href.endsWith('.svg')) {
|
||||
assert.match(bytes.toString(), /viewBox="0 0 512 512"/);
|
||||
assert.doesNotMatch(bytes.toString(), /<(?:image|text|use|foreignObject)\b|(?:href|font-family)=/);
|
||||
assert.doesNotMatch(bytes.toString(), /<(?:image|text|use|foreignObject|linearGradient|radialGradient|filter)\b|(?:href|font-family)=/);
|
||||
} else {
|
||||
assert.equal(bytes.readUInt16LE(2), 1);
|
||||
assert.equal(bytes.readUInt16LE(4), 3);
|
||||
@@ -95,11 +97,37 @@ test('favicon and Apple links resolve to real sizes, including all legacy ICO fr
|
||||
assert.equal(bytes[entry], size); assert.equal(bytes[entry + 1], size);
|
||||
const length = bytes.readUInt32LE(entry + 8), offset = bytes.readUInt32LE(entry + 12);
|
||||
assert.ok(length > 0 && offset >= 54 && offset + length <= bytes.length);
|
||||
// Decode the legacy 24-bit DIB and its 1-bit transparency mask. Both row
|
||||
// types need 4-byte alignment; malformed masks can punch holes in an icon.
|
||||
assert.equal(bytes.readUInt32LE(offset), 40);
|
||||
assert.equal(bytes.readInt32LE(offset + 4), size);
|
||||
assert.equal(bytes.readInt32LE(offset + 8), size * 2);
|
||||
assert.equal(bytes.readUInt16LE(offset + 14), 24);
|
||||
assert.equal(bytes.readUInt32LE(offset + 16), 0);
|
||||
const colorStride = Math.ceil(size * 3 / 4) * 4, maskStride = Math.ceil(size / 32) * 4;
|
||||
assert.equal(length, 40 + (colorStride + maskStride) * size, `${size}px aligned ICO frame`);
|
||||
const mask = bytes.subarray(offset + 40 + colorStride * size, offset + length);
|
||||
assert.ok(mask.every((byte) => byte === 0), `${size}px ICO is fully opaque`);
|
||||
const { rgba } = decodePng(readFileSync(new URL(`../../public/icons/favicon-${size}.png`, import.meta.url)));
|
||||
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
||||
const bmp = offset + 40 + (size - 1 - y) * colorStride + x * 3;
|
||||
const png = (y * size + x) * 4;
|
||||
for (let c = 0; c < 3; c++) assert.equal(bytes[bmp + 2 - c], rgba[png + c], `${size}px ICO matches PNG`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const size of [16, 32, 48]) {
|
||||
const { w, h } = decodePng(readFileSync(new URL(`../../public/icons/favicon-${size}.png`, import.meta.url)));
|
||||
const { w, h, rgba } = decodePng(readFileSync(new URL(`../../public/icons/favicon-${size}.png`, import.meta.url)));
|
||||
assert.equal(w, size); assert.equal(h, size);
|
||||
// At actual favicon sizes, the three battlements must stay separated and the
|
||||
// tower must remain narrower than its base. Sample visible raster features.
|
||||
const row = (y) => Array.from({ length: w }, (_, x) => rgba[(Math.floor(y * h) * w + x) * 4 + 1] > 100);
|
||||
const runs = (pixels) => pixels.filter((on, x) => on && !pixels[x - 1]).length;
|
||||
const crown = row(0.19), tower = row(0.56), base = row(0.79);
|
||||
assert.equal(runs(crown), 3, `${size}px battlement gaps stay open`);
|
||||
assert.equal(runs(tower), 1, `${size}px tower stays connected`);
|
||||
assert.equal(runs(base), 1, `${size}px base stays connected`);
|
||||
assert.ok(tower.filter(Boolean).length < base.filter(Boolean).length, `${size}px inset tower remains legible`);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user