viewof energyHarvestingAnimation = {
// ===========================================================================
// ENERGY HARVESTING CYCLE ANIMATION
// ===========================================================================
// Demonstrates IoT energy harvesting through:
// - Multiple energy sources (solar, vibration, thermal, RF)
// - Energy storage (supercapacitor/battery)
// - Voltage regulation (DC-DC converter)
// - Load consumption
// - Energy balance and efficiency metrics
//
// IEEE Color Palette:
// Navy: #2C3E50 (primary, headers)
// Teal: #16A085 (positive energy flow)
// Orange: #E67E22 (highlights, energy)
// Gray: #7F8C8D (neutral, inactive)
// LtGray: #ECF0F1 (backgrounds)
// Green: #27AE60 (storage, positive)
// Red: #E74C3C (consumption, warning)
// ===========================================================================
// ---------------------------------------------------------------------------
// CONFIGURATION
// ---------------------------------------------------------------------------
const config = {
width: 900,
height: 750,
colors: {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
green: "#27AE60",
red: "#E74C3C",
yellow: "#F1C40F",
purple: "#9B59B6"
},
// Energy source characteristics
sources: {
solar: {
name: "Solar Panel",
icon: "sun",
maxPower: 100, // mW at 100% intensity
voltage: 5.0, // V open circuit
color: "#F1C40F",
efficiency: 0.20 // 20% typical
},
vibration: {
name: "Piezoelectric",
icon: "vibration",
maxPower: 10, // mW at 100% intensity
voltage: 12.0, // V peak
color: "#9B59B6",
efficiency: 0.05 // 5% typical
},
thermal: {
name: "Thermoelectric",
icon: "temperature",
maxPower: 30, // mW at 100% intensity
voltage: 3.0, // V
color: "#E74C3C",
efficiency: 0.03 // 3% typical
},
rf: {
name: "RF Harvesting",
icon: "antenna",
maxPower: 1, // mW at 100% intensity
voltage: 2.0, // V
color: "#3498DB",
efficiency: 0.40 // 40% rectenna efficiency
}
},
// Storage characteristics
storage: {
capacitor: {
name: "Supercapacitor",
capacity: 100, // mF (millifarads) = 0.1F
maxVoltage: 5.5, // V
leakage: 0.01 // mA
}
},
// Regulator characteristics
regulator: {
efficiency: 0.85, // 85% typical
outputVoltage: 3.3, // V
minInput: 1.8, // V minimum
maxInput: 5.5 // V maximum
},
timing: {
updateInterval: 50, // ms
flowSpeed: 2 // particle speed
}
};
// ---------------------------------------------------------------------------
// STATE MANAGEMENT
// ---------------------------------------------------------------------------
let state = {
isPlaying: false,
animationFrame: null,
selectedSource: "solar",
sourceIntensity: 50, // 0-100%
loadConsumption: 5, // mA
storedEnergy: 50, // mJ (starting at 50%)
maxStoredEnergy: 100, // mJ (based on capacitor)
storageVoltage: 3.3, // V current storage voltage
harvestedPower: 0, // mW
regulatedCurrent: 0, // mA
systemEfficiency: 0, // %
energyBalance: 0, // mW (harvest - consumption)
flowParticles: [],
time: 0
};
// Calculate max stored energy from capacitor
// E = 0.5 * C * V^2, C in F, V in V, E in J
// For 100mF at 5.5V: E = 0.5 * 0.1 * 5.5^2 = 1.5125 J = 1512.5 mJ
state.maxStoredEnergy = 0.5 * (config.storage.capacitor.capacity / 1000) *
Math.pow(config.storage.capacitor.maxVoltage, 2) * 1000;
// ---------------------------------------------------------------------------
// CREATE CONTAINER
// ---------------------------------------------------------------------------
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", `${config.width}px`)
.style("margin", "0 auto");
// ---------------------------------------------------------------------------
// CONTROL PANEL
// ---------------------------------------------------------------------------
const controlPanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.gray}`);
controlPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "16px")
.text("Energy Harvesting Controls");
const controlGrid = controlPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "15px");
// Energy Source Selector
const sourceGroup = controlGrid.append("div");
sourceGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Energy Source");
const sourceSelect = sourceGroup.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px")
.style("cursor", "pointer");
Object.entries(config.sources).forEach(([key, source]) => {
sourceSelect.append("option")
.attr("value", key)
.attr("selected", key === state.selectedSource ? true : null)
.text(`${source.name} (max ${source.maxPower} mW)`);
});
sourceSelect.on("change", function() {
state.selectedSource = this.value;
updateSourceDisplay();
updateCalculations();
});
// Source Intensity Slider
const intensityGroup = controlGrid.append("div");
intensityGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Source Intensity");
const intensitySlider = intensityGroup.append("input")
.attr("type", "range")
.attr("min", 0)
.attr("max", 100)
.attr("value", state.sourceIntensity)
.style("width", "100%")
.style("cursor", "pointer");
const intensityValue = intensityGroup.append("span")
.style("color", config.colors.orange)
.style("font-weight", "bold")
.style("font-size", "14px")
.text(` ${state.sourceIntensity}%`);
const intensityLabel = intensityGroup.append("div")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-top", "3px")
.text("(Bright sunlight / Strong vibration)");
intensitySlider.on("input", function() {
state.sourceIntensity = +this.value;
intensityValue.text(` ${state.sourceIntensity}%`);
updateIntensityLabel();
updateCalculations();
});
// Load Consumption Slider
const loadGroup = controlGrid.append("div");
loadGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Load Consumption (mA)");
const loadSlider = loadGroup.append("input")
.attr("type", "range")
.attr("min", 0.1)
.attr("max", 50)
.attr("step", 0.1)
.attr("value", state.loadConsumption)
.style("width", "100%")
.style("cursor", "pointer");
const loadValue = loadGroup.append("span")
.style("color", config.colors.red)
.style("font-weight", "bold")
.style("font-size", "14px")
.text(` ${state.loadConsumption} mA`);
const loadPowerLabel = loadGroup.append("div")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-top", "3px");
loadSlider.on("input", function() {
state.loadConsumption = +this.value;
loadValue.text(` ${state.loadConsumption.toFixed(1)} mA`);
updateCalculations();
});
// Storage Level Display
const storageGroup = controlGrid.append("div");
storageGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Storage Level");
const storageBar = storageGroup.append("div")
.style("width", "100%")
.style("height", "24px")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("border", `2px solid ${config.colors.gray}`)
.style("overflow", "hidden")
.style("position", "relative");
const storageFill = storageBar.append("div")
.style("height", "100%")
.style("width", "50%")
.style("background", `linear-gradient(90deg, ${config.colors.green}, ${config.colors.teal})`)
.style("border-radius", "10px")
.style("transition", "width 0.3s ease");
const storageText = storageGroup.append("div")
.style("font-size", "12px")
.style("color", config.colors.navy)
.style("margin-top", "5px")
.style("text-align", "center")
.text("50% (825 mJ)");
// Button Row
const buttonRow = controlPanel.append("div")
.style("display", "flex")
.style("gap", "10px")
.style("margin-top", "15px")
.style("flex-wrap", "wrap");
const playBtn = buttonRow.append("button")
.style("padding", "12px 24px")
.style("background", config.colors.teal)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "8px")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.text("Start Simulation")
.on("click", toggleAnimation)
.on("mouseenter", function() {
d3.select(this).style("background", config.colors.navy);
})
.on("mouseleave", function() {
d3.select(this).style("background", state.isPlaying ? config.colors.orange : config.colors.teal);
});
const resetBtn = buttonRow.append("button")
.style("padding", "12px 24px")
.style("background", config.colors.gray)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "8px")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.text("Reset")
.on("click", resetAnimation)
.on("mouseenter", function() {
d3.select(this).style("background", config.colors.red);
})
.on("mouseleave", function() {
d3.select(this).style("background", config.colors.gray);
});
// Energy Balance Indicator
const balanceDisplay = buttonRow.append("div")
.style("margin-left", "auto")
.style("padding", "10px 20px")
.style("background", config.colors.white)
.style("border-radius", "8px")
.style("border", `2px solid ${config.colors.gray}`)
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px");
balanceDisplay.append("span")
.style("font-size", "12px")
.style("color", config.colors.navy)
.text("Energy Balance:");
const balanceValue = balanceDisplay.append("span")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("color", config.colors.green)
.text("+0.0 mW");
// ---------------------------------------------------------------------------
// SVG CANVAS
// ---------------------------------------------------------------------------
const svg = container.append("svg")
.attr("viewBox", `0 0 ${config.width} ${config.height - 200}`)
.attr("width", "100%")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("border", `2px solid ${config.colors.gray}`);
// Definitions for gradients and markers
const defs = svg.append("defs");
// Arrow marker for flow
defs.append("marker")
.attr("id", "flowArrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 8)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", config.colors.orange);
// Energy gradient
const energyGradient = defs.append("linearGradient")
.attr("id", "energyGradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "0%");
energyGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", config.colors.orange);
energyGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", config.colors.yellow);
// ---------------------------------------------------------------------------
// LAYOUT CONSTANTS
// ---------------------------------------------------------------------------
const layout = {
sourceX: 100,
sourceY: 200,
harvesterX: 280,
harvesterY: 200,
storageX: 460,
storageY: 200,
regulatorX: 640,
regulatorY: 200,
loadX: 820,
loadY: 200,
componentWidth: 100,
componentHeight: 80
};
// ---------------------------------------------------------------------------
// TITLE
// ---------------------------------------------------------------------------
svg.append("text")
.attr("x", config.width / 2)
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "20px")
.attr("font-weight", "bold")
.text("Energy Harvesting System");
svg.append("text")
.attr("x", config.width / 2)
.attr("y", 55)
.attr("text-anchor", "middle")
.attr("fill", config.colors.gray)
.attr("font-size", "14px")
.text("Harvest \u2192 Store \u2192 Regulate \u2192 Consume");
// ---------------------------------------------------------------------------
// DRAW SYSTEM COMPONENTS
// ---------------------------------------------------------------------------
// Energy Source
const sourceGroup2 = svg.append("g")
.attr("class", "energy-source")
.attr("transform", `translate(${layout.sourceX}, ${layout.sourceY})`);
sourceGroup2.append("rect")
.attr("x", -50)
.attr("y", -40)
.attr("width", layout.componentWidth)
.attr("height", layout.componentHeight)
.attr("rx", 10)
.attr("fill", config.colors.white)
.attr("stroke", config.colors.yellow)
.attr("stroke-width", 3)
.attr("class", "source-box");
const sourceIcon = sourceGroup2.append("g")
.attr("class", "source-icon");
// Solar icon (default)
drawSolarIcon(sourceIcon);
sourceGroup2.append("text")
.attr("y", 55)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("class", "source-label")
.text("Solar Panel");
const sourcePowerText = sourceGroup2.append("text")
.attr("y", 72)
.attr("text-anchor", "middle")
.attr("fill", config.colors.orange)
.attr("font-size", "11px")
.attr("class", "source-power")
.text("50 mW");
// Harvester/Converter
const harvesterGroup = svg.append("g")
.attr("transform", `translate(${layout.harvesterX}, ${layout.harvesterY})`);
harvesterGroup.append("rect")
.attr("x", -50)
.attr("y", -40)
.attr("width", layout.componentWidth)
.attr("height", layout.componentHeight)
.attr("rx", 10)
.attr("fill", config.colors.white)
.attr("stroke", config.colors.teal)
.attr("stroke-width", 3);
harvesterGroup.append("text")
.attr("y", -15)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text("DC-DC");
harvesterGroup.append("text")
.attr("y", 5)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text("Converter");
// MPPT indicator
harvesterGroup.append("rect")
.attr("x", -35)
.attr("y", 12)
.attr("width", 70)
.attr("height", 18)
.attr("rx", 4)
.attr("fill", config.colors.teal);
harvesterGroup.append("text")
.attr("y", 25)
.attr("text-anchor", "middle")
.attr("fill", config.colors.white)
.attr("font-size", "9px")
.attr("font-weight", "bold")
.text("MPPT");
harvesterGroup.append("text")
.attr("y", 55)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Harvester");
const harvesterPowerText = harvesterGroup.append("text")
.attr("y", 72)
.attr("text-anchor", "middle")
.attr("fill", config.colors.teal)
.attr("font-size", "11px")
.text("42.5 mW");
// Storage (Supercapacitor)
const storageGroup2 = svg.append("g")
.attr("transform", `translate(${layout.storageX}, ${layout.storageY})`);
// Capacitor symbol
storageGroup2.append("rect")
.attr("x", -50)
.attr("y", -40)
.attr("width", layout.componentWidth)
.attr("height", layout.componentHeight)
.attr("rx", 10)
.attr("fill", config.colors.white)
.attr("stroke", config.colors.green)
.attr("stroke-width", 3);
// Battery level visualization
const batteryContainer = storageGroup2.append("g");
batteryContainer.append("rect")
.attr("x", -30)
.attr("y", -25)
.attr("width", 60)
.attr("height", 40)
.attr("rx", 4)
.attr("fill", config.colors.lightGray)
.attr("stroke", config.colors.gray);
const batteryFill = batteryContainer.append("rect")
.attr("class", "battery-fill")
.attr("x", -28)
.attr("y", 13)
.attr("width", 56)
.attr("height", 0)
.attr("fill", config.colors.green);
const batteryText = batteryContainer.append("text")
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "10px")
.attr("font-weight", "bold")
.text("50%");
storageGroup2.append("text")
.attr("y", 55)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Supercapacitor");
const storageVoltageText = storageGroup2.append("text")
.attr("y", 72)
.attr("text-anchor", "middle")
.attr("fill", config.colors.green)
.attr("font-size", "11px")
.text("3.3 V");
// Voltage Regulator
const regulatorGroup = svg.append("g")
.attr("transform", `translate(${layout.regulatorX}, ${layout.regulatorY})`);
regulatorGroup.append("rect")
.attr("x", -50)
.attr("y", -40)
.attr("width", layout.componentWidth)
.attr("height", layout.componentHeight)
.attr("rx", 10)
.attr("fill", config.colors.white)
.attr("stroke", config.colors.navy)
.attr("stroke-width", 3);
regulatorGroup.append("text")
.attr("y", -10)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text("LDO/Buck");
regulatorGroup.append("text")
.attr("y", 8)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text("Regulator");
// Output voltage indicator
regulatorGroup.append("rect")
.attr("x", -30)
.attr("y", 15)
.attr("width", 60)
.attr("height", 18)
.attr("rx", 4)
.attr("fill", config.colors.navy);
regulatorGroup.append("text")
.attr("y", 28)
.attr("text-anchor", "middle")
.attr("fill", config.colors.white)
.attr("font-size", "10px")
.attr("font-weight", "bold")
.text("3.3V OUT");
regulatorGroup.append("text")
.attr("y", 55)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Regulator");
const regulatorEffText = regulatorGroup.append("text")
.attr("y", 72)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "11px")
.text("85% eff.");
// Load (IoT Device)
const loadGroup2 = svg.append("g")
.attr("transform", `translate(${layout.loadX}, ${layout.loadY})`);
loadGroup2.append("rect")
.attr("x", -50)
.attr("y", -40)
.attr("width", layout.componentWidth)
.attr("height", layout.componentHeight)
.attr("rx", 10)
.attr("fill", config.colors.white)
.attr("stroke", config.colors.red)
.attr("stroke-width", 3)
.attr("class", "load-box");
// MCU icon
loadGroup2.append("rect")
.attr("x", -25)
.attr("y", -25)
.attr("width", 50)
.attr("height", 35)
.attr("rx", 4)
.attr("fill", config.colors.navy);
loadGroup2.append("text")
.attr("y", -5)
.attr("text-anchor", "middle")
.attr("fill", config.colors.white)
.attr("font-size", "9px")
.attr("font-weight", "bold")
.text("MCU +");
loadGroup2.append("text")
.attr("y", 7)
.attr("text-anchor", "middle")
.attr("fill", config.colors.white)
.attr("font-size", "9px")
.attr("font-weight", "bold")
.text("SENSORS");
// Status LED
const statusLed = loadGroup2.append("circle")
.attr("class", "status-led")
.attr("cx", 30)
.attr("cy", -30)
.attr("r", 6)
.attr("fill", config.colors.green);
loadGroup2.append("text")
.attr("y", 55)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("IoT Device");
const loadPowerText = loadGroup2.append("text")
.attr("y", 72)
.attr("text-anchor", "middle")
.attr("fill", config.colors.red)
.attr("font-size", "11px")
.text("16.5 mW");
// ---------------------------------------------------------------------------
// POWER FLOW ARROWS
// ---------------------------------------------------------------------------
const flowGroup = svg.append("g").attr("class", "flow-arrows");
// Arrow paths
const arrows = [
{ x1: layout.sourceX + 50, y1: layout.sourceY, x2: layout.harvesterX - 50, y2: layout.harvesterY, label: "Raw" },
{ x1: layout.harvesterX + 50, y1: layout.harvesterY, x2: layout.storageX - 50, y2: layout.storageY, label: "DC" },
{ x1: layout.storageX + 50, y1: layout.storageY, x2: layout.regulatorX - 50, y2: layout.regulatorY, label: "Variable" },
{ x1: layout.regulatorX + 50, y1: layout.regulatorY, x2: layout.loadX - 50, y2: layout.loadY, label: "3.3V" }
];
arrows.forEach((arrow, i) => {
// Arrow line
flowGroup.append("line")
.attr("class", `flow-line flow-line-${i}`)
.attr("x1", arrow.x1)
.attr("y1", arrow.y1)
.attr("x2", arrow.x2)
.attr("y2", arrow.y2)
.attr("stroke", config.colors.orange)
.attr("stroke-width", 3)
.attr("marker-end", "url(#flowArrow)")
.style("opacity", 0.6);
// Label
flowGroup.append("text")
.attr("x", (arrow.x1 + arrow.x2) / 2)
.attr("y", arrow.y1 - 15)
.attr("text-anchor", "middle")
.attr("fill", config.colors.gray)
.attr("font-size", "10px")
.text(arrow.label);
});
// Particle container for animation
const particleGroup = svg.append("g").attr("class", "particles");
// ---------------------------------------------------------------------------
// METRICS PANEL
// ---------------------------------------------------------------------------
const metricsY = 320;
svg.append("rect")
.attr("x", 30)
.attr("y", metricsY)
.attr("width", config.width - 60)
.attr("height", 120)
.attr("rx", 10)
.attr("fill", config.colors.lightGray)
.attr("stroke", config.colors.gray);
svg.append("text")
.attr("x", config.width / 2)
.attr("y", metricsY + 25)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("System Metrics");
// Metrics boxes
const metrics = [
{ label: "Harvested Power", value: "50.0 mW", color: config.colors.orange, id: "harvested" },
{ label: "Stored Energy", value: "825 mJ", color: config.colors.green, id: "stored" },
{ label: "Regulated Output", value: "16.5 mW", color: config.colors.navy, id: "regulated" },
{ label: "System Efficiency", value: "85%", color: config.colors.teal, id: "efficiency" }
];
const metricWidth = 180;
const metricStartX = (config.width - (metrics.length * metricWidth)) / 2;
metrics.forEach((metric, i) => {
const g = svg.append("g")
.attr("transform", `translate(${metricStartX + i * metricWidth + metricWidth/2}, ${metricsY + 70})`);
g.append("rect")
.attr("x", -75)
.attr("y", -30)
.attr("width", 150)
.attr("height", 55)
.attr("rx", 8)
.attr("fill", config.colors.white)
.attr("stroke", metric.color)
.attr("stroke-width", 2);
g.append("text")
.attr("y", -12)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "11px")
.text(metric.label);
g.append("text")
.attr("class", `metric-${metric.id}`)
.attr("y", 12)
.attr("text-anchor", "middle")
.attr("fill", metric.color)
.attr("font-size", "18px")
.attr("font-weight", "bold")
.text(metric.value);
});
// ---------------------------------------------------------------------------
// ENERGY FLOW DIAGRAM
// ---------------------------------------------------------------------------
const diagramY = 470;
svg.append("text")
.attr("x", config.width / 2)
.attr("y", diagramY)
.attr("text-anchor", "middle")
.attr("fill", config.colors.navy)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Energy Balance Over Time");
// Simple bar chart for energy flow
const chartX = 100;
const chartWidth = config.width - 200;
const chartHeight = 60;
const chartY = diagramY + 20;
svg.append("rect")
.attr("x", chartX)
.attr("y", chartY)
.attr("width", chartWidth)
.attr("height", chartHeight)
.attr("fill", config.colors.lightGray)
.attr("stroke", config.colors.gray)
.attr("rx", 4);
// Harvested bar
const harvestedBar = svg.append("rect")
.attr("class", "harvested-bar")
.attr("x", chartX + 10)
.attr("y", chartY + 10)
.attr("width", 0)
.attr("height", 15)
.attr("fill", config.colors.orange)
.attr("rx", 3);
svg.append("text")
.attr("x", chartX + 15)
.attr("y", chartY + 22)
.attr("fill", config.colors.white)
.attr("font-size", "10px")
.text("Harvested");
// Consumed bar
const consumedBar = svg.append("rect")
.attr("class", "consumed-bar")
.attr("x", chartX + 10)
.attr("y", chartY + 35)
.attr("width", 0)
.attr("height", 15)
.attr("fill", config.colors.red)
.attr("rx", 3);
svg.append("text")
.attr("x", chartX + 15)
.attr("y", chartY + 47)
.attr("fill", config.colors.white)
.attr("font-size", "10px")
.text("Consumed");
// ---------------------------------------------------------------------------
// HELPER FUNCTIONS
// ---------------------------------------------------------------------------
function drawSolarIcon(group) {
group.selectAll("*").remove();
// Sun rays
const rayLength = 20;
for (let i = 0; i < 8; i++) {
const angle = (i * 45) * Math.PI / 180;
group.append("line")
.attr("x1", Math.cos(angle) * 12)
.attr("y1", Math.sin(angle) * 12)
.attr("x2", Math.cos(angle) * rayLength)
.attr("y2", Math.sin(angle) * rayLength)
.attr("stroke", config.colors.yellow)
.attr("stroke-width", 2);
}
group.append("circle")
.attr("r", 10)
.attr("fill", config.colors.yellow);
}
function drawVibrationIcon(group) {
group.selectAll("*").remove();
// Vibration waves
[-15, 0, 15].forEach((x, i) => {
group.append("path")
.attr("d", `M${x},-15 Q${x+8},0 ${x},15`)
.attr("fill", "none")
.attr("stroke", config.colors.purple)
.attr("stroke-width", 3)
.style("opacity", 1 - i * 0.2);
});
}
function drawThermalIcon(group) {
group.selectAll("*").remove();
// Thermometer
group.append("rect")
.attr("x", -6)
.attr("y", -20)
.attr("width", 12)
.attr("height", 30)
.attr("rx", 6)
.attr("fill", config.colors.white)
.attr("stroke", config.colors.red)
.attr("stroke-width", 2);
group.append("circle")
.attr("cy", 15)
.attr("r", 8)
.attr("fill", config.colors.red);
group.append("rect")
.attr("x", -3)
.attr("y", -5)
.attr("width", 6)
.attr("height", 20)
.attr("fill", config.colors.red);
}
function drawRfIcon(group) {
group.selectAll("*").remove();
// Antenna with waves
group.append("line")
.attr("x1", 0)
.attr("y1", 20)
.attr("x2", 0)
.attr("y2", -5)
.attr("stroke", config.colors.navy)
.attr("stroke-width", 3);
[10, 18, 26].forEach((r, i) => {
group.append("path")
.attr("d", `M${-r},-5 A${r},${r} 0 0,1 ${r},-5`)
.attr("fill", "none")
.attr("stroke", "#3498DB")
.attr("stroke-width", 2)
.style("opacity", 1 - i * 0.25);
});
}
function updateSourceDisplay() {
const source = config.sources[state.selectedSource];
// Update icon
const icon = svg.select(".source-icon");
switch(state.selectedSource) {
case "solar": drawSolarIcon(icon); break;
case "vibration": drawVibrationIcon(icon); break;
case "thermal": drawThermalIcon(icon); break;
case "rf": drawRfIcon(icon); break;
}
// Update label
svg.select(".source-label").text(source.name);
// Update box color
svg.select(".source-box").attr("stroke", source.color);
updateIntensityLabel();
}
function updateIntensityLabel() {
const labels = {
solar: state.sourceIntensity > 70 ? "Bright sunlight" : state.sourceIntensity > 30 ? "Cloudy day" : "Indoor light",
vibration: state.sourceIntensity > 70 ? "Heavy machinery" : state.sourceIntensity > 30 ? "Walking motion" : "Light vibration",
thermal: state.sourceIntensity > 70 ? "Large temp diff (>20C)" : state.sourceIntensity > 30 ? "Moderate diff (10C)" : "Small diff (<5C)",
rf: state.sourceIntensity > 70 ? "Near transmitter" : state.sourceIntensity > 30 ? "Wi-Fi environment" : "Weak RF"
};
intensityLabel.text(`(${labels[state.selectedSource]})`);
}
function updateCalculations() {
const source = config.sources[state.selectedSource];
// Calculate harvested power
const rawPower = source.maxPower * (state.sourceIntensity / 100);
state.harvestedPower = rawPower * config.regulator.efficiency;
// Calculate load power
const loadPower = state.loadConsumption * config.regulator.outputVoltage;
// Energy balance
state.energyBalance = state.harvestedPower - loadPower;
// System efficiency
if (rawPower > 0) {
state.systemEfficiency = (Math.min(loadPower, state.harvestedPower) / rawPower) * 100;
} else {
state.systemEfficiency = 0;
}
// Update displays
sourcePowerText.text(`${rawPower.toFixed(1)} mW`);
harvesterPowerText.text(`${state.harvestedPower.toFixed(1)} mW`);
loadPowerText.text(`${loadPower.toFixed(1)} mW`);
loadPowerLabel.text(`= ${loadPower.toFixed(1)} mW @ 3.3V`);
// Update metrics
svg.select(".metric-harvested").text(`${state.harvestedPower.toFixed(1)} mW`);
svg.select(".metric-regulated").text(`${loadPower.toFixed(1)} mW`);
svg.select(".metric-efficiency").text(`${state.systemEfficiency.toFixed(0)}%`);
// Update balance indicator
balanceValue
.text(`${state.energyBalance >= 0 ? '+' : ''}${state.energyBalance.toFixed(1)} mW`)
.style("color", state.energyBalance >= 0 ? config.colors.green : config.colors.red);
// Update energy bars
const maxBarWidth = chartWidth - 120;
const maxPower = Math.max(100, rawPower, loadPower);
harvestedBar.attr("width", (state.harvestedPower / maxPower) * maxBarWidth);
consumedBar.attr("width", (loadPower / maxPower) * maxBarWidth);
// Update flow line opacity based on power
svg.selectAll(".flow-line").style("opacity", state.harvestedPower > 0 ? 0.8 : 0.3);
// Update load status
if (state.storedEnergy < 10 && state.energyBalance < 0) {
statusLed.attr("fill", config.colors.red);
svg.select(".load-box").attr("stroke", config.colors.gray);
} else if (state.energyBalance < 0) {
statusLed.attr("fill", config.colors.orange);
svg.select(".load-box").attr("stroke", config.colors.orange);
} else {
statusLed.attr("fill", config.colors.green);
svg.select(".load-box").attr("stroke", config.colors.red);
}
}
function updateStorage() {
// Update storage based on energy balance
// Energy change per update interval (mJ)
const energyChange = state.energyBalance * (config.timing.updateInterval / 1000);
state.storedEnergy = Math.max(0, Math.min(state.maxStoredEnergy, state.storedEnergy + energyChange));
// Calculate voltage from energy (E = 0.5 * C * V^2)
// V = sqrt(2 * E / C)
const capacitance = config.storage.capacitor.capacity / 1000; // F
state.storageVoltage = Math.sqrt(2 * (state.storedEnergy / 1000) / capacitance);
// Update displays
const percentage = (state.storedEnergy / state.maxStoredEnergy) * 100;
storageFill.style("width", `${percentage}%`);
storageText.text(`${percentage.toFixed(0)}% (${state.storedEnergy.toFixed(0)} mJ)`);
// Battery fill in SVG
const fillHeight = (percentage / 100) * 36;
batteryFill
.attr("y", -23 + (36 - fillHeight))
.attr("height", fillHeight);
batteryText.text(`${percentage.toFixed(0)}%`);
storageVoltageText.text(`${state.storageVoltage.toFixed(2)} V`);
svg.select(".metric-stored").text(`${state.storedEnergy.toFixed(0)} mJ`);
// Update fill color based on level
if (percentage < 20) {
batteryFill.attr("fill", config.colors.red);
storageFill.style("background", config.colors.red);
} else if (percentage < 50) {
batteryFill.attr("fill", config.colors.orange);
storageFill.style("background", `linear-gradient(90deg, ${config.colors.orange}, ${config.colors.yellow})`);
} else {
batteryFill.attr("fill", config.colors.green);
storageFill.style("background", `linear-gradient(90deg, ${config.colors.green}, ${config.colors.teal})`);
}
}
function createFlowParticle(startX, startY, endX, endY, delay) {
return {
startX, startY, endX, endY,
progress: -delay,
speed: 0.02 + Math.random() * 0.01
};
}
function updateParticles() {
// Create new particles if needed
if (state.harvestedPower > 0 && Math.random() < 0.3) {
arrows.forEach((arrow, i) => {
if (state.flowParticles.filter(p => p.segment === i).length < 5) {
const particle = createFlowParticle(arrow.x1, arrow.y1, arrow.x2, arrow.y2, Math.random() * 0.5);
particle.segment = i;
state.flowParticles.push(particle);
}
});
}
// Update particle positions
state.flowParticles.forEach(p => {
p.progress += p.speed * (state.harvestedPower / 50);
});
// Remove completed particles
state.flowParticles = state.flowParticles.filter(p => p.progress < 1);
// Render particles
const particles = particleGroup.selectAll("circle")
.data(state.flowParticles);
particles.enter()
.append("circle")
.attr("r", 4)
.attr("fill", config.colors.orange)
.merge(particles)
.attr("cx", d => d.progress >= 0 ? d.startX + (d.endX - d.startX) * d.progress : d.startX)
.attr("cy", d => d.progress >= 0 ? d.startY + (d.endY - d.startY) * d.progress : d.startY)
.style("opacity", d => d.progress >= 0 ? 0.8 : 0);
particles.exit().remove();
}
// ---------------------------------------------------------------------------
// ANIMATION FUNCTIONS
// ---------------------------------------------------------------------------
function toggleAnimation() {
if (state.isPlaying) {
stopAnimation();
playBtn.text("Start Simulation")
.style("background", config.colors.teal);
} else {
startAnimation();
playBtn.text("Stop Simulation")
.style("background", config.colors.orange);
}
}
function startAnimation() {
state.isPlaying = true;
state.time = 0;
function animate() {
if (!state.isPlaying) return;
state.time += config.timing.updateInterval;
// Update energy storage
updateStorage();
// Update particles
updateParticles();
// Pulse effect on source icon
const pulseScale = 1 + 0.1 * Math.sin(state.time / 200);
svg.select(".source-icon")
.attr("transform", `scale(${pulseScale})`);
// LED blink if active
if (state.storedEnergy > 10) {
const ledBrightness = 0.7 + 0.3 * Math.sin(state.time / 300);
statusLed.style("opacity", ledBrightness);
}
state.animationFrame = setTimeout(animate, config.timing.updateInterval);
}
animate();
}
function stopAnimation() {
state.isPlaying = false;
if (state.animationFrame) {
clearTimeout(state.animationFrame);
}
svg.select(".source-icon").attr("transform", "scale(1)");
statusLed.style("opacity", 1);
}
function resetAnimation() {
stopAnimation();
// Reset state
state.selectedSource = "solar";
state.sourceIntensity = 50;
state.loadConsumption = 5;
state.storedEnergy = state.maxStoredEnergy * 0.5;
state.flowParticles = [];
// Reset inputs
sourceSelect.property("value", "solar");
intensitySlider.property("value", 50);
intensityValue.text(" 50%");
loadSlider.property("value", 5);
loadValue.text(" 5 mA");
// Reset displays
updateSourceDisplay();
updateCalculations();
updateStorage();
playBtn.text("Start Simulation").style("background", config.colors.teal);
// Clear particles
particleGroup.selectAll("*").remove();
}
// ---------------------------------------------------------------------------
// INITIALIZE
// ---------------------------------------------------------------------------
updateSourceDisplay();
updateCalculations();
updateStorage();
return container.node();
}