This is a NextJS app developed in Firebase Studio.
You can view the live application at: https://bencepapa.github.io/studio/
This project serves as both a showcase and a laboratory for a library of pure JavaScript/TypeScript visual effects (VFX). You can easily add your own effects to the lab or copy existing ones into your own game or application.
-
Create the Effect File: Create a new TypeScript file in
src/effects/. It's best to copy an existing effect file (e.g.,src/effects/healing.ts) to use as a template. -
Implement the
VFXEffectInterface: Your effect class must implement theVFXEffectinterface found insrc/effects/types.ts. This includes:static effectName: A user-friendly name for your effect that will appear in the UI.static defaultSettings: An object defining the controllable parameters for your effect and their default values. These will automatically appear as controls in the sidebar.init(canvas, settings): Called once when the effect is loaded. Use this to set up your canvas, initial variables, and any objects you need.update(time, deltaTime, settings): Called on every frame before rendering. This is where you update animations, particle positions, and other logic based on the currenttimeandsettings.render(ctx): Called on every frame afterupdate(). This is where you draw everything to the 2D canvas context.destroy(): Called when the effect is switched. Use this to clean up any resources, event listeners, or intervals.
-
Import and Register:
- Open
src/app/page.tsx. - Import your new effect class at the top of the file (e.g.,
import { MyNewEffect } from '@/effects/my-new-effect';). - Add your effect to the
availableEffectsobject. The key should be a unique string identifier, and the value is your effect class:const availableEffects: Record<string, VFXEffectClass> = { "my-new-effect": MyNewEffect, // ... existing effects };
- Open
-
Enable Dependency Generation (Optional):
- Open
src/app/actions.ts. - Add an entry to the
effectFileMapobject. The key must match the one you used inavailableEffects, and the value should be the filename of your effect:const effectFileMap: { [key: string]: string } = { "my-new-effect": "my-new-effect.ts", // ... existing effects };
- This allows the "Get Dependencies" feature to find and analyze your effect's source code.
- Open
Each effect is designed to be self-contained for easy integration.
-
Get Dependencies:
- In the VFX Lab, select the effect you want to use.
- Click the Get Dependencies button in the control panel.
- This will analyze the effect's code using Genkit and provide instructions for any helper functions (like
mapRangeorseededRandomfromsrc/effects/utils.ts) it relies on.
-
Copy Files:
- Copy the effect's TypeScript file from the
src/effects/directory into your project. - If the dependency check mentioned any utilities, copy the
src/effects/utils.tsfile (or just the specific functions you need) into your project as well. - Copy the
src/effects/types.tsfile, as it contains the interfaces the effect class depends on.
- Copy the effect's TypeScript file from the
-
Instantiate and Use in Your Component:
- Import the effect class in your component file.
- Create a
<canvas>element in your JSX and get a reference to it usinguseRef. - In a
useEffecthook, create a new instance of your effect class and initialize it. - Use
requestAnimationFrameto create a render loop that calls the effect'supdate()andrender()methods. - Ensure you call
effect.destroy()in theuseEffectcleanup function to prevent memory leaks.
Example (React):
import React, { useRef, useEffect } from 'react'; import { MyCoolEffect } from './effects/my-cool-effect'; import type { VFXEffect } from './effects/types'; const MyComponent = () => { const canvasRef = useRef<HTMLCanvasElement>(null); const effectRef = useRef<VFXEffect | null>(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; // Instantiate the effect const effect = new MyCoolEffect(); effectRef.current = effect; // Initialize with default settings effect.init(canvas, MyCoolEffect.defaultSettings); let animationFrameId: number; let lastTime = 0; const renderLoop = (timestamp: number) => { if (lastTime === 0) lastTime = timestamp; const deltaTime = (timestamp - lastTime) / 1000; lastTime = timestamp; const time = timestamp / 1000; // Clear canvas before each render const ctx = canvas.getContext('2d'); if (ctx) { ctx.clearRect(0, 0, canvas.width, canvas.height); } effect.update(time, deltaTime, MyCoolEffect.defaultSettings); if (ctx) { effect.render(ctx); } animationFrameId = window.requestAnimationFrame(renderLoop); }; renderLoop(0); return () => { window.cancelAnimationFrame(animationFrameId); effect.destroy(); }; }, []); return <canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />; };