Bug fixes
This commit is contained in:
423
SWIPE_ANIMATIONS.md
Normal file
423
SWIPE_ANIMATIONS.md
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
# Enhanced Swipe Animation System
|
||||||
|
|
||||||
|
A comprehensive physics-based animation system for the image swiper app, providing realistic swipe interactions with spring effects, momentum calculations, and dynamic visual feedback.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The enhanced swipe animation system consists of several key components:
|
||||||
|
|
||||||
|
- **SwipePhysics Engine**: Core physics calculations for realistic motion
|
||||||
|
- **EnhancedSwipeCard**: Advanced swipe card component with visual effects
|
||||||
|
- **SwipeIntegrationManager**: Seamless switching between animation modes
|
||||||
|
- **Visual Effects System**: Dynamic feedback during swipe gestures
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🎯 Physics-Based Animations
|
||||||
|
- **Spring Effects**: Realistic spring-back animations when swipe threshold isn't met
|
||||||
|
- **Momentum Calculations**: Velocity-based swipe detection and exit animations
|
||||||
|
- **Smooth Transitions**: 60fps animations with proper GPU acceleration
|
||||||
|
|
||||||
|
### 🎨 Visual Feedback
|
||||||
|
- **Dynamic Blur**: Motion blur effects based on swipe velocity
|
||||||
|
- **Color Shifts**: Direction-based hue rotation for visual feedback
|
||||||
|
- **3D Transforms**: Enhanced perspective and rotation effects
|
||||||
|
- **Particle Effects**: Trailing particles during swipe gestures
|
||||||
|
|
||||||
|
### 📱 Device Optimization
|
||||||
|
- **Adaptive Quality**: Automatic animation quality adjustment based on device capabilities
|
||||||
|
- **Performance Monitoring**: Real-time FPS tracking and optimization
|
||||||
|
- **Reduced Motion Support**: Accessibility compliance for motion-sensitive users
|
||||||
|
|
||||||
|
### ⚙️ Customization
|
||||||
|
- **Multiple Modes**: Original, Enhanced Lite, and Full Enhanced modes
|
||||||
|
- **User Settings**: Configurable physics parameters and visual effects
|
||||||
|
- **Debug Tools**: Performance metrics and state inspection
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
#### SwipePhysics Class
|
||||||
|
```javascript
|
||||||
|
import { SwipePhysics } from './js/swipe-physics.js';
|
||||||
|
|
||||||
|
const physics = new SwipePhysics({
|
||||||
|
springTension: 300, // Spring stiffness
|
||||||
|
springFriction: 30, // Damping coefficient
|
||||||
|
mass: 1, // Object mass
|
||||||
|
velocityThreshold: 0.5, // Minimum velocity for momentum
|
||||||
|
momentumDecay: 0.95 // Momentum decay rate
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Methods:**
|
||||||
|
- `updatePosition(x, y, timestamp)`: Track position and calculate velocity
|
||||||
|
- `animateSpringTo(targetX, targetY, onUpdate, onComplete)`: Spring-back animation
|
||||||
|
- `animateMomentumExit(direction, onUpdate, onComplete)`: Momentum-based exit
|
||||||
|
- `getVelocityMagnitude()`: Current velocity magnitude
|
||||||
|
- `hasSwipeMomentum(threshold)`: Check if swipe has sufficient momentum
|
||||||
|
|
||||||
|
#### EnhancedSwipeCard Component
|
||||||
|
```javascript
|
||||||
|
import EnhancedSwipeCard from './components/enhanced-swipe-card.js';
|
||||||
|
|
||||||
|
const swipeCard = new EnhancedSwipeCard({
|
||||||
|
container: document.querySelector('.swipe-container'),
|
||||||
|
onSwipe: (direction) => console.log('Swiped:', direction),
|
||||||
|
threshold: 100,
|
||||||
|
velocityThreshold: 2,
|
||||||
|
springTension: 280,
|
||||||
|
springFriction: 25
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Physics-based drag interactions
|
||||||
|
- Enhanced 3D tilt effects
|
||||||
|
- Visual feedback during swipes
|
||||||
|
- Smooth loading animations
|
||||||
|
- Performance optimizations
|
||||||
|
|
||||||
|
#### SwipeIntegrationManager
|
||||||
|
```javascript
|
||||||
|
import { swipeIntegration } from './js/swipe-integration.js';
|
||||||
|
|
||||||
|
// Initialize with automatic mode detection
|
||||||
|
await swipeIntegration.initialize(container, onSwipe);
|
||||||
|
|
||||||
|
// Switch modes dynamically
|
||||||
|
await swipeIntegration.switchMode('enhanced');
|
||||||
|
|
||||||
|
// Update configuration
|
||||||
|
swipeIntegration.updateConfig({
|
||||||
|
enablePhysics: true,
|
||||||
|
enableVisualEffects: true,
|
||||||
|
animationQuality: 'high'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Animation Modes
|
||||||
|
|
||||||
|
### Original Mode
|
||||||
|
- **Use Case**: Low-end devices, reduced motion preferences
|
||||||
|
- **Features**: Basic swipe detection, simple CSS transitions
|
||||||
|
- **Performance**: Minimal CPU/GPU usage
|
||||||
|
|
||||||
|
### Enhanced Lite Mode
|
||||||
|
- **Use Case**: Mobile devices, moderate performance
|
||||||
|
- **Features**: Physics-based springs, reduced visual effects
|
||||||
|
- **Performance**: Balanced performance and visual quality
|
||||||
|
|
||||||
|
### Enhanced Mode
|
||||||
|
- **Use Case**: Desktop, high-performance devices
|
||||||
|
- **Features**: Full physics simulation, all visual effects
|
||||||
|
- **Performance**: Maximum visual quality, GPU-accelerated
|
||||||
|
|
||||||
|
## Physics Implementation
|
||||||
|
|
||||||
|
### Spring Dynamics
|
||||||
|
The system uses Hooke's Law with damping for realistic spring behavior:
|
||||||
|
|
||||||
|
```
|
||||||
|
F = -kx - cv
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
- `F` = Spring force
|
||||||
|
- `k` = Spring tension (stiffness)
|
||||||
|
- `x` = Displacement from rest position
|
||||||
|
- `c` = Friction coefficient (damping)
|
||||||
|
- `v` = Velocity
|
||||||
|
|
||||||
|
### Velocity Tracking
|
||||||
|
Velocity is calculated using time-weighted sampling:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const velocity = (currentPosition - previousPosition) / deltaTime;
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple samples are averaged with recent values weighted more heavily for smooth velocity calculations.
|
||||||
|
|
||||||
|
### Momentum-Based Actions
|
||||||
|
Swipe actions are triggered by either:
|
||||||
|
1. **Distance threshold**: Displacement exceeds minimum distance
|
||||||
|
2. **Velocity threshold**: Swipe velocity exceeds minimum speed
|
||||||
|
|
||||||
|
## Visual Effects
|
||||||
|
|
||||||
|
### Dynamic Blur
|
||||||
|
Motion blur is applied based on velocity magnitude:
|
||||||
|
```css
|
||||||
|
filter: blur(${velocity * 0.1}px);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Temperature Shifts
|
||||||
|
Direction-based hue rotation provides visual feedback:
|
||||||
|
- **Left (Discard)**: Red shift (-30°)
|
||||||
|
- **Right (Keep)**: Green shift (120°)
|
||||||
|
- **Up (Favorite)**: Blue shift (240°)
|
||||||
|
- **Down (Review)**: Yellow shift (60°)
|
||||||
|
|
||||||
|
### 3D Transforms
|
||||||
|
Enhanced perspective transforms create depth:
|
||||||
|
```css
|
||||||
|
transform:
|
||||||
|
perspective(1200px)
|
||||||
|
rotateX(${rotationX}deg)
|
||||||
|
rotateY(${rotationY}deg)
|
||||||
|
translateZ(${depth}px)
|
||||||
|
scale3d(${scale}, ${scale}, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimizations
|
||||||
|
|
||||||
|
### GPU Acceleration
|
||||||
|
- `transform3d()` for hardware acceleration
|
||||||
|
- `will-change` property for optimization hints
|
||||||
|
- `backface-visibility: hidden` to prevent flickering
|
||||||
|
|
||||||
|
### Frame Rate Monitoring
|
||||||
|
```javascript
|
||||||
|
// Automatic quality adjustment based on FPS
|
||||||
|
if (fps < 30 && animationQuality === 'high') {
|
||||||
|
animationQuality = 'medium';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
- Particle system with limited count
|
||||||
|
- Velocity history with bounded size
|
||||||
|
- Automatic cleanup of animation frames
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Physics Parameters
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
springTension: 300, // Spring stiffness (50-500)
|
||||||
|
springFriction: 30, // Damping coefficient (10-50)
|
||||||
|
mass: 1, // Object mass (0.5-2.0)
|
||||||
|
velocityThreshold: 0.5, // Minimum velocity for momentum
|
||||||
|
momentumDecay: 0.95, // Momentum decay rate (0.8-0.98)
|
||||||
|
maxVelocity: 50, // Maximum velocity clamp
|
||||||
|
dampingRatio: 0.8, // Overall damping (0.1-1.0)
|
||||||
|
precision: 0.01, // Animation stop precision
|
||||||
|
maxDuration: 2000 // Maximum animation duration (ms)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Effects
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
maxBlur: 8, // Maximum blur amount (px)
|
||||||
|
maxOpacity: 0.3, // Maximum opacity reduction
|
||||||
|
colorShiftIntensity: 0.2, // Color shift strength
|
||||||
|
particleCount: 20, // Maximum particles
|
||||||
|
enable3DTilt: true, // Enable 3D tilt effects
|
||||||
|
enableParticles: true, // Enable particle trails
|
||||||
|
enableHaptics: true // Enable haptic feedback
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Integration
|
||||||
|
```javascript
|
||||||
|
import { swipeIntegration } from './js/swipe-integration.js';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
const container = document.querySelector('.swipe-container');
|
||||||
|
|
||||||
|
await swipeIntegration.initialize(container, (direction) => {
|
||||||
|
console.log('Swiped:', direction);
|
||||||
|
// Handle swipe action
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Configuration
|
||||||
|
```javascript
|
||||||
|
// Configure before initialization
|
||||||
|
swipeIntegration.updateConfig({
|
||||||
|
enablePhysics: true,
|
||||||
|
enableVisualEffects: true,
|
||||||
|
animationQuality: 'high',
|
||||||
|
springTension: 320,
|
||||||
|
springFriction: 25
|
||||||
|
});
|
||||||
|
|
||||||
|
await swipeIntegration.initialize(container, onSwipe);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode Switching
|
||||||
|
```javascript
|
||||||
|
// Switch to enhanced mode
|
||||||
|
await swipeIntegration.switchMode('enhanced');
|
||||||
|
|
||||||
|
// Switch to lite mode for performance
|
||||||
|
await swipeIntegration.switchMode('enhanced-lite');
|
||||||
|
|
||||||
|
// Fallback to original mode
|
||||||
|
await swipeIntegration.switchMode('original');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Settings Panel
|
||||||
|
```javascript
|
||||||
|
import { createSwipeSettingsPanel } from './js/swipe-integration.js';
|
||||||
|
|
||||||
|
// Create settings panel
|
||||||
|
const settingsPanel = createSwipeSettingsPanel(swipeIntegration);
|
||||||
|
document.body.appendChild(settingsPanel);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Compatibility
|
||||||
|
|
||||||
|
### Supported Features
|
||||||
|
- **Modern Browsers**: Full enhanced mode support
|
||||||
|
- **Safari**: Full support with vendor prefixes
|
||||||
|
- **Mobile Browsers**: Enhanced lite mode recommended
|
||||||
|
- **Legacy Browsers**: Automatic fallback to original mode
|
||||||
|
|
||||||
|
### Required APIs
|
||||||
|
- `requestAnimationFrame` (required)
|
||||||
|
- `performance.now()` (required)
|
||||||
|
- `CSS transforms` (required)
|
||||||
|
- `CSS filters` (optional, for visual effects)
|
||||||
|
- `Vibration API` (optional, for haptic feedback)
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
### Reduced Motion Support
|
||||||
|
```css
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.enhanced-card {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### High Contrast Mode
|
||||||
|
```css
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.enhanced-hint {
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
border-width: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyboard Navigation
|
||||||
|
- **Arrow Keys**: Trigger swipe actions
|
||||||
|
- **WASD Keys**: Alternative swipe controls
|
||||||
|
- **Ctrl/Cmd + ,**: Open settings panel
|
||||||
|
- **Escape**: Close modals and panels
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
```javascript
|
||||||
|
swipeIntegration.updateConfig({ debugMode: true });
|
||||||
|
|
||||||
|
// Access debug information
|
||||||
|
const debugInfo = swipeIntegration.getDebugInfo();
|
||||||
|
console.log('Debug Info:', debugInfo);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Metrics
|
||||||
|
```javascript
|
||||||
|
const metrics = swipeIntegration.getPerformanceMetrics();
|
||||||
|
console.log('Performance:', metrics);
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Inspection
|
||||||
|
```javascript
|
||||||
|
// Access global debug objects
|
||||||
|
console.log('Swipe State:', window.enhancedSwipeState);
|
||||||
|
console.log('Swipe Card:', window.enhancedSwipeCard);
|
||||||
|
console.log('Integration:', window.swipeIntegration);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Poor Performance
|
||||||
|
- **Solution**: Switch to 'enhanced-lite' or 'original' mode
|
||||||
|
- **Check**: Device memory and CPU capabilities
|
||||||
|
- **Adjust**: Reduce particle count and visual effects
|
||||||
|
|
||||||
|
#### Animations Not Working
|
||||||
|
- **Check**: Browser support for CSS transforms
|
||||||
|
- **Verify**: JavaScript modules are loading correctly
|
||||||
|
- **Test**: Fallback to original mode
|
||||||
|
|
||||||
|
#### Touch Events Not Responding
|
||||||
|
- **Check**: Touch event listeners are properly attached
|
||||||
|
- **Verify**: `touch-action: none` is applied to card element
|
||||||
|
- **Test**: Mouse events as fallback
|
||||||
|
|
||||||
|
### Performance Tips
|
||||||
|
|
||||||
|
1. **Use Enhanced Lite on Mobile**: Better performance with good visual quality
|
||||||
|
2. **Monitor Frame Rate**: Enable debug mode to track performance
|
||||||
|
3. **Reduce Particle Count**: Lower particle count for better performance
|
||||||
|
4. **Disable Visual Effects**: Turn off blur and color effects on low-end devices
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Planned Features
|
||||||
|
- **Card Stacking**: Multiple cards with depth layering
|
||||||
|
- **Gesture Recognition**: Advanced swipe patterns
|
||||||
|
- **Sound Effects**: Audio feedback for actions
|
||||||
|
- **Custom Animations**: User-defined animation curves
|
||||||
|
|
||||||
|
### API Improvements
|
||||||
|
- **React Integration**: React component wrapper
|
||||||
|
- **Vue Integration**: Vue component wrapper
|
||||||
|
- **TypeScript Support**: Full type definitions
|
||||||
|
- **Web Components**: Custom element implementation
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone <repository-url>
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run unit tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run performance tests
|
||||||
|
npm run test:performance
|
||||||
|
|
||||||
|
# Run accessibility tests
|
||||||
|
npm run test:a11y
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
- Use ESLint configuration
|
||||||
|
- Follow JSDoc conventions
|
||||||
|
- Maintain 80% test coverage
|
||||||
|
- Use semantic versioning
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This enhanced swipe animation system is part of the image swiper application and follows the same licensing terms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more information, see the individual component documentation files or contact the development team.
|
||||||
497
components/enhanced-swipe-card.js
Normal file
497
components/enhanced-swipe-card.js
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
/**
|
||||||
|
* Enhanced Swipe Card Component with Physics-Based Animations
|
||||||
|
* Integrates SwipePhysics engine for realistic swipe interactions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SwipePhysics, SwipeVisualEffects } from '../js/swipe-physics.js';
|
||||||
|
|
||||||
|
export default class EnhancedSwipeCard {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.container = options.container || document.querySelector('.swipe-container');
|
||||||
|
this.onSwipe = options.onSwipe || (() => {});
|
||||||
|
this.threshold = options.threshold || 100;
|
||||||
|
this.velocityThreshold = options.velocityThreshold || 2;
|
||||||
|
|
||||||
|
// Physics engine configuration
|
||||||
|
this.physics = new SwipePhysics({
|
||||||
|
springTension: options.springTension || 280,
|
||||||
|
springFriction: options.springFriction || 25,
|
||||||
|
mass: options.mass || 1.2,
|
||||||
|
velocityThreshold: this.velocityThreshold,
|
||||||
|
momentumDecay: options.momentumDecay || 0.92,
|
||||||
|
maxVelocity: options.maxVelocity || 40,
|
||||||
|
dampingRatio: options.dampingRatio || 0.75
|
||||||
|
});
|
||||||
|
|
||||||
|
// State management
|
||||||
|
this.state = {
|
||||||
|
isDragging: false,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
hasMoved: false,
|
||||||
|
touchStartTime: 0,
|
||||||
|
currentImageInfo: null,
|
||||||
|
isAnimating: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
this.card = null;
|
||||||
|
this.actionHints = null;
|
||||||
|
this.decisionIndicators = {};
|
||||||
|
this.visualEffects = null;
|
||||||
|
|
||||||
|
// Performance optimization
|
||||||
|
this.rafId = null;
|
||||||
|
this.lastUpdateTime = 0;
|
||||||
|
this.updateThrottle = 16; // ~60fps
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.setupCard();
|
||||||
|
this.setupActionHints();
|
||||||
|
this.setupDecisionIndicators();
|
||||||
|
this.setupVisualEffects();
|
||||||
|
this.addEventListeners();
|
||||||
|
this.optimizePerformance();
|
||||||
|
|
||||||
|
console.log('Enhanced SwipeCard initialized with physics engine');
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCard() {
|
||||||
|
this.card = this.container.querySelector('.image-card') || this.createCardElement();
|
||||||
|
|
||||||
|
// Optimize for animations
|
||||||
|
this.card.style.willChange = 'transform, filter, opacity';
|
||||||
|
this.card.style.backfaceVisibility = 'hidden';
|
||||||
|
this.card.style.transformStyle = 'preserve-3d';
|
||||||
|
}
|
||||||
|
|
||||||
|
createCardElement() {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'image-card enhanced-card';
|
||||||
|
card.id = 'current-card';
|
||||||
|
card.setAttribute('role', 'img');
|
||||||
|
card.setAttribute('aria-label', 'Image to be swiped');
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22400%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-size%3D%2220%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20fill%3D%22%23999%22%3ELoading...%3C%2Ftext%3E%3C%2Fsvg%3E';
|
||||||
|
img.alt = 'Image';
|
||||||
|
|
||||||
|
const loadingIndicator = document.createElement('div');
|
||||||
|
loadingIndicator.className = 'loading-indicator enhanced-loading';
|
||||||
|
loadingIndicator.innerHTML = `
|
||||||
|
<div class="loading-spinner physics-spinner"></div>
|
||||||
|
<div class="loading-text">Loading...</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
card.appendChild(img);
|
||||||
|
card.appendChild(loadingIndicator);
|
||||||
|
this.container.appendChild(card);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupActionHints() {
|
||||||
|
this.actionHints = {
|
||||||
|
left: this.container.querySelector('.left-hint') || this.createActionHint('left', 'Discard'),
|
||||||
|
right: this.container.querySelector('.right-hint') || this.createActionHint('right', 'Keep'),
|
||||||
|
up: this.container.querySelector('.up-hint') || this.createActionHint('up', 'Favorite'),
|
||||||
|
down: this.container.querySelector('.down-hint') || this.createActionHint('down', 'Review')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createActionHint(direction, text) {
|
||||||
|
const hint = document.createElement('div');
|
||||||
|
hint.className = `action-hint ${direction}-hint enhanced-hint`;
|
||||||
|
hint.innerHTML = `
|
||||||
|
<div class="hint-icon"></div>
|
||||||
|
<span class="hint-text">${text}</span>
|
||||||
|
`;
|
||||||
|
this.container.appendChild(hint);
|
||||||
|
return hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDecisionIndicators() {
|
||||||
|
// Decision indicators removed - no longer creating visual icons
|
||||||
|
this.decisionIndicators = {
|
||||||
|
left: null,
|
||||||
|
right: null,
|
||||||
|
up: null,
|
||||||
|
down: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setupVisualEffects() {
|
||||||
|
this.visualEffects = new SwipeVisualEffects(this.card, {
|
||||||
|
maxBlur: 6,
|
||||||
|
maxOpacity: 0.25,
|
||||||
|
colorShiftIntensity: 0.3,
|
||||||
|
particleCount: 15
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
optimizePerformance() {
|
||||||
|
// Enable GPU acceleration for container
|
||||||
|
this.container.style.transform = 'translateZ(0)';
|
||||||
|
this.container.style.willChange = 'transform';
|
||||||
|
|
||||||
|
// Optimize child elements
|
||||||
|
const elements = this.container.querySelectorAll('.action-hint, .swipe-decision');
|
||||||
|
elements.forEach(el => {
|
||||||
|
el.style.willChange = 'transform, opacity';
|
||||||
|
el.style.backfaceVisibility = 'hidden';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListeners() {
|
||||||
|
// Bind methods to preserve context for removal
|
||||||
|
this.boundHandlePointerDown = (e) => this.handlePointerDown(e.clientX, e.clientY, e.timeStamp);
|
||||||
|
this.boundHandlePointerMove = (e) => this.handlePointerMove(e.clientX, e.clientY, e.timeStamp);
|
||||||
|
this.boundHandlePointerUp = (e) => this.handlePointerUp(e.timeStamp);
|
||||||
|
|
||||||
|
// Mouse events
|
||||||
|
this.card.addEventListener('mousedown', this.boundHandlePointerDown);
|
||||||
|
document.addEventListener('mousemove', this.boundHandlePointerMove);
|
||||||
|
document.addEventListener('mouseup', this.boundHandlePointerUp);
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
this.card.addEventListener('touchstart', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
this.handlePointerDown(touch.clientX, touch.clientY, e.timeStamp);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
this.card.addEventListener('touchmove', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
this.handlePointerMove(touch.clientX, touch.clientY, e.timeStamp);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
this.card.addEventListener('touchend', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handlePointerUp(e.timeStamp);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
// Enhanced 3D tilt effect
|
||||||
|
this.setupEnhanced3DTilt();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEnhanced3DTilt() {
|
||||||
|
// 3D tilt effect disabled for cleaner UX - no hover animations on image
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerDown(x, y, timestamp) {
|
||||||
|
if (this.state.isAnimating) return;
|
||||||
|
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.startX = x;
|
||||||
|
this.state.startY = y;
|
||||||
|
this.state.hasMoved = false;
|
||||||
|
this.state.touchStartTime = timestamp;
|
||||||
|
|
||||||
|
// Initialize physics
|
||||||
|
this.physics.reset();
|
||||||
|
this.physics.updatePosition(0, 0, timestamp);
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
this.card.classList.add('swiping', 'enhanced-swiping');
|
||||||
|
this.card.style.transition = '';
|
||||||
|
this.card.style.cursor = 'grabbing';
|
||||||
|
|
||||||
|
// Start update loop
|
||||||
|
this.startUpdateLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerMove(x, y, timestamp) {
|
||||||
|
if (!this.state.isDragging) return;
|
||||||
|
|
||||||
|
// Throttle updates for performance
|
||||||
|
if (timestamp - this.lastUpdateTime < this.updateThrottle) return;
|
||||||
|
this.lastUpdateTime = timestamp;
|
||||||
|
|
||||||
|
const moveX = x - this.state.startX;
|
||||||
|
const moveY = y - this.state.startY;
|
||||||
|
|
||||||
|
if (Math.abs(moveX) > 10 || Math.abs(moveY) > 10) {
|
||||||
|
this.state.hasMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update physics
|
||||||
|
this.physics.updatePosition(moveX, moveY, timestamp);
|
||||||
|
|
||||||
|
// Apply visual effects
|
||||||
|
this.updateVisualFeedback(moveX, moveY);
|
||||||
|
|
||||||
|
// Update action hints
|
||||||
|
this.updateActionHints(moveX, moveY);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVisualFeedback(moveX, moveY) {
|
||||||
|
const displacement = { x: moveX, y: moveY };
|
||||||
|
const velocity = this.physics.velocity;
|
||||||
|
const direction = this.getSwipeDirection(moveX, moveY);
|
||||||
|
|
||||||
|
// Apply physics-based visual effects
|
||||||
|
this.visualEffects.updateEffects(displacement, velocity, direction);
|
||||||
|
|
||||||
|
// Create particle trail based on velocity
|
||||||
|
if (this.physics.getVelocityMagnitude() > 5) {
|
||||||
|
const rect = this.card.getBoundingClientRect();
|
||||||
|
this.visualEffects.createParticleTrail(
|
||||||
|
rect.left + rect.width / 2 + moveX,
|
||||||
|
rect.top + rect.height / 2 + moveY,
|
||||||
|
direction
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActionHints(moveX, moveY) {
|
||||||
|
const absX = Math.abs(moveX);
|
||||||
|
const absY = Math.abs(moveY);
|
||||||
|
const threshold = 60;
|
||||||
|
|
||||||
|
// Hide all hints first
|
||||||
|
Object.values(this.actionHints).forEach(hint => {
|
||||||
|
hint.classList.remove('visible', 'enhanced-visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show appropriate hint with enhanced effects
|
||||||
|
if (absX > threshold || absY > threshold) {
|
||||||
|
let activeHint;
|
||||||
|
|
||||||
|
if (absX > absY) {
|
||||||
|
activeHint = moveX > 0 ? this.actionHints.right : this.actionHints.left;
|
||||||
|
} else {
|
||||||
|
activeHint = moveY > 0 ? this.actionHints.down : this.actionHints.up;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeHint) {
|
||||||
|
activeHint.classList.add('visible', 'enhanced-visible');
|
||||||
|
|
||||||
|
// Add intensity based on distance
|
||||||
|
const intensity = Math.min((Math.max(absX, absY) - threshold) / 100, 1);
|
||||||
|
activeHint.style.transform = `scale(${1 + intensity * 0.2})`;
|
||||||
|
activeHint.style.opacity = 0.8 + intensity * 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerUp(timestamp) {
|
||||||
|
if (!this.state.isDragging) return;
|
||||||
|
|
||||||
|
this.state.isDragging = false;
|
||||||
|
this.stopUpdateLoop();
|
||||||
|
|
||||||
|
// Remove visual states
|
||||||
|
this.card.classList.remove('swiping', 'enhanced-swiping');
|
||||||
|
this.card.style.cursor = '';
|
||||||
|
|
||||||
|
// Hide action hints
|
||||||
|
Object.values(this.actionHints).forEach(hint => {
|
||||||
|
hint.classList.remove('visible', 'enhanced-visible');
|
||||||
|
hint.style.transform = '';
|
||||||
|
hint.style.opacity = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine action based on physics
|
||||||
|
const displacement = this.physics.position;
|
||||||
|
const velocity = this.physics.velocity;
|
||||||
|
const velocityMagnitude = this.physics.getVelocityMagnitude();
|
||||||
|
|
||||||
|
const absX = Math.abs(displacement.x);
|
||||||
|
const absY = Math.abs(displacement.y);
|
||||||
|
|
||||||
|
// Check if swipe meets threshold or has sufficient momentum
|
||||||
|
const meetsThreshold = absX > this.threshold || absY > this.threshold;
|
||||||
|
const hasMomentum = velocityMagnitude > this.velocityThreshold;
|
||||||
|
|
||||||
|
if (this.state.hasMoved && (meetsThreshold || hasMomentum)) {
|
||||||
|
// Determine direction
|
||||||
|
let direction;
|
||||||
|
if (absX > absY) {
|
||||||
|
direction = displacement.x > 0 ? 'right' : 'left';
|
||||||
|
} else {
|
||||||
|
direction = displacement.y > 0 ? 'down' : 'up';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.performSwipe(direction);
|
||||||
|
} else {
|
||||||
|
// Spring back to center
|
||||||
|
this.animateSpringBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
performSwipe(direction) {
|
||||||
|
this.state.isAnimating = true;
|
||||||
|
|
||||||
|
// Show decision indicator
|
||||||
|
this.showEnhancedDecisionIndicator(direction);
|
||||||
|
|
||||||
|
// Animate momentum-based exit
|
||||||
|
this.physics.animateMomentumExit(
|
||||||
|
direction,
|
||||||
|
(x, y, velocity, progress) => {
|
||||||
|
// Update card position during exit
|
||||||
|
const rotation = this.getExitRotation(direction, progress);
|
||||||
|
const scale = 1 - progress * 0.3;
|
||||||
|
const opacity = 1 - progress;
|
||||||
|
|
||||||
|
this.card.style.transform = `
|
||||||
|
translate3d(${x}px, ${y}px, 0)
|
||||||
|
rotate(${rotation}deg)
|
||||||
|
scale3d(${scale}, ${scale}, 1)
|
||||||
|
`;
|
||||||
|
this.card.style.opacity = opacity;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Animation complete
|
||||||
|
this.state.isAnimating = false;
|
||||||
|
this.onSwipe(direction);
|
||||||
|
this.resetCard();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
animateSpringBack() {
|
||||||
|
this.state.isAnimating = true;
|
||||||
|
|
||||||
|
this.physics.animateSpringTo(
|
||||||
|
0, 0,
|
||||||
|
(x, y, velocity) => {
|
||||||
|
// Update card position during spring-back
|
||||||
|
const velocityMagnitude = Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
|
||||||
|
const wobble = Math.sin(performance.now() * 0.01) * velocityMagnitude * 0.1;
|
||||||
|
|
||||||
|
this.card.style.transform = `
|
||||||
|
translate3d(${x}px, ${y}px, 0)
|
||||||
|
rotate(${wobble}deg)
|
||||||
|
scale3d(1, 1, 1)
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Spring-back complete
|
||||||
|
this.state.isAnimating = false;
|
||||||
|
this.visualEffects.resetEffects();
|
||||||
|
this.card.style.transform = '';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showEnhancedDecisionIndicator(direction) {
|
||||||
|
// Decision indicators removed - no visual feedback needed
|
||||||
|
// The swipe action will be handled directly without visual indicators
|
||||||
|
console.log(`Swipe direction: ${direction}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getExitRotation(direction, progress) {
|
||||||
|
const maxRotation = {
|
||||||
|
left: -45,
|
||||||
|
right: 45,
|
||||||
|
up: 15,
|
||||||
|
down: -15
|
||||||
|
};
|
||||||
|
|
||||||
|
return (maxRotation[direction] || 0) * progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSwipeDirection(x, y) {
|
||||||
|
const absX = Math.abs(x);
|
||||||
|
const absY = Math.abs(y);
|
||||||
|
|
||||||
|
if (absX > absY) {
|
||||||
|
return x > 0 ? 'right' : 'left';
|
||||||
|
} else {
|
||||||
|
return y > 0 ? 'down' : 'up';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startUpdateLoop() {
|
||||||
|
if (this.rafId) return;
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
if (this.state.isDragging) {
|
||||||
|
this.rafId = requestAnimationFrame(update);
|
||||||
|
} else {
|
||||||
|
this.rafId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.rafId = requestAnimationFrame(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopUpdateLoop() {
|
||||||
|
if (this.rafId) {
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(imageInfo) {
|
||||||
|
this.state.currentImageInfo = imageInfo;
|
||||||
|
|
||||||
|
if (!imageInfo) {
|
||||||
|
this.card.innerHTML = '<div class="no-images-message enhanced-message">No more images available.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardImage = this.card.querySelector('img');
|
||||||
|
if (!cardImage) return;
|
||||||
|
|
||||||
|
// Enhanced image loading with fade effect
|
||||||
|
const preloadImg = new Image();
|
||||||
|
preloadImg.onload = () => {
|
||||||
|
cardImage.style.opacity = 0;
|
||||||
|
cardImage.src = imageInfo.path;
|
||||||
|
|
||||||
|
// Smooth fade-in with spring animation
|
||||||
|
setTimeout(() => {
|
||||||
|
cardImage.style.transition = 'opacity 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
|
||||||
|
cardImage.style.opacity = 1;
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
preloadImg.src = imageInfo.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading() {
|
||||||
|
this.card.classList.add('loading', 'enhanced-loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading() {
|
||||||
|
this.card.classList.remove('loading', 'enhanced-loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCard() {
|
||||||
|
this.physics.reset();
|
||||||
|
this.visualEffects.resetEffects();
|
||||||
|
this.card.style.transform = '';
|
||||||
|
this.card.style.opacity = '';
|
||||||
|
this.card.style.filter = '';
|
||||||
|
this.state.hasMoved = false;
|
||||||
|
this.state.isAnimating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.stopUpdateLoop();
|
||||||
|
this.physics.stopAnimation();
|
||||||
|
|
||||||
|
// Remove event listeners
|
||||||
|
if (this.boundHandlePointerDown) {
|
||||||
|
this.card.removeEventListener('mousedown', this.boundHandlePointerDown);
|
||||||
|
}
|
||||||
|
if (this.boundHandlePointerMove) {
|
||||||
|
document.removeEventListener('mousemove', this.boundHandlePointerMove);
|
||||||
|
}
|
||||||
|
if (this.boundHandlePointerUp) {
|
||||||
|
document.removeEventListener('mouseup', this.boundHandlePointerUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
this.visualEffects = null;
|
||||||
|
this.physics = null;
|
||||||
|
this.boundHandlePointerDown = null;
|
||||||
|
this.boundHandlePointerMove = null;
|
||||||
|
this.boundHandlePointerUp = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
168
components/fallback-swipe-card.js
Normal file
168
components/fallback-swipe-card.js
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* Fallback Swipe Card Component
|
||||||
|
* Simple swipe implementation for compatibility mode
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class FallbackSwipeCard {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.container = options.container || document.querySelector('.swipe-container');
|
||||||
|
this.onSwipe = options.onSwipe || (() => {});
|
||||||
|
this.threshold = options.threshold || 100;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isDragging: false,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
moveX: 0,
|
||||||
|
moveY: 0,
|
||||||
|
hasMoved: false,
|
||||||
|
currentImageInfo: null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.card = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.card = this.container.querySelector('.image-card') || this.createCardElement();
|
||||||
|
this.addEventListeners();
|
||||||
|
console.log('FallbackSwipeCard initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createCardElement() {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'image-card';
|
||||||
|
card.id = 'current-card';
|
||||||
|
card.setAttribute('role', 'img');
|
||||||
|
card.setAttribute('aria-label', 'Image to be swiped');
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22400%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-size%3D%2220%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20fill%3D%22%23999%22%3ELoading...%3C%2Ftext%3E%3C%2Fsvg%3E';
|
||||||
|
img.alt = 'Image';
|
||||||
|
|
||||||
|
const loadingIndicator = document.createElement('div');
|
||||||
|
loadingIndicator.className = 'loading-indicator';
|
||||||
|
loadingIndicator.textContent = 'Loading...';
|
||||||
|
|
||||||
|
card.appendChild(img);
|
||||||
|
card.appendChild(loadingIndicator);
|
||||||
|
this.container.appendChild(card);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListeners() {
|
||||||
|
// Mouse events
|
||||||
|
this.card.addEventListener('mousedown', (e) => this.handlePointerDown(e.clientX, e.clientY));
|
||||||
|
document.addEventListener('mousemove', (e) => this.handlePointerMove(e.clientX, e.clientY));
|
||||||
|
document.addEventListener('mouseup', () => this.handlePointerUp());
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
this.card.addEventListener('touchstart', (e) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
this.handlePointerDown(touch.clientX, touch.clientY);
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
this.card.addEventListener('touchmove', (e) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
this.handlePointerMove(touch.clientX, touch.clientY);
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
this.card.addEventListener('touchend', () => this.handlePointerUp());
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerDown(x, y) {
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.startX = x;
|
||||||
|
this.state.startY = y;
|
||||||
|
this.state.hasMoved = false;
|
||||||
|
this.card.classList.add('swiping');
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerMove(x, y) {
|
||||||
|
if (!this.state.isDragging) return;
|
||||||
|
|
||||||
|
this.state.moveX = x - this.state.startX;
|
||||||
|
this.state.moveY = y - this.state.startY;
|
||||||
|
|
||||||
|
if (Math.abs(this.state.moveX) > 10 || Math.abs(this.state.moveY) > 10) {
|
||||||
|
this.state.hasMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple transform without physics
|
||||||
|
this.card.style.transform = `translate(${this.state.moveX}px, ${this.state.moveY}px) rotate(${this.state.moveX * 0.05}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerUp() {
|
||||||
|
if (!this.state.isDragging) return;
|
||||||
|
|
||||||
|
this.state.isDragging = false;
|
||||||
|
this.card.classList.remove('swiping');
|
||||||
|
|
||||||
|
const absX = Math.abs(this.state.moveX);
|
||||||
|
const absY = Math.abs(this.state.moveY);
|
||||||
|
|
||||||
|
if (this.state.hasMoved && (absX > this.threshold || absY > this.threshold)) {
|
||||||
|
let direction;
|
||||||
|
if (absX > absY) {
|
||||||
|
direction = this.state.moveX > 0 ? 'right' : 'left';
|
||||||
|
} else {
|
||||||
|
direction = this.state.moveY > 0 ? 'down' : 'up';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.performSwipe(direction);
|
||||||
|
} else {
|
||||||
|
// Reset position
|
||||||
|
this.card.style.transition = 'transform 0.3s ease';
|
||||||
|
this.card.style.transform = '';
|
||||||
|
setTimeout(() => {
|
||||||
|
this.card.style.transition = '';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.moveX = 0;
|
||||||
|
this.state.moveY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
performSwipe(direction) {
|
||||||
|
this.card.classList.add(`swipe-${direction}`);
|
||||||
|
this.onSwipe(direction);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.card.classList.remove(`swipe-${direction}`);
|
||||||
|
this.card.style.transform = '';
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(imageInfo) {
|
||||||
|
this.state.currentImageInfo = imageInfo;
|
||||||
|
|
||||||
|
if (!imageInfo) {
|
||||||
|
this.card.innerHTML = '<div class="no-images-message">No more images available.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardImage = this.card.querySelector('img');
|
||||||
|
if (cardImage) {
|
||||||
|
cardImage.src = imageInfo.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading() {
|
||||||
|
this.card.classList.add('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading() {
|
||||||
|
this.card.classList.remove('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
// Remove event listeners
|
||||||
|
this.card.removeEventListener('mousedown', this.handlePointerDown);
|
||||||
|
document.removeEventListener('mousemove', this.handlePointerMove);
|
||||||
|
document.removeEventListener('mouseup', this.handlePointerUp);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
this.card = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,22 +80,13 @@ class SwipeCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createDecisionIndicators() {
|
createDecisionIndicators() {
|
||||||
const directions = ['left', 'right', 'up', 'down'];
|
// Decision indicators removed - no longer creating visual icons
|
||||||
const icons = ['fa-trash', 'fa-folder-plus', 'fa-star', 'fa-clock'];
|
this.decisionIndicators = {
|
||||||
|
left: null,
|
||||||
directions.forEach((direction, index) => {
|
right: null,
|
||||||
// Check if indicator already exists
|
up: null,
|
||||||
let indicator = this.container.querySelector(`.decision-${direction}`);
|
down: null
|
||||||
|
};
|
||||||
if (!indicator) {
|
|
||||||
indicator = document.createElement('div');
|
|
||||||
indicator.className = `swipe-decision decision-${direction}`;
|
|
||||||
indicator.innerHTML = `<i class="fa-solid ${icons[index]} fa-bounce"></i>`;
|
|
||||||
this.container.appendChild(indicator);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.decisionIndicators[direction] = indicator;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListeners() {
|
addEventListeners() {
|
||||||
@@ -243,14 +234,8 @@ class SwipeCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showDecisionIndicator(direction) {
|
showDecisionIndicator(direction) {
|
||||||
const indicator = this.decisionIndicators[direction];
|
// Decision indicators removed - no visual feedback needed
|
||||||
if (indicator) {
|
console.log(`Swipe direction: ${direction}`);
|
||||||
indicator.classList.add('visible');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
indicator.classList.remove('visible');
|
|
||||||
}, 800);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setImage(imageInfo) {
|
setImage(imageInfo) {
|
||||||
|
|||||||
23
index.html
23
index.html
@@ -20,13 +20,6 @@
|
|||||||
<img src="data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22400%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-size%3D%2220%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20fill%3D%22%23999%22%3ELoading...%3C%2Ftext%3E%3C%2Fsvg%3E" alt="Image">
|
<img src="data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22400%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-size%3D%2220%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20fill%3D%22%23999%22%3ELoading...%3C%2Ftext%3E%3C%2Fsvg%3E" alt="Image">
|
||||||
<div class="loading-indicator">Loading...</div>
|
<div class="loading-indicator">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="swipe-actions">
|
|
||||||
<div class="action-hint left-hint">Discard</div>
|
|
||||||
<div class="action-hint right-hint">Keep</div>
|
|
||||||
<div class="action-hint up-hint">Favorite</div>
|
|
||||||
<div class="action-hint down-hint">Review</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="side-panel">
|
<aside class="side-panel">
|
||||||
@@ -123,6 +116,20 @@
|
|||||||
<img src="static/icons/fullscreen.svg" class="btn-icon" alt="Fullscreen">
|
<img src="static/icons/fullscreen.svg" class="btn-icon" alt="Fullscreen">
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script src="js/main.js" type="module"></script>
|
<!-- Enhanced Swipe System Integration -->
|
||||||
|
<script src="js/enhanced-main.js" type="module"></script>
|
||||||
|
|
||||||
|
<!-- Settings Panel for Swipe Mode Selection -->
|
||||||
|
<div id="swipe-settings-panel" class="settings-panel" style="display: none;">
|
||||||
|
<!-- Will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Toggle Button -->
|
||||||
|
<button id="swipe-settings-toggle" class="settings-toggle" aria-label="Swipe Settings" title="Swipe Animation Settings">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
<path d="M12 1v6m0 6v6m11-7h-6m-6 0H1"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
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">
|
<input type="checkbox" class="selection-checkbox">
|
||||||
</div>
|
</div>
|
||||||
<img src="${ensureImagePath(selection.image_path)}" alt="Selected image" loading="lazy" class="${blurClass}">
|
<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">
|
<div class="selection-info">
|
||||||
<p>${selection.image_path.split('/').pop()}</p>
|
<p>${selection.image_path.split('/').pop()}</p>
|
||||||
<p>Resolution: ${selection.resolution}</p>
|
<p>Resolution: ${selection.resolution}</p>
|
||||||
@@ -183,8 +183,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const actionClass = (action) => {
|
const actionClass = (action) => {
|
||||||
|
// Direct mapping for full action names
|
||||||
const map = { 'Discarded':'discard', 'Kept':'keep', 'Favourited':'favorite', 'Reviewing':'review' };
|
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
|
// Map action names to icon filenames for display
|
||||||
@@ -194,6 +209,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
'Favourited': 'fav.svg',
|
'Favourited': 'fav.svg',
|
||||||
'Reviewing': 'review.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 getActionName = (action) => action;
|
||||||
|
|
||||||
const formatDate = (ts) => {
|
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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,21 +26,52 @@ html, body {
|
|||||||
.fullscreen-mode .header,
|
.fullscreen-mode .header,
|
||||||
.fullscreen-mode .side-panel {
|
.fullscreen-mode .side-panel {
|
||||||
display: none;
|
display: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen-mode .container {
|
.fullscreen-mode .container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen-mode .main-section {
|
.fullscreen-mode .main-section {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
transition: height 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen-mode .swipe-container {
|
.fullscreen-mode .swipe-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-width: 0; /* Remove borders in fullscreen */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced fullscreen transitions */
|
||||||
|
.container {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-section {
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-container {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure fullscreen toggle stays visible and accessible in fullscreen mode */
|
||||||
|
.fullscreen-mode .fullscreen-toggle {
|
||||||
|
z-index: 2000;
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-mode .fullscreen-toggle:hover {
|
||||||
|
opacity: 0.95;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ultra-wide toggle button */
|
/* Ultra-wide toggle button */
|
||||||
@@ -65,8 +96,8 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen-toggle:hover {
|
.fullscreen-toggle:hover {
|
||||||
opacity: 1;
|
opacity: 0.9;
|
||||||
transform: scale(1.1);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|||||||
462
scss/enhanced-animations.scss
Normal file
462
scss/enhanced-animations.scss
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
/**
|
||||||
|
* Enhanced Physics-Based Animation Styles
|
||||||
|
* Supports the SwipePhysics engine and EnhancedSwipeCard component
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Physics Animation Variables */
|
||||||
|
--spring-gentle: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
--spring-bouncy: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
|
--spring-wobbly: cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
--spring-stiff: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||||
|
--spring-smooth: cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||||
|
|
||||||
|
/* Enhanced Color Palette */
|
||||||
|
--physics-primary: #667eea;
|
||||||
|
--physics-success: #f093fb;
|
||||||
|
--physics-danger: #ffecd2;
|
||||||
|
--physics-warning: #fcb69f;
|
||||||
|
|
||||||
|
/* Gradient Definitions */
|
||||||
|
--gradient-physics-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
--gradient-physics-success: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
--gradient-physics-danger: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||||
|
--gradient-physics-warning: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||||
|
|
||||||
|
/* Animation Timing */
|
||||||
|
--physics-duration-fast: 0.2s;
|
||||||
|
--physics-duration-normal: 0.4s;
|
||||||
|
--physics-duration-slow: 0.8s;
|
||||||
|
|
||||||
|
/* 3D Perspective */
|
||||||
|
--physics-perspective: 1200px;
|
||||||
|
--physics-depth: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Card Styles */
|
||||||
|
.enhanced-card {
|
||||||
|
position: relative;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
will-change: transform, filter, opacity;
|
||||||
|
|
||||||
|
/* Subtle glow effect */
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
background: var(--gradient-physics-primary);
|
||||||
|
border-radius: inherit;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--physics-duration-normal) var(--spring-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.enhanced-swiping::before {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Loading Animation */
|
||||||
|
.enhanced-loading {
|
||||||
|
.loading-indicator {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
transition: all var(--physics-duration-normal) var(--spring-wobbly);
|
||||||
|
}
|
||||||
|
|
||||||
|
.physics-spinner {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border: 4px solid rgba(102, 126, 234, 0.2);
|
||||||
|
border-top-color: var(--physics-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: physicsSpinnerRotate 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
right: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top-color: rgba(102, 126, 234, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: physicsSpinnerRotate 0.6s linear infinite reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--physics-primary);
|
||||||
|
animation: loadingPulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes physicsSpinnerRotate {
|
||||||
|
0% { transform: rotate(0deg) scale(1); }
|
||||||
|
50% { transform: rotate(180deg) scale(1.1); }
|
||||||
|
100% { transform: rotate(360deg) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loadingPulse {
|
||||||
|
0%, 100% { opacity: 0.6; transform: scale(1); }
|
||||||
|
50% { opacity: 1; transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Action Hints */
|
||||||
|
.enhanced-hint {
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
transform: scale(0.8) translateZ(0);
|
||||||
|
transition: all var(--physics-duration-normal) var(--spring-wobbly);
|
||||||
|
|
||||||
|
.hint-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--gradient-physics-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: hintPulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.enhanced-visible {
|
||||||
|
transform: scale(1) translateZ(var(--physics-depth));
|
||||||
|
|
||||||
|
.hint-icon::before {
|
||||||
|
animation-duration: 0.8s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Direction-specific styling */
|
||||||
|
&.left-hint {
|
||||||
|
border-left-color: #ff6b6b;
|
||||||
|
.hint-icon { background: linear-gradient(135deg, #ff6b6b, #ee5a52); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right-hint {
|
||||||
|
border-right-color: #51cf66;
|
||||||
|
.hint-icon { background: linear-gradient(135deg, #51cf66, #40c057); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.up-hint {
|
||||||
|
border-top-color: #339af0;
|
||||||
|
.hint-icon { background: linear-gradient(135deg, #339af0, #228be6); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.down-hint {
|
||||||
|
border-bottom-color: #ffb84d;
|
||||||
|
.hint-icon { background: linear-gradient(135deg, #ffb84d, #fd7e14); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hintPulse {
|
||||||
|
0%, 100% { transform: scale(1); opacity: 1; }
|
||||||
|
50% { transform: scale(1.3); opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Decision Indicators */
|
||||||
|
.enhanced-decision {
|
||||||
|
transform: translate(-50%, -50%) scale(0) rotateZ(0deg);
|
||||||
|
transition: all var(--physics-duration-normal) var(--spring-bouncy);
|
||||||
|
filter: drop-shadow(0 0 20px rgba(0, 0, 0, 0.5));
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
.decision-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
animation: decisionFloat 3s ease-in-out infinite;
|
||||||
|
text-shadow: 0 0 30px currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision-particles {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle {
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
animation: particleBurst 1s ease-out forwards;
|
||||||
|
|
||||||
|
@for $i from 1 through 8 {
|
||||||
|
&:nth-child(#{$i}) {
|
||||||
|
transform: rotate(#{$i * 45}deg) translateX(40px);
|
||||||
|
animation-delay: #{$i * 0.05}s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.enhanced-visible {
|
||||||
|
transform: translate(-50%, -50%) scale(1) rotateZ(360deg);
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Direction-specific colors */
|
||||||
|
&.decision-left {
|
||||||
|
color: #ff6b6b;
|
||||||
|
.decision-icon { animation-duration: 2s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.decision-right {
|
||||||
|
color: #51cf66;
|
||||||
|
.decision-icon { animation-duration: 2.5s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.decision-up {
|
||||||
|
color: #339af0;
|
||||||
|
.decision-icon { animation-duration: 3s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.decision-down {
|
||||||
|
color: #ffb84d;
|
||||||
|
.decision-icon { animation-duration: 2.2s; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes decisionFloat {
|
||||||
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
|
50% { transform: translateY(-10px) scale(1.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes particleBurst {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(var(--rotation, 0deg)) translateX(0) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotate(var(--rotation, 0deg)) translateX(60px) scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Swipe Container */
|
||||||
|
.swipe-container {
|
||||||
|
perspective: var(--physics-perspective);
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: -10px;
|
||||||
|
right: -10px;
|
||||||
|
bottom: -10px;
|
||||||
|
background: var(--gradient-physics-primary);
|
||||||
|
border-radius: inherit;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(20px);
|
||||||
|
transition: opacity var(--physics-duration-slow) var(--spring-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Physics-based Button Enhancements */
|
||||||
|
.action-btn {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateZ(0);
|
||||||
|
transition: all var(--physics-duration-normal) var(--spring-wobbly);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: all var(--physics-duration-fast) ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active::before {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-8px) scale(1.05) translateZ(var(--physics-depth));
|
||||||
|
box-shadow:
|
||||||
|
0 20px 40px rgba(0, 0, 0, 0.2),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(-4px) scale(0.98) translateZ(calc(var(--physics-depth) / 2));
|
||||||
|
transition-duration: var(--physics-duration-fast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Modal Animations */
|
||||||
|
.modal {
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
animation: modalFadeIn var(--physics-duration-slow) var(--spring-wobbly);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
transform: scale(0.8) translateY(50px) rotateX(10deg);
|
||||||
|
transition: all var(--physics-duration-slow) var(--spring-wobbly);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.show .modal-content {
|
||||||
|
transform: scale(1) translateY(0) rotateX(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalFadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Toast Notifications */
|
||||||
|
.toast {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transform: translateX(-50%) translateY(100px) scale(0.8);
|
||||||
|
transition: all var(--physics-duration-normal) var(--spring-bouncy);
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
transform: translateX(-50%) translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--gradient-physics-primary);
|
||||||
|
border-radius: 50px 50px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance Optimizations */
|
||||||
|
.enhanced-card,
|
||||||
|
.enhanced-hint,
|
||||||
|
.enhanced-decision,
|
||||||
|
.action-btn {
|
||||||
|
will-change: transform, opacity, filter;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Optimizations */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:root {
|
||||||
|
--physics-perspective: 800px;
|
||||||
|
--physics-depth: 50px;
|
||||||
|
--physics-duration-normal: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-hint {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
.hint-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-decision .decision-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced Motion Support */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-card::before,
|
||||||
|
.swipe-container::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High Contrast Mode Support */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.enhanced-hint {
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
border-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-decision {
|
||||||
|
filter: contrast(1.5) drop-shadow(0 0 10px rgba(0, 0, 0, 0.8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.enhanced-hint,
|
||||||
|
.enhanced-decision,
|
||||||
|
.physics-spinner {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-card {
|
||||||
|
transform: none !important;
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
517
scss/main.scss
517
scss/main.scss
@@ -1,2 +1,519 @@
|
|||||||
@use 'variables';
|
@use 'variables';
|
||||||
@use 'components';
|
@use 'components';
|
||||||
|
@use 'enhanced-animations';
|
||||||
|
|
||||||
|
/* Enhanced Ripple Effects */
|
||||||
|
.ripple-effect {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
transform: scale(0);
|
||||||
|
animation: ripple 0.6s linear;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
to {
|
||||||
|
transform: scale(4);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Keyword Pills */
|
||||||
|
.enhanced-pill {
|
||||||
|
animation: pillSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||||
|
transform: translateY(20px) scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
.pill-text {
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-keyword {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pillSlideIn {
|
||||||
|
to {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Toast Styles */
|
||||||
|
.enhanced-toast {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
|
||||||
|
.toast-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
animation: toastIconBounce 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toast-left {
|
||||||
|
border-left: 3px solid #ff6b6b;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 107, 107, 0.1), rgba(0, 0, 0, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toast-right {
|
||||||
|
border-left: 3px solid #51cf66;
|
||||||
|
background: linear-gradient(135deg, rgba(81, 207, 102, 0.1), rgba(0, 0, 0, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toast-up {
|
||||||
|
border-left: 3px solid #339af0;
|
||||||
|
background: linear-gradient(135deg, rgba(51, 154, 240, 0.1), rgba(0, 0, 0, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toast-down {
|
||||||
|
border-left: 3px solid #ffb84d;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 184, 77, 0.1), rgba(0, 0, 0, 0.9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastIconBounce {
|
||||||
|
0%, 20%, 60%, 100% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(-10px) scale(1.1);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translateY(-5px) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Error and No Images States */
|
||||||
|
.enhanced-message,
|
||||||
|
.enhanced-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
animation: messageSlideIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
|
||||||
|
.no-images-icon,
|
||||||
|
.error-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
animation: iconFloat 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-images-text,
|
||||||
|
.error-text {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: var(--gradient-physics-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-3px) scale(1.05);
|
||||||
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(-1px) scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes messageSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes iconFloat {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0) rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px) rotate(5deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Button States */
|
||||||
|
.action-btn {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active::after {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Filter Button States */
|
||||||
|
.filter-btn {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
box-shadow: 0 0 20px rgba(102, 126, 234, 0.4);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
right: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Container Hover Effects - Removed for cleaner UX */
|
||||||
|
|
||||||
|
/* Performance Optimizations */
|
||||||
|
.enhanced-card,
|
||||||
|
.enhanced-hint,
|
||||||
|
.enhanced-decision,
|
||||||
|
.enhanced-pill,
|
||||||
|
.enhanced-toast {
|
||||||
|
will-change: transform, opacity;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility Enhancements */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.enhanced-card,
|
||||||
|
.enhanced-hint,
|
||||||
|
.enhanced-decision,
|
||||||
|
.enhanced-pill,
|
||||||
|
.enhanced-toast {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple-effect {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High Contrast Mode */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.enhanced-hint {
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
border-width: 3px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-decision {
|
||||||
|
filter: contrast(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-toast {
|
||||||
|
background: rgba(0, 0, 0, 0.95) !important;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-color: #1a1a1a;
|
||||||
|
--card-background: #2d2d2d;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--light-color: #3a3a3a;
|
||||||
|
--dark-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-card::before {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-hint {
|
||||||
|
background: rgba(45, 45, 45, 0.95);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-message,
|
||||||
|
.enhanced-error {
|
||||||
|
.no-images-text,
|
||||||
|
.error-text {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.enhanced-hint,
|
||||||
|
.enhanced-decision,
|
||||||
|
.ripple-effect,
|
||||||
|
.enhanced-toast {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-card {
|
||||||
|
transform: none !important;
|
||||||
|
filter: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Panel Styles */
|
||||||
|
.settings-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
right: 20px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 320px;
|
||||||
|
background: var(--card-background);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
z-index: 1500;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(-50%) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-settings {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--light-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(30, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.apply-btn {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #1c86e3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reset-btn {
|
||||||
|
background: var(--light-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Toggle Button */
|
||||||
|
.settings-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 80px;
|
||||||
|
right: 20px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1400;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(30, 144, 255, 0.3);
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1) rotate(90deg);
|
||||||
|
box-shadow: 0 6px 20px rgba(30, 144, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover svg {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive Settings */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.settings-panel {
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
width: auto;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle {
|
||||||
|
bottom: 100px;
|
||||||
|
right: 15px;
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user