Files
ai.interactive.fiction/public/js/ui-effects.js

342 lines
12 KiB
JavaScript

import { BaseModule } from './base-module.js';
import { moduleRegistry } from './module-registry.js';
class UIEffects extends BaseModule {
constructor() {
super('ui-effects', 'UI Effects');
// No external dependencies
this.dependencies = [];
// Effects state
this.activeEffects = new Map();
this.ambientEffectsActive = false;
// Effects configuration - use the config object from BaseModule
this.updateConfig({
candleFlicker: {
intensity: 0.5,
speed: 0.8
},
textShadow: {
enabled: true,
color: 'rgba(0, 0, 0, 0.5)'
},
backgroundEffects: {
enabled: true
}
});
// Use bindMethods from parent class
this.bindMethods([
'updateCandleEffect',
'setupEffectElements',
'createEffectsOverlay',
'createCandleEffect',
'createLightingElement',
'setupAmbientEffects',
'setupCandleFlickerEffect',
'startAmbientEffects',
'stopAmbientEffects',
'applyEffect',
'applyShakeEffect',
'applyFlashEffect',
'applyTextEmphasis',
'processCommand',
'handleLightingAnimationEnd'
]);
console.log('UIEffects: Constructor initialized');
}
async initialize() {
this.reportProgress(0, 'Initializing UI Effects');
try {
this.reportProgress(30, 'Setting up effect elements');
// Create or get effect elements
this.setupEffectElements();
this.reportProgress(60, 'Preparing ambient effects');
// Set up ambient effect animations
this.setupAmbientEffects();
// Start ambient effects immediately after initialization
this.startAmbientEffects();
this.reportProgress(100, 'UI Effects ready');
// Use the parent's dispatchEvent method
this.dispatchEvent('ui:effects:ready', {});
return true;
} catch (error) {
console.error('Error initializing UI Effects:', error);
this.changeState('ERROR');
return false;
}
}
setupEffectElements() {
console.log('UIEffects: Setting up effect elements');
// Create overlay for effects if it doesn't exist
this.effectsOverlay = document.getElementById('effects-overlay') || this.createEffectsOverlay();
// Create candle effect element
this.candleEffectElement = document.getElementById('candle-effect') || this.createCandleEffect();
// Ensure lighting element exists
this.lightingElement = document.getElementById('lighting') || this.createLightingElement();
}
createEffectsOverlay() {
const overlay = document.createElement('div');
overlay.id = 'effects-overlay';
overlay.className = 'effects-overlay';
document.body.appendChild(overlay);
return overlay;
}
createCandleEffect() {
const candleEffect = document.createElement('div');
candleEffect.id = 'candle-effect';
candleEffect.className = 'candle-effect';
if (this.effectsOverlay) {
this.effectsOverlay.appendChild(candleEffect);
} else {
document.body.appendChild(candleEffect);
}
return candleEffect;
}
createLightingElement() {
const lighting = document.createElement('div');
lighting.id = 'lighting';
document.body.appendChild(lighting);
return lighting;
}
setupAmbientEffects() {
// Initialize candle flicker effect
if (this.candleEffectElement && this.config.candleFlicker.enabled !== false) {
this.setupCandleFlickerEffect();
}
}
setupCandleFlickerEffect() {
// Store the animation frame ID for later cancellation
this.candleAnimationId = null;
// Add animation end event listener to create continuous random animations
if (this.lightingElement) {
this.lightingElement.addEventListener('animationend', this.handleLightingAnimationEnd.bind(this));
}
}
/**
* Handle the end of a lighting animation by setting a new random duration
* @param {AnimationEvent} event - The animation end event
*/
handleLightingAnimationEnd(event) {
if (!this.lightingElement || !this.ambientEffectsActive) return;
// Generate a random duration between 0.5 and 4 seconds
const randomDuration = Math.random() * 3.5 + 0.5;
// Toggle between grow and shrink animations
const previousAnimation = event.animationName;
if (previousAnimation === 'gradient-animation-grow') {
this.lightingElement.style.animation = `gradient-animation-shrink ${randomDuration}s 1`;
} else {
this.lightingElement.style.animation = `gradient-animation-grow ${randomDuration}s 1`;
}
}
updateCandleEffect() {
if (!this.candleEffectElement || !this.ambientEffectsActive) return;
const { intensity, speed } = this.config.candleFlicker;
// Create subtle random flickering effect
const flickerAmount = Math.random() * intensity;
const opacity = 0.2 + flickerAmount * 0.2;
this.candleEffectElement.style.opacity = opacity;
// Schedule next update
this.candleAnimationId = requestAnimationFrame(this.updateCandleEffect);
}
// Public methods
startAmbientEffects() {
if (this.ambientEffectsActive) return;
this.ambientEffectsActive = true;
// Start candle flicker
if (this.candleEffectElement) {
this.updateCandleEffect();
}
// Apply lighting animation with initial random duration
if (this.lightingElement) {
const initialDuration = Math.random() * 3.5 + 0.5;
this.lightingElement.style.animation = `gradient-animation-shrink ${initialDuration}s 1`;
}
}
stopAmbientEffects() {
this.ambientEffectsActive = false;
// Stop candle flicker
if (this.candleAnimationId) {
cancelAnimationFrame(this.candleAnimationId);
this.candleAnimationId = null;
}
// Stop lighting animation
if (this.lightingElement) {
this.lightingElement.style.animation = '';
}
}
applyEffect(effectName, options = {}) {
switch (effectName) {
case 'shake':
return this.applyShakeEffect(options);
case 'flash':
return this.applyFlashEffect(options);
case 'emphasis':
return this.applyTextEmphasis(options.text, options);
default:
console.warn(`Unknown effect: ${effectName}`);
return null;
}
}
applyShakeEffect(options = {}) {
const target = options.target || document.getElementById('book');
if (!target) return null;
const intensity = options.intensity || 'medium';
const duration = options.duration || 500;
// Store original styles
const originalTransition = target.style.transition;
const originalTransform = target.style.transform;
// Apply the shake animation
target.style.transition = `transform ${duration}ms ease`;
target.style.transform = 'translate(0, 0)';
target.style.animation = `shake ${duration}ms 1`;
// Return to normal after animation
const effectId = setTimeout(() => {
target.style.transition = originalTransition;
target.style.transform = originalTransform;
target.style.animation = '';
this.activeEffects.delete(effectId);
}, duration);
this.activeEffects.set(effectId, { name: 'shake', target });
return effectId;
}
applyFlashEffect(options = {}) {
const color = options.color || 'white';
const duration = options.duration || 300;
// Create flash overlay if not exists
let flashOverlay = document.getElementById('flash-overlay');
if (!flashOverlay) {
flashOverlay = document.createElement('div');
flashOverlay.id = 'flash-overlay';
flashOverlay.style.position = 'fixed';
flashOverlay.style.top = '0';
flashOverlay.style.left = '0';
flashOverlay.style.width = '100%';
flashOverlay.style.height = '100%';
flashOverlay.style.pointerEvents = 'none';
flashOverlay.style.zIndex = '9999';
flashOverlay.style.opacity = '0';
flashOverlay.style.transition = `opacity ${duration / 2}ms ease-in-out`;
document.body.appendChild(flashOverlay);
}
// Set color and make visible
flashOverlay.style.backgroundColor = color;
// Start animation
setTimeout(() => { flashOverlay.style.opacity = '0.8'; }, 10);
const effectId = setTimeout(() => {
flashOverlay.style.opacity = '0';
this.activeEffects.delete(effectId);
// Remove element after fade out
setTimeout(() => {
if (flashOverlay.parentNode) {
flashOverlay.parentNode.removeChild(flashOverlay);
}
}, duration / 2);
}, duration / 2);
this.activeEffects.set(effectId, { name: 'flash' });
return effectId;
}
applyTextEmphasis(text, options = {}) {
// Use existing display handler to show emphasized text
const displayHandler = moduleRegistry.getModule('ui-display-handler');
if (!displayHandler) return null;
const style = {
fontWeight: 'bold',
color: options.color || '#990000',
fontSize: options.size || '1.2em'
};
return displayHandler.displayText(text, { style, speak: true });
}
processCommand(command) {
switch (command.action) {
case 'apply':
return this.applyEffect(command.effect, command.options);
case 'cancel':
const effectId = command.effectId;
if (this.activeEffects.has(effectId)) {
clearTimeout(effectId);
this.activeEffects.delete(effectId);
}
break;
case 'ambient':
if (command.state === 'start') {
this.startAmbientEffects();
} else if (command.state === 'stop') {
this.stopAmbientEffects();
}
break;
default:
console.warn(`Unknown effect command: ${command.action}`);
}
}
}
// Create the singleton instance
const uiEffects = new UIEffects();
// Register with the module registry
moduleRegistry.register(uiEffects);
// Export the module
export { uiEffects as UIEffects };
// Keep a reference in window for loader system
console.log('UIEffects: Registering with window');
window.UIEffects = uiEffects;