Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd73ecbe12 | ||
|
|
50dd6976ee |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cobble",
|
"name": "cobble",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
+21
-1
@@ -160,6 +160,20 @@ export const PwaDefaultInstallIcon: PwaInstallIcon = {
|
|||||||
export const PwaCornerMarkInset = 0.2;
|
export const PwaCornerMarkInset = 0.2;
|
||||||
export const PwaCornerMarkScale = 0.22;
|
export const PwaCornerMarkScale = 0.22;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The corner mark on the regular (rounded-square) icons, which aren't cut
|
||||||
|
* to a circle: larger and nearer the corner, clear of its rounding.
|
||||||
|
*/
|
||||||
|
export const PwaSquareCornerMarkInset = 0.16;
|
||||||
|
export const PwaSquareCornerMarkScale = 0.3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The corner mark on a round favicon: larger, to stay readable at 32px,
|
||||||
|
* and still inside the circle.
|
||||||
|
*/
|
||||||
|
export const PwaFaviconCornerMarkInset = 0.15;
|
||||||
|
export const PwaFaviconCornerMarkScale = 0.32;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads must be at least this many pixels on their shorter side, and are
|
* Uploads must be at least this many pixels on their shorter side, and are
|
||||||
* kept as a square of the source size.
|
* kept as a square of the source size.
|
||||||
@@ -167,12 +181,18 @@ export const PwaCornerMarkScale = 0.22;
|
|||||||
export const PwaInstallIconMinSide = 512;
|
export const PwaInstallIconMinSide = 512;
|
||||||
export const PwaInstallIconSourceSide = 1024;
|
export const PwaInstallIconSourceSide = 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bump when `renderInstallIcons` draws differently: it's part of a set's
|
||||||
|
* version, so a re-saved icon gets a new URL past the `immutable` cache.
|
||||||
|
*/
|
||||||
|
export const PwaInstallIconRenderRevision = 4;
|
||||||
|
|
||||||
export type PwaInstallIconFile = {
|
export type PwaInstallIconFile = {
|
||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size: number;
|
||||||
/**
|
/**
|
||||||
* `plain` is the image without the mark (the settings preview);
|
* `plain` is the image without the mark (the settings preview);
|
||||||
* `favicon` never gets the mark (unreadable at 32px).
|
* `favicon` is cut to a circle, the mark kept inside it.
|
||||||
*/
|
*/
|
||||||
kind: "any" | "maskable" | "favicon" | "plain";
|
kind: "any" | "maskable" | "favicon" | "plain";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
|
|||||||
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {
|
import {
|
||||||
|
PwaInstallIconRenderRevision,
|
||||||
PwaInstallIconSettingKey,
|
PwaInstallIconSettingKey,
|
||||||
type PwaInstallIcon,
|
type PwaInstallIcon,
|
||||||
type PwaInstallIconMode,
|
type PwaInstallIconMode,
|
||||||
@@ -57,7 +58,7 @@ export default async function saveInstallIcon({ mode, stone, mark, image }: Para
|
|||||||
const version = is_stock
|
const version = is_stock
|
||||||
? null
|
? null
|
||||||
: createHash("sha256")
|
: createHash("sha256")
|
||||||
.update(JSON.stringify({ mode, stone, mark }))
|
.update(JSON.stringify({ mode, stone, mark, render: PwaInstallIconRenderRevision }))
|
||||||
.update(source || "")
|
.update(source || "")
|
||||||
.digest("hex")
|
.digest("hex")
|
||||||
.slice(0, 12);
|
.slice(0, 12);
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import sharp from "sharp";
|
import sharp, { type OverlayOptions } from "sharp";
|
||||||
import {
|
import {
|
||||||
PwaAppIconMarkScale,
|
PwaAppIconMarkScale,
|
||||||
PwaCornerMarkInset,
|
PwaCornerMarkInset,
|
||||||
PwaCornerMarkScale,
|
PwaCornerMarkScale,
|
||||||
|
PwaFaviconCornerMarkInset,
|
||||||
|
PwaFaviconCornerMarkScale,
|
||||||
PwaInstallIconFiles,
|
PwaInstallIconFiles,
|
||||||
PwaMaskableMarkScale,
|
PwaMaskableMarkScale,
|
||||||
|
PwaSquareCornerMarkInset,
|
||||||
|
PwaSquareCornerMarkScale,
|
||||||
type PwaInstallIcon,
|
type PwaInstallIcon,
|
||||||
type PwaInstallIconFile,
|
type PwaInstallIconFile,
|
||||||
} from "@/src/data/pwa-data";
|
} from "@/src/data/pwa-data";
|
||||||
@@ -26,7 +30,8 @@ type Params = {
|
|||||||
/**
|
/**
|
||||||
* Every file of an install's icon set (`PwaInstallIconFiles`), by name.
|
* Every file of an install's icon set (`PwaInstallIconFiles`), by name.
|
||||||
* A stone icon is the stone on paper, like the stock one. An image fills
|
* A stone icon is the stone on paper, like the stock one. An image fills
|
||||||
* the icon, with the stone in the top-left corner when `mark` is on.
|
* the icon, with the stone in the top-left corner when `mark` is on; its
|
||||||
|
* favicon is cut to a circle, the mark kept inside it.
|
||||||
*/
|
*/
|
||||||
export default async function renderInstallIcons({ icon, stone_svg, image, background }: Params) {
|
export default async function renderInstallIcons({ icon, stone_svg, image, background }: Params) {
|
||||||
const files: Record<string, Buffer> = {};
|
const files: Record<string, Buffer> = {};
|
||||||
@@ -79,19 +84,41 @@ async function renderImageFile({
|
|||||||
.resize(file.size, file.size, { fit: "cover" })
|
.resize(file.size, file.size, { fit: "cover" })
|
||||||
.flatten({ background });
|
.flatten({ background });
|
||||||
|
|
||||||
if (!mark || file.kind == "favicon" || file.kind == "plain") {
|
const layers: OverlayOptions[] = [];
|
||||||
return base.png().toBuffer();
|
|
||||||
|
if (mark && file.kind != "plain") {
|
||||||
|
const { scale, inset_scale } = markPlacement({ kind: file.kind });
|
||||||
|
const mark_size = Math.max(1, Math.round(file.size * scale));
|
||||||
|
const inset = Math.round(file.size * inset_scale);
|
||||||
|
const stone = await sharp(stone_svg)
|
||||||
|
.resize(mark_size, mark_size, { fit: "contain" })
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
layers.push({ input: stone, left: inset, top: inset });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mark_size = Math.max(1, Math.round(file.size * PwaCornerMarkScale));
|
if (file.kind == "favicon") {
|
||||||
const inset = Math.round(file.size * PwaCornerMarkInset);
|
const half = file.size / 2;
|
||||||
const stone = await sharp(stone_svg)
|
const circle = Buffer.from(
|
||||||
.resize(mark_size, mark_size, { fit: "contain" })
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${file.size}" height="${file.size}"><circle cx="${half}" cy="${half}" r="${half}"/></svg>`,
|
||||||
.png()
|
);
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
return base
|
layers.push({ input: circle, blend: "dest-in" });
|
||||||
.composite([{ input: stone, left: inset, top: inset }])
|
}
|
||||||
.png()
|
|
||||||
.toBuffer();
|
// Layers apply in order, so the circle cuts the mark too.
|
||||||
|
return base.ensureAlpha().composite(layers).png().toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function markPlacement({ kind }: { kind: PwaInstallIconFile["kind"] }) {
|
||||||
|
if (kind == "favicon") {
|
||||||
|
return { scale: PwaFaviconCornerMarkScale, inset_scale: PwaFaviconCornerMarkInset };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == "any") {
|
||||||
|
return { scale: PwaSquareCornerMarkScale, inset_scale: PwaSquareCornerMarkInset };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { scale: PwaCornerMarkScale, inset_scale: PwaCornerMarkInset };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ type Props = {
|
|||||||
/**
|
/**
|
||||||
* The app icon as a launcher would show it, laid out like the generated
|
* The app icon as a launcher would show it, laid out like the generated
|
||||||
* files (`renderInstallIcons`): a stone at 52% (maskable) or 80% on paper,
|
* files (`renderInstallIcons`): a stone at 52% (maskable) or 80% on paper,
|
||||||
* or the image with the stone 20% in from the top-left at 22% of the side.
|
* or the image with the stone in the top-left: 20% in at 22% of the side
|
||||||
|
* (maskable, round), 16% in at 30% (rounded square).
|
||||||
*/
|
*/
|
||||||
export default function InstallIconShape({ shape, mode, stone, mark, image_src, className }: Props) {
|
export default function InstallIconShape({ shape, mode, stone, mark, image_src, className }: Props) {
|
||||||
const stone_src = `/brand/icons/${stone}.svg`;
|
const stone_src = `/brand/icons/${stone}.svg`;
|
||||||
@@ -41,7 +42,12 @@ export default function InstallIconShape({ shape, mode, stone, mark, image_src,
|
|||||||
<Img
|
<Img
|
||||||
src={stone_src}
|
src={stone_src}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute left-[20%] top-[20%] size-[22%]"
|
className={twMerge(
|
||||||
|
"absolute",
|
||||||
|
shape == "circle"
|
||||||
|
? "left-[20%] top-[20%] size-[22%]"
|
||||||
|
: "left-[16%] top-[16%] size-[30%]",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user