| 108 | gradient.addColorStop(0, centerColor); |
| 109 | gradient.addColorStop(1, edgeColor); |
| 110 | |
| 111 | ctx.fillStyle = gradient; |
| 112 | ctx.fillRect(0, 0, canvas.width, canvas.height); |
| 113 | |
| 114 | const pattern = ctx.createPattern(imageAlpha(patternImage, patternAlpha), "repeat"); |
| 115 | ctx.fillStyle = pattern; |
| 116 | ctx.fillRect(0, 0, canvas.width, canvas.height); |
| 117 | } |
| 118 | |
| 119 | // Wallpaper colors derived from the bubble color. The bubble must sit ON the |
| 120 | // wallpaper, not dissolve into it: |
| 121 | // • dark bubbles → a much darker backdrop (luminance drop + vignette); |
| 122 | // • light bubbles → a PASTEL backdrop, like Telegram's light wallpapers: |
| 123 | // saturate the hue and keep lightness high — darkening a near-white just |
| 124 | // makes mud. Near-gray inputs fall back to a soft Telegram-ish blue. |
| 125 | function wallpaperColors(colorOne) { |
| 126 | if (lightOrDark(colorOne) === "dark") { |
| 127 | return { |
| 128 | center: colorLuminance(colorOne, -0.35), |
| 129 | edge: colorLuminance(colorOne, -0.6), |
| 130 | patternAlpha: 0.22, |
| 131 | }; |
| 132 | } |
| 133 | let [h, s] = hexToHsl(colorOne); |
| 134 | if (s < 0.08) { |
| 135 | h = 207; // washed-out gray/white → soft blue |
| 136 | s = 0.45; |
| 137 | } else { |
| 138 | s = Math.min(1, Math.max(s * 1.8, 0.35)); |
| 139 | } |
| 140 | return { |
| 141 | center: hslToHex(h, s, 0.8), |