Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
184
resources/js/modules/canvas-animations/CanvasManager.js
Normal file
184
resources/js/modules/canvas-animations/CanvasManager.js
Normal file
@@ -0,0 +1,184 @@
|
||||
// modules/canvas-animations/CanvasManager.js
|
||||
import { useEvent } from '../../core/useEvent.js';
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
/**
|
||||
* Canvas Manager - Handles canvas setup, resizing, and basic operations
|
||||
*/
|
||||
export class CanvasManager {
|
||||
constructor(canvas, options = {}) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d');
|
||||
this.options = {
|
||||
responsive: true,
|
||||
pixelRatio: window.devicePixelRatio || 1,
|
||||
...options
|
||||
};
|
||||
|
||||
this.animationId = null;
|
||||
this.isAnimating = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize canvas manager
|
||||
*/
|
||||
init() {
|
||||
this.setupCanvas();
|
||||
|
||||
if (this.options.responsive) {
|
||||
this.setupResponsive();
|
||||
}
|
||||
|
||||
Logger.info('[CanvasManager] Initialized', {
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height,
|
||||
pixelRatio: this.options.pixelRatio
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup initial canvas properties
|
||||
*/
|
||||
setupCanvas() {
|
||||
this.resize();
|
||||
|
||||
// Set CSS to prevent blurriness on high-DPI displays
|
||||
this.canvas.style.width = this.canvas.width + 'px';
|
||||
this.canvas.style.height = this.canvas.height + 'px';
|
||||
|
||||
// Scale context for high-DPI displays
|
||||
if (this.options.pixelRatio > 1) {
|
||||
this.ctx.scale(this.options.pixelRatio, this.options.pixelRatio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup responsive behavior
|
||||
*/
|
||||
setupResponsive() {
|
||||
// Resize on window resize
|
||||
useEvent(window, 'resize', () => {
|
||||
this.resize();
|
||||
}, 'canvas-manager');
|
||||
|
||||
// Optional: Resize on orientation change for mobile
|
||||
useEvent(window, 'orientationchange', () => {
|
||||
setTimeout(() => this.resize(), 100);
|
||||
}, 'canvas-manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize canvas to match container or window
|
||||
*/
|
||||
resize() {
|
||||
const container = this.canvas.parentElement;
|
||||
const pixelRatio = this.options.pixelRatio;
|
||||
|
||||
// Get display size
|
||||
let displayWidth, displayHeight;
|
||||
|
||||
if (container && getComputedStyle(container).position !== 'static') {
|
||||
// Use container size if it has positioning
|
||||
displayWidth = container.clientWidth;
|
||||
displayHeight = container.clientHeight;
|
||||
} else {
|
||||
// Fallback to canvas CSS size or window size
|
||||
displayWidth = this.canvas.clientWidth || window.innerWidth;
|
||||
displayHeight = this.canvas.clientHeight || window.innerHeight;
|
||||
}
|
||||
|
||||
// Set actual canvas size
|
||||
this.canvas.width = Math.floor(displayWidth * pixelRatio);
|
||||
this.canvas.height = Math.floor(displayHeight * pixelRatio);
|
||||
|
||||
// Set CSS size
|
||||
this.canvas.style.width = displayWidth + 'px';
|
||||
this.canvas.style.height = displayHeight + 'px';
|
||||
|
||||
// Re-scale context if needed
|
||||
if (pixelRatio > 1) {
|
||||
this.ctx.scale(pixelRatio, pixelRatio);
|
||||
}
|
||||
|
||||
Logger.info('[CanvasManager] Resized', {
|
||||
displayWidth,
|
||||
displayHeight,
|
||||
canvasWidth: this.canvas.width,
|
||||
canvasHeight: this.canvas.height
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire canvas
|
||||
*/
|
||||
clear() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canvas dimensions (display size, not pixel size)
|
||||
*/
|
||||
getSize() {
|
||||
return {
|
||||
width: this.canvas.clientWidth,
|
||||
height: this.canvas.clientHeight,
|
||||
pixelWidth: this.canvas.width,
|
||||
pixelHeight: this.canvas.height
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mouse position relative to canvas
|
||||
*/
|
||||
getMousePosition(event) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: (event.clientX - rect.left) * this.options.pixelRatio,
|
||||
y: (event.clientY - rect.top) * this.options.pixelRatio
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start animation loop
|
||||
*/
|
||||
startAnimation(animationFunction) {
|
||||
if (this.isAnimating) {
|
||||
this.stopAnimation();
|
||||
}
|
||||
|
||||
this.isAnimating = true;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!this.isAnimating) return;
|
||||
|
||||
animationFunction(timestamp);
|
||||
this.animationId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
this.animationId = requestAnimationFrame(animate);
|
||||
Logger.info('[CanvasManager] Animation started');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop animation loop
|
||||
*/
|
||||
stopAnimation() {
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
this.isAnimating = false;
|
||||
Logger.info('[CanvasManager] Animation stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup canvas manager
|
||||
*/
|
||||
destroy() {
|
||||
this.stopAnimation();
|
||||
// Event cleanup is handled by useEvent system
|
||||
Logger.info('[CanvasManager] Destroyed');
|
||||
}
|
||||
}
|
||||
464
resources/js/modules/canvas-animations/DataVisualization.js
Normal file
464
resources/js/modules/canvas-animations/DataVisualization.js
Normal file
@@ -0,0 +1,464 @@
|
||||
// modules/canvas-animations/DataVisualization.js
|
||||
import { CanvasManager } from './CanvasManager.js';
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
/**
|
||||
* Data Visualization Canvas Components - Animated charts and graphs
|
||||
*/
|
||||
export const DataVisualization = {
|
||||
|
||||
/**
|
||||
* Initialize data visualization canvas
|
||||
*/
|
||||
init(canvas, config) {
|
||||
const manager = new CanvasManager(canvas);
|
||||
const vizType = config.type || 'bar';
|
||||
|
||||
// Get data from config or data attributes
|
||||
const data = this.parseData(canvas, config);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
Logger.warn('[DataVisualization] No data provided for canvas');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (vizType) {
|
||||
case 'bar':
|
||||
this.createBarChart(manager, data, config);
|
||||
break;
|
||||
case 'line':
|
||||
this.createLineChart(manager, data, config);
|
||||
break;
|
||||
case 'pie':
|
||||
this.createPieChart(manager, data, config);
|
||||
break;
|
||||
case 'progress':
|
||||
this.createProgressChart(manager, data, config);
|
||||
break;
|
||||
default:
|
||||
this.createBarChart(manager, data, config);
|
||||
}
|
||||
|
||||
Logger.info('[DataVisualization] Initialized', vizType, 'chart with', data.length, 'data points');
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse data from canvas element or config
|
||||
*/
|
||||
parseData(canvas, config) {
|
||||
let data = config.data;
|
||||
|
||||
// Try to get data from data-canvas-data attribute
|
||||
if (!data && canvas.dataset.canvasData) {
|
||||
try {
|
||||
data = JSON.parse(canvas.dataset.canvasData);
|
||||
} catch (e) {
|
||||
Logger.warn('[DataVisualization] Failed to parse canvas data', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get data from script tag
|
||||
if (!data) {
|
||||
const scriptTag = canvas.nextElementSibling;
|
||||
if (scriptTag && scriptTag.tagName === 'SCRIPT' && scriptTag.type === 'application/json') {
|
||||
try {
|
||||
data = JSON.parse(scriptTag.textContent);
|
||||
} catch (e) {
|
||||
Logger.warn('[DataVisualization] Failed to parse script data', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure data is always an array
|
||||
if (!data) {
|
||||
Logger.warn('[DataVisualization] No data found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// If data is not an array, wrap it or convert it
|
||||
if (!Array.isArray(data)) {
|
||||
// If it's a single number (for progress charts)
|
||||
if (typeof data === 'number') {
|
||||
data = [{ value: data, label: 'Progress' }];
|
||||
}
|
||||
// If it's an object with values, convert to array
|
||||
else if (typeof data === 'object') {
|
||||
data = Object.entries(data).map(([key, value]) => ({
|
||||
label: key,
|
||||
value: typeof value === 'object' ? value.value || value : value
|
||||
}));
|
||||
}
|
||||
// Fallback: empty array
|
||||
else {
|
||||
Logger.warn('[DataVisualization] Data format not recognized:', data);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create animated bar chart
|
||||
*/
|
||||
createBarChart(manager, data, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
const padding = config.padding || 40;
|
||||
const chartWidth = width - padding * 2;
|
||||
const chartHeight = height - padding * 2;
|
||||
|
||||
const barWidth = chartWidth / data.length * 0.8;
|
||||
const barSpacing = chartWidth / data.length * 0.2;
|
||||
const maxValue = Math.max(...data.map(d => d.value));
|
||||
|
||||
let animationProgress = 0;
|
||||
const animationDuration = config.animationDuration || 2000;
|
||||
let startTime = null;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
animationProgress = Math.min(elapsed / animationDuration, 1);
|
||||
|
||||
// Easing function (easeOutCubic)
|
||||
const progress = 1 - Math.pow(1 - animationProgress, 3);
|
||||
|
||||
manager.clear();
|
||||
this.renderBarChart(manager, data, config, progress, {
|
||||
padding,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
barWidth,
|
||||
barSpacing,
|
||||
maxValue
|
||||
});
|
||||
|
||||
if (animationProgress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render bar chart frame
|
||||
*/
|
||||
renderBarChart(manager, data, config, progress, layout) {
|
||||
const { ctx } = manager;
|
||||
const { padding, chartHeight, barWidth, barSpacing, maxValue } = layout;
|
||||
|
||||
// Draw bars
|
||||
data.forEach((item, index) => {
|
||||
const x = padding + index * (barWidth + barSpacing);
|
||||
const barHeight = (item.value / maxValue) * chartHeight * progress;
|
||||
const y = padding + chartHeight - barHeight;
|
||||
|
||||
// Bar
|
||||
ctx.fillStyle = item.color || config.barColor || 'rgba(100, 150, 255, 0.8)';
|
||||
ctx.fillRect(x, y, barWidth, barHeight);
|
||||
|
||||
// Label
|
||||
if (config.showLabels !== false) {
|
||||
ctx.fillStyle = config.textColor || '#333';
|
||||
ctx.font = config.font || '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(
|
||||
item.label,
|
||||
x + barWidth / 2,
|
||||
padding + chartHeight + 20
|
||||
);
|
||||
|
||||
// Value
|
||||
if (config.showValues !== false) {
|
||||
ctx.fillText(
|
||||
Math.round(item.value * progress),
|
||||
x + barWidth / 2,
|
||||
y - 5
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Draw axes if enabled
|
||||
if (config.showAxes !== false) {
|
||||
ctx.strokeStyle = config.axisColor || '#ccc';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
// Y-axis
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padding, padding);
|
||||
ctx.lineTo(padding, padding + chartHeight);
|
||||
ctx.stroke();
|
||||
|
||||
// X-axis
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padding, padding + chartHeight);
|
||||
ctx.lineTo(padding + layout.chartWidth, padding + chartHeight);
|
||||
ctx.stroke();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create animated line chart
|
||||
*/
|
||||
createLineChart(manager, data, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
const padding = config.padding || 40;
|
||||
const chartWidth = width - padding * 2;
|
||||
const chartHeight = height - padding * 2;
|
||||
|
||||
const maxValue = Math.max(...data.map(d => d.value));
|
||||
const minValue = Math.min(...data.map(d => d.value));
|
||||
const valueRange = maxValue - minValue;
|
||||
|
||||
let animationProgress = 0;
|
||||
const animationDuration = config.animationDuration || 2000;
|
||||
let startTime = null;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
animationProgress = Math.min(elapsed / animationDuration, 1);
|
||||
|
||||
manager.clear();
|
||||
this.renderLineChart(manager, data, config, animationProgress, {
|
||||
padding,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
maxValue,
|
||||
minValue,
|
||||
valueRange
|
||||
});
|
||||
|
||||
if (animationProgress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render line chart frame
|
||||
*/
|
||||
renderLineChart(manager, data, config, progress, layout) {
|
||||
const { ctx } = manager;
|
||||
const { padding, chartWidth, chartHeight, minValue, valueRange } = layout;
|
||||
|
||||
const pointsToShow = Math.floor(data.length * progress);
|
||||
|
||||
ctx.strokeStyle = config.lineColor || 'rgba(100, 150, 255, 0.8)';
|
||||
ctx.lineWidth = config.lineWidth || 3;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
// Draw line
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i <= pointsToShow && i < data.length; i++) {
|
||||
const x = padding + (i / (data.length - 1)) * chartWidth;
|
||||
const y = padding + chartHeight - ((data[i].value - minValue) / valueRange) * chartHeight;
|
||||
|
||||
if (i === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw points
|
||||
if (config.showPoints !== false) {
|
||||
ctx.fillStyle = config.pointColor || config.lineColor || 'rgba(100, 150, 255, 0.8)';
|
||||
|
||||
for (let i = 0; i <= pointsToShow && i < data.length; i++) {
|
||||
const x = padding + (i / (data.length - 1)) * chartWidth;
|
||||
const y = padding + chartHeight - ((data[i].value - minValue) / valueRange) * chartHeight;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, config.pointSize || 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Labels
|
||||
if (config.showLabels !== false && data[i].label) {
|
||||
ctx.fillStyle = config.textColor || '#333';
|
||||
ctx.font = config.font || '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(data[i].label, x, padding + chartHeight + 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create animated pie chart
|
||||
*/
|
||||
createPieChart(manager, data, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const radius = Math.min(width, height) / 2 * 0.8;
|
||||
|
||||
const total = data.reduce((sum, item) => sum + item.value, 0);
|
||||
|
||||
let animationProgress = 0;
|
||||
const animationDuration = config.animationDuration || 2000;
|
||||
let startTime = null;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
animationProgress = Math.min(elapsed / animationDuration, 1);
|
||||
|
||||
manager.clear();
|
||||
this.renderPieChart(manager, data, config, animationProgress, {
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
total
|
||||
});
|
||||
|
||||
if (animationProgress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render pie chart frame
|
||||
*/
|
||||
renderPieChart(manager, data, config, progress, layout) {
|
||||
const { ctx } = manager;
|
||||
const { centerX, centerY, radius, total } = layout;
|
||||
|
||||
let currentAngle = -Math.PI / 2; // Start at top
|
||||
const maxAngle = currentAngle + (Math.PI * 2 * progress);
|
||||
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
if (currentAngle >= maxAngle) break;
|
||||
|
||||
const item = data[index];
|
||||
const sliceAngle = (item.value / total) * Math.PI * 2;
|
||||
const endAngle = Math.min(currentAngle + sliceAngle, maxAngle);
|
||||
|
||||
if (endAngle > currentAngle) {
|
||||
// Draw slice
|
||||
ctx.fillStyle = item.color || this.getDefaultColor(index);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX, centerY);
|
||||
ctx.arc(centerX, centerY, radius, currentAngle, endAngle);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Draw labels if enabled
|
||||
if (config.showLabels !== false && endAngle === currentAngle + sliceAngle) {
|
||||
const labelAngle = currentAngle + sliceAngle / 2;
|
||||
const labelX = centerX + Math.cos(labelAngle) * radius * 0.7;
|
||||
const labelY = centerY + Math.sin(labelAngle) * radius * 0.7;
|
||||
|
||||
ctx.fillStyle = config.textColor || '#333';
|
||||
ctx.font = config.font || '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(item.label, labelX, labelY);
|
||||
}
|
||||
}
|
||||
|
||||
currentAngle += sliceAngle;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create progress visualization
|
||||
*/
|
||||
createProgressChart(manager, data, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
let animationProgress = 0;
|
||||
const animationDuration = config.animationDuration || 1500;
|
||||
let startTime = null;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
animationProgress = Math.min(elapsed / animationDuration, 1);
|
||||
|
||||
manager.clear();
|
||||
this.renderProgressChart(manager, data, config, animationProgress);
|
||||
|
||||
if (animationProgress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render progress chart
|
||||
*/
|
||||
renderProgressChart(manager, data, config, progress) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const radius = Math.min(width, height) / 2 * 0.8;
|
||||
const lineWidth = config.lineWidth || 20;
|
||||
|
||||
// Get progress value from data array
|
||||
const value = data[0]?.value || 0;
|
||||
const maxValue = config.maxValue || 100;
|
||||
const progressValue = (value / maxValue) * progress;
|
||||
|
||||
// Background circle
|
||||
ctx.strokeStyle = config.backgroundColor || 'rgba(200, 200, 200, 0.3)';
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Progress arc
|
||||
ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.8)';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + (progressValue / maxValue) * Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Center text
|
||||
if (config.showText !== false) {
|
||||
ctx.fillStyle = config.textColor || '#333';
|
||||
ctx.font = config.font || 'bold 24px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(Math.round(progressValue) + '%', centerX, centerY);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get default color for data point
|
||||
*/
|
||||
getDefaultColor(index) {
|
||||
const colors = [
|
||||
'rgba(100, 150, 255, 0.8)',
|
||||
'rgba(255, 100, 150, 0.8)',
|
||||
'rgba(150, 255, 100, 0.8)',
|
||||
'rgba(255, 200, 100, 0.8)',
|
||||
'rgba(200, 100, 255, 0.8)',
|
||||
'rgba(100, 255, 200, 0.8)'
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
};
|
||||
335
resources/js/modules/canvas-animations/InteractiveEffects.js
Normal file
335
resources/js/modules/canvas-animations/InteractiveEffects.js
Normal file
@@ -0,0 +1,335 @@
|
||||
// modules/canvas-animations/InteractiveEffects.js
|
||||
import { CanvasManager } from './CanvasManager.js';
|
||||
import { useEvent } from '../../core/useEvent.js';
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
/**
|
||||
* Interactive Canvas Effects - Mouse/touch interactions, hover effects
|
||||
*/
|
||||
export const InteractiveEffects = {
|
||||
|
||||
/**
|
||||
* Initialize interactive canvas
|
||||
*/
|
||||
init(canvas, config) {
|
||||
const manager = new CanvasManager(canvas);
|
||||
const effectType = config.effect || 'ripple';
|
||||
|
||||
const state = {
|
||||
mouse: { x: 0, y: 0, isOver: false },
|
||||
effects: [],
|
||||
lastTime: 0
|
||||
};
|
||||
|
||||
this.setupInteractionEvents(canvas, manager, state, config);
|
||||
this.startAnimationLoop(manager, state, effectType, config);
|
||||
|
||||
Logger.info('[InteractiveEffects] Initialized with effect:', effectType);
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup mouse/touch interaction events
|
||||
*/
|
||||
setupInteractionEvents(canvas, manager, state, config) {
|
||||
// Mouse events
|
||||
useEvent(canvas, 'mousemove', (e) => {
|
||||
const pos = manager.getMousePosition(e);
|
||||
state.mouse.x = pos.x;
|
||||
state.mouse.y = pos.y;
|
||||
|
||||
if (config.effect === 'trail') {
|
||||
this.addTrailPoint(state, pos.x, pos.y);
|
||||
}
|
||||
}, 'interactive-effects');
|
||||
|
||||
useEvent(canvas, 'mouseenter', () => {
|
||||
state.mouse.isOver = true;
|
||||
}, 'interactive-effects');
|
||||
|
||||
useEvent(canvas, 'mouseleave', () => {
|
||||
state.mouse.isOver = false;
|
||||
}, 'interactive-effects');
|
||||
|
||||
useEvent(canvas, 'click', (e) => {
|
||||
const pos = manager.getMousePosition(e);
|
||||
this.addClickEffect(state, pos.x, pos.y, config);
|
||||
}, 'interactive-effects');
|
||||
|
||||
// Touch events for mobile
|
||||
useEvent(canvas, 'touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
const pos = manager.getMousePosition(touch);
|
||||
this.addClickEffect(state, pos.x, pos.y, config);
|
||||
}, 'interactive-effects');
|
||||
|
||||
useEvent(canvas, 'touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
const pos = manager.getMousePosition(touch);
|
||||
state.mouse.x = pos.x;
|
||||
state.mouse.y = pos.y;
|
||||
state.mouse.isOver = true;
|
||||
|
||||
if (config.effect === 'trail') {
|
||||
this.addTrailPoint(state, pos.x, pos.y);
|
||||
}
|
||||
}, 'interactive-effects');
|
||||
|
||||
useEvent(canvas, 'touchend', () => {
|
||||
state.mouse.isOver = false;
|
||||
}, 'interactive-effects');
|
||||
},
|
||||
|
||||
/**
|
||||
* Start animation loop
|
||||
*/
|
||||
startAnimationLoop(manager, state, effectType, config) {
|
||||
const animate = (timestamp) => {
|
||||
const deltaTime = timestamp - state.lastTime;
|
||||
state.lastTime = timestamp;
|
||||
|
||||
manager.clear();
|
||||
|
||||
switch (effectType) {
|
||||
case 'ripple':
|
||||
this.renderRippleEffect(manager, state, config);
|
||||
break;
|
||||
case 'trail':
|
||||
this.renderTrailEffect(manager, state, config);
|
||||
break;
|
||||
case 'particles':
|
||||
this.renderParticleEffect(manager, state, config, deltaTime);
|
||||
break;
|
||||
case 'magnetic':
|
||||
this.renderMagneticEffect(manager, state, config);
|
||||
break;
|
||||
default:
|
||||
this.renderRippleEffect(manager, state, config);
|
||||
}
|
||||
|
||||
// Update effects
|
||||
this.updateEffects(state.effects, deltaTime);
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add click effect (ripple, explosion, etc.)
|
||||
*/
|
||||
addClickEffect(state, x, y, config) {
|
||||
const effect = {
|
||||
x,
|
||||
y,
|
||||
age: 0,
|
||||
maxAge: config.duration || 1000,
|
||||
type: 'click',
|
||||
intensity: config.intensity || 1
|
||||
};
|
||||
|
||||
state.effects.push(effect);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add trail point for mouse trail effect
|
||||
*/
|
||||
addTrailPoint(state, x, y) {
|
||||
const point = {
|
||||
x,
|
||||
y,
|
||||
age: 0,
|
||||
maxAge: 500,
|
||||
type: 'trail'
|
||||
};
|
||||
|
||||
state.effects.push(point);
|
||||
|
||||
// Limit trail length
|
||||
const trailLength = 20;
|
||||
const trailPoints = state.effects.filter(e => e.type === 'trail');
|
||||
if (trailPoints.length > trailLength) {
|
||||
const oldestIndex = state.effects.indexOf(trailPoints[0]);
|
||||
state.effects.splice(oldestIndex, 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render ripple effect
|
||||
*/
|
||||
renderRippleEffect(manager, state, config) {
|
||||
const { ctx } = manager;
|
||||
|
||||
// Draw active ripples
|
||||
state.effects.forEach(effect => {
|
||||
if (effect.type === 'click') {
|
||||
const progress = effect.age / effect.maxAge;
|
||||
const radius = progress * (config.maxRadius || 100);
|
||||
const opacity = (1 - progress) * 0.8;
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = opacity;
|
||||
ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 1)';
|
||||
ctx.lineWidth = config.lineWidth || 3;
|
||||
ctx.beginPath();
|
||||
ctx.arc(effect.x / manager.options.pixelRatio, effect.y / manager.options.pixelRatio, radius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw hover effect
|
||||
if (state.mouse.isOver) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.fillStyle = config.hoverColor || 'rgba(100, 150, 255, 0.3)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
state.mouse.x / manager.options.pixelRatio,
|
||||
state.mouse.y / manager.options.pixelRatio,
|
||||
config.hoverRadius || 30,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render trail effect
|
||||
*/
|
||||
renderTrailEffect(manager, state, config) {
|
||||
const { ctx } = manager;
|
||||
const trailPoints = state.effects.filter(e => e.type === 'trail');
|
||||
|
||||
if (trailPoints.length < 2) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.8)';
|
||||
ctx.lineWidth = config.lineWidth || 5;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
// Draw trail path
|
||||
ctx.beginPath();
|
||||
trailPoints.forEach((point, index) => {
|
||||
const progress = 1 - (point.age / point.maxAge);
|
||||
const x = point.x / manager.options.pixelRatio;
|
||||
const y = point.y / manager.options.pixelRatio;
|
||||
|
||||
ctx.globalAlpha = progress * 0.8;
|
||||
|
||||
if (index === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
/**
|
||||
* Render particle effect
|
||||
*/
|
||||
renderParticleEffect(manager, state, config, deltaTime) {
|
||||
const { ctx } = manager;
|
||||
|
||||
// Spawn particles on mouse move
|
||||
if (state.mouse.isOver && Math.random() < 0.1) {
|
||||
const particle = {
|
||||
x: state.mouse.x,
|
||||
y: state.mouse.y,
|
||||
vx: (Math.random() - 0.5) * 4,
|
||||
vy: (Math.random() - 0.5) * 4,
|
||||
age: 0,
|
||||
maxAge: 1000,
|
||||
size: Math.random() * 5 + 2,
|
||||
type: 'particle'
|
||||
};
|
||||
state.effects.push(particle);
|
||||
}
|
||||
|
||||
// Update and draw particles
|
||||
state.effects.forEach(effect => {
|
||||
if (effect.type === 'particle') {
|
||||
// Update position
|
||||
effect.x += effect.vx;
|
||||
effect.y += effect.vy;
|
||||
effect.vy += 0.1; // Gravity
|
||||
|
||||
const progress = effect.age / effect.maxAge;
|
||||
const opacity = (1 - progress) * 0.8;
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = opacity;
|
||||
ctx.fillStyle = config.color || 'rgba(100, 150, 255, 1)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
effect.x / manager.options.pixelRatio,
|
||||
effect.y / manager.options.pixelRatio,
|
||||
effect.size * (1 - progress * 0.5),
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Render magnetic effect
|
||||
*/
|
||||
renderMagneticEffect(manager, state, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
if (!state.mouse.isOver) return;
|
||||
|
||||
// Draw magnetic field lines
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const mouseX = state.mouse.x / manager.options.pixelRatio;
|
||||
const mouseY = state.mouse.y / manager.options.pixelRatio;
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.6)';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
const lines = 8;
|
||||
for (let i = 0; i < lines; i++) {
|
||||
const angle = (i / lines) * Math.PI * 2;
|
||||
const startX = centerX + Math.cos(angle) * 50;
|
||||
const startY = centerY + Math.sin(angle) * 50;
|
||||
|
||||
// Curve towards mouse
|
||||
const controlX = (startX + mouseX) / 2 + Math.sin(angle) * 30;
|
||||
const controlY = (startY + mouseY) / 2 + Math.cos(angle) * 30;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, startY);
|
||||
ctx.quadraticCurveTo(controlX, controlY, mouseX, mouseY);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update all effects (age and cleanup)
|
||||
*/
|
||||
updateEffects(effects, deltaTime) {
|
||||
for (let i = effects.length - 1; i >= 0; i--) {
|
||||
effects[i].age += deltaTime;
|
||||
|
||||
if (effects[i].age >= effects[i].maxAge) {
|
||||
effects.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
244
resources/js/modules/canvas-animations/ScrollEffects.js
Normal file
244
resources/js/modules/canvas-animations/ScrollEffects.js
Normal file
@@ -0,0 +1,244 @@
|
||||
// modules/canvas-animations/ScrollEffects.js
|
||||
import { CanvasManager } from './CanvasManager.js';
|
||||
import { useEvent } from '../../core/useEvent.js';
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
/**
|
||||
* Scroll-based Canvas Effects - Parallax and scroll animations
|
||||
*/
|
||||
export const ScrollEffects = {
|
||||
|
||||
/**
|
||||
* Initialize parallax canvas effect
|
||||
*/
|
||||
initParallax(canvas, config) {
|
||||
const manager = new CanvasManager(canvas);
|
||||
const elements = this.createParallaxElements(canvas, config);
|
||||
|
||||
let ticking = false;
|
||||
|
||||
const updateParallax = () => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
this.renderParallax(manager, elements, config);
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to scroll events
|
||||
useEvent(window, 'scroll', updateParallax, 'scroll-parallax');
|
||||
useEvent(window, 'resize', updateParallax, 'scroll-parallax');
|
||||
|
||||
// Initial render
|
||||
updateParallax();
|
||||
|
||||
Logger.info('[ScrollEffects] Parallax initialized with', elements.length, 'elements');
|
||||
},
|
||||
|
||||
/**
|
||||
* Create parallax elements based on config
|
||||
*/
|
||||
createParallaxElements(canvas, config) {
|
||||
const elements = [];
|
||||
const layerCount = config.layers || 3;
|
||||
const elementCount = config.elements || 20;
|
||||
|
||||
for (let i = 0; i < elementCount; i++) {
|
||||
elements.push({
|
||||
x: Math.random() * canvas.clientWidth,
|
||||
y: Math.random() * canvas.clientHeight * 2, // Allow elements outside viewport
|
||||
size: Math.random() * 20 + 5,
|
||||
layer: Math.floor(Math.random() * layerCount),
|
||||
speed: 0.1 + (Math.random() * 0.5), // Different parallax speeds
|
||||
opacity: Math.random() * 0.7 + 0.3,
|
||||
color: this.getLayerColor(Math.floor(Math.random() * layerCount), config)
|
||||
});
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get color for parallax layer
|
||||
*/
|
||||
getLayerColor(layer, config) {
|
||||
const colors = config.colors || [
|
||||
'rgba(100, 150, 255, 0.6)', // Front layer - more opaque
|
||||
'rgba(150, 100, 255, 0.4)', // Middle layer
|
||||
'rgba(200, 100, 150, 0.2)' // Back layer - more transparent
|
||||
];
|
||||
return colors[layer] || colors[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Render parallax effect
|
||||
*/
|
||||
renderParallax(manager, elements, config) {
|
||||
manager.clear();
|
||||
|
||||
const scrollY = window.pageYOffset;
|
||||
const canvasRect = manager.canvas.getBoundingClientRect();
|
||||
const canvasTop = canvasRect.top + scrollY;
|
||||
|
||||
// Calculate relative scroll position
|
||||
const relativeScroll = scrollY - canvasTop;
|
||||
const scrollProgress = relativeScroll / window.innerHeight;
|
||||
|
||||
elements.forEach(element => {
|
||||
// Apply parallax offset based on layer and scroll
|
||||
const parallaxOffset = scrollProgress * element.speed * 100;
|
||||
const y = element.y - parallaxOffset;
|
||||
|
||||
// Only render elements that are potentially visible
|
||||
if (y > -element.size && y < manager.canvas.clientHeight + element.size) {
|
||||
manager.ctx.save();
|
||||
manager.ctx.globalAlpha = element.opacity;
|
||||
manager.ctx.fillStyle = element.color;
|
||||
manager.ctx.beginPath();
|
||||
manager.ctx.arc(element.x, y, element.size, 0, Math.PI * 2);
|
||||
manager.ctx.fill();
|
||||
manager.ctx.restore();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize scroll-based animations
|
||||
*/
|
||||
initScrollAnimation(canvas, config) {
|
||||
const manager = new CanvasManager(canvas);
|
||||
const animationType = config.animation || 'wave';
|
||||
|
||||
let ticking = false;
|
||||
|
||||
const updateAnimation = () => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
this.renderScrollAnimation(manager, animationType, config);
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
};
|
||||
|
||||
useEvent(window, 'scroll', updateAnimation, 'scroll-animation');
|
||||
useEvent(window, 'resize', updateAnimation, 'scroll-animation');
|
||||
|
||||
// Initial render
|
||||
updateAnimation();
|
||||
|
||||
Logger.info('[ScrollEffects] Scroll animation initialized:', animationType);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render scroll-based animations
|
||||
*/
|
||||
renderScrollAnimation(manager, animationType, config) {
|
||||
manager.clear();
|
||||
|
||||
const scrollY = window.pageYOffset;
|
||||
const canvasRect = manager.canvas.getBoundingClientRect();
|
||||
const canvasTop = canvasRect.top + scrollY;
|
||||
const relativeScroll = scrollY - canvasTop;
|
||||
const scrollProgress = Math.max(0, Math.min(1, relativeScroll / window.innerHeight));
|
||||
|
||||
switch (animationType) {
|
||||
case 'wave':
|
||||
this.renderWaveAnimation(manager, scrollProgress, config);
|
||||
break;
|
||||
case 'progress':
|
||||
this.renderProgressAnimation(manager, scrollProgress, config);
|
||||
break;
|
||||
case 'morph':
|
||||
this.renderMorphAnimation(manager, scrollProgress, config);
|
||||
break;
|
||||
default:
|
||||
this.renderWaveAnimation(manager, scrollProgress, config);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render wave animation based on scroll
|
||||
*/
|
||||
renderWaveAnimation(manager, progress, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.8)';
|
||||
ctx.lineWidth = config.lineWidth || 3;
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
const amplitude = (config.amplitude || 50) * progress;
|
||||
const frequency = config.frequency || 0.02;
|
||||
const phase = progress * Math.PI * 2;
|
||||
|
||||
for (let x = 0; x <= width; x += 2) {
|
||||
const y = height / 2 + Math.sin(x * frequency + phase) * amplitude;
|
||||
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
},
|
||||
|
||||
/**
|
||||
* Render progress bar animation
|
||||
*/
|
||||
renderProgressAnimation(manager, progress, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
const barHeight = config.barHeight || 10;
|
||||
const y = height / 2 - barHeight / 2;
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = config.backgroundColor || 'rgba(255, 255, 255, 0.2)';
|
||||
ctx.fillRect(0, y, width, barHeight);
|
||||
|
||||
// Progress
|
||||
ctx.fillStyle = config.color || 'rgba(100, 150, 255, 0.8)';
|
||||
ctx.fillRect(0, y, width * progress, barHeight);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render morphing shapes
|
||||
*/
|
||||
renderMorphAnimation(manager, progress, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
ctx.fillStyle = config.color || 'rgba(100, 150, 255, 0.6)';
|
||||
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const maxRadius = Math.min(width, height) / 3;
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
const points = config.points || 6;
|
||||
for (let i = 0; i <= points; i++) {
|
||||
const angle = (i / points) * Math.PI * 2;
|
||||
const radiusVariation = Math.sin(progress * Math.PI * 4 + angle * 3) * 0.3 + 1;
|
||||
const radius = maxRadius * progress * radiusVariation;
|
||||
|
||||
const x = centerX + Math.cos(angle) * radius;
|
||||
const y = centerY + Math.sin(angle) * radius;
|
||||
|
||||
if (i === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
153
resources/js/modules/canvas-animations/index.js
Normal file
153
resources/js/modules/canvas-animations/index.js
Normal file
@@ -0,0 +1,153 @@
|
||||
// modules/canvas-animations/index.js
|
||||
import { Logger } from '../../core/logger.js';
|
||||
import { CanvasManager } from './CanvasManager.js';
|
||||
import { ScrollEffects } from './ScrollEffects.js';
|
||||
import { InteractiveEffects } from './InteractiveEffects.js';
|
||||
import { DataVisualization } from './DataVisualization.js';
|
||||
|
||||
const CanvasAnimationsModule = {
|
||||
name: 'canvas-animations',
|
||||
|
||||
// Module-level init (called by module system)
|
||||
init(config = {}, state = null) {
|
||||
Logger.info('[CanvasAnimations] Module initialized');
|
||||
this.initializeCanvasElements();
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize all canvas elements with data-canvas attributes
|
||||
*/
|
||||
initializeCanvasElements() {
|
||||
// Auto-discover canvas elements
|
||||
const canvasElements = document.querySelectorAll('canvas[data-canvas-type]');
|
||||
|
||||
canvasElements.forEach(canvas => {
|
||||
this.initElement(canvas);
|
||||
});
|
||||
|
||||
Logger.info(`[CanvasAnimations] Initialized ${canvasElements.length} canvas elements`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize individual canvas element
|
||||
*/
|
||||
initElement(canvas, options = {}) {
|
||||
const canvasType = canvas.dataset.canvasType;
|
||||
const canvasConfig = this.parseCanvasConfig(canvas);
|
||||
|
||||
Logger.info(`[CanvasAnimations] Initializing canvas type: ${canvasType}`);
|
||||
|
||||
switch (canvasType) {
|
||||
case 'interactive':
|
||||
InteractiveEffects.init(canvas, canvasConfig);
|
||||
break;
|
||||
case 'scroll-parallax':
|
||||
ScrollEffects.initParallax(canvas, canvasConfig);
|
||||
break;
|
||||
case 'scroll-animation':
|
||||
ScrollEffects.initScrollAnimation(canvas, canvasConfig);
|
||||
break;
|
||||
case 'data-viz':
|
||||
DataVisualization.init(canvas, canvasConfig);
|
||||
break;
|
||||
case 'background':
|
||||
this.initBackgroundAnimation(canvas, canvasConfig);
|
||||
break;
|
||||
default:
|
||||
Logger.warn(`[CanvasAnimations] Unknown canvas type: ${canvasType}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse configuration from data attributes
|
||||
*/
|
||||
parseCanvasConfig(canvas) {
|
||||
const config = {};
|
||||
|
||||
// Parse all data-canvas-* attributes
|
||||
Object.keys(canvas.dataset).forEach(key => {
|
||||
if (key.startsWith('canvas')) {
|
||||
const configKey = key.replace('canvas', '').toLowerCase();
|
||||
let value = canvas.dataset[key];
|
||||
|
||||
// Try to parse as JSON if it looks like an object/array
|
||||
if ((value.startsWith('{') && value.endsWith('}')) ||
|
||||
(value.startsWith('[') && value.endsWith(']'))) {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (e) {
|
||||
Logger.warn(`[CanvasAnimations] Failed to parse JSON config: ${key}`, e);
|
||||
}
|
||||
}
|
||||
// Parse boolean strings
|
||||
else if (value === 'true') value = true;
|
||||
else if (value === 'false') value = false;
|
||||
// Parse numbers
|
||||
else if (!isNaN(value) && value !== '') {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
config[configKey] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize background animation
|
||||
*/
|
||||
initBackgroundAnimation(canvas, config) {
|
||||
const manager = new CanvasManager(canvas);
|
||||
|
||||
// Default background animation: floating particles
|
||||
const particleCount = config.particles || 50;
|
||||
const particles = [];
|
||||
|
||||
// Create particles
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
vx: (Math.random() - 0.5) * 0.5,
|
||||
vy: (Math.random() - 0.5) * 0.5,
|
||||
size: Math.random() * 3 + 1,
|
||||
opacity: Math.random() * 0.5 + 0.2
|
||||
});
|
||||
}
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
manager.clear();
|
||||
manager.ctx.fillStyle = config.color || 'rgba(100, 150, 255, 0.6)';
|
||||
|
||||
particles.forEach(particle => {
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
|
||||
// Wrap around edges
|
||||
if (particle.x < 0) particle.x = canvas.width;
|
||||
if (particle.x > canvas.width) particle.x = 0;
|
||||
if (particle.y < 0) particle.y = canvas.height;
|
||||
if (particle.y > canvas.height) particle.y = 0;
|
||||
|
||||
// Draw particle
|
||||
manager.ctx.save();
|
||||
manager.ctx.globalAlpha = particle.opacity;
|
||||
manager.ctx.beginPath();
|
||||
manager.ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||
manager.ctx.fill();
|
||||
manager.ctx.restore();
|
||||
});
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animate();
|
||||
}
|
||||
};
|
||||
|
||||
// Export für module system
|
||||
export const init = CanvasAnimationsModule.init.bind(CanvasAnimationsModule);
|
||||
export default CanvasAnimationsModule;
|
||||
Reference in New Issue
Block a user