refactor(helix): nodes on rungs, not strand peaks
Each project's node now sits at the rung centre (x = CX) instead of at its strand's instantaneous extreme. The "line in between" the strands becomes the natural place for the project marker — true to the base-pair metaphor — and frees Y positions from having to coincide with strand-peak intervals. What this unlocks - Slot Y is decoupled from the wave period. SLOT_HEIGHT drops from 240 → 180 and the wave period from SLOT_HEIGHT*2 → a fixed 320, giving the helix a denser twist without re-spacing the cards. - Cards pack tighter (TOP/BOTTOM padding 120 → 80, summary clamp 3 → 2 lines, padding shaved). 3 projects now fit in ~620 px of helix section instead of ~960 px — empty space goes away. Visual additions - Per-project base-pair: a prominent strand-coloured line at each node's Y, drawn from strand A's x to strand B's x. This is the rung the node sits on. - Connector dash line continues from the node centre outward to the card edge. Side mapping - Strand identity is now purely the card's side + stripe + badge: A → left, B → right. (Previously the side followed the strand's instantaneous extreme, which gave inconsistent groupings.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,12 +39,15 @@
|
||||
// Geometry (user-space units inside the SVG)
|
||||
const W = 1000;
|
||||
const CX = W / 2;
|
||||
const AMP = 140;
|
||||
const TOP_PAD = 120;
|
||||
const BOTTOM_PAD = 120;
|
||||
const SLOT_HEIGHT = 240;
|
||||
const AMP = 130;
|
||||
const TOP_PAD = 80;
|
||||
const BOTTOM_PAD = 80;
|
||||
const SLOT_HEIGHT = 180;
|
||||
|
||||
const PERIOD = SLOT_HEIGHT * 2;
|
||||
// Wave period decoupled from slot spacing — nodes are at rung centers, so
|
||||
// they don't need to line up with strand extremes. Pick a period that
|
||||
// gives the helix a pleasant twist density.
|
||||
const PERIOD = 320;
|
||||
const k = (2 * Math.PI) / PERIOD;
|
||||
|
||||
type Rung = { y: number; x1: number; x2: number; depth: number; aFront: boolean };
|
||||
@@ -52,6 +55,11 @@
|
||||
project: StrandProject;
|
||||
index: number;
|
||||
y: number;
|
||||
/** strand A x at the slot's y (where the node's rung meets strand A) */
|
||||
strandAx: number;
|
||||
/** strand B x at the slot's y (where the node's rung meets strand B) */
|
||||
strandBx: number;
|
||||
/** node always sits at the rung centre (x = CX) */
|
||||
nodeX: number;
|
||||
side: 'left' | 'right';
|
||||
};
|
||||
@@ -98,15 +106,21 @@
|
||||
|
||||
const slots = $derived<Slot[]>(
|
||||
projects.map((p, i) => {
|
||||
const y = TOP_PAD + i * SLOT_HEIGHT;
|
||||
const sign = p.strand === 'A' ? 1 : -1;
|
||||
const nodeX = CX + sign * AMP * Math.sin(k * y);
|
||||
// Y positions are slot-based but no longer constrained by the wave
|
||||
// period — nodes sit on rungs (x = CX) at any Y. Tighten the spacing
|
||||
// to let cards pack dynamically.
|
||||
const y = TOP_PAD + i * SLOT_HEIGHT + SLOT_HEIGHT / 2;
|
||||
const sA = Math.sin(k * y);
|
||||
return {
|
||||
project: p,
|
||||
index: i,
|
||||
y,
|
||||
nodeX,
|
||||
side: nodeX > CX ? 'right' : 'left'
|
||||
strandAx: CX + AMP * sA,
|
||||
strandBx: CX - AMP * sA,
|
||||
nodeX: CX,
|
||||
// Side is determined by strand identity, not geometry.
|
||||
// Strand A (Projects) → left, Strand B (Innovations) → right.
|
||||
side: p.strand === 'A' ? 'left' : 'right'
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -211,7 +225,37 @@
|
||||
filter="url(#vhelix-glow)"
|
||||
/>
|
||||
|
||||
<!-- Project nodes -->
|
||||
<!-- Per-project base-pair rungs: strand-to-strand line at the node's Y,
|
||||
drawn in the project's strand colour. This is the "line in between"
|
||||
the strands that each node sits on. -->
|
||||
{#each slots as s}
|
||||
<line
|
||||
x1={s.strandAx}
|
||||
y1={s.y}
|
||||
x2={s.strandBx}
|
||||
y2={s.y}
|
||||
stroke={s.project.strand === 'A' ? '#1fa0db' : '#bed137'}
|
||||
stroke-opacity="0.75"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<!-- Connector lines: from the node (rung centre) outward to the card edge -->
|
||||
{#each slots as s}
|
||||
<line
|
||||
x1={s.nodeX}
|
||||
y1={s.y}
|
||||
x2={s.side === 'right' ? W - 70 : 70}
|
||||
y2={s.y}
|
||||
stroke={s.project.strand === 'A' ? '#1fa0db' : '#bed137'}
|
||||
stroke-opacity="0.35"
|
||||
stroke-width="1"
|
||||
stroke-dasharray="2 4"
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<!-- Project nodes — always at rung centre (x = CX) -->
|
||||
{#each slots as s}
|
||||
<g class="vnode" data-strand={s.project.strand}>
|
||||
<circle
|
||||
@@ -238,20 +282,6 @@
|
||||
/>
|
||||
</g>
|
||||
{/each}
|
||||
|
||||
<!-- Connector dash lines from node to card -->
|
||||
{#each slots as s}
|
||||
<line
|
||||
x1={s.nodeX}
|
||||
y1={s.y}
|
||||
x2={s.side === 'right' ? W - 80 : 80}
|
||||
y2={s.y}
|
||||
stroke={s.project.strand === 'A' ? '#1fa0db' : '#bed137'}
|
||||
stroke-opacity="0.32"
|
||||
stroke-width="1"
|
||||
stroke-dasharray="2 4"
|
||||
/>
|
||||
{/each}
|
||||
</svg>
|
||||
|
||||
<!-- HTML lab-slide cards layered over the SVG -->
|
||||
@@ -331,8 +361,8 @@
|
||||
.slide {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
max-width: calc(50% - 90px);
|
||||
padding: 1.1rem 1.25rem 0.95rem;
|
||||
max-width: calc(50% - 60px);
|
||||
padding: 0.95rem 1.15rem 0.85rem;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in oklab, var(--color-helix-bg-2) 88%, transparent);
|
||||
border: 1px solid var(--color-helix-border);
|
||||
@@ -455,12 +485,12 @@
|
||||
|
||||
.summary {
|
||||
color: var(--color-helix-ink-dim);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 0.7rem;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.45;
|
||||
margin: 0 0 0.55rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user