mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-05-05 00:58:56 -05:00
style: apply consistent whitespace and formatting across codebase
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
font-family: 'Georgia', 'Times New Roman', Times, serif;
|
||||
color: #333;
|
||||
/* Default to hidden for desktop app-feel, changed via JS for mobile */
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
canvas {
|
||||
@@ -65,22 +65,22 @@
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
color: #333;
|
||||
pointer-events: auto;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s ease, transform 0.3s ease;
|
||||
width: 140px;
|
||||
width: 140px;
|
||||
border-radius: 8px;
|
||||
/* Fix: Start off-screen to prevent bunching at 0,0 */
|
||||
top: -1000px;
|
||||
left: -1000px;
|
||||
}
|
||||
|
||||
|
||||
.milestone-label:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
transform: scale(1.05);
|
||||
z-index: 10;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.milestone-label.visible {
|
||||
@@ -89,11 +89,11 @@
|
||||
|
||||
/* Fix: Only apply opacity to future labels when they are also visible */
|
||||
.milestone-label.future.visible {
|
||||
opacity: 0.4;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
|
||||
.milestone-label.future:hover {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.milestone-label h3 {
|
||||
@@ -105,19 +105,19 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
.milestone-label span {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
color: #ff6600;
|
||||
color: #ff6600;
|
||||
margin-bottom: 2px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
||||
.milestone-label.future span {
|
||||
color: #999;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.milestone-label .desc {
|
||||
@@ -167,7 +167,7 @@
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
#modal-overlay.active #modal-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
@@ -182,7 +182,7 @@
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
#modal-close:hover {
|
||||
color: #333;
|
||||
}
|
||||
@@ -215,7 +215,7 @@
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #eee;
|
||||
@@ -231,13 +231,13 @@
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
|
||||
/* New Community Section Styles */
|
||||
.community-section {
|
||||
border-top: 2px solid #eee;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
|
||||
.community-header {
|
||||
font-size: 1.1rem;
|
||||
color: #222;
|
||||
@@ -274,7 +274,7 @@
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
/* Mobile Specifics */
|
||||
@media (max-width: 768px) {
|
||||
#ui-layer {
|
||||
@@ -307,7 +307,7 @@
|
||||
<div class="modal-subtitle" id="m-subtitle">Module ID</div>
|
||||
<span class="modal-year" id="m-year">Approximate Era</span>
|
||||
<p id="m-desc">Description</p>
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -338,7 +338,7 @@
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const labelsContainer = document.getElementById('labels-container');
|
||||
|
||||
|
||||
// Modal Elements
|
||||
const modalOverlay = document.getElementById('modal-overlay');
|
||||
const modalClose = document.getElementById('modal-close');
|
||||
@@ -353,117 +353,117 @@
|
||||
let isMobile = false;
|
||||
let drawingProgress = 0; // 0 to 1
|
||||
let animationId;
|
||||
let hoveredNodeIndex = -1;
|
||||
|
||||
let hoveredNodeIndex = -1;
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
lineColor: '#ff6600',
|
||||
futureColor: '#cccccc',
|
||||
baseLineWidth: 1,
|
||||
maxLineWidth: 6,
|
||||
speed: 0.003,
|
||||
padding: 60,
|
||||
baseLineWidth: 1,
|
||||
maxLineWidth: 6,
|
||||
speed: 0.003,
|
||||
padding: 60,
|
||||
currentStageIndex: 8 // Module 09
|
||||
};
|
||||
|
||||
// Data: 20 Modules
|
||||
const milestones = [
|
||||
{
|
||||
{
|
||||
id: "01_tensor", title: "Tensors", year: "1900s", val: 0.05,
|
||||
desc: "Foundation of N-dimensional arrays.",
|
||||
people: [{name: "Gregorio Ricci-Curbastro", role: "Tensor Calculus"}, {name: "Bernhard Riemann", role: "Manifolds"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "02_activations", title: "Activations", year: "1940s", val: 0.10,
|
||||
desc: "ReLU, Sigmoid, Softmax.",
|
||||
people: [{name: "McCulloch & Pitts", role: "Threshold Logic"}, {name: "Vinod Nair", role: "Rectified Linear Units (2010)"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "03_layers", title: "Linear Layers", year: "1958", val: 0.15,
|
||||
desc: "The Perceptron & Dense connections.",
|
||||
people: [{name: "Frank Rosenblatt", role: "The Perceptron"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "04_losses", title: "Loss Functions", year: "1800s", val: 0.18,
|
||||
desc: "MSE, CrossEntropy, Optimization targets.",
|
||||
people: [{name: "Adrien-Marie Legendre", role: "Least Squares"}, {name: "C.E. Shannon", role: "Information Entropy"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "05_autograd", title: "Autograd", year: "1970", val: 0.22,
|
||||
desc: "Automatic Differentiation.",
|
||||
people: [{name: "Seppo Linnainmaa", role: "Reverse Mode AD"}, {name: "Rumelhart et al.", role: "Applied to NN"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "06_optimizers", title: "Optimizers", year: "1951", val: 0.26,
|
||||
desc: "SGD, Adam, Scheduling.",
|
||||
people: [{name: "Robbins & Monro", role: "Stochastic Approximation"}, {name: "Kingma & Ba", role: "Adam (2014)"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "07_training", title: "Training Loops", year: "1986", val: 0.30,
|
||||
desc: "The learning cycle logic.",
|
||||
people: [{name: "Geoffrey Hinton", role: "Backpropagation"}, {name: "Yann LeCun", role: "Early frameworks"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "08_dataloader", title: "Data Pipelines", year: "2010s", val: 0.35,
|
||||
desc: "Efficient batching and pre-fetching.",
|
||||
people: [{name: "Jeff Dean", role: "DistBelief/MapReduce"}, {name: "PyTorch Team", role: "DataLoader utility"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "09_spatial", title: "Spatial (CNN)", year: "1998", val: 0.40,
|
||||
desc: "Conv2d, MaxPool, Vision.",
|
||||
people: [{name: "Yann LeCun", role: "LeNet-5"}, {name: "Alex Krizhevsky", role: "AlexNet (2012)"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "10_tokenization", title: "Tokenization", year: "1950s", val: 0.45,
|
||||
desc: "BPE, WordPiece, Text processing.",
|
||||
people: [{name: "Noam Chomsky", role: "Formal Grammars"}, {name: "Philip Gage", role: "Byte Pair Encoding (1994)"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "11_embeddings", title: "Embeddings", year: "2013", val: 0.50,
|
||||
desc: "Vector representations of meaning.",
|
||||
people: [{name: "Tomas Mikolov", role: "Word2Vec"}, {name: "Yoshua Bengio", role: "Neural Probabilistic Language Model"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "12_attention", title: "Attention", year: "2014", val: 0.55,
|
||||
desc: "Focusing on relevant context.",
|
||||
people: [{name: "Dzmitry Bahdanau", role: "Neural Machine Translation"}, {name: "KyungHyun Cho", role: "Encoder-Decoder"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "13_transformers", title: "Transformers", year: "2017", val: 0.60,
|
||||
desc: "Self-attention blocks.",
|
||||
people: [{name: "Ashish Vaswani", role: "Attention Is All You Need"}, {name: "Noam Shazeer", role: "Architecture Scaling"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "14_profiling", title: "Profiling", year: "2018", val: 0.65,
|
||||
desc: "Bottleneck analysis.",
|
||||
people: [{name: "SysML Community", role: "ML Systems"}, {name: "NVIDIA", role: "Nsight Systems"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "15_quantization", title: "Quantization", year: "2017", val: 0.70,
|
||||
desc: "INT8, FP16, precision reduction.",
|
||||
people: [{name: "Song Han", role: "Deep Compression"}, {name: "Google/Qualcomm", role: "Mobile Inference"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "16_compression", title: "Compression", year: "2016", val: 0.75,
|
||||
desc: "Pruning & Distillation.",
|
||||
people: [{name: "Geoffrey Hinton", role: "Distillation"}, {name: "Lottery Ticket Hypothesis", role: "Frankle & Carbin"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "17_memoization", title: "Memoization", year: "2022", val: 0.80,
|
||||
desc: "KV-Cache for fast inference.",
|
||||
people: [{name: "Pope et al.", role: "Efficiently Scaling Transformers"}, {name: "vLLM Team", role: "PagedAttention"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "18_acceleration", title: "Acceleration", year: "2016", val: 0.85,
|
||||
desc: "Hardware optimization (TPU/GPU).",
|
||||
people: [{name: "NVIDIA", role: "CUDA/Tensor Cores"}, {name: "Google", role: "TPU Architecture"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "19_benchmarking", title: "Benchmarking", year: "2018", val: 0.90,
|
||||
desc: "Performance measurement.",
|
||||
people: [{name: "MLCommons", role: "MLPerf"}, {name: "Stanford DAWN", role: "DawnBench"}]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "20_capstone", title: "Capstone", year: "2024", val: 1.00,
|
||||
desc: "Complete End-to-End System.",
|
||||
people: [{name: "The Student", role: "System Architect"}, {name: "Open Source", role: "Ecosystem"}]
|
||||
@@ -473,26 +473,26 @@
|
||||
// Assign pct and currentRadius
|
||||
milestones.forEach((m, i) => {
|
||||
m.pct = i / (milestones.length - 1);
|
||||
m.currentRadius = isMobile ? 8 : 4;
|
||||
m.currentRadius = isMobile ? 8 : 4;
|
||||
});
|
||||
|
||||
// --- RANDOM BUILDER GENERATOR ---
|
||||
const adjectives = ["Cyber", "Neural", "Torch", "Data", "Gradient", "Tensor", "Hyper", "Deep", "Logic", "Binary"];
|
||||
const nouns = ["Ninja", "Wizard", "Coder", "Surfer", "Explorer", "Smith", "Hacker", "Architect", "Voyager", "Mind"];
|
||||
|
||||
|
||||
function generateBuilders() {
|
||||
const count = Math.floor(Math.random() * 4) + 2; // 2 to 5 builders
|
||||
let html = '';
|
||||
for(let i=0; i<count; i++) {
|
||||
const name = adjectives[Math.floor(Math.random()*adjectives.length)] +
|
||||
nouns[Math.floor(Math.random()*nouns.length)] +
|
||||
const name = adjectives[Math.floor(Math.random()*adjectives.length)] +
|
||||
nouns[Math.floor(Math.random()*nouns.length)] +
|
||||
Math.floor(Math.random()*100);
|
||||
|
||||
|
||||
// Random date within last 6 months
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 180));
|
||||
const dateStr = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
||||
|
||||
|
||||
html += `
|
||||
<div class="builder-row">
|
||||
<span class="builder-name">@${name}</span>
|
||||
@@ -510,7 +510,7 @@
|
||||
mSubtitle.textContent = `Module ${data.id}`;
|
||||
mYear.textContent = `Concept Era: ${data.year}`;
|
||||
mDesc.textContent = data.desc;
|
||||
|
||||
|
||||
// Populate Pioneer Table
|
||||
mTableBody.innerHTML = '';
|
||||
data.people.forEach(person => {
|
||||
@@ -559,11 +559,11 @@
|
||||
for (let i = milestones.length - 1; i >= 0; i--) {
|
||||
const m = milestones[i];
|
||||
const pos = getNodePosition(m, i);
|
||||
|
||||
|
||||
// Distance check
|
||||
const dist = Math.sqrt(Math.pow(mx - pos.x, 2) + Math.pow(my - pos.y, 2));
|
||||
|
||||
if (dist < 20) {
|
||||
|
||||
if (dist < 20) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -575,7 +575,7 @@
|
||||
function resize() {
|
||||
// Check mobile breakpoint
|
||||
isMobile = window.innerWidth < 768;
|
||||
|
||||
|
||||
if (isMobile) {
|
||||
// Vertical Spiral Mode
|
||||
width = canvas.width = window.innerWidth;
|
||||
@@ -592,7 +592,7 @@
|
||||
|
||||
createLabels();
|
||||
if (drawingProgress >= 1) {
|
||||
draw();
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,51 +603,51 @@
|
||||
const startY = 150; // Padding top
|
||||
const availableHeight = height - 200;
|
||||
const y = startY + (m.pct * availableHeight);
|
||||
|
||||
|
||||
// X oscillates
|
||||
const centerX = width / 2;
|
||||
const amplitude = (width / 2) - 60; // Keep within edges
|
||||
const freq = 4; // Number of full waves down the page
|
||||
const x = centerX + Math.sin(m.pct * Math.PI * freq) * amplitude;
|
||||
|
||||
|
||||
return { x, y };
|
||||
} else {
|
||||
// Horizontal Graph (Desktop)
|
||||
const startX = config.padding;
|
||||
const totalWidth = width - (config.padding * 2);
|
||||
const x = startX + (totalWidth * m.pct);
|
||||
|
||||
|
||||
// Y calculation (Complexity Curve)
|
||||
// Need to replicate getLineY logic here
|
||||
let progress = (x - startX) / totalWidth;
|
||||
if (progress < 0) progress = 0;
|
||||
if (progress > 1) progress = 1;
|
||||
|
||||
|
||||
// Cosine interp logic simplified for node lookup
|
||||
const baseline = height * 0.9;
|
||||
const maxHeight = height * 0.15;
|
||||
const range = baseline - maxHeight;
|
||||
|
||||
|
||||
// Interpolate value
|
||||
// Since we store val in milestones, we interpolate between them
|
||||
// But for the exact node, we just map m.val
|
||||
// Wait, the curve interpolates, so the node sits exactly on the curve
|
||||
// For the node itself, its 'x' maps exactly to 'm.pct', so its 'val' is exactly 'm.val'
|
||||
// BUT, my getLineY function does smooth interpolation.
|
||||
// BUT, my getLineY function does smooth interpolation.
|
||||
// However, at the exact milestone pct, the value IS m.val.
|
||||
|
||||
|
||||
const y = baseline - (m.val * range);
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper for drawing the continuous line
|
||||
function getPathPoint(progress) {
|
||||
if (isMobile) {
|
||||
const startY = 150;
|
||||
const availableHeight = height - 200;
|
||||
const y = startY + (progress * availableHeight);
|
||||
|
||||
|
||||
const centerX = width / 2;
|
||||
const amplitude = (width / 2) - 60;
|
||||
const freq = 4;
|
||||
@@ -657,7 +657,7 @@
|
||||
const startX = config.padding;
|
||||
const totalWidth = width - (config.padding * 2);
|
||||
const x = startX + (totalWidth * progress);
|
||||
|
||||
|
||||
// Interpolation logic required here for smooth line between nodes
|
||||
// Find p0 and p1
|
||||
let p0 = milestones[0];
|
||||
@@ -671,16 +671,16 @@
|
||||
}
|
||||
const segLen = p1.pct - p0.pct;
|
||||
const local = (progress - p0.pct) / segLen;
|
||||
|
||||
|
||||
// Cosine interp
|
||||
const mu2 = (1 - Math.cos(local * Math.PI)) / 2;
|
||||
const val = (p0.val * (1 - mu2) + p1.val * mu2);
|
||||
|
||||
|
||||
const baseline = height * 0.9;
|
||||
const maxHeight = height * 0.15;
|
||||
const range = baseline - maxHeight;
|
||||
const y = baseline - (val * range);
|
||||
|
||||
|
||||
return {x, y};
|
||||
}
|
||||
}
|
||||
@@ -690,7 +690,7 @@
|
||||
milestones.forEach((m, index) => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'milestone-label';
|
||||
|
||||
|
||||
if (index > config.currentStageIndex) {
|
||||
el.classList.add('future');
|
||||
}
|
||||
@@ -700,12 +700,12 @@
|
||||
<h3>${m.title}</h3>
|
||||
<div class="desc">${m.desc}</div>
|
||||
`;
|
||||
|
||||
|
||||
// Events
|
||||
el.addEventListener('mouseenter', () => { hoveredNodeIndex = index; });
|
||||
el.addEventListener('mouseleave', () => { hoveredNodeIndex = -1; });
|
||||
el.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation();
|
||||
openModal(m);
|
||||
});
|
||||
|
||||
@@ -723,7 +723,7 @@
|
||||
const startX = config.padding;
|
||||
const totalWidth = width - (config.padding * 2);
|
||||
const baseline = height * 0.9;
|
||||
|
||||
|
||||
// Simple Time Axis
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, baseline + 10);
|
||||
@@ -737,15 +737,15 @@
|
||||
drawGrid();
|
||||
|
||||
// Calculate progress limits
|
||||
const totalLen = 1.0;
|
||||
const totalLen = 1.0;
|
||||
const endProgress = drawingProgress; // 0 to 1
|
||||
const splitPct = milestones[config.currentStageIndex].pct;
|
||||
|
||||
// Draw Line
|
||||
// Iteration depends on screen size/resolution.
|
||||
// Iteration depends on screen size/resolution.
|
||||
// We iterate by 't' (0 to 1) instead of pixels to handle both vertical/horizontal uniformly
|
||||
const step = 0.002;
|
||||
|
||||
const step = 0.002;
|
||||
|
||||
for (let t = 0; t < endProgress; t += step) {
|
||||
let nextT = t + step;
|
||||
if (nextT > endProgress) nextT = endProgress;
|
||||
@@ -769,20 +769,20 @@
|
||||
|
||||
if (t < splitPct) {
|
||||
ctx.strokeStyle = config.lineColor;
|
||||
ctx.setLineDash([]);
|
||||
ctx.setLineDash([]);
|
||||
ctx.lineWidth = currentLineWidth;
|
||||
ctx.globalAlpha = 1.0;
|
||||
} else {
|
||||
ctx.strokeStyle = config.futureColor;
|
||||
ctx.setLineDash([2, 5]);
|
||||
ctx.lineWidth = currentLineWidth;
|
||||
ctx.setLineDash([2, 5]);
|
||||
ctx.lineWidth = currentLineWidth;
|
||||
ctx.globalAlpha = 0.5;
|
||||
}
|
||||
|
||||
|
||||
ctx.lineCap = 'round';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
|
||||
ctx.setLineDash([]);
|
||||
ctx.globalAlpha = 1.0;
|
||||
|
||||
@@ -790,12 +790,12 @@
|
||||
if (endProgress >= splitPct) {
|
||||
const pos = getNodePosition(milestones[config.currentStageIndex], config.currentStageIndex);
|
||||
const pulse = Math.sin(Date.now() / 200) * 3;
|
||||
|
||||
|
||||
const isHovered = (config.currentStageIndex === hoveredNodeIndex);
|
||||
const m = milestones[config.currentStageIndex];
|
||||
const targetRadius = isHovered ? 14 : 7;
|
||||
const animSpeed = 0.4;
|
||||
|
||||
const targetRadius = isHovered ? 14 : 7;
|
||||
const animSpeed = 0.4;
|
||||
|
||||
if (m.currentRadius < targetRadius) {
|
||||
m.currentRadius = Math.min(m.currentRadius + animSpeed, targetRadius);
|
||||
} else if (m.currentRadius > targetRadius) {
|
||||
@@ -806,7 +806,7 @@
|
||||
ctx.beginPath();
|
||||
ctx.arc(pos.x, pos.y, m.currentRadius + pulse, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
|
||||
|
||||
ctx.strokeStyle = config.lineColor;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
@@ -817,24 +817,24 @@
|
||||
// Milestone Nodes & Labels
|
||||
milestones.forEach((m, i) => {
|
||||
const pos = getNodePosition(m, i);
|
||||
|
||||
|
||||
// Only draw if line has reached here
|
||||
if (drawingProgress >= m.pct) {
|
||||
|
||||
|
||||
// Skip current node to avoid overlap
|
||||
// Simple distance check from split point
|
||||
// We can check index actually
|
||||
if (i !== config.currentStageIndex) {
|
||||
const isHovered = (i === hoveredNodeIndex);
|
||||
const targetRadius = isHovered ? 12 : (isMobile ? 6 : 4);
|
||||
const targetRadius = isHovered ? 12 : (isMobile ? 6 : 4);
|
||||
const animSpeed = 0.4;
|
||||
|
||||
|
||||
if (m.currentRadius < targetRadius) {
|
||||
m.currentRadius = Math.min(m.currentRadius + animSpeed, targetRadius);
|
||||
} else if (m.currentRadius > targetRadius) {
|
||||
m.currentRadius = Math.max(m.currentRadius - animSpeed, targetRadius);
|
||||
}
|
||||
|
||||
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.strokeStyle = i > config.currentStageIndex ? '#ccc' : '#444';
|
||||
if (isHovered) ctx.strokeStyle = config.lineColor;
|
||||
@@ -850,15 +850,15 @@
|
||||
// Label Positioning
|
||||
if (m.el) {
|
||||
m.el.classList.add('visible');
|
||||
|
||||
|
||||
// Mobile Layout: Labels alternate left/right to save space
|
||||
if (isMobile) {
|
||||
const isLeft = i % 2 === 0;
|
||||
const labelWidth = 120;
|
||||
const offset = 30;
|
||||
|
||||
|
||||
m.el.style.top = `${pos.y - 20}px`;
|
||||
|
||||
|
||||
if (isLeft) {
|
||||
m.el.style.left = `${pos.x - labelWidth - offset}px`;
|
||||
m.el.style.textAlign = 'right';
|
||||
@@ -866,7 +866,7 @@
|
||||
m.el.style.left = `${pos.x + offset}px`;
|
||||
m.el.style.textAlign = 'left';
|
||||
}
|
||||
|
||||
|
||||
// Guide line
|
||||
ctx.strokeStyle = '#eee';
|
||||
ctx.lineWidth = 1;
|
||||
@@ -881,13 +881,13 @@
|
||||
const stagger = (i % 4 === 0 || i % 4 === 3) ? (isTop ? -40 : 40) : 0;
|
||||
const offset = isTop ? -80 : 40;
|
||||
const labelY = pos.y + offset + stagger;
|
||||
|
||||
|
||||
// Guide line
|
||||
ctx.strokeStyle = '#eee';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pos.x, pos.y);
|
||||
ctx.lineTo(pos.x, isTop ? pos.y - 20 : pos.y + 20);
|
||||
ctx.lineTo(pos.x, isTop ? pos.y - 20 : pos.y + 20);
|
||||
ctx.stroke();
|
||||
|
||||
const labelWidth = 140;
|
||||
|
||||
Reference in New Issue
Block a user