Bug fixes
This commit is contained in:
1036
js/enhanced-main.js
1036
js/enhanced-main.js
File diff suppressed because it is too large
Load Diff
@@ -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
471
js/swipe-integration.js
Normal 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
427
js/swipe-physics.js
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user