Skip to content

Commit 978ccc6

Browse files
kuco23claude
andcommitted
claude:design: invert hero canvas — full-viewport field, brighter rune, gradient-faded edges
Reworks the HeroRuneCanvas from a small centered sprite into a full-screen ASCII wave with the Stakecore mark embedded as a brighter silhouette. Five linked changes: 1. Canvas geometry (hero.scss). The .hero-rune-canvas now spans `position: absolute; top: 0; left: 0; width: 100%; height: 100vh` instead of a 360-480px centered square. Still hidden below md to spare mobile devices the work. 2. Inverted colouring (hero.tsx drawFrame). Previously the wave only modulated cells INSIDE the rune silhouette; now every cell participates and the silhouette flips the fillStyle: cells outside the mark render in the original #6B6B6B gray, cells inside switch to #FFFFFF, so the rune reads as a lighter ASCII shape embedded in a dimmer field. Drawn as two passes per frame to keep fillStyle constant within each pass. 3. Aspect-preserving rune sprite (hero.tsx rasterize). With the canvas now full-viewport, blindly drawing the SVG into the full `cols × rows` buffer would stretch the mark horizontally on landscape (and squish it on portrait). Instead, rasterize into a centred sprite sized at 55% of the shorter grid axis, preserving the 340:380 SVG aspect ratio. 4. Top + bottom gradient fade. CSS mask-image `linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%)` keeps the middle 50% of the canvas (where the rune sits) at full opacity and softly fades both the top and bottom edges into the page background, so the wave doesn't cut off hard at the hero's overflow boundary. 5. Wave-speed dial-down (hero.tsx tick). Per-frame phase increment drops from `dt * 0.003` to `dt * 0.002` — wave advances ~33% slower. Frame rate (12fps throttle) unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 75aeb19 commit 978ccc6

2 files changed

Lines changed: 67 additions & 47 deletions

File tree

src/components/sections/hero.scss

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,28 @@
2121
@include t.up(lg) { padding: 140px 0 64px; }
2222
}
2323

24-
// Fixed-grid ASCII display. Per-cell intensity = base sampled from the
25-
// rune SVG + a radial sine wave breathing outward. The rune is drawn as
26-
// dense glyphs; the wave makes everything pulse. No CSS mask needed —
27-
// the rune shape comes from the grid itself.
24+
// Fixed-grid ASCII display. The wave fills the full viewport (capped at
25+
// the initial screen height so it doesn't keep growing on long pages);
26+
// cells inside the rune silhouette render brighter, painting the
27+
// Stakecore mark as a lighter ASCII shape embedded in the dimmer field.
2828
.hero-rune-canvas {
2929
display: none;
3030

3131
@include t.up(md) {
3232
display: block;
3333
position: absolute;
34-
top: 50%;
35-
left: 50%;
36-
transform: translate(-50%, -50%);
37-
width: 360px;
38-
height: 360px;
34+
top: 0;
35+
left: 0;
36+
width: 100%;
37+
height: 100vh;
3938
opacity: 0.3;
4039
pointer-events: none;
4140
z-index: -1;
42-
}
43-
44-
@include t.up(lg) {
45-
width: 480px;
46-
height: 480px;
41+
// Fade the lower half toward the page background so the wave
42+
// dissolves into the dark body instead of cutting off at the
43+
// hero's overflow edge.
44+
mask-image: linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%);
45+
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%);
4746
}
4847
}
4948

src/components/sections/hero.tsx

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -115,23 +115,40 @@ const HeroRuneCanvas = () => {
115115
cy = rows / 2
116116
}
117117

