Skip to content

Commit bbbe3fb

Browse files
committed
Update home page effect
1 parent c17f60c commit bbbe3fb

3 files changed

Lines changed: 357 additions & 65 deletions

File tree

src/components/home/HeroRankMatrix.astro

Lines changed: 143 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ interface Props {
55
cellSize?: number; // cell width & height in SVG units
66
cellGap?: number; // gap between cells
77
cellRadius?: number; // border-radius (SVG rx/ry)
8-
strokeWidth?: number; // stroke thickness for inactive cells
9-
opacityMin?: number; // active cell min opacity (animation start)
10-
opacityMax?: number; // active cell max opacity (animation end)
11-
breatheDuration?: number; // animation cycle in seconds
8+
opacityStart?: number; // resting opacity at load + the field's baseline
9+
opacityMin?: number; // dimmest the wave can drive a cell
10+
opacityMax?: number; // brightest the wave can drive a cell
1211
}
1312
1413
const {
@@ -17,43 +16,45 @@ const {
1716
cellSize = 60,
1817
cellGap = 8,
1918
cellRadius = 1,
20-
strokeWidth = 1.4,
19+
opacityStart = 0.75,
2120
opacityMin = 0.4,
22-
opacityMax = 0.8,
23-
breatheDuration = 8,
21+
opacityMax = 0.85,
2422
} = Astro.props;
2523
2624
// Logo-inspired pattern: matches the trace-studio logo-notext shape
27-
// 1 = filled (active), 0 = empty (inactive)
25+
// 1 = filled (active), 0 = empty. Empty cells are intentionally NOT
26+
// rendered — only the filled cells that form the logo are drawn.
2827
const pattern = [
2928
[1, 1, 0, 0, 1, 1],
3029
[1, 0, 1, 0, 0, 1],
3130
[1, 0, 0, 1, 0, 1],
3231
[1, 1, 0, 0, 1, 1],
3332
];
3433
35-
type Cell = {
36-
row: number;
37-
col: number;
38-
active: boolean;
39-
delay: number;
40-
};
34+
type Cell = { row: number; col: number };
4135
4236
const cells: Cell[] = [];
4337
4438
for (let row = 0; row < rows; row++) {
4539
for (let col = 0; col < cols; col++) {
46-
const active = pattern[row]?.[col] === 1;
47-
const delay = ((row * 5 + col * 3) % 11) * 0.7;
48-
cells.push({ row, col, active, delay });
40+
if (pattern[row]?.[col] !== 1) continue; // skip empty cells
41+
cells.push({ row, col });
4942
}
5043
}
5144
5245
const svgW = cols * (cellSize + cellGap) - cellGap;
5346
const svgH = rows * (cellSize + cellGap) - cellGap;
5447
---
5548

56-
<div class="rank-matrix" aria-hidden="true">
49+
<div
50+
class="rank-matrix"
51+
aria-hidden="true"
52+
data-cols={cols}
53+
data-rows={rows}
54+
data-op-start={opacityStart}
55+
data-op-min={opacityMin}
56+
data-op-max={opacityMax}
57+
>
5758
<svg
5859
viewBox={`0 0 ${svgW} ${svgH}`}
5960
xmlns="http://www.w3.org/2000/svg"
@@ -66,28 +67,17 @@ const svgH = rows * (cellSize + cellGap) - cellGap;
6667
height={cellSize}
6768
rx={cellRadius}
6869
ry={cellRadius}
69-
fill={cell.active ? "currentColor" : "none"}
70-
stroke={cell.active ? "none" : "currentColor"}
71-
stroke-width={cell.active ? "0" : String(strokeWidth)}
72-
class:list={[
73-
"rank-matrix__cell",
74-
{ "rank-matrix__cell--active": cell.active },
75-
]}
76-
style={cell.active
77-
? `opacity: ${opacityMin}; animation-delay: ${cell.delay}s`
78-
: undefined}
70+
fill="currentColor"
71+
class="rank-matrix__cell--active"
72+
data-col={cell.col}
73+
data-row={cell.row}
74+
style={`opacity: ${opacityStart}`}
7975
/>
8076
))}
8177
</svg>
8278
</div>
8379

