// D3.js Animations for Homepage - Training & Learning Focus function initD3Animation() { const container = d3.select("#d3-animation"); if (container.empty()) return; const containerRect = container.node().getBoundingClientRect(); const width = containerRect.width; const height = containerRect.height || 400; // Clear any existing SVG container.selectAll("*").remove(); const svg = container.append("svg") .attr("width", width) .attr("height", height) .attr("viewBox", `0 0 ${width} ${height}`) .style("background", "transparent") .style("overflow", "hidden"); // Create a main group for all elements const mainGroup = svg.append("g") .attr("class", "main-group"); // Create animated network visualization representing Odoo training modules const nodes = [ { id: "Fundamentos", group: 1, size: 35, level: 1 }, { id: "CRM Básico", group: 2, size: 25, level: 2 }, { id: "Ventas", group: 2, size: 25, level: 2 }, { id: "Inventario", group: 3, size: 25, level: 2 }, { id: "Contabilidad", group: 4, size: 30, level: 2 }, { id: "CRM Avanzado", group: 2, size: 20, level: 3 }, { id: "Reportes", group: 4, size: 20, level: 3 }, { id: "Integraciones", group: 3, size: 25, level: 3 }, { id: "Certificación", group: 1, size: 30, level: 4 }, { id: "Especialización", group: 1, size: 25, level: 4 } ]; const links = [ { source: "Fundamentos", target: "CRM Básico", value: 3 }, { source: "Fundamentos", target: "Ventas", value: 3 }, { source: "Fundamentos", target: "Inventario", value: 3 }, { source: "Fundamentos", target: "Contabilidad", value: 3 }, { source: "CRM Básico", target: "CRM Avanzado", value: 2 }, { source: "Ventas", target: "CRM Avanzado", value: 2 }, { source: "Contabilidad", target: "Reportes", value: 3 }, { source: "Inventario", target: "Integraciones", value: 3 }, { source: "CRM Avanzado", target: "Certificación", value: 3 }, { source: "Reportes", target: "Certificación", value: 2 }, { source: "Integraciones", target: "Especialización", value: 2 }, { source: "Certificación", target: "Especialización", value: 3 } ]; // Color scale with orange theme const color = d3.scaleOrdinal() .domain([1, 2, 3, 4]) .range(["#FF6B35", "#FF8A50", "#FFA726", "#FFB74D"]); // Add boundary force to keep nodes within container const boundaryForce = () => { nodes.forEach(node => { node.x = Math.max(node.size, Math.min(width - node.size, node.x)); node.y = Math.max(node.size, Math.min(height - node.size, node.y)); }); }; // Create force simulation with boundaries const simulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id(d => d.id).distance(80)) .force("charge", d3.forceManyBody().strength(-250)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collision", d3.forceCollide().radius(d => d.size + 8)) .force("boundary", boundaryForce); // Create links const link = mainGroup.append("g") .attr("class", "links") .selectAll("line") .data(links) .enter().append("line") .attr("stroke", "#FF6B35") .attr("stroke-opacity", 0.3) .attr("stroke-width", d => Math.sqrt(d.value)); // Create nodes container const node = mainGroup.append("g") .attr("class", "nodes") .selectAll("g") .data(nodes) .enter().append("g") .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended)); // Add circles node.append("circle") .attr("r", d => d.size) .attr("fill", d => color(d.group)) .attr("stroke", "#1a1a1a") .attr("stroke-width", 2) .style("cursor", "pointer") .on("mouseover", function(event, d) { d3.select(this) .transition() .duration(200) .attr("r", d.size * 1.2); }) .on("mouseout", function(event, d) { d3.select(this) .transition() .duration(200) .attr("r", d.size); }); // Add text labels node.append("text") .text(d => d.id) .attr("text-anchor", "middle") .attr("dy", ".35em") .style("fill", "#1a1a1a") .style("font-size", "11px") .style("font-weight", "700") .style("pointer-events", "none"); // Add animated pulses node.append("circle") .attr("r", d => d.size) .attr("fill", "none") .attr("stroke", d => color(d.group)) .attr("stroke-width", 2) .attr("opacity", 0) .style("pointer-events", "none") .each(function(d, i) { d3.select(this) .transition() .delay(i * 200) .duration(2000) .attr("r", d => d.size * 2) .attr("opacity", 0) .on("end", function repeat() { d3.select(this) .attr("r", d.size) .attr("opacity", 0.5) .transition() .duration(2000) .attr("r", d.size * 2) .attr("opacity", 0) .on("end", repeat); }); }); // Update positions on tick simulation.on("tick", () => { // Apply boundary force boundaryForce(); link .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => d.target.x) .attr("y2", d => d.target.y); node .attr("transform", d => `translate(${d.x},${d.y})`); }); // Drag functions with boundaries function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = Math.max(d.size, Math.min(width - d.size, event.x)); d.fy = Math.max(d.size, Math.min(height - d.size, event.y)); } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } // Add floating particles with orange theme const particles = mainGroup.append("g") .attr("class", "particles"); function createParticle() { const particle = particles.append("circle") .attr("r", Math.random() * 2 + 1) .attr("cx", Math.random() * width) .attr("cy", height + 5) .attr("fill", "#FF6B35") .attr("opacity", 0.2); particle.transition() .duration(Math.random() * 8000 + 4000) .ease(d3.easeLinear) .attr("cy", -5) .attr("opacity", 0) .remove(); } // Create particles periodically setInterval(createParticle, 500); } // Initialize on page load document.addEventListener('DOMContentLoaded', function() { initD3Animation(); // Reinitialize on window resize let resizeTimer; window.addEventListener('resize', function() { clearTimeout(resizeTimer); resizeTimer = setTimeout(function() { initD3Animation(); }, 250); }); }); // Smooth scroll for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); });