/**
* @fileoverview Utility functions for interactive animations
* @module InteractionUtils
*/
/**
* @typedef {Object} InteractionPoint
* @property {number} x - X coordinate
* @property {number} y - Y coordinate
* @property {number} force - Interaction force (0-1)
* @property {string} type - 'mouse' | 'touch'
*/
/**
* @typedef {Object} InteractionConfig
* @property {string} effect - 'attract' | 'repel' | 'follow' | 'burst' | 'gravity' | 'magnetic' | 'vortex' | 'wave' | 'elastic'
* @property {number} strength - Effect strength (0-1)
* @property {number} radius - Interaction radius in pixels
* @property {boolean} continuous - Whether effect continues after interaction ends
*/
/**
* Creates an interaction handler for canvas animations
* @param {HTMLCanvasElement} canvas - The canvas element
* @param {InteractionConfig} config - Interaction configuration
* @returns {Object} Interaction handler with event listeners and state
*/
export const createInteractionHandler = (canvas, config = {}) => {
const {
effect = 'attract',
strength = 0.5,
radius = 100,
continuous = false
} = config;
let isInteracting = false;
let interactionPoints = [];
let touchPoints = new Map();
/**
* Get normalized coordinates from event
* @param {Event} event - Mouse or touch event
* @returns {InteractionPoint|null} Normalized interaction point
*/
const getInteractionPoint = (event) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
if (event.type.startsWith('touch')) {
const touch = event.touches[0] || event.changedTouches[0];
return {
x: (touch.clientX - rect.left) * scaleX,
y: (touch.clientY - rect.top) * scaleY,
force: touch.force || 0.5,
type: 'touch'
};
} else {
return {
x: (event.clientX - rect.left) * scaleX,
y: (event.clientY - rect.top) * scaleY,
force: 0.5,
type: 'mouse'
};
}
};
/**
* Calculate interaction force between two points
* @param {Object} particle - Particle with x, y coordinates
* @param {InteractionPoint} interactionPoint - Interaction point
* @returns {Object} Force vector {fx, fy, distance}
*/
const calculateInteractionForce = (particle, interactionPoint) => {
const dx = interactionPoint.x - particle.x;
const dy = interactionPoint.y - particle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0 || distance > radius) {
return { fx: 0, fy: 0, distance };
}
const normalizedDistance = distance / radius;
let forceMagnitude = (1 - normalizedDistance) * strength * interactionPoint.force;
switch (effect) {
case 'attract':
forceMagnitude *= 1;
break;
case 'repel':
forceMagnitude *= -1;
break;
case 'gravity':
forceMagnitude *= (1 / (distance * distance + 1));
break;
case 'burst':
forceMagnitude *= distance < radius * 0.3 ? -2 : 0;
break;
case 'magnetic':
// New: Magnetic field effect - stronger at poles
const angle = Math.atan2(dy, dx);
forceMagnitude *= Math.sin(angle * 2) * 1.5;
break;
case 'vortex':
// New: Spinning vortex effect
const perpX = -dy / distance;
const perpY = dx / distance;
return {
fx: perpX * forceMagnitude + (dx / distance) * forceMagnitude * 0.3,
fy: perpY * forceMagnitude + (dy / distance) * forceMagnitude * 0.3,
distance
};
case 'wave':
// New: Wave ripple effect
const time = Date.now() * 0.005;
const wavePhase = Math.sin(distance * 0.05 + time) * 0.5 + 0.5;
forceMagnitude *= wavePhase;
break;
case 'elastic':
// New: Elastic spring effect
forceMagnitude = distance < radius * 0.5 ? -forceMagnitude * 2 : forceMagnitude;
break;
default:
forceMagnitude = 0;
}
const fx = (dx / distance) * forceMagnitude;
const fy = (dy / distance) * forceMagnitude;
return { fx, fy, distance };
};
// Event handlers
const handleMouseDown = (event) => {
isInteracting = true;
const point = getInteractionPoint(event);
if (point) interactionPoints = [point];
};
const handleMouseMove = (event) => {
if (isInteracting || continuous) {
const point = getInteractionPoint(event);
if (point) interactionPoints = [point];
}
};
const handleMouseUp = () => {
isInteracting = false;
if (!continuous) interactionPoints = [];
};
const handleTouchStart = (event) => {
event.preventDefault();
Array.from(event.touches).forEach((touch, index) => {
const point = {
x: (touch.clientX - canvas.getBoundingClientRect().left) * (canvas.width / canvas.getBoundingClientRect().width),
y: (touch.clientY - canvas.getBoundingClientRect().top) * (canvas.height / canvas.getBoundingClientRect().height),
force: touch.force || 0.5,
type: 'touch'
};
touchPoints.set(touch.identifier, point);
});
interactionPoints = Array.from(touchPoints.values());
};
const handleTouchMove = (event) => {
event.preventDefault();
Array.from(event.touches).forEach((touch) => {
const point = {
x: (touch.clientX - canvas.getBoundingClientRect().left) * (canvas.width / canvas.getBoundingClientRect().width),
y: (touch.clientY - canvas.getBoundingClientRect().top) * (canvas.height / canvas.getBoundingClientRect().height),
force: touch.force || 0.5,
type: 'touch'
};
touchPoints.set(touch.identifier, point);
});
interactionPoints = Array.from(touchPoints.values());
};
const handleTouchEnd = (event) => {
event.preventDefault();
Array.from(event.changedTouches).forEach((touch) => {
touchPoints.delete(touch.identifier);
});
interactionPoints = Array.from(touchPoints.values());
};
// Attach event listeners
const attachListeners = () => {
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mouseleave', handleMouseUp);
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
canvas.addEventListener('touchend', handleTouchEnd, { passive: false });
canvas.addEventListener('touchcancel', handleTouchEnd, { passive: false });
};
// Remove event listeners
const removeListeners = () => {
canvas.removeEventListener('mousedown', handleMouseDown);
canvas.removeEventListener('mousemove', handleMouseMove);
canvas.removeEventListener('mouseup', handleMouseUp);
canvas.removeEventListener('mouseleave', handleMouseUp);
canvas.removeEventListener('touchstart', handleTouchStart);
canvas.removeEventListener('touchmove', handleTouchMove);
canvas.removeEventListener('touchend', handleTouchEnd);
canvas.removeEventListener('touchcancel', handleTouchEnd);
};
return {
attachListeners,
removeListeners,
calculateInteractionForce,
getInteractionPoints: () => interactionPoints,
isInteracting: () => isInteracting,
updateConfig: (newConfig) => {
Object.assign(config, newConfig);
}
};
};
/**
* Gesture recognition utilities
*/
export const GestureRecognizer = {
/**
* Detect pinch gesture for zoom
* @param {TouchEvent} event - Touch event
* @returns {Object|null} Pinch data or null
*/
detectPinch: (event) => {
if (event.touches.length !== 2) return null;
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const distance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
const centerX = (touch1.clientX + touch2.clientX) / 2;
const centerY = (touch1.clientY + touch2.clientY) / 2;
return {
distance,
centerX,
centerY,
rotation: Math.atan2(touch2.clientY - touch1.clientY, touch2.clientX - touch1.clientX)
};
},
/**
* Detect swipe gesture
* @param {Object} startPoint - Starting touch point
* @param {Object} endPoint - Ending touch point
* @param {number} minDistance - Minimum distance for swipe
* @returns {Object|null} Swipe data or null
*/
detectSwipe: (startPoint, endPoint, minDistance = 50) => {
const deltaX = endPoint.x - startPoint.x;
const deltaY = endPoint.y - startPoint.y;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < minDistance) return null;
const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
let direction = 'right';
if (angle >= -45 && angle < 45) direction = 'right';
else if (angle >= 45 && angle < 135) direction = 'down';
else if (angle >= 135 || angle < -135) direction = 'left';
else direction = 'up';
return {
direction,
distance,
deltaX,
deltaY,
velocity: distance / (endPoint.time - startPoint.time)
};
}
};
Source