tilekit / src /render.ts
osolmaz's picture
osolmaz HF Staff
perf: cache emoji artwork during rendering
7a74796 verified
Raw
History Blame Contribute Delete
4.71 kB
import { PLACEMENT_STRIDE } from "./layout";
type RenderContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
export type WallpaperSymbol = CanvasImageSource | string;
let noiseTile: HTMLCanvasElement | undefined;
const emojiTiles = new Map<string, HTMLCanvasElement>();
function getNoiseTile(): HTMLCanvasElement {
if (noiseTile) return noiseTile;
noiseTile = document.createElement("canvas");
noiseTile.width = 96;
noiseTile.height = 96;
const context = noiseTile.getContext("2d", { alpha: true });
if (!context) throw new Error("Could not create noise texture");
const pixels = context.createImageData(96, 96);
let state = 31;
for (let index = 0; index < pixels.data.length; index += 4) {
state = Math.imul(state ^ (state >>> 15), 1 | state);
state ^= state + Math.imul(state ^ (state >>> 7), 61 | state);
const value = (state ^ (state >>> 14)) & 0xff;
pixels.data[index] = value;
pixels.data[index + 1] = value;
pixels.data[index + 2] = value;
pixels.data[index + 3] = 28;
}
context.putImageData(pixels, 0, 0);
return noiseTile;
}
function drawBackground(context: RenderContext, width: number, height: number): void {
const linear = context.createLinearGradient(0, 0, width, height * 0.35);
linear.addColorStop(0, "#1688e9");
linear.addColorStop(0.52, "#4aabef");
linear.addColorStop(1, "#c2e7f8");
context.fillStyle = linear;
context.fillRect(0, 0, width, height);
const glow = context.createRadialGradient(width * 0.56, height * 0.47, 0, width * 0.56, height * 0.47, Math.max(width, height) * 0.72);
glow.addColorStop(0, "rgba(255,255,255,0.16)");
glow.addColorStop(0.7, "rgba(255,255,255,0.02)");
glow.addColorStop(1, "rgba(2,74,153,0.08)");
context.fillStyle = glow;
context.fillRect(0, 0, width, height);
const pattern = context.createPattern(getNoiseTile(), "repeat");
if (pattern) {
context.save();
context.globalAlpha = 0.08;
context.fillStyle = pattern;
context.fillRect(0, 0, width, height);
context.restore();
}
}
function emojiTile(symbol: string, targetSize: number, highQuality: boolean): HTMLCanvasElement {
const requestedSize = highQuality ? Math.max(256, Math.min(2048, 2 ** Math.ceil(Math.log2(targetSize * 1.25)))) : 256;
const key = `${symbol}:${requestedSize}`;
const cached = emojiTiles.get(key);
if (cached) return cached;
const tile = document.createElement("canvas");
tile.width = requestedSize;
tile.height = requestedSize;
const context = tile.getContext("2d", { alpha: true });
if (!context) throw new Error("Could not create emoji tile");
context.font = `${requestedSize * 0.88}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
context.fillStyle = "#ffffff";
context.textAlign = "center";
context.textBaseline = "alphabetic";
const metrics = context.measureText(symbol);
const visualCenter = (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) * 0.5;
context.fillText(symbol, requestedSize * 0.5, requestedSize * 0.5 + visualCenter, requestedSize);
emojiTiles.set(key, tile);
if (emojiTiles.size > 4) {
const oldestKey = emojiTiles.keys().next().value;
if (oldestKey) emojiTiles.delete(oldestKey);
}
return tile;
}
export function renderWallpaper(
context: RenderContext,
width: number,
height: number,
placements: Float32Array,
symbol: WallpaperSymbol,
highQuality = true,
): void {
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, width, height);
drawBackground(context, width, height);
const unit = Math.min(width, height);
const drawSoftShadows = highQuality || placements.length / PLACEMENT_STRIDE <= 240;
let maxSymbolWidth = 1;
for (let offset = 2; offset < placements.length; offset += PLACEMENT_STRIDE) {
maxSymbolWidth = Math.max(maxSymbolWidth, (placements[offset] ?? 0) * unit);
}
const artwork = typeof symbol === "string" ? emojiTile(symbol, maxSymbolWidth, highQuality) : symbol;
for (let offset = 0; offset < placements.length; offset += PLACEMENT_STRIDE) {
const x = (placements[offset] ?? 0) * unit;
const y = (placements[offset + 1] ?? 0) * unit;
const logoWidth = (placements[offset + 2] ?? 0) * unit;
const angle = (placements[offset + 3] ?? 0) * (Math.PI / 180);
context.save();
context.translate(x, y);
context.rotate(angle);
if (drawSoftShadows) {
context.shadowColor = "rgba(8, 39, 78, 0.24)";
context.shadowBlur = Math.max(2, logoWidth * 0.035);
context.shadowOffsetY = Math.max(2, logoWidth * 0.032);
}
context.drawImage(artwork, -logoWidth * 0.5, -logoWidth * 0.5, logoWidth, logoWidth);
context.restore();
}
}