Files
cs249r_book/tinytorch/site/extra/community/modules/cloud.js
2026-01-20 07:46:18 -05:00

230 lines
6.4 KiB
JavaScript

// Satellite Configuration
const satelliteConfig = {
radius: 90,
rotationSpeed: 0.005,
tilt: 0.3
};
let satelliteGroup;
let satelliteBody;
let solarPanels;
let satUsers = [];
let tooltipSelection;
let currentAngle = 0;
let originX = 0;
let originY = 0;
export function initCloud(svgSelection, width, height, tooltipSel) {
// Store origin for animation loop
originX = width - 180;
originY = height / 2;
tooltipSelection = tooltipSel;
satelliteGroup = svgSelection.append('g')
.attr("transform", `translate(${originX}, ${originY})`)
.style("cursor", "pointer");
// Draw Satellite Structure (Outline Style)
solarPanels = satelliteGroup.append("g").attr("class", "solar-panels");
const panelWidth = 75;
const panelHeight = 30;
// Left Panel
solarPanels.append("rect")
.attr("x", -panelWidth - 35)
.attr("y", -panelHeight/2)
.attr("width", panelWidth)
.attr("height", panelHeight)
.attr("fill", "rgba(255,255,255,0.1)")
.attr("stroke", "#333")
.attr("stroke-width", 1.5)
.attr("rx", 2);
// Right Panel
solarPanels.append("rect")
.attr("x", 35)
.attr("y", -panelHeight/2)
.attr("width", panelWidth)
.attr("height", panelHeight)
.attr("fill", "rgba(255,255,255,0.1)")
.attr("stroke", "#333")
.attr("stroke-width", 1.5)
.attr("rx", 2);
// Connecting Rod
solarPanels.append("line")
.attr("x1", -panelWidth - 35)
.attr("x2", panelWidth + 35)
.attr("y1", 0)
.attr("y2", 0)
.attr("stroke", "#555")
.attr("stroke-width", 2);
// Central Body
satelliteBody = satelliteGroup.append("circle")
.attr("r", 35)
.attr("fill", "#fff")
.attr("stroke", "#333")
.attr("stroke-width", 2);
// Detail Ring
satelliteGroup.append("circle")
.attr("r", 18)
.attr("fill", "none")
.attr("stroke", "#777")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "3,3");
// Hint Line Group (Vertical line + label) - Static
const hintGroup = svgSelection.append("g")
.attr("transform", `translate(${originX - 130}, ${originY})`); // Position to the left
// Vertical line
hintGroup.append("line")
.attr("x1", 0).attr("y1", -40)
.attr("x2", 0).attr("y2", 40)
.attr("stroke", "#ccc")
.attr("stroke-width", 1);
// Top Serif
hintGroup.append("line")
.attr("x1", -3).attr("y1", -40)
.attr("x2", 3).attr("y2", -40)
.attr("stroke", "#ccc")
.attr("stroke-width", 1);
// Bottom Serif
hintGroup.append("line")
.attr("x1", -3).attr("y1", 40)
.attr("x2", 3).attr("y2", 40)
.attr("stroke", "#ccc")
.attr("stroke-width", 1);
// Location Update Hint Box
const noteGroup = hintGroup.append("g")
.attr("transform", "translate(-105, -23)");
// Shadow Rectangle (matching Hello World style)
noteGroup.append("rect")
.attr("x", 2)
.attr("y", 2)
.attr("width", 100)
.attr("height", 46)
.attr("fill", "rgba(255, 102, 0, 0.3)");
// Background Box
noteGroup.append("rect")
.attr("width", 100)
.attr("height", 46)
.attr("fill", "#fff")
.attr("stroke", "#555")
.attr("stroke-width", 0.8)
.attr("rx", 0);
// Text Content
const textElem = noteGroup.append("text")
.attr("x", 5)
.attr("y", 12)
.style("font-family", "Courier New")
.style("font-size", "7px")
.style("font-weight", "bold")
.style("fill", "#333")
.style("pointer-events", "all");
textElem.append("tspan")
.attr("x", 5)
.attr("dy", 0)
.style("fill", "#777")
.text("Launched: 12.2025 🔥");
textElem.append("tspan")
.attr("x", 5)
.attr("dy", 12)
.style("fill", "#333")
.text("Update location in");
const linkTspan = textElem.append("tspan")
.attr("x", 5)
.attr("dy", 10)
.style("fill", "#ff6600")
.style("text-decoration", "underline")
.style("cursor", "pointer")
.text("your profile")
.on("click", () => {
const authBtn = document.getElementById('authBtn');
if (authBtn) authBtn.click();
});
textElem.append("tspan")
.style("fill", "#333")
.style("text-decoration", "none")
.text("");
}
export function updateCloudUsers(users) {
satUsers = users.map((d, i) => {
// Random distribution inside the habitat
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * 25; // Keep within body r=35
return {
...d,
lx: Math.cos(angle) * radius,
ly: Math.sin(angle) * radius
};
});
const markers = satelliteGroup.selectAll('circle.sat-marker')
.data(satUsers, d => d.user);
markers.exit().remove();
markers.enter()
.append('circle')
.attr('class', 'sat-marker')
.attr('r', 3)
.attr("fill", "#2ecc71")
.attr("stroke", "#fff")
.attr("stroke-width", 1)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
d3.select(this).attr("r", 5);
showSatTooltip(event, d);
})
.on("mouseout", function() {
d3.select(this).attr("r", 3);
tooltipSelection.style("opacity", 0);
});
}
export function animateCloud() {
currentAngle += satelliteConfig.rotationSpeed;
// Rocking animation
const deg = Math.sin(currentAngle) * 15;
// Use stored origin coordinates to prevent jumping
satelliteGroup.attr("transform", `translate(${originX}, ${originY}) rotate(${deg})`);
// Update markers
const markers = satelliteGroup.selectAll('circle.sat-marker');
markers
.attr("cx", d => d.lx)
.attr("cy", d => d.ly);
}
function showSatTooltip(event, d) {
tooltipSelection.html(`
<h3>${d.displayName}</h3>
<div class="info-row">Status: <span class="highlight">${d.completed}</span></div>
<div class="info-row"><i>${d.institution}</i></div>
<div class="info-row" style="color:#2ecc71;">🛰️ Unknown Location</div>
`);
tooltipSelection
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 15) + "px")
.style("opacity", 1);
}