118-
// Rasterize the rune SVG into an offscreen canvas at grid resolution,
119-
// then read alpha-weighted luminance as the per-cell base intensity.
118+
// Rasterize the rune SVG into a centered region of the grid so the
119+
// wave can fill the full viewport without stretching the mark.
120+
// Target ~40% of the shorter grid dimension; preserve SVG aspect
121+
// (340 × 380); clamp + centre so the mark sits at the visual middle.
122+
const SVG_ASPECT = 340 / 380
120123
const rasterize = () => new Promise<Float32Array>((resolve) => {
124+
const arr = new Float32Array(cols * rows)
121125
const img = new Image()
122126
img.onload = () => {
127+
// Sprite size in cells. Tied to the shorter grid axis so portrait
128+
// and landscape viewports both show a centred, well-proportioned
129+
// rune (not a stretched-to-edges blob).
130+
const minor = Math.min(cols, rows)
131+
let runeH = Math.max(20, Math.round(minor * 0.55))
132+
let runeW = Math.round(runeH * SVG_ASPECT)
133+
if (runeW > cols) {
134+
runeW = cols
135+
runeH = Math.round(runeW / SVG_ASPECT)
136+
}
123137
const off = document.createElement('canvas')
124-
off.width = cols
125-
off.height = rows
138+
off.width = runeW
139+
off.height = runeH
126140
const octx = off.getContext('2d')
127-
if (!octx) { resolve(new Float32Array(cols * rows)); return }
128-
octx.drawImage(img, 0, 0, cols, rows)
129-
const data = octx.getImageData(0, 0, cols, rows).data
130-
const arr = new Float32Array(cols * rows)
131-
for (let i = 0; i < cols * rows; i++) {
132-
const r = data[i * 4]
133-
const a = data[i * 4 + 3]
134-
arr[i] = (r / 255) * (a / 255)
141+
if (!octx) { resolve(arr); return }
142+
octx.drawImage(img, 0, 0, runeW, runeH)
143+
const data = octx.getImageData(0, 0, runeW, runeH).data
144+
const xOffset = Math.floor((cols - runeW) / 2)
145+
const yOffset = Math.floor((rows - runeH) / 2)
146+
for (let y = 0; y < runeH; y++) {
147+
for (let x = 0; x < runeW; x++) {
148+
const r = data[(y * runeW + x) * 4]
149+
const a = data[(y * runeW + x) * 4 + 3]
150+
arr[(y + yOffset) * cols + (x + xOffset)] = (r / 255) * (a / 255)
151+
}
135152
}
136153
resolve(arr)
137154
}
@@ -143,28 +160,32 @@ const HeroRuneCanvas = () => {
143160
ctx.fillStyle = '#000'
144161
ctx.fillRect(0, 0, w, h)
145162
ctx.font = `${cellSize}px 'Roboto Mono', ui-monospace, monospace`
146-
ctx.fillStyle = '#6B6B6B'
147163
ctx.textBaseline = 'top'
148164

149-
for (let y = 0; y < rows; y++) {
150-
for (let x = 0; x < cols; x++) {
151-
// Cells outside the rune silhouette never render — the wave
152-
// only modulates the brightness of cells where the SVG alpha
153-
// mask said "inside".
154-
const b = base[y * cols + x]
155-
if (b < 0.05) continue
156-
const dx = x - cx
157-
const dy = y - cy
158-
const dist = Math.sqrt(dx * dx + dy * dy)
159-
// Two-octave radial wave for organic pulsing.
160-
const wave =
161-
0.5 + 0.5 * Math.sin(dist * 0.42 - phase) * 0.7
162-
+ 0.5 * Math.sin(dist * 0.18 - phase * 0.6) * 0.3
163-
const intensity = b * 0.65 + wave * 0.35
164-
const idx = Math.min(RAMP.length - 1, Math.floor(intensity * RAMP.length))
165-
const c = RAMP[idx]
166-
if (c === ' ') continue
167-
ctx.fillText(c, x * cellSize, y * cellSize)
165+
// Inverted layout: every cell renders the radial wave glyph; cells
166+
// inside the rune silhouette use a brighter colour, so the mark
167+
// appears as a lighter ASCII silhouette embedded in the dimmer
168+
// field. Two passes keep fillStyle constant per pass.
169+
for (let pass = 0; pass < 2; pass++) {
170+
ctx.fillStyle = pass === 0 ? '#6B6B6B' : '#FFFFFF'
171+
const wantInside = pass === 1
172+
for (let y = 0; y < rows; y++) {
173+
for (let x = 0; x < cols; x++) {
174+
const b = base[y * cols + x]
175+
const inside = b > 0.05
176+
if (inside !== wantInside) continue
177+
const dx = x - cx
178+
const dy = y - cy
179+
const dist = Math.sqrt(dx * dx + dy * dy)
180+
// Two-octave radial wave for organic pulsing.
181+
const wave =
182+
0.5 + 0.5 * Math.sin(dist * 0.42 - phase) * 0.7
183+
+ 0.5 * Math.sin(dist * 0.18 - phase * 0.6) * 0.3
184+
const idx = Math.min(RAMP.length - 1, Math.floor(wave * RAMP.length))
185+
const c = RAMP[idx]
186+
if (c === ' ') continue
187+
ctx.fillText(c, x * cellSize, y * cellSize)
188+
}
168189
}
169190
}
170191
}
@@ -188,7 +209,7 @@ const HeroRuneCanvas = () => {
188209
if (t - last < 80) return
189210
const dt = last === 0 ? 16 : t - last
190211
last = t
191-
phase += dt * 0.003
212+
phase += dt * 0.002
192213
drawFrame(phase)
193214
}
194215
if (!reduceMotion) raf = requestAnimationFrame(tick)

0 commit comments

Comments
 (0)