import { PLACEMENT_STRIDE } from "./layout"; type RenderContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; export type WallpaperSymbol = CanvasImageSource | string; let noiseTile: HTMLCanvasElement | undefined; const emojiTiles = new Map(); 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(); } }