File size: 4,708 Bytes
5aed101
 
 
6e309e5
5aed101
 
7a74796
5aed101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7a74796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5aed101
 
 
 
 
6e309e5
7bd60d9
5aed101
 
 
 
 
 
7bd60d9
7a74796
 
 
 
 
5aed101
 
 
 
 
 
 
 
7bd60d9
 
 
 
 
7a74796
5aed101
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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();
  }
}