Bug fixes

This commit is contained in:
Aodhan Collins
2025-07-31 01:38:07 +01:00
parent ec6d40bf8f
commit 5e893a0c9d
12 changed files with 3917 additions and 212 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -160,7 +160,7 @@ document.addEventListener('DOMContentLoaded', function() {
<input type="checkbox" class="selection-checkbox">
</div>
<img src="${ensureImagePath(selection.image_path)}" alt="Selected image" loading="lazy" class="${blurClass}">
<div class="selection-action action-${actionClass(selection.action)}"><img src="/static/icons/${actionIconMap[selection.action]}" alt="${selection.action}" class="selection-action"></div>
<div class="selection-action action-${actionClass(selection.action)}"><img src="/static/icons/${getActionIcon(selection.action)}" alt="${selection.action}" class="selection-action"></div>
<div class="selection-info">
<p>${selection.image_path.split('/').pop()}</p>
<p>Resolution: ${selection.resolution}</p>
@@ -183,8 +183,23 @@ document.addEventListener('DOMContentLoaded', function() {
});
const actionClass = (action) => {
// Direct mapping for full action names
const map = { 'Discarded':'discard', 'Kept':'keep', 'Favourited':'favorite', 'Reviewing':'review' };
return map[action] || 'discard';
// Check direct mapping first
if (map[action]) {
return map[action];
}
// Backwards compatibility: check first letter for single-letter actions and legacy actions
const firstLetter = action ? action.charAt(0).toUpperCase() : '';
switch (firstLetter) {
case 'K': return 'keep'; // Keep/Kept
case 'D': return 'discard'; // Discard/Discarded
case 'F': return 'favorite'; // Favorite/Favourited
case 'R': return 'review'; // Review/Reviewed/Reviewing
default: return 'discard'; // Default fallback
}
};
// Map action names to icon filenames for display
@@ -194,6 +209,24 @@ document.addEventListener('DOMContentLoaded', function() {
'Favourited': 'fav.svg',
'Reviewing': 'review.svg'
};
// Get appropriate icon with backwards compatibility
const getActionIcon = (action) => {
// Check direct mapping first
if (actionIconMap[action]) {
return actionIconMap[action];
}
// Backwards compatibility: check first letter
const firstLetter = action ? action.charAt(0).toUpperCase() : '';
switch (firstLetter) {
case 'K': return 'keep.svg'; // Keep/Kept
case 'D': return 'discard.svg'; // Discard/Discarded
case 'F': return 'fav.svg'; // Favorite/Favourited
case 'R': return 'review.svg'; // Review/Reviewed/Reviewing
default: return 'discard.svg'; // Default fallback
}
};
const getActionName = (action) => action;
const formatDate = (ts) => {

471
js/swipe-integration.js Normal file
View File

@@ -0,0 +1,471 @@
/**
* Swipe Integration Manager
* Allows seamless switching between original and enhanced swipe systems
*/
import { showToast, updateImageInfo } from './utils.js';
import EnhancedSwipeCard from '../components/enhanced-swipe-card.js';
export class SwipeIntegrationManager {
constructor() {
this.currentMode = this.detectOptimalMode();
this.swipeCard = null;
this.isInitialized = false;
// Configuration options
this.config = {
enablePhysics: true,
enableVisualEffects: true,
enableHapticFeedback: true,
animationQuality: 'auto', // 'low', 'medium', 'high', 'auto'
performanceMode: 'auto', // 'low', 'normal', 'high', 'auto'
debugMode: false
};
// Load user preferences
this.loadUserPreferences();
console.log(`SwipeIntegrationManager initialized in ${this.currentMode} mode`);
}
/**
* Detect optimal swipe mode based on device capabilities
*/
detectOptimalMode() {
const userAgent = navigator.userAgent.toLowerCase();
const isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/.test(userAgent);
const isLowEnd = /android.*chrome\/[1-6][0-9]/.test(userAgent);
const hasReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Check device memory if available
const deviceMemory = navigator.deviceMemory || 4;
const hasLowMemory = deviceMemory < 2;
// Check connection speed if available
let hasSlowConnection = false;
if ('connection' in navigator) {
const connection = navigator.connection;
hasSlowConnection = connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g';
}
// Determine optimal mode
if (hasReducedMotion || isLowEnd || hasLowMemory || hasSlowConnection) {
return 'original';
} else if (isMobile) {
return 'enhanced-lite';
} else {
return 'enhanced';
}
}
/**
* Initialize the swipe system
*/
async initialize(container, onSwipe, options = {}) {
if (this.isInitialized) {
console.warn('SwipeIntegrationManager already initialized');
return;
}
// Merge options with config
this.config = { ...this.config, ...options };
try {
switch (this.currentMode) {
case 'enhanced':
await this.initializeEnhancedMode(container, onSwipe);
break;
case 'enhanced-lite':
await this.initializeEnhancedLiteMode(container, onSwipe);
break;
case 'original':
default:
await this.initializeOriginalMode(container, onSwipe);
break;
}
this.isInitialized = true;
this.logInitialization();
} catch (error) {
console.error('Failed to initialize swipe system:', error);
// Clean up any partially initialized swipe card
if (this.swipeCard && typeof this.swipeCard.destroy === 'function') {
this.swipeCard.destroy();
this.swipeCard = null;
}
// Fallback to original mode
await this.initializeOriginalMode(container, onSwipe);
this.currentMode = 'original';
this.isInitialized = true;
}
}
/**
* Initialize enhanced mode with full physics
*/
async initializeEnhancedMode(container, onSwipe) {
this.swipeCard = new EnhancedSwipeCard({
container,
onSwipe,
threshold: 100,
velocityThreshold: 2.5,
springTension: 320,
springFriction: 25,
mass: 1.0,
momentumDecay: 0.90
});
// Add enhanced CSS classes
document.body.classList.add('enhanced-swipe-mode');
container.classList.add('enhanced-container');
// Load enhanced styles if not already loaded
await this.loadEnhancedStyles();
}
/**
* Initialize enhanced lite mode for mobile devices
*/
async initializeEnhancedLiteMode(container, onSwipe) {
this.swipeCard = new EnhancedSwipeCard({
container,
onSwipe,
threshold: 80,
velocityThreshold: 2.0,
springTension: 280,
springFriction: 30,
mass: 1.2,
momentumDecay: 0.92
});
// Add lite mode classes
document.body.classList.add('enhanced-swipe-lite-mode');
container.classList.add('enhanced-container-lite');
// Disable some heavy effects for performance
this.config.enableVisualEffects = false;
await this.loadEnhancedStyles();
}
/**
* Initialize original mode as fallback
*/
async initializeOriginalMode(container, onSwipe) {
// Import and initialize fallback swipe card
const { default: FallbackSwipeCard } = await import('../components/fallback-swipe-card.js');
this.swipeCard = new FallbackSwipeCard({
container,
onSwipe,
threshold: 100
});
document.body.classList.add('original-swipe-mode');
}
/**
* Load enhanced styles dynamically
*/
async loadEnhancedStyles() {
return new Promise((resolve, reject) => {
// Check if enhanced styles are already loaded
if (document.querySelector('link[href*="enhanced-animations"]')) {
resolve();
return;
}
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'scss/enhanced-animations.css'; // Compiled CSS
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
}
/**
* Switch between swipe modes dynamically
*/
async switchMode(newMode) {
if (newMode === this.currentMode) {
console.log(`Already in ${newMode} mode`);
return;
}
console.log(`Switching from ${this.currentMode} to ${newMode} mode`);
// Clean up current mode
if (this.swipeCard && typeof this.swipeCard.destroy === 'function') {
this.swipeCard.destroy();
}
// Remove current mode classes
document.body.classList.remove(
'enhanced-swipe-mode',
'enhanced-swipe-lite-mode',
'original-swipe-mode'
);
// Update mode and reinitialize
this.currentMode = newMode;
this.isInitialized = false;
// Save user preference
this.saveUserPreference('swipeMode', newMode);
// Show feedback to user
showToast(`Switched to ${newMode} swipe mode`, 'info');
}
/**
* Get current swipe card instance
*/
getSwipeCard() {
return this.swipeCard;
}
/**
* Get current mode
*/
getCurrentMode() {
return this.currentMode;
}
/**
* Get available modes
*/
getAvailableModes() {
return ['original', 'enhanced-lite', 'enhanced'];
}
/**
* Update configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
// Apply config changes to current swipe card if applicable
if (this.swipeCard && typeof this.swipeCard.updateConfig === 'function') {
this.swipeCard.updateConfig(this.config);
}
// Save configuration
this.saveUserPreference('swipeConfig', this.config);
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Load user preferences from localStorage
*/
loadUserPreferences() {
try {
const savedMode = localStorage.getItem('swipeMode');
if (savedMode && this.getAvailableModes().includes(savedMode)) {
this.currentMode = savedMode;
}
const savedConfig = localStorage.getItem('swipeConfig');
if (savedConfig) {
this.config = { ...this.config, ...JSON.parse(savedConfig) };
}
} catch (error) {
console.warn('Failed to load user preferences:', error);
}
}
/**
* Save user preference to localStorage
*/
saveUserPreference(key, value) {
try {
if (typeof value === 'object') {
localStorage.setItem(key, JSON.stringify(value));
} else {
localStorage.setItem(key, value);
}
} catch (error) {
console.warn('Failed to save user preference:', error);
}
}
/**
* Performance monitoring
*/
getPerformanceMetrics() {
if (this.swipeCard && typeof this.swipeCard.getPerformanceMetrics === 'function') {
return this.swipeCard.getPerformanceMetrics();
}
return null;
}
/**
* Debug information
*/
getDebugInfo() {
return {
currentMode: this.currentMode,
config: this.config,
isInitialized: this.isInitialized,
deviceInfo: {
userAgent: navigator.userAgent,
deviceMemory: navigator.deviceMemory,
hardwareConcurrency: navigator.hardwareConcurrency,
connection: navigator.connection ? {
effectiveType: navigator.connection.effectiveType,
downlink: navigator.connection.downlink
} : null
},
performanceMetrics: this.getPerformanceMetrics()
};
}
/**
* Log initialization details
*/
logInitialization() {
if (this.config.debugMode) {
console.group('SwipeIntegrationManager Initialization');
console.log('Mode:', this.currentMode);
console.log('Config:', this.config);
console.log('Debug Info:', this.getDebugInfo());
console.groupEnd();
}
}
/**
* Destroy the swipe system
*/
destroy() {
if (this.swipeCard && typeof this.swipeCard.destroy === 'function') {
this.swipeCard.destroy();
}
// Remove mode classes
document.body.classList.remove(
'enhanced-swipe-mode',
'enhanced-swipe-lite-mode',
'original-swipe-mode'
);
this.swipeCard = null;
this.isInitialized = false;
console.log('SwipeIntegrationManager destroyed');
}
}
/**
* Create settings panel for swipe mode selection
*/
export function createSwipeSettingsPanel(integrationManager) {
const panel = document.createElement('div');
panel.className = 'swipe-settings-panel';
panel.innerHTML = `
<div class="settings-header">
<h3>Swipe Settings</h3>
<button class="close-settings">×</button>
</div>
<div class="settings-content">
<div class="setting-group">
<label>Swipe Mode:</label>
<select id="swipe-mode-select">
<option value="original">Original (Compatible)</option>
<option value="enhanced-lite">Enhanced Lite (Mobile)</option>
<option value="enhanced">Enhanced (Full Physics)</option>
</select>
</div>
<div class="setting-group">
<label>
<input type="checkbox" id="enable-physics"> Enable Physics
</label>
</div>
<div class="setting-group">
<label>
<input type="checkbox" id="enable-visual-effects"> Visual Effects
</label>
</div>
<div class="setting-group">
<label>
<input type="checkbox" id="enable-haptic"> Haptic Feedback
</label>
</div>
<div class="setting-group">
<label>Animation Quality:</label>
<select id="animation-quality">
<option value="auto">Auto</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div class="settings-actions">
<button id="apply-settings" class="apply-btn">Apply</button>
<button id="reset-settings" class="reset-btn">Reset</button>
</div>
</div>
`;
// Set current values
const config = integrationManager.getConfig();
panel.querySelector('#swipe-mode-select').value = integrationManager.getCurrentMode();
panel.querySelector('#enable-physics').checked = config.enablePhysics;
panel.querySelector('#enable-visual-effects').checked = config.enableVisualEffects;
panel.querySelector('#enable-haptic').checked = config.enableHapticFeedback;
panel.querySelector('#animation-quality').value = config.animationQuality;
// Event listeners
panel.querySelector('.close-settings').addEventListener('click', () => {
panel.remove();
});
panel.querySelector('#apply-settings').addEventListener('click', () => {
const newMode = panel.querySelector('#swipe-mode-select').value;
const newConfig = {
enablePhysics: panel.querySelector('#enable-physics').checked,
enableVisualEffects: panel.querySelector('#enable-visual-effects').checked,
enableHapticFeedback: panel.querySelector('#enable-haptic').checked,
animationQuality: panel.querySelector('#animation-quality').value
};
integrationManager.updateConfig(newConfig);
if (newMode !== integrationManager.getCurrentMode()) {
integrationManager.switchMode(newMode);
}
showToast('Settings applied successfully', 'success');
});
panel.querySelector('#reset-settings').addEventListener('click', () => {
integrationManager.updateConfig({
enablePhysics: true,
enableVisualEffects: true,
enableHapticFeedback: true,
animationQuality: 'auto',
performanceMode: 'auto'
});
// Reset form values
panel.querySelector('#enable-physics').checked = true;
panel.querySelector('#enable-visual-effects').checked = true;
panel.querySelector('#enable-haptic').checked = true;
panel.querySelector('#animation-quality').value = 'auto';
showToast('Settings reset to defaults', 'info');
});
return panel;
}
// Export singleton instance
export const swipeIntegration = new SwipeIntegrationManager();

427
js/swipe-physics.js Normal file
View File

@@ -0,0 +1,427 @@
/**
* SwipePhysics - Advanced physics engine for realistic swipe animations
* Provides spring-based animations, velocity tracking, and momentum calculations
*/
export class SwipePhysics {
constructor(options = {}) {
// Spring physics constants
this.springTension = options.springTension || 300;
this.springFriction = options.springFriction || 30;
this.mass = options.mass || 1;
// Velocity and momentum settings
this.velocityThreshold = options.velocityThreshold || 0.5;
this.momentumDecay = options.momentumDecay || 0.95;
this.maxVelocity = options.maxVelocity || 50;
// Animation settings
this.dampingRatio = options.dampingRatio || 0.8;
this.precision = options.precision || 0.01;
this.maxDuration = options.maxDuration || 2000;
// State tracking
this.isAnimating = false;
this.animationId = null;
this.startTime = 0;
// Velocity tracking
this.velocityHistory = [];
this.maxHistoryLength = 5;
this.lastPosition = { x: 0, y: 0 };
this.lastTimestamp = 0;
// Current physics state
this.position = { x: 0, y: 0 };
this.velocity = { x: 0, y: 0 };
this.acceleration = { x: 0, y: 0 };
// Bind methods - remove this line as animate method doesn't exist
// this.animate = this.animate.bind(this);
}
/**
* Update position and calculate velocity
*/
updatePosition(x, y, timestamp = performance.now()) {
if (this.lastTimestamp > 0) {
const deltaTime = Math.max(timestamp - this.lastTimestamp, 1);
const deltaX = x - this.lastPosition.x;
const deltaY = y - this.lastPosition.y;
// Calculate instantaneous velocity
const velocityX = (deltaX / deltaTime) * 1000; // pixels per second
const velocityY = (deltaY / deltaTime) * 1000;
// Add to velocity history for smoothing
this.velocityHistory.push({
x: velocityX,
y: velocityY,
timestamp
});
// Keep history within bounds
if (this.velocityHistory.length > this.maxHistoryLength) {
this.velocityHistory.shift();
}
// Calculate smoothed velocity
this.velocity = this.getSmoothedVelocity();
// Clamp velocity to maximum
this.velocity.x = Math.max(-this.maxVelocity, Math.min(this.maxVelocity, this.velocity.x));
this.velocity.y = Math.max(-this.maxVelocity, Math.min(this.maxVelocity, this.velocity.y));
}
this.position.x = x;
this.position.y = y;
this.lastPosition.x = x;
this.lastPosition.y = y;
this.lastTimestamp = timestamp;
}
/**
* Get smoothed velocity from history
*/
getSmoothedVelocity() {
if (this.velocityHistory.length === 0) {
return { x: 0, y: 0 };
}
// Weight recent velocities more heavily
let totalWeight = 0;
let weightedVelocityX = 0;
let weightedVelocityY = 0;
this.velocityHistory.forEach((entry, index) => {
const weight = (index + 1) / this.velocityHistory.length;
totalWeight += weight;
weightedVelocityX += entry.x * weight;
weightedVelocityY += entry.y * weight;
});
return {
x: weightedVelocityX / totalWeight,
y: weightedVelocityY / totalWeight
};
}
/**
* Calculate spring force based on displacement and velocity
*/
calculateSpringForce(displacement, velocity) {
// Hooke's law with damping: F = -kx - cv
return -this.springTension * displacement - this.springFriction * velocity;
}
/**
* Get current velocity magnitude
*/
getVelocityMagnitude() {
return Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y);
}
/**
* Check if swipe has enough momentum to trigger action
*/
hasSwipeMomentum(threshold = this.velocityThreshold) {
return this.getVelocityMagnitude() > threshold;
}
/**
* Animate spring-back to target position
*/
animateSpringTo(targetX, targetY, onUpdate, onComplete) {
if (this.isAnimating) {
cancelAnimationFrame(this.animationId);
}
this.isAnimating = true;
this.startTime = performance.now();
const startX = this.position.x;
const startY = this.position.y;
const startVelX = this.velocity.x;
const startVelY = this.velocity.y;
const animate = (currentTime) => {
const elapsed = currentTime - this.startTime;
const t = elapsed / 1000; // Convert to seconds
// Calculate spring physics
const displacementX = this.position.x - targetX;
const displacementY = this.position.y - targetY;
// Apply spring forces
const forceX = this.calculateSpringForce(displacementX, this.velocity.x);
const forceY = this.calculateSpringForce(displacementY, this.velocity.y);
// Update acceleration (F = ma, so a = F/m)
this.acceleration.x = forceX / this.mass;
this.acceleration.y = forceY / this.mass;
// Update velocity with acceleration
this.velocity.x += this.acceleration.x * (1/60); // Assume 60fps
this.velocity.y += this.acceleration.y * (1/60);
// Update position with velocity
this.position.x += this.velocity.x * (1/60);
this.position.y += this.velocity.y * (1/60);
// Check if animation should stop
const velocityMagnitude = this.getVelocityMagnitude();
const displacementMagnitude = Math.sqrt(displacementX * displacementX + displacementY * displacementY);
const shouldStop = (
velocityMagnitude < this.precision &&
displacementMagnitude < this.precision
) || elapsed > this.maxDuration;
if (shouldStop) {
// Snap to target
this.position.x = targetX;
this.position.y = targetY;
this.velocity.x = 0;
this.velocity.y = 0;
this.isAnimating = false;
if (onUpdate) onUpdate(this.position.x, this.position.y, this.velocity);
if (onComplete) onComplete();
} else {
if (onUpdate) onUpdate(this.position.x, this.position.y, this.velocity);
this.animationId = requestAnimationFrame(animate);
}
};
this.animationId = requestAnimationFrame(animate);
}
/**
* Animate momentum-based exit
*/
animateMomentumExit(direction, onUpdate, onComplete) {
if (this.isAnimating) {
cancelAnimationFrame(this.animationId);
}
this.isAnimating = true;
this.startTime = performance.now();
// Calculate exit target based on direction and momentum
const exitDistance = 1000; // pixels
const velocityMagnitude = this.getVelocityMagnitude();
const momentumMultiplier = Math.max(1, velocityMagnitude / 10);
let targetX = this.position.x;
let targetY = this.position.y;
switch (direction) {
case 'left':
targetX = -exitDistance * momentumMultiplier;
break;
case 'right':
targetX = exitDistance * momentumMultiplier;
break;
case 'up':
targetY = -exitDistance * momentumMultiplier;
break;
case 'down':
targetY = exitDistance * momentumMultiplier;
break;
}
const animate = (currentTime) => {
const elapsed = currentTime - this.startTime;
const progress = Math.min(elapsed / 500, 1); // 500ms exit animation
// Use easing function for smooth exit
const easeOut = 1 - Math.pow(1 - progress, 3);
const currentX = this.position.x + (targetX - this.position.x) * easeOut;
const currentY = this.position.y + (targetY - this.position.y) * easeOut;
// Apply momentum decay
this.velocity.x *= this.momentumDecay;
this.velocity.y *= this.momentumDecay;
if (progress >= 1) {
this.isAnimating = false;
if (onComplete) onComplete();
} else {
if (onUpdate) onUpdate(currentX, currentY, this.velocity, progress);
this.animationId = requestAnimationFrame(animate);
}
};
this.animationId = requestAnimationFrame(animate);
}
/**
* Stop current animation
*/
stopAnimation() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
this.isAnimating = false;
}
/**
* Reset physics state
*/
reset() {
this.stopAnimation();
this.position = { x: 0, y: 0 };
this.velocity = { x: 0, y: 0 };
this.acceleration = { x: 0, y: 0 };
this.velocityHistory = [];
this.lastPosition = { x: 0, y: 0 };
this.lastTimestamp = 0;
}
/**
* Get physics state for debugging
*/
getState() {
return {
position: { ...this.position },
velocity: { ...this.velocity },
acceleration: { ...this.acceleration },
velocityMagnitude: this.getVelocityMagnitude(),
isAnimating: this.isAnimating
};
}
}
/**
* Visual Effects Engine for dynamic feedback during swipes
*/
export class SwipeVisualEffects {
constructor(element, options = {}) {
this.element = element;
this.options = {
maxBlur: options.maxBlur || 8,
maxOpacity: options.maxOpacity || 0.3,
colorShiftIntensity: options.colorShiftIntensity || 0.2,
particleCount: options.particleCount || 20,
...options
};
this.particles = [];
this.setupEffects();
}
setupEffects() {
// Ensure element has proper CSS properties for effects
this.element.style.willChange = 'transform, filter, opacity';
this.element.style.backfaceVisibility = 'hidden';
this.element.style.perspective = '1000px';
}
/**
* Update visual effects based on swipe progress
*/
updateEffects(displacement, velocity, direction) {
const distance = Math.sqrt(displacement.x * displacement.x + displacement.y * displacement.y);
const maxDistance = 200; // Maximum distance for full effect
const intensity = Math.min(distance / maxDistance, 1);
const velocityMagnitude = Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
// Apply blur effect based on velocity
const blur = Math.min(velocityMagnitude * 0.1, this.options.maxBlur);
// Apply opacity shift based on distance
const opacity = 1 - (intensity * this.options.maxOpacity);
// Apply color temperature shift based on direction
const hueShift = this.getDirectionHueShift(direction) * intensity;
// Apply 3D rotation based on displacement
const rotationX = (displacement.y / maxDistance) * 15; // Max 15 degrees
const rotationY = (displacement.x / maxDistance) * 15;
const rotationZ = (displacement.x / maxDistance) * 10;
// Apply scale effect
const scale = 1 - (intensity * 0.1); // Slight scale down
// Combine all transforms
const transform = `
translate3d(${displacement.x}px, ${displacement.y}px, 0)
rotateX(${rotationX}deg)
rotateY(${rotationY}deg)
rotateZ(${rotationZ}deg)
scale3d(${scale}, ${scale}, 1)
`;
const filter = `
blur(${blur}px)
hue-rotate(${hueShift}deg)
brightness(${1 - intensity * 0.2})
`;
// Apply effects
this.element.style.transform = transform;
this.element.style.filter = filter;
this.element.style.opacity = opacity;
}
/**
* Get hue shift based on swipe direction
*/
getDirectionHueShift(direction) {
const shifts = {
left: -30, // Red shift for discard
right: 120, // Green shift for keep
up: 240, // Blue shift for favorite
down: 60 // Yellow shift for review
};
return shifts[direction] || 0;
}
/**
* Create particle trail effect
*/
createParticleTrail(x, y, direction) {
const particle = {
x,
y,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
life: 1,
decay: 0.02,
color: this.getDirectionColor(direction),
size: Math.random() * 4 + 2
};
this.particles.push(particle);
// Limit particle count
if (this.particles.length > this.options.particleCount) {
this.particles.shift();
}
}
/**
* Get color for direction
*/
getDirectionColor(direction) {
const colors = {
left: '#e74c3c', // Red for discard
right: '#2ecc71', // Green for keep
up: '#3498db', // Blue for favorite
down: '#f39c12' // Orange for review
};
return colors[direction] || '#ffffff';
}
/**
* Reset all visual effects
*/
resetEffects() {
this.element.style.transform = '';
this.element.style.filter = '';
this.element.style.opacity = '';
this.particles = [];
}
}