This document outlines the process for developing new animation plugins for the Tell Me A Tale AI interactive storytelling application. By following this guide, you can create custom visual effects that enhance the storytelling experience.
The animation system uses a simple plugin registry. Each animation is a self-contained JavaScript file that registers itself with a global object, window.TellMeATaleAnimationPlugins. The main application script (main_script.js) then calls upon these registered plugins to start and stop animations.
window.TellMeATaleAnimationPlugins provides methods to register(plugin) and get(id) animation plugins..js file, typically placed in an /animations subfolder.start method. The start method must return a specific stop function for that animation instance.Each animation plugin must be an object with the following properties:
| Property | Type | Description | Required |
|---|---|---|---|
id |
String | A unique identifier for the animation (e.g., "myCoolEffect", "gentleRain"). This ID is used in theme_settings.animation_name to trigger the animation. Must be URL-safe if used in parameters. |
Yes |
name |
String | A human-readable name for the animation (e.g., "My Cool Effect", "Gentle Rain"). Used for logging and potentially UI. | Yes |
type |
String | Indicates the primary mechanism of the animation. Can be:
|
Yes |
start |
Function |
function(targetElement, animationOverlayElement)This function is called by the main script to begin the animation.
stop function for this specific animation instance.
|
Yes |
start method must return a function. This returned function will be called by the main script to stop and clean up the *specific instance* of the animation that was started. This allows each animation instance to manage its own resources (like animationFrameId, event listeners, or dynamically created DOM elements).
Create a new .js file in the /animations/ directory (e.g., /animations/myNewAnimation.js).
Wrap your plugin code in an Immediately Invoked Function Expression (IIFE) to avoid polluting the global scope and to ensure it runs upon loading.
// animations/myNewAnimation.js
(function() {
// Ensure the global registry exists
if (typeof window.TellMeATaleAnimationPlugins === 'undefined') {
console.error("TellMeATaleAnimationPlugins registry is not defined. MyNewAnimation cannot be registered.");
// Optionally, create a placeholder registry if this script might load first,
// though ideally the main script (or a dedicated manager script) defines it first.
// window.TellMeATaleAnimationPlugins = { _plugins: {}, register: function(p){this._plugins[p.id]=p;}, get: function(id){ return this._plugins[id];}};
return;
}
// --- Your Animation-Specific Variables and Functions Here ---
// Example for a canvas animation:
let animationFrameId;
let canvas, ctx;
let particles = []; // Example state
let resizeObserverInstance; // Store the observer instance
let lastTimestamp = 0;
function initMyAnimation() {
// Initialize particles, positions, etc.
// Ensure canvas and ctx are valid.
particles = []; // Clear previous
if (!canvas || canvas.width === 0 || canvas.height === 0) return;
// ... setup logic ...
lastTimestamp = performance.now();
}
function drawMyAnimation(timestamp) {
if (!canvas || !ctx) { // Check if canvas/context still exist (might have been stopped)
if (animationFrameId) cancelAnimationFrame(animationFrameId);
return;
}
const deltaTime = timestamp - lastTimestamp; // Time since last frame in ms
lastTimestamp = timestamp;
// Your drawing logic using deltaTime for smooth animation
// ctx.clearRect(0, 0, canvas.width, canvas.height);
// ... draw particles/elements ...
animationFrameId = requestAnimationFrame(drawMyAnimation);
}
// --- Register the Plugin ---
window.TellMeATaleAnimationPlugins.register({
id: 'myNewAnimationId', // Unique ID
name: 'My New Awesome Animation', // Human-readable name
type: 'canvas', // or 'css', 'dom'
start: function(targetElement, animationOverlayElement) {
// This function is called when the animation should begin.
// For 'canvas' type:
if (this.type === 'canvas') {
if (!animationOverlayElement) {
console.error(this.name + ": animationOverlayElement is required for canvas animations.");
return () => {}; // Return a no-op stop function
}
// Clear any previous content from the overlay
animationOverlayElement.innerHTML = '';
canvas = document.createElement('canvas');
animationOverlayElement.appendChild(canvas);
canvas.width = animationOverlayElement.offsetWidth;
canvas.height = animationOverlayElement.offsetHeight;
ctx = canvas.getContext('2d');
// Handle resize - IMPORTANT for canvas animations
if (resizeObserverInstance) resizeObserverInstance.disconnect(); // Disconnect old one if any
resizeObserverInstance = new ResizeObserver(entries => {
if (!canvas) return; // If canvas was removed, do nothing
for (let entry of entries) {
if (canvas.width !== entry.contentRect.width || canvas.height !== entry.contentRect.height) {
canvas.width = entry.contentRect.width;
canvas.height = entry.contentRect.height;
initMyAnimation(); // Re-initialize elements for new size
}
}
});
resizeObserverInstance.observe(animationOverlayElement);
}
// For 'css' type:
else if (this.type === 'css') {
if (!targetElement) {
console.error(this.name + ": targetElement is required for this CSS animation.");
return () => {};
}
// Example: targetElement.classList.add('my-css-animation-active');
}
// Initialize and start the animation loop (for canvas)
initMyAnimation();
if (this.type === 'canvas') {
lastTimestamp = performance.now(); // Reset timestamp before starting loop
animationFrameId = requestAnimationFrame(drawMyAnimation);
}
// MUST RETURN THE STOP FUNCTION FOR THIS INSTANCE
return function stopMyAnimationInstance() {
console.log('Stopping animation instance:', this.id); // 'this' here refers to the plugin object if not careful with context
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (resizeObserverInstance) {
resizeObserverInstance.disconnect();
resizeObserverInstance = null;
}
// Cleanup for 'canvas' type
if (canvas && canvas.parentElement) {
canvas.parentElement.removeChild(canvas);
}
if (animationOverlayElement && this.type === 'canvas') { // Ensure check matches type
animationOverlayElement.innerHTML = '';
}
canvas = null;
ctx = null;
particles = []; // Clear internal state
// Cleanup for 'css' type (example)
// if (targetElement && this.type === 'css') {
// targetElement.classList.remove('my-css-animation-active');
// }
}.bind(this); // Bind 'this' if the stop function needs to refer to plugin properties like 'this.type'
}
});
})();
animationOverlayElement to create and append your canvas element.ctx).init function to set up initial states (e.g., particle positions, colors).draw function that:
ctx.clearRect(0, 0, canvas.width, canvas.height);).requestAnimationFrame(drawFunction) for smooth looping. Store the ID returned by requestAnimationFrame.
// Inside your draw function:
const deltaTime = (timestamp - lastTimestamp) / 16.66; // Normalize to 60FPS base (1000ms / 60fps ≈ 16.66ms)
lastTimestamp = timestamp;
// ...
// particle.x += particle.speedX * deltaTimeFactor;
ResizeObserver on the animationOverlayElement to detect size changes. When the overlay resizes, update your canvas dimensions (canvas.width, canvas.height) and typically re-initialize or adjust your animation elements.stop function must:
cancelAnimationFrame(animationFrameId).ResizeObserver.particles = []).canvas and ctx variables to null.style.css file.start function will typically add a specific CSS class to the targetElement (or another relevant element) to trigger the CSS animation/transition.stop function must remove the CSS class that was added.
// In plugin's start method:
// targetElement.classList.add('my-animation-active-class');
// return function() { targetElement.classList.remove('my-animation-active-class'); };
Add a script tag for your new animation file in your main HTML file (e.g., index.html), before main_script.js is loaded, but ideally after any script that defines window.TellMeATaleAnimationPlugins.
<!-- Animation Plugins -->
<script src="animations/snowfall.js"></script>
<script src="animations/rain.js"></script>
<!-- ... other existing animation plugins ... -->
<script src="animations/myNewAnimation.js"></script> <!-- Your new plugin -->
<!-- Your main application script -->
<script src="main_script.js">
To use your new animation, set the animation_name property within a page's theme_settings in your story data to the id of your new plugin.
{
"page_content": "A chilling wind...",
"choices": ["Investigate", "Ignore"],
// ... other page properties ...
"theme_settings": {
// ... other theme settings ...
"animation_name": "myNewAnimationId" // Use the ID you defined
}
}
When this page is rendered, and if dynamic theming is enabled, main_script.js will call applyPageAnimation('myNewAnimationId').
id is unique to avoid conflicts.requestAnimationFrame correctly. Optimize drawing operations.stop function (animation frame IDs, event listeners, DOM elements, observers, large arrays). Leaks can degrade performance over time.animationOverlayElement) and log errors gracefully.
console.log during development but consider removing or reducing them for production to keep the console clean.this in Stop Function: If your returned stop function needs to access properties of the plugin object itself (e.g., this.type), you might need to bind the context:
// In start method, when returning the stop function:
return function stopInstance() {
// 'this' here will refer to the plugin object
if (this.type === 'canvas') { /* ... */ }
}.bind(this); // Binds the plugin object as 'this' for the stop function
Alternatively, capture necessary plugin properties in closure variables within the start function that the returned stop function can access.
animations/simplePulse.js
(function() {
if (typeof window.TellMeATaleAnimationPlugins === 'undefined') { return; }
const PULSE_CLASS = 'simple-pulse-active';
window.TellMeATaleAnimationPlugins.register({
id: 'simplePulse',
name: 'Simple CSS Pulse',
type: 'css',
start: function(targetElement, animationOverlayElement) {
if (!targetElement) {
console.error("SimplePulse: targetElement is required.");
return () => {};
}
targetElement.classList.add(PULSE_CLASS);
const originalTarget = targetElement; // Capture in closure
return function stopSimplePulse() {
if (originalTarget) {
originalTarget.classList.remove(PULSE_CLASS);
}
};
}
});
})();
And in style.css:
.simple-pulse-active {
animation: simplePulseKeyframes 1.5s infinite ease-in-out;
}
@keyframes simplePulseKeyframes {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.7; }
}
By following these guidelines, you can effectively extend the Tell Me A Tale AI application with new and exciting visual animations!