diff --git a/src/animation/FadeIn.ts b/src/animation/FadeIn.ts index 6b59045..9c141e1 100644 --- a/src/animation/FadeIn.ts +++ b/src/animation/FadeIn.ts @@ -1,46 +1,138 @@ import { Animation } from "./Animation.js"; import * as THREE from "three"; - +// Configuration types +type FadeInConfig = { + duration?: number; + family?: boolean; + preserveOpacity?: boolean; + targetOpacity?: { + fillOpacity?: number; + strokeOpacity?: number; + }; +} export default class FadeIn extends Animation { - public initialOpacity = new Map(); - - constructor(object: THREE.Object3D, config?: any) { - let family = true; - if (config && config.family === false) { - family = false; - } - - super( - (elapsedTime, _deltaTime) => { - if (family) { - this.object.traverse((child: THREE.Object3D) => { - if (child instanceof THREE.Mesh) { - child.material.opacity = THREE.MathUtils.lerp( - 0, - config?.preserveOpacity ? this.initialOpacity.get(child) : 1, - elapsedTime, - ); - } + public initialOpacity = new Map(); + private preserveOpacity: boolean; + private targetFillOpacity?: number; + private targetStrokeOpacity?: number; + + constructor(object: THREE.Object3D, config: FadeInConfig = {}) { + // Default preserveOpacity to true unless explicitly set to false + const preserveOpacity = config.preserveOpacity !== false; + + // Extract target opacities if provided + const targetFillOpacity = config.targetOpacity?.fillOpacity; + const targetStrokeOpacity = config.targetOpacity?.strokeOpacity; + + // Default family to true unless explicitly set to false + const family = config.family !== false; + + super( + (elapsedTime, _deltaTime) => { + // Special handling for objects with restyle method (Text or Shape) + if (typeof (object as any).restyle === 'function') { + // Get target fill opacity + const targetFill = targetFillOpacity !== undefined ? targetFillOpacity : + (preserveOpacity ? this.initialOpacity.get(object)?.fillOpacity || 1 : 1); + + // For Text objects (no strokeOpacity) + if (object.constructor.name === 'Text') { + (object as any).restyle({ + fillOpacity: THREE.MathUtils.lerp(0, targetFill, elapsedTime) }); - } else { - [this.object.stroke, this.object.fill].forEach((mesh) => { - if (!mesh) return; - mesh.material.opacity = THREE.MathUtils.lerp( + } + // For Shape objects (has strokeOpacity) + else { + // Get target stroke opacity + const targetStroke = targetStrokeOpacity !== undefined ? targetStrokeOpacity : + (preserveOpacity ? this.initialOpacity.get(object)?.strokeOpacity || 1 : 1); + + (object as any).restyle({ + fillOpacity: THREE.MathUtils.lerp(0, targetFill, elapsedTime), + strokeOpacity: THREE.MathUtils.lerp(0, targetStroke, elapsedTime) + }); + } + } + // Standard THREE.js object handling (original code with preserveOpacity logic updated) + else if (family) { + this.object.traverse((child: THREE.Object3D) => { + if (child instanceof THREE.Mesh) { + const targetOpacity = targetFillOpacity !== undefined ? targetFillOpacity : + (preserveOpacity ? this.initialOpacity.get(child) : 1); + + child.material.opacity = THREE.MathUtils.lerp( 0, - config?.preserveOpacity ? this.initialOpacity.get(mesh) : 1, + targetOpacity, elapsedTime, ); - }); + } + }); + } else { + [this.object.stroke, this.object.fill].forEach((mesh) => { + if (!mesh) return; + + const targetOpacity = targetFillOpacity !== undefined ? targetFillOpacity : + (preserveOpacity ? this.initialOpacity.get(mesh) : 1); + + mesh.material.opacity = THREE.MathUtils.lerp( + 0, + targetOpacity, + elapsedTime, + ); + }); + } + }, + { object, reveal: true, ...config }, + ); + + this.preserveOpacity = preserveOpacity; + this.targetFillOpacity = targetFillOpacity; + this.targetStrokeOpacity = targetStrokeOpacity; + } + + setUp() { + super.setUp(); + + // Handle objects with restyle method + if (typeof (this.object as any).restyle === 'function') { + const opacities: { fillOpacity?: number, strokeOpacity?: number } = {}; + + // Extract current opacities + this.object.traverse((child: THREE.Object3D) => { + if (child instanceof THREE.Mesh && child.material) { + if (!opacities.fillOpacity && child.material.opacity !== undefined) { + opacities.fillOpacity = child.material.opacity; } - }, - { object, reveal: true, ...config }, - ); - } - - setUp() { - super.setUp(); + } + }); + + // For shapes, try to find stroke opacity + if (this.object.constructor.name !== 'Text') { + // Placeholder - customize based on your implementation + if ((this.object as any).strokeOpacity !== undefined) { + opacities.strokeOpacity = (this.object as any).strokeOpacity; + } else if ((this.object as any).style && (this.object as any).style.strokeOpacity !== undefined) { + opacities.strokeOpacity = (this.object as any).style.strokeOpacity; + } + } + + // Store opacities and initialize + this.initialOpacity.set(this.object, opacities); + + // Set initial opacity to 0 + if (this.object.constructor.name === 'Text') { + (this.object as any).restyle({ fillOpacity: 0 }); + } else { + (this.object as any).restyle({ + fillOpacity: 0, + strokeOpacity: 0 + }); + } + } + // Original code for standard THREE.js objects + else { this.object.traverse((child: THREE.Object3D) => { if (child instanceof THREE.Mesh) { this.initialOpacity.set(child, child.material.opacity); @@ -48,4 +140,24 @@ export default class FadeIn extends Animation { }); } } - \ No newline at end of file +} + +/* + +Example 1: Basic usage - preserving original opacity +new Animation.FadeIn(triangle) + + +Example2: Fade in everything to 100% opacity, ignoring original opacity +new Animation.FadeIn(triangle, { preserveOpacity: false }) + + +Example 3: Set specific target opacities +new Animation.FadeIn(triangle, { + targetOpacity: { + fillOpacity: 0.5, + strokeOpacity: 0.8 + } + }) + +*/ \ No newline at end of file diff --git a/src/animation/Stagger.ts b/src/animation/Stagger.ts index 3363dba..cf479ad 100644 --- a/src/animation/Stagger.ts +++ b/src/animation/Stagger.ts @@ -1,20 +1,39 @@ import { Animation } from "./Animation.js"; import * as THREE from "three"; +import { Text } from "../text.js"; // Import your Text class + +// Configuration types +type StaggerConfig = { + duration?: number; + preserveOpacity?: boolean; + targetOpacity?: { + fillOpacity?: number; + strokeOpacity?: number; + }; +} export default class Stagger extends Animation { private objects: THREE.Object3D[]; - private initialOpacities: Map = new Map(); private duration: number; private staggerDelay: number; + private preserveOpacity: boolean; + private targetFillOpacity?: number; + private targetStrokeOpacity?: number; + private initialOpacities: Map = new Map(); /** * Creates a staggered fade-in animation for multiple objects * @param objects Array of objects to animate in sequence * @param config Additional configuration options */ - constructor(objects: THREE.Object3D[], config: { duration?: number } = {}) { - const { duration = 2 / (objects.length + 1) } = config; - const staggerDelay = (1 - duration) / (objects.length - 1); + constructor(objects: THREE.Object3D[], config: StaggerConfig = {}) { + const { + duration = 2 / (objects.length + 1), + preserveOpacity = true, + targetOpacity = {} + } = config; + + const staggerDelay = objects.length > 1 ? (1 - duration) / (objects.length - 1) : 0; super( (elapsedTime, _deltaTime) => { @@ -28,18 +47,7 @@ export default class Stagger extends Animation { // Only process if animation has started for this object if (objectProgress > 0) { - object.traverse((child: THREE.Object3D) => { - if (child instanceof THREE.Mesh && child.material) { - const initialOpacity = config?.preserveOpacity - ? this.initialOpacities.get(child) || 1 - : 1; - child.material.opacity = THREE.MathUtils.lerp( - 0, - initialOpacity, - objectProgress, - ); - } - }); + this.animateObject(object, objectProgress); } }); }, @@ -49,19 +57,211 @@ export default class Stagger extends Animation { this.objects = objects; this.duration = duration; this.staggerDelay = staggerDelay; + this.preserveOpacity = preserveOpacity; + this.targetFillOpacity = targetOpacity.fillOpacity; + this.targetStrokeOpacity = targetOpacity.strokeOpacity; } - setUp() { - super.setUp(); - // Store initial opacity values for all objects - this.objects.forEach((object) => { + /** + * Determine if an object supports stroke opacity + */ + private hasStrokeOpacity(object: any): boolean { + // Text objects don't have strokeOpacity + return object.constructor.name !== 'Text' && typeof object.restyle === 'function'; + } + + /** + * Store the initial opacity of an object + */ + private storeInitialOpacity(object: any) { + const opacities: { fillOpacity?: number, strokeOpacity?: number } = {}; + + // For objects with restyle method + if (typeof object.restyle === 'function') { + // We need to extract current opacities from the object + // This depends on your implementation object.traverse((child: THREE.Object3D) => { if (child instanceof THREE.Mesh && child.material) { - this.initialOpacities.set(child, child.material.opacity); - // Start with opacity 0 - child.material.opacity = 0; + if (!opacities.fillOpacity && child.material.opacity !== undefined) { + opacities.fillOpacity = child.material.opacity; + } } }); + + // For shapes, try to find stroke opacity + if (this.hasStrokeOpacity(object)) { + // You might need to adapt this to get the actual stroke opacity + // This is a placeholder and depends on your implementation + if (object.strokeOpacity !== undefined) { + opacities.strokeOpacity = object.strokeOpacity; + } else if (object.style && object.style.strokeOpacity !== undefined) { + opacities.strokeOpacity = object.style.strokeOpacity; + } + } + } + + // Store the initial opacities + this.initialOpacities.set(object, opacities); + } + + /** + * Get target opacity for an object + */ + private getTargetOpacity(object: any, type: 'fillOpacity' | 'strokeOpacity'): number { + // If specific target is set in config, use that + if (type === 'fillOpacity' && this.targetFillOpacity !== undefined) { + return this.targetFillOpacity; + } + if (type === 'strokeOpacity' && this.targetStrokeOpacity !== undefined) { + return this.targetStrokeOpacity; + } + + // If preserving original opacity, use the stored value or default to 1 + if (this.preserveOpacity) { + const initialOpacities = this.initialOpacities.get(object) || {}; + return initialOpacities[type] !== undefined ? initialOpacities[type]! : 1; + } + + // Default to 1 + return 1; + } + + /** + * Animate a single object based on progress + */ + private animateObject(object: any, progress: number) { + // Handle Text objects (they only have fillOpacity) + if (object instanceof Text) { + const targetFillOpacity = this.getTargetOpacity(object, 'fillOpacity'); + object.restyle({ + fillOpacity: progress * targetFillOpacity + }); + } + // Handle Shape objects (or any objects with both fillOpacity and strokeOpacity) + else if (typeof object.restyle === 'function') { + if (this.hasStrokeOpacity(object)) { + const targetFillOpacity = this.getTargetOpacity(object, 'fillOpacity'); + const targetStrokeOpacity = this.getTargetOpacity(object, 'strokeOpacity'); + + object.restyle({ + fillOpacity: progress * targetFillOpacity, + strokeOpacity: progress * targetStrokeOpacity + }); + } else { + // Fall back to just fillOpacity if no stroke opacity + const targetFillOpacity = this.getTargetOpacity(object, 'fillOpacity'); + object.restyle({ + fillOpacity: progress * targetFillOpacity + }); + } + } + // Default case for standard THREE.js objects + else { + const targetOpacity = this.getTargetOpacity(object, 'fillOpacity'); + object.traverse((child: THREE.Object3D) => { + if (child instanceof THREE.Mesh && child.material) { + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat.opacity !== undefined) { + mat.opacity = progress * targetOpacity; + } + }); + } else if (child.material.opacity !== undefined) { + child.material.opacity = progress * targetOpacity; + } + } + }); + } + } + + setUp() { + super.setUp(); + + // Store initial opacities if we need to preserve them + if (this.preserveOpacity) { + this.objects.forEach(object => { + this.storeInitialOpacity(object); + }); + } + + // Make sure all objects start with opacity 0 + this.objects.forEach(object => { + if (object instanceof Text) { + object.restyle({ + fillOpacity: 0 + }); + } else if (typeof object.restyle === 'function') { + if (this.hasStrokeOpacity(object)) { + object.restyle({ + fillOpacity: 0, + strokeOpacity: 0 + }); + } else { + object.restyle({ + fillOpacity: 0 + }); + } + } else { + // Standard THREE.js objects + object.traverse((child: THREE.Object3D) => { + if (child instanceof THREE.Mesh && child.material) { + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat.opacity !== undefined) { + mat.opacity = 0; + } + }); + } else if (child.material.opacity !== undefined) { + child.material.opacity = 0; + } + } + }); + } }); } } +/* +// Example 1: Basic usage - fade to full opacity +new Stagger( + [ + formula.greenTriangleArea, + formula.areaOfHexagon, + formula.areaOfHexagonAnswer, + ], + { duration: 0.2 } +); + +// Example 2: Preserve original opacities +new Stagger( + [ + formula.greenTriangleArea, + formula.areaOfHexagon, + formula.areaOfHexagonAnswer, + ], + { + duration: 0.2, + preserveOpacity: true + } +); + +// Example 3: Set specific target opacities +new Stagger( + [ + formula.greenTriangleArea, + formula.areaOfHexagon, + formula.areaOfHexagonAnswer, + ], + { + duration: 0.2, + preserveOpacity: false, + targetOpacity: { + fillOpacity: 0.8, + strokeOpacity: 1.0 + } + } +); + + + + +*/ \ No newline at end of file diff --git a/src/animation/StaggerEmphasize.ts b/src/animation/StaggerEmphasize.ts new file mode 100644 index 0000000..80240d3 --- /dev/null +++ b/src/animation/StaggerEmphasize.ts @@ -0,0 +1,159 @@ +import { Animation } from "./Animation.js"; +import * as THREE from "three"; + +export default class StaggerEmphasize extends Animation { + private objectGroups: THREE.Object3D[][] = []; + private initialScales: Map = new Map(); + private largeScale: number; + private stayEmphasized: boolean; + private keyframe = 0.9; + + /** + * Creates a staggered sequence of emphasis animations. + * Objects inside arrays will be emphasized simultaneously. + * + * @param args Objects or groups of objects (in arrays) to emphasize in sequence + * @param config Configuration options including largeScale and stayEmphasized + * @example + * // Emphasize obj1, then obj2 and obj3 together, then obj4 + * new StaggerEmphasize(obj1, [obj2, obj3], obj4, { largeScale: 1.2, stayEmphasized: true }) + */ + constructor( + ...args: Array + ) { + // Must call super first before accessing 'this' + super((elapsedTime) => { + const totalGroups = this.objectGroups.length; + if (totalGroups === 0) return; + + const totalDuration = 1.0; // Total animation takes 1 second + const groupDuration = totalDuration / totalGroups; + + // First, reset all objects to initial scale if they're not yet active + this.objectGroups.forEach((group, groupIndex) => { + const startTime = groupIndex * groupDuration; + if (elapsedTime < startTime) { + // Not yet active, ensure initial scale + group.forEach(obj => { + const initialScale = this.initialScales.get(obj) || 1; + obj.scale.setScalar(initialScale); + }); + } + }); + + // Then animate active groups + this.objectGroups.forEach((group, groupIndex) => { + const startTime = groupIndex * groupDuration; + const endTime = startTime + groupDuration; + + // Skip if we haven't reached this group yet + if (elapsedTime < startTime) return; + + // For each object in this group + group.forEach(obj => { + const initialScale = this.initialScales.get(obj) || 1; + let scale; + + // Calculate normalized local time for this group + const localTime = (elapsedTime - startTime) / groupDuration; + + if (this.stayEmphasized) { + // If we're past the end time for this group and need to stay emphasized + if (elapsedTime > endTime) { + // Stay at keyframe (peak emphasis) + scale = initialScale * this.largeScale; + } + // Otherwise animate normally up to keyframe + else if (localTime <= this.keyframe) { + // Interpolate from initial to large scale + const t = localTime / this.keyframe; + scale = initialScale * (1 + (this.largeScale - 1) * t); + } else { + // At or past keyframe, stay at large scale + scale = initialScale * this.largeScale; + } + } + // Regular animation (not staying emphasized) + else { + // Only animate if within our time window + if (elapsedTime <= endTime) { + if (localTime <= this.keyframe) { + // Growing phase - to peak + const t = localTime / this.keyframe; + scale = initialScale * (1 + (this.largeScale - 1) * t); + } else { + // Shrinking phase - back to normal + const t = (localTime - this.keyframe) / (1 - this.keyframe); + scale = initialScale * (this.largeScale - (this.largeScale - 1) * t); + } + } else { + // Past our time window, return to initial scale + scale = initialScale; + } + } + + // Apply the calculated scale + obj.scale.setScalar(scale); + }); + }); + }, { reveal: true }); + + // Parse arguments + let config = {}; + let objects = [...args]; + + // Extract config if the last argument is an object but not a THREE.Object3D + if (args.length > 0 && !Array.isArray(args[args.length-1]) && + !(args[args.length-1] instanceof THREE.Object3D)) { + config = objects.pop() as any; + } + + const { largeScale = 1.1, stayEmphasized = false } = config as any; + this.largeScale = largeScale; + this.stayEmphasized = stayEmphasized; + + // Process each argument - organize into groups + objects.forEach((item) => { + if (Array.isArray(item)) { + // This is already a group, filter to only include THREE.Object3D instances + const validObjects = item.filter(obj => obj instanceof THREE.Object3D) as THREE.Object3D[]; + if (validObjects.length > 0) { + this.objectGroups.push(validObjects); + } + } else if (item instanceof THREE.Object3D) { + // Single object becomes its own group + this.objectGroups.push([item]); + } + }); + + // Debug output + console.log(`StaggerEmphasize: Created with ${this.objectGroups.length} groups`); + this.objectGroups.forEach((group, i) => { + console.log(`Group ${i}: ${group.length} objects`); + }); + } + + setUp() { + super.setUp(); + + // Store initial scales of all objects + this.objectGroups.forEach(group => { + group.forEach(obj => { + this.initialScales.set(obj, obj.scale.x); + console.log(`Initial scale for object: ${obj.scale.x}`); + }); + }); + } + + tearDown() { + // Restore all objects to their initial scales + this.objectGroups.forEach(group => { + group.forEach(obj => { + const initialScale = this.initialScales.get(obj) || 1; + obj.scale.setScalar(initialScale); + }); + }); + + super.tearDown(); + } +} \ No newline at end of file diff --git a/src/animation/index.ts b/src/animation/index.ts index 8cb45a8..162613e 100644 --- a/src/animation/index.ts +++ b/src/animation/index.ts @@ -12,4 +12,5 @@ export { default as Wait } from "./Wait.js"; export { default as Emphasize } from "./Emphasize.js"; export { default as Shake } from "./Shake.js"; export { default as Grow } from "./Grow.js"; -export { default as Stagger } from "./Stagger.js"; \ No newline at end of file +export { default as Stagger } from "./Stagger.js"; +export { default as StaggerEmphasize } from "./StaggerEmphasize.js"; \ No newline at end of file diff --git a/src/text.ts b/src/text.ts index b2ca691..dcf6bfe 100644 --- a/src/text.ts +++ b/src/text.ts @@ -187,6 +187,37 @@ class Text extends THREE.Group { this.rotation.copy(rotation); this.scale.copy(scale); } + + restyle( + style: { fillColor?: THREE.Color; fillOpacity?: number }, + config: { includeDescendents: boolean } = { includeDescendents: false }, + ): void { + const { fillColor, fillOpacity } = style; + + // Update all meshes in the text group + this.traverse((child) => { + if (child instanceof THREE.Mesh) { + const material = child.material as THREE.MeshBasicMaterial; + + if (fillColor !== undefined) { + material.color = fillColor; + } + + if (fillOpacity !== undefined) { + material.opacity = fillOpacity; + } + } + }); + + // Handle descendents if requested + if (config.includeDescendents) { + this.traverse((child) => { + if (child instanceof Text && child !== this) { + child.restyle(style, { includeDescendents: false }); + } + }); + } + } } export { Text };