diff --git a/theme/assets/css/main.css b/theme/assets/css/main.css index beefafb..12e6dac 100644 --- a/theme/assets/css/main.css +++ b/theme/assets/css/main.css @@ -2601,6 +2601,140 @@ p:last-child { margin-bottom: 0; } 0% { opacity: 1; } 100% { opacity: 1; } } + +/* ── Dashboard Chart Animations ────────────────────────── */ +.dashboard-chart-container { + position: relative; + width: 100%; + max-width: 900px; + margin: 0 auto; + padding: 1.5rem; + background: var(--card-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + will-change: opacity; + data-dashboard-container: true; +} + +.dashboard-chart { + width: 100%; + height: auto; + display: block; + font-family: var(--font-sans); + user-select: none; +} + +/* Bar animation - smooth height transitions */ +.dashboard-chart .bar { + fill: url(#barGradient); + will-change: height; + transition: height 0.3s ease-out; + vector-effect: non-scaling-stroke; +} + +/* Line path - draw animation */ +.dashboard-chart .line-path { + stroke: var(--color-accent); + fill: none; + stroke-width: 2.5; + stroke-linecap: round; + stroke-linejoin: round; + will-change: stroke-dashoffset, d; + transition: stroke 0.3s ease; +} + +.dashboard-chart .line-fill { + fill: url(#lineGradient); + will-change: d; + opacity: 0.5; +} + +/* Pie chart segment animations */ +.dashboard-chart .pie-segment { + will-change: transform; + transform-origin: center; + transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +/* Chart title styling */ +.dashboard-chart .chart-title { + font-size: 14px; + font-weight: 600; + fill: var(--color-heading); + letter-spacing: 0.3px; +} + +.dashboard-chart .chart-label { + font-size: 12px; + fill: var(--color-text-muted); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.2px; +} + +.dashboard-chart .chart-value { + font-size: 13px; + font-weight: 600; + fill: var(--color-text); + transition: fill 0.2s ease; +} + +/* Dark mode support */ +[data-theme="dark"] .dashboard-chart { + --text-primary: #E0E0E0; + --text-secondary: #9E9E9E; + --primary-color: #66BB6A; + --accent-color: #4CAF50; + --border-color: #333333; +} + +[data-theme="light"] .dashboard-chart { + --text-primary: #1a1a1a; + --text-secondary: #666; + --primary-color: #3b82f6; + --accent-color: #10b981; + --border-color: #e5e7eb; +} + +/* Performance: disable animations on reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + .dashboard-chart .bar, + .dashboard-chart .line-path, + .dashboard-chart .line-fill, + .dashboard-chart .pie-segment { + animation: none !important; + transition: none !important; + } +} + +/* Responsive: Adjust for smaller screens */ +@media (max-width: 768px) { + .dashboard-chart-container { + padding: 1rem; + border-radius: var(--radius-md); + } + + .dashboard-chart .chart-title { + font-size: 13px; + } + + .dashboard-chart .chart-label { + font-size: 11px; + } + + .dashboard-chart .chart-value { + font-size: 12px; + } +} + +/* Touch devices: No hover effects */ +@media (hover: none) { + .dashboard-chart-container:hover { + box-shadow: var(--shadow-md); + } +} + .cta-banner { position: relative; overflow: hidden; diff --git a/theme/assets/js/dashboard-animator.js b/theme/assets/js/dashboard-animator.js new file mode 100644 index 0000000..b8a7ef4 --- /dev/null +++ b/theme/assets/js/dashboard-animator.js @@ -0,0 +1,224 @@ +/** + * Dashboard Chart Animator + * Continuously animates bar charts, line graphs, and pie charts + * Performance-optimized with IntersectionObserver and requestAnimationFrame + */ + +class DashboardAnimator { + constructor(svgElement) { + this.svg = svgElement; + this.animationId = null; + this.isPaused = false; + this.isReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + this.startTime = Date.now(); + + // Chart dimensions + this.barChartHeight = 120; + this.chartUpdateSpeed = 2000; // Base cycle speed in ms (controls smoothness) + + // Cache DOM elements + this.barsGroup1 = svg.querySelectorAll('#bars-group-1 .bar'); + this.barsGroup2 = svg.querySelectorAll('#bars-group-2 .bar'); + this.valuesGroup1 = svg.querySelectorAll('#values-group-1 .chart-value'); + this.valuesGroup2 = svg.querySelectorAll('#values-group-2 .chart-value'); + this.linePath = svg.getElementById('line-path'); + this.lineFill = svg.getElementById('line-fill'); + this.pieSegments = svg.querySelectorAll('.pie-segment'); + + // Noise generators for smooth, realistic data + this.noisePhase1 = Math.random() * Math.PI * 2; + this.noisePhase2 = Math.random() * Math.PI * 2; + this.noisePhase3 = Math.random() * Math.PI * 2; + this.noisePhase4 = Math.random() * Math.PI * 2; + this.noisePhase5 = Math.random() * Math.PI * 2; + + this.init(); + } + + init() { + if (!this.isReducedMotion) { + this.animate(); + this.attachHoverListeners(); + } + } + + /** + * Perlin-like noise for smooth, natural-looking data + * Uses sine waves at different frequencies + */ + generateNoise(phase, frequency = 0.5) { + return Math.sin(phase * frequency) * 0.5 + 0.5; // Normalize to 0-1 + } + + /** + * Generate smooth, continuous data values + * Uses combination of sine waves for organic motion + */ + generateValue(basePhase, index, max = 100) { + const time = (Date.now() - this.startTime) / this.chartUpdateSpeed; + const phase = basePhase + time * 0.3 + index * 0.4; + + // Multi-frequency sine wave for natural variation + const noise1 = Math.sin(phase * 0.7) * 0.4; + const noise2 = Math.sin(phase * 1.3) * 0.3; + const noise3 = Math.sin(phase * 0.3) * 0.2; + const base = 0.5 + noise1 + noise2 + noise3; + + // Ensure value stays in range + return Math.max(10, Math.min(max, base * max * 0.9)); + } + + /** + * Update bar chart with smooth animation + */ + updateBars(barsElements, valuesElements, maxValue = 100, isPercent = true) { + barsElements.forEach((bar, index) => { + const value = this.generateValue(this.noisePhase1, index, maxValue); + const heightPercent = (value / maxValue) * 100; + const height = (heightPercent / 100) * this.barChartHeight; + + bar.setAttribute('height', height); + + if (valuesElements[index]) { + valuesElements[index].textContent = isPercent ? + Math.round(value) + '%' : + Math.round(value); + valuesElements[index].setAttribute('data-value', value.toFixed(1)); + } + }); + } + + /** + * Update line graph with smooth curve + */ + updateLineGraph() { + const time = (Date.now() - this.startTime) / this.chartUpdateSpeed; + const points = []; + + // Generate 3 points for quadratic bezier curve + for (let i = 0; i < 3; i++) { + const x = i * 100; + const basePhase = this.noisePhase3 + time * 0.2 + i * 0.5; + const noise = Math.sin(basePhase) * 0.3 + Math.sin(basePhase * 0.5) * 0.2; + const y = 80 + noise * 60 - i * 15; + points.push({ x, y }); + } + + // Build quadratic bezier path + const pathData = `M${points[0].x},${points[0].y} Q${points[1].x},${points[1].y} ${points[2].x},${points[2].y}`; + this.linePath.setAttribute('d', pathData); + this.lineFill.setAttribute('d', `${pathData} L200,160 Q100,140 0,160 Z`); + + // Animate stroke-dasharray for drawing effect + const strokeLength = 200; + const offset = ((time * 50) % strokeLength); + this.linePath.setAttribute('stroke-dashoffset', offset); + } + + /** + * Update pie chart with smooth segment rotation + */ + updatePieChart() { + const time = (Date.now() - this.startTime) / (this.chartUpdateSpeed * 0.5); + const baseRotation = (time * 15) % 360; // Slow rotation + + this.pieSegments.forEach((segment, index) => { + // Add wobble effect to each segment + const wobble = Math.sin(time * 0.5 + index * Math.PI / 2) * 3; + segment.setAttribute('transform', `rotate(${baseRotation + index * 90 + wobble})`); + }); + } + + /** + * Main animation loop using requestAnimationFrame + */ + animate = () => { + if (!this.isPaused) { + this.updateBars(this.barsGroup1, this.valuesGroup1, 100, true); + this.updateBars(this.barsGroup2, this.valuesGroup2, 5000, false); + this.updateLineGraph(); + this.updatePieChart(); + } + + this.animationId = requestAnimationFrame(this.animate); + } + + /** + * Attach hover event listeners to pause/resume animation + */ + attachHoverListeners() { + const container = this.svg.closest('[data-dashboard-container]') || this.svg.parentElement; + + if (container) { + container.addEventListener('mouseenter', () => this.pause()); + container.addEventListener('mouseleave', () => this.resume()); + + // Touch support: pause on touch + container.addEventListener('touchstart', () => this.pause()); + container.addEventListener('touchend', () => this.resume()); + } + } + + /** + * Pause animation (on hover) + */ + pause() { + this.isPaused = true; + } + + /** + * Resume animation (after hover) + */ + resume() { + this.isPaused = false; + } + + /** + * Stop animation and clean up + */ + destroy() { + if (this.animationId) { + cancelAnimationFrame(this.animationId); + this.animationId = null; + } + } +} + +/** + * Initialize dashboard animation when element enters viewport + */ +function initDashboardAnimation() { + const dashboardCharts = document.querySelectorAll('.dashboard-chart'); + + if (!dashboardCharts.length) return; + + const observerOptions = { + root: null, + rootMargin: '50px', + threshold: 0.1 + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + if (!entry.target._dashboardAnimator) { + entry.target._dashboardAnimator = new DashboardAnimator(entry.target); + } + } else { + // Pause animation when out of view for performance + if (entry.target._dashboardAnimator) { + entry.target._dashboardAnimator.pause(); + } + } + }); + }, observerOptions); + + dashboardCharts.forEach(chart => observer.observe(chart)); +} + +// Auto-initialize when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initDashboardAnimation); +} else { + initDashboardAnimation(); +} diff --git a/theme/assets/svg/dashboard-chart.svg b/theme/assets/svg/dashboard-chart.svg new file mode 100644 index 0000000..82706e5 --- /dev/null +++ b/theme/assets/svg/dashboard-chart.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + Performance + + + + + + + + + API + Cache + DB + Queue + Worker + + + 0% + 0% + 0% + 0% + 0% + + + + Requests/sec + + + + + + + + Read + Write + Update + Delete + + + 0 + 0 + 0 + 0 + + + + + + Traffic Trend + + + + + + + + + + + + + + Distribution + + + + + + + + + + + + + + + + + 100% + + + + + Service A + + + Service B + + + Service C + + + Service D + + + \ No newline at end of file diff --git a/theme/blocks/index.php b/theme/blocks/index.php index 40b74ed..d63e8bf 100644 --- a/theme/blocks/index.php +++ b/theme/blocks/index.php @@ -1339,7 +1339,118 @@ function oribi_render_platform_row( $a ) { $img_alt = ! empty( $a['imgAlt'] ) ? $a['imgAlt'] : ''; $img_w = ! empty( $a['imgWidth'] ) ? intval( $a['imgWidth'] ) : 300; - if ( $img_url ) { + // Check if this is a dashboard card (by heading content or dashboard flag) + $heading_text = $a['heading'] ?? ''; + $is_dashboard = ! empty( $a['isDashboard'] ) || stripos( $heading_text, 'dashboard' ) !== false || stripos( $heading_text, 'data' ) !== false; + + if ( $is_dashboard ) { + // Render dashboard chart animation + $visual_html = '
+ + + + + + + + + + + + + Performance + + + + + + + + + API + Cache + DB + Queue + Worker + + + 0% + 0% + 0% + 0% + 0% + + Requests/sec + + + + + + + + Read + Write + Update + Delete + + + 0 + 0 + 0 + 0 + + + + Traffic Trend + + + + + + + + + + + Distribution + + + + + + + + + + + + + + + 100% + + + + Service A + + Service B + + Service C + + Service D + + +
'; + $visual_cls = 'platform-visual has-dashboard'; + } elseif ( $img_url ) { $img_style = 'width:' . $img_w . 'px;max-width:100%;height:auto;border-radius:var(--radius-sm);object-fit:contain;display:block;margin-inline:auto;'; if ( $img_id ) { $visual_html = wp_get_attachment_image( $img_id, 'full', false, [ 'style' => $img_style, 'alt' => $img_alt ] ); diff --git a/theme/inc/enqueue.php b/theme/inc/enqueue.php index 1500471..22e18bb 100644 --- a/theme/inc/enqueue.php +++ b/theme/inc/enqueue.php @@ -29,6 +29,15 @@ add_action( 'wp_enqueue_scripts', function () { true ); + // Dashboard chart animator - smooth continuous animations for dashboard cards + wp_enqueue_script( + 'oribi-dashboard-animator', + ORIBI_URI . '/assets/js/dashboard-animator.js', + [], + ORIBI_VERSION, + true + ); + // Localize AJAX endpoint for the contact form wp_localize_script( 'oribi-main', 'oribiAjax', [ 'url' => admin_url( 'admin-ajax.php' ),