84-
<style
85-
define:vars={{
86-
opacityMin: String(opacityMin),
87-
opacityMax: String(opacityMax),
88-
breatheDuration: `${breatheDuration}s`,
89-
}}
90-
>
80+
<style>
9181
.rank-matrix {
9282
width: 100%;
9383
max-width: 360px;
@@ -98,27 +88,127 @@ const svgH = rows * (cellSize + cellGap) - cellGap;
9888
width: 100%;
9989
height: auto;
10090
}
91+
</style>
10192

102-
.rank-matrix__cell {
103-
opacity: 0.05;
104-
}
93+
<script>
94+
// ── Coordinated "water surface" shimmer for the logo matrix ────────────
95+
// Per-cell independent breathing looks ragged. Instead, a couple of virtual
96+
// points drift across the logo and each cell's opacity is a smooth function
97+
// of its distance to those points — so brightness ripples across the matrix
98+
// like light moving on water.
99+
//
100+
// Performance: only ~14 cells × 2 points per frame, and the rAF loop runs
101+
// ONLY while the logo is on-screen (IntersectionObserver) and motion is
102+
// allowed (prefers-reduced-motion disables it entirely → static at rest).
103+
const root = document.querySelector<HTMLElement>(".rank-matrix");
104+
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
105105

106-
.rank-matrix__cell--active {
107-
opacity: var(--opacityMin);
108-
}
106+
if (root && !reduceMotion) {
107+
const cellEls = Array.from(
108+
root.querySelectorAll<SVGRectElement>(".rank-matrix__cell--active"),
109+
);
109110

110-
@media (prefers-reduced-motion: no-preference) {
111-
.rank-matrix__cell--active {
112-
animation: cell-breathe var(--breatheDuration) ease-in-out infinite alternate;
113-
}
114-
}
111+
if (cellEls.length) {
112+
const cols = Number(root.dataset.cols) || 6;
113+
const rows = Number(root.dataset.rows) || 4;
114+
const opStart = Number(root.dataset.opStart) || 0.75;
115+
const opMin = Number(root.dataset.opMin) || 0.4;
116+
const opMax = Number(root.dataset.opMax) || 0.85;
115117

116-
@keyframes cell-breathe {
117-
0% {
118-
opacity: var(--opacityMin);
119-
}
120-
100% {
121-
opacity: var(--opacityMax);
118+
// ── Tuning knobs ──────────────────────────────────────────────────
119+
const RADIUS = 2.2; // falloff radius in grid units (how wide each blob is)
120+
const AMP = 0.32; // brightness push at a point's centre
121+
const SPEED = 1.0; // drift speed, grid units / second (lower = calmer)
122+
const INTRO_MS = 800; // fade the effect in from the resting state
123+
const R2 = RADIUS * RADIUS;
124+
125+
const cells = cellEls.map((el) => ({
126+
el,
127+
col: Number(el.dataset.col),
128+
row: Number(el.dataset.row),
129+
}));
130+
131+
type Pt = { x: number; y: number; tx: number; ty: number; sign: number };
132+
const rnd = (a: number, b: number) => a + Math.random() * (b - a);
133+
const retarget = (p: Pt) => {
134+
// Aim slightly past the edges so border cells also reach the extremes.
135+
p.tx = rnd(-0.5, cols - 0.5);
136+
p.ty = rnd(-0.5, rows - 0.5);
137+
};
138+
// One brightening point, one dimming point → crests and troughs glide.
139+
const points: Pt[] = [
140+
{ x: rnd(0, cols), y: rnd(0, rows), tx: 0, ty: 0, sign: 1 },
141+
{ x: rnd(0, cols), y: rnd(0, rows), tx: 0, ty: 0, sign: -1 },
142+
];
143+
points.forEach(retarget);
144+
145+
let raf = 0;
146+
let prev = 0;
147+
let t0 = 0;
148+
149+
function tick(now: number) {
150+
if (!t0) t0 = now;
151+
const dt = prev ? Math.min((now - prev) / 1000, 0.05) : 0.016;
152+
prev = now;
153+
154+
// Ease the whole effect in from the flat resting state on (re)start.
155+
const intro = Math.min(1, (now - t0) / INTRO_MS);
156+
const amp = AMP * (intro * intro * (3 - 2 * intro)); // smoothstep
157+
158+
// Drift each point toward its target; pick a new one on arrival.
159+
for (const p of points) {
160+
const dx = p.tx - p.x;
161+
const dy = p.ty - p.y;
162+
const dist = Math.hypot(dx, dy);
163+
if (dist < 0.15) {
164+
retarget(p);
165+
} else {
166+
const step = Math.min(SPEED * dt, dist);
167+
p.x += (dx / dist) * step;
168+
p.y += (dy / dist) * step;
169+
}
170+
}
171+
172+
// Sample the distance field at each cell centre.
173+
for (const c of cells) {
174+
let v = opStart;
175+
for (const p of points) {
176+
const dx = c.col - p.x;
177+
const dy = c.row - p.y;
178+
v += p.sign * amp * Math.exp(-(dx * dx + dy * dy) / R2);
179+
}
180+
v = v < opMin ? opMin : v > opMax ? opMax : v;
181+
c.el.style.opacity = v.toFixed(3);
182+
}
183+
184+
raf = requestAnimationFrame(tick);
185+
}
186+
187+
function start() {
188+
if (!raf) {
189+
prev = 0;
190+
t0 = 0;
191+
raf = requestAnimationFrame(tick);
192+
}
193+
}
194+
function stop() {
195+
if (raf) {
196+
cancelAnimationFrame(raf);
197+
raf = 0;
198+
}
199+
}
200+
201+
// Animate only while the logo is actually in view.
202+
const io = new IntersectionObserver(
203+
(entries) => {
204+
for (const e of entries) {
205+
if (e.isIntersecting) start();
206+
else stop();
207+
}
208+
},
209+
{ threshold: 0 },
210+
);
211+
io.observe(root);
122212
}
123213
}
124-
</style>
214+
</script>

src/components/report/BarChart.astro

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,28 @@ const ariaLabel = `Bar chart: ${data
4949
</div>
5050

5151
<style>
52+
/* One grid for the WHOLE chart (not per-row) so every bar shares a common
53+
label-column width — and therefore a common left edge. Previously each
54+
row was its own grid, so the `auto` label column sized to that row's own
55+
text: longer labels pushed their bar rightward. And because the labels
56+
use a web font, whether a label exceeded the min-width flipped with
57+
font-load timing, so the misalignment surfaced only on some refreshes. */
5258
.bar-chart {
53-
display: flex;
54-
flex-direction: column;
55-
gap: var(--space-3);
59+
display: grid;
60+
grid-template-columns: minmax(6rem, max-content) 1fr minmax(3rem, max-content);
61+
align-items: center;
62+
column-gap: var(--space-4);
63+
row-gap: var(--space-3);
5664
width: 100%;
5765
padding: var(--space-6);
5866
border: var(--guideline-width) solid var(--guideline-color);
5967
background: var(--c-gray-50);
6068
}
6169

70+
/* Rows are transparent: their three children join the parent grid so the
71+
columns line up across every row. */
6272
.bar-chart__row {
63-
display: grid;
64-
grid-template-columns: minmax(6rem, auto) 1fr minmax(3rem, auto);
65-
align-items: center;
66-
gap: var(--space-4);
73+
display: contents;
6774
}
6875

6976
.bar-chart__label {
@@ -94,11 +101,8 @@ const ariaLabel = `Bar chart: ${data
94101
@media (max-width: 480px) {
95102
.bar-chart {
96103
padding: var(--space-4);
97-
}
98-
99-
.bar-chart__row {
100-
grid-template-columns: minmax(4.5rem, auto) 1fr minmax(2.5rem, auto);
101-
gap: var(--space-2);
104+
grid-template-columns: minmax(4.5rem, max-content) 1fr minmax(2.5rem, max-content);
105+
column-gap: var(--space-2);
102106
}
103107
}
104108
</style>

0 commit comments

Comments
 (0)