| 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(); |
| } |
| } |
|
|