|
14 | 14 | w: 0, |
15 | 15 | h: 0, |
16 | 16 | loaded: false, |
| 17 | + maskCanvas: null, |
| 18 | + }; |
| 19 | + |
| 20 | + const buildInkMask = () => { |
| 21 | + const w = img.naturalWidth || img.width || 0; |
| 22 | + const h = img.naturalHeight || img.height || 0; |
| 23 | + if (!w || !h) return null; |
| 24 | + |
| 25 | + const off = document.createElement("canvas"); |
| 26 | + off.width = w; |
| 27 | + off.height = h; |
| 28 | + const octx = off.getContext("2d"); |
| 29 | + if (!octx) return null; |
| 30 | + octx.drawImage(img, 0, 0, w, h); |
| 31 | + |
| 32 | + const imageData = octx.getImageData(0, 0, w, h); |
| 33 | + const data = imageData.data; |
| 34 | + for (let i = 0; i < data.length; i += 4) { |
| 35 | + const r = data[i]; |
| 36 | + const g = data[i + 1]; |
| 37 | + const b = data[i + 2]; |
| 38 | + const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b; |
| 39 | + const ink = 255 - lum; |
| 40 | + if (ink < 22) { |
| 41 | + data[i + 3] = 0; |
| 42 | + continue; |
| 43 | + } |
| 44 | + const alpha = Math.min(255, (ink - 22) * 1.6); |
| 45 | + data[i] = 210; |
| 46 | + data[i + 1] = 210; |
| 47 | + data[i + 2] = 214; |
| 48 | + data[i + 3] = alpha; |
| 49 | + } |
| 50 | + octx.putImageData(imageData, 0, 0); |
| 51 | + return off; |
17 | 52 | }; |
18 | 53 |
|
19 | 54 | const draw = () => { |
|
27 | 62 | if (!state.loaded) return; |
28 | 63 |
|
29 | 64 | // Cover fit to fill viewport. |
30 | | - const iw = img.naturalWidth || 1; |
31 | | - const ih = img.naturalHeight || 1; |
| 65 | + const source = state.maskCanvas || img; |
| 66 | + const iw = source.width || img.naturalWidth || 1; |
| 67 | + const ih = source.height || img.naturalHeight || 1; |
32 | 68 | const scale = Math.max(state.w / iw, state.h / ih); |
33 | 69 | const dw = iw * scale; |
34 | 70 | const dh = ih * scale; |
35 | 71 | const dx = (state.w - dw) * 0.5; |
36 | 72 | const dy = (state.h - dh) * 0.5; |
37 | 73 | const mobile = Math.max(0, Math.min(1, state.w / 900)); |
38 | 74 |
|
39 | | - // Near-black but visible, with stronger darkening on mobile. |
40 | | - const brightness = 0.14 + mobile * 0.06; // ~0.166 on iPhone widths, ~0.20 desktop |
41 | | - const baseAlpha = 0.62 + mobile * 0.28; // reduce image weight on smaller screens |
42 | | - ctx.filter = `grayscale(1) contrast(1.05) brightness(${brightness.toFixed(3)})`; |
| 75 | + // Draw extracted ASCII ink only (white background removed), darker on mobile. |
| 76 | + const baseAlpha = 0.24 + mobile * 0.26; |
| 77 | + ctx.filter = "none"; |
43 | 78 | ctx.globalAlpha = baseAlpha; |
44 | | - ctx.drawImage(img, dx, dy, dw, dh); |
| 79 | + ctx.drawImage(source, dx, dy, dw, dh); |
45 | 80 |
|
46 | 81 | // Slight dark veil so foreground content stays readable. |
47 | 82 | ctx.filter = "none"; |
48 | 83 | ctx.globalAlpha = 1; |
49 | | - const veil = 0.62 - mobile * 0.20; // stronger veil on mobile |
| 84 | + const veil = 0.70 - mobile * 0.22; // stronger veil on mobile |
50 | 85 | ctx.fillStyle = `rgba(0, 0, 0, ${veil.toFixed(3)})`; |
51 | 86 | ctx.fillRect(0, 0, state.w, state.h); |
52 | 87 |
|
|
81 | 116 | }; |
82 | 117 |
|
83 | 118 | img.onload = () => { |
| 119 | + state.maskCanvas = buildInkMask(); |
84 | 120 | state.loaded = true; |
85 | 121 | draw(); |
86 | 122 | }; |
|
0 commit comments