perf: cache emoji artwork during rendering
Browse files- src/render.ts +33 -10
src/render.ts
CHANGED
|
@@ -4,6 +4,7 @@ type RenderContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2
|
|
| 4 |
export type WallpaperSymbol = CanvasImageSource | string;
|
| 5 |
|
| 6 |
let noiseTile: HTMLCanvasElement | undefined;
|
|
|
|
| 7 |
|
| 8 |
function getNoiseTile(): HTMLCanvasElement {
|
| 9 |
if (noiseTile) return noiseTile;
|
|
@@ -52,6 +53,32 @@ function drawBackground(context: RenderContext, width: number, height: number):
|
|
| 52 |
}
|
| 53 |
}
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
export function renderWallpaper(
|
| 56 |
context: RenderContext,
|
| 57 |
width: number,
|
|
@@ -66,6 +93,11 @@ export function renderWallpaper(
|
|
| 66 |
|
| 67 |
const unit = Math.min(width, height);
|
| 68 |
const drawSoftShadows = highQuality || placements.length / PLACEMENT_STRIDE <= 240;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
for (let offset = 0; offset < placements.length; offset += PLACEMENT_STRIDE) {
|
| 70 |
const x = (placements[offset] ?? 0) * unit;
|
| 71 |
const y = (placements[offset + 1] ?? 0) * unit;
|
|
@@ -79,16 +111,7 @@ export function renderWallpaper(
|
|
| 79 |
context.shadowBlur = Math.max(2, logoWidth * 0.035);
|
| 80 |
context.shadowOffsetY = Math.max(2, logoWidth * 0.032);
|
| 81 |
}
|
| 82 |
-
|
| 83 |
-
context.font = `${logoWidth * 0.88}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
| 84 |
-
context.textAlign = "center";
|
| 85 |
-
context.textBaseline = "alphabetic";
|
| 86 |
-
const metrics = context.measureText(symbol);
|
| 87 |
-
const visualCenter = ((metrics.actualBoundingBoxAscent ?? logoWidth * 0.7) - (metrics.actualBoundingBoxDescent ?? logoWidth * 0.15)) * 0.5;
|
| 88 |
-
context.fillText(symbol, 0, visualCenter, logoWidth);
|
| 89 |
-
} else {
|
| 90 |
-
context.drawImage(symbol, -logoWidth * 0.5, -logoWidth * 0.5, logoWidth, logoWidth);
|
| 91 |
-
}
|
| 92 |
context.restore();
|
| 93 |
}
|
| 94 |
}
|
|
|
|
| 4 |
export type WallpaperSymbol = CanvasImageSource | string;
|
| 5 |
|
| 6 |
let noiseTile: HTMLCanvasElement | undefined;
|
| 7 |
+
const emojiTiles = new Map<string, HTMLCanvasElement>();
|
| 8 |
|
| 9 |
function getNoiseTile(): HTMLCanvasElement {
|
| 10 |
if (noiseTile) return noiseTile;
|
|
|
|
| 53 |
}
|
| 54 |
}
|
| 55 |
|
| 56 |
+
function emojiTile(symbol: string, targetSize: number, highQuality: boolean): HTMLCanvasElement {
|
| 57 |
+
const requestedSize = highQuality ? Math.max(256, Math.min(2048, 2 ** Math.ceil(Math.log2(targetSize * 1.25)))) : 256;
|
| 58 |
+
const key = `${symbol}:${requestedSize}`;
|
| 59 |
+
const cached = emojiTiles.get(key);
|
| 60 |
+
if (cached) return cached;
|
| 61 |
+
|
| 62 |
+
const tile = document.createElement("canvas");
|
| 63 |
+
tile.width = requestedSize;
|
| 64 |
+
tile.height = requestedSize;
|
| 65 |
+
const context = tile.getContext("2d", { alpha: true });
|
| 66 |
+
if (!context) throw new Error("Could not create emoji tile");
|
| 67 |
+
context.font = `${requestedSize * 0.88}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
| 68 |
+
context.fillStyle = "#ffffff";
|
| 69 |
+
context.textAlign = "center";
|
| 70 |
+
context.textBaseline = "alphabetic";
|
| 71 |
+
const metrics = context.measureText(symbol);
|
| 72 |
+
const visualCenter = (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) * 0.5;
|
| 73 |
+
context.fillText(symbol, requestedSize * 0.5, requestedSize * 0.5 + visualCenter, requestedSize);
|
| 74 |
+
emojiTiles.set(key, tile);
|
| 75 |
+
if (emojiTiles.size > 4) {
|
| 76 |
+
const oldestKey = emojiTiles.keys().next().value;
|
| 77 |
+
if (oldestKey) emojiTiles.delete(oldestKey);
|
| 78 |
+
}
|
| 79 |
+
return tile;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
export function renderWallpaper(
|
| 83 |
context: RenderContext,
|
| 84 |
width: number,
|
|
|
|
| 93 |
|
| 94 |
const unit = Math.min(width, height);
|
| 95 |
const drawSoftShadows = highQuality || placements.length / PLACEMENT_STRIDE <= 240;
|
| 96 |
+
let maxSymbolWidth = 1;
|
| 97 |
+
for (let offset = 2; offset < placements.length; offset += PLACEMENT_STRIDE) {
|
| 98 |
+
maxSymbolWidth = Math.max(maxSymbolWidth, (placements[offset] ?? 0) * unit);
|
| 99 |
+
}
|
| 100 |
+
const artwork = typeof symbol === "string" ? emojiTile(symbol, maxSymbolWidth, highQuality) : symbol;
|
| 101 |
for (let offset = 0; offset < placements.length; offset += PLACEMENT_STRIDE) {
|
| 102 |
const x = (placements[offset] ?? 0) * unit;
|
| 103 |
const y = (placements[offset + 1] ?? 0) * unit;
|
|
|
|
| 111 |
context.shadowBlur = Math.max(2, logoWidth * 0.035);
|
| 112 |
context.shadowOffsetY = Math.max(2, logoWidth * 0.032);
|
| 113 |
}
|
| 114 |
+
context.drawImage(artwork, -logoWidth * 0.5, -logoWidth * 0.5, logoWidth, logoWidth);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
context.restore();
|
| 116 |
}
|
| 117 |
}
|