🎨 Beyond Games: 5 Surprising Ways to Use Limn Engine
From data visualization to interactive art — Limn Engine isn't just for games.
📖 Introduction
When most people hear "game engine," they think of games. And that makes sense — the name says it all. But here's the thing: game engines are really just interactive canvas engines with some extra features bolted on.
Limn Engine is no exception. Under the hood, it's a powerful 2D rendering system with:
- A smooth 60fps animation loop
- Camera controls (follow, shake, zoom)
- Input handling (keyboard, mouse, touch)
- Component-based object management
- Particle systems for visual effects
These features aren't just for games. They're for anything that needs real-time, interactive graphics in the browser.
So let's explore five surprising ways you can use Limn Engine — without making a single game.
🎯 Try the Code Yourself
All the code examples in this article can be tested directly in Limn Studio:
👉 limn-engine-doc.vercel.app/editor
Just copy and paste the code into the editor, click Run, and see it come to life. No downloads, no setup, no hassle.
🎨 1. Interactive Data Visualization
What It Is
Data visualization is the graphical representation of information and data. By using visual elements like charts, graphs, and maps, data visualization tools provide an accessible way to see and understand trends, outliers, and patterns in data.
Traditionally, web developers use libraries like D3.js or Chart.js to create charts and graphs. These libraries are powerful, but they can be rigid — custom animations, real-time updates, and interactive elements are often difficult to implement.
What We're Going to Build
We're going to build a live-updating bar chart that responds to random data changes. Each bar will smoothly animate to new heights, creating a dynamic visualization that feels alive.
Here's what it will look like:
- Six colorful bars arranged in a row
- Each bar randomly changes height every few seconds
- The bars animate smoothly from one height to another
- Each bar displays its current value as a number
Why This Matters
Real-time data visualization is everywhere:
- Stock market dashboards that update every second
- Sensor data displays in factories and smart homes
- Live sports statistics
- Social media analytics
With Limn Engine, you can create custom visualizations that update at 60fps with smooth animations — something that's difficult to achieve with traditional charting libraries.
The Code
const display = new Display();
display.perform();
display.start(800, 400);
display.backgroundColor("#0a0a0a");
const bars = [];
const colors = ["#ff6b6b", "#ffd93d", "#6bcb77", "#4d96ff", "#ff6bb5", "#ff9f43"];
// ── CREATE BARS ──
for (let i = 0; i < 6; i++) {
const bar = new Component(80, 100, colors[i % colors.length], 60 + i * 120, 200, "rect");
display.add(bar);
bars.push({
component: bar,
targetHeight: 100 + Math.random() * 250,
currentHeight: 100
});
}
// ── UPDATE LOOP ──
function update(dt) {
bars.forEach((bar, index) => {
// Randomly change target height
if (Math.random() < 0.01) {
bar.targetHeight = 50 + Math.random() * 300;
}
// Smoothly animate toward target
bar.currentHeight += (bar.targetHeight - bar.currentHeight) * 0.05;
bar.component.height = bar.currentHeight;
bar.component.y = 300 - bar.currentHeight;
// Add labels
const label = new Tctxt("14px", "Arial", "white", bar.component.x + 40, 320);
label.setText(Math.floor(bar.currentHeight));
// Note: In a full implementation, you'd manage labels to prevent duplicates
});
}
What's Happening Line by Line
Line 1-4: We create the Display, enable the dual-canvas pipeline for 60fps performance, and start the canvas at 800x400 pixels. The dual-canvas pipeline uses an offscreen canvas to cache static content, which improves performance — especially important for visualizations that update frequently. The background is set to dark (#0a0a0a) so the bars stand out.
Line 6-7: We create an array to store our bars and a set of six colors. Each color is a bright, distinct hex code so the bars are easy to distinguish.
Line 10-15: We loop six times to create six bars. Each bar is a Component — the basic building block of Limn Engine. The parameters are: width (80), height (100), color, x position (spaced 120 pixels apart), and y position (starting at 200). The display.add(bar) line is crucial — without it, the bar exists in memory but never appears on screen.
Line 16: We store each bar in the bars array with a random target height (between 100 and 350) and a current height (starting at 100). The target height is where the bar wants to be, and the current height is where it actually is.
Line 22-35: In the update function — which runs 60 times per second — we do the following for each bar:
- Line 24: Randomly change the target height (1% chance per frame). This creates the dynamic, changing behavior.
- Line 28: Smoothly animate the current height toward the target using a "lerp" (linear interpolation) calculation:
current += (target - current) * 0.05. The 0.05 value controls the speed of animation — smaller numbers mean slower, smoother animation. - Line 29-30: Update the bar's height and y position so it grows upward from the bottom. The y position is set to
300 - barHeightbecause the bars are drawn from the top-left corner, and we want them to sit on the bottom. - Line 33-35: Create a text label showing the current value. In a full implementation, you'd manage these labels to prevent duplicates.
What You Just Built
You built a live-updating bar chart where each bar smoothly animates toward random target heights. The bars change color, the values update in real-time, and it runs at a smooth 60fps.
This is perfect for:
- Live dashboards
- Stock market visualizations
- Sensor data displays
- Performance monitoring tools
🎭 2. Interactive Art Installations
What It Is
Interactive art installations are artworks that respond to their audience — usually through sensors, cameras, or computer vision. In the digital realm, this often takes the form of generative art: visuals that are created algorithmically and respond to user input in real-time.
What We're Going to Build
We're going to build a particle-based art piece that responds to mouse movement. When you move your mouse, a colorful trail of particles follows, creating a stunning visual effect.
Here's what it will look like:
- A dark canvas with colorful particles
- Particles follow the mouse position
- Each particle has a random color from a vibrant palette
- Particles float and fade out over time
Why This Matters
Generative art is becoming increasingly popular in galleries, installations, and digital experiences:
- Museum exhibits that respond to visitors
- Music festivals with interactive projections
- Digital art galleries
- Creative coding projects
The Code
const display = new Display();
display.perform();
display.start(800, 600);
display.backgroundColor("#0a0a0a");
const ps = new ParticleSystem(display);
const colors = ["#ff006e", "#8338ec", "#3a86ff", "#ffbe0b", "#fb5607"];
// ── CREATE EMITTER THAT FOLLOWS MOUSE ──
const emitter = ps.createEmitter(400, 300, {
rate: 200,
life: 60,
speedX: 0,
speedY: -50,
randomSpeed: 100,
width: 4,
height: 4,
alphaFade: 0.02,
colors: colors,
randomColor: true,
gravity: 0,
type: "circle"
});
// ── UPDATE LOOP ──
function update(dt) {
// Follow mouse
if (display.x && display.y) {
emitter.setPosition(display.x, display.y);
}
ps.update();
}
What's Happening Line by Line
Line 1-4: Standard Display setup — create, perform, start, set background. The dual-canvas pipeline is ideal for particle effects because it caches static content, allowing the main canvas to focus on rendering the dynamic particles.
Line 6: We create a ParticleSystem — this is the core of our art installation. It manages all particles and emitters, handling creation, updating, and cleanup automatically.
Line 7: We define five vibrant colors: pink (#ff006e), purple (#8338ec), blue (#3a86ff), yellow (#ffbe0b), and orange (#fb5607). These colors are inspired by vaporwave and cyberpunk aesthetics.
Line 10-22: We create an emitter — a source that continuously produces particles. The options mean:
-
rate: 200— 200 particles per second. This creates a dense, continuous trail. -
life: 60— each particle lives for 60 frames (1 second at 60fps). This is long enough to create a visible trail but short enough that particles don't linger. -
speedY: -50— particles initially move upward by default, creating a fountain-like effect. -
randomSpeed: 100— random variation in speed, making the particles spread out. -
width: 4, height: 4— particle size. Small particles create a more delicate, sparkly effect. -
alphaFade: 0.02— particles fade out gradually, creating a smooth disappearing trail. -
randomColor: true— each particle gets a random color from thecolorsarray, creating a rainbow effect. -
gravity: 0— no gravity, so particles float in the direction they were emitted. -
type: "circle"— particles are drawn as circles, which looks more organic than squares.
Line 25-28: In the update loop, we check if the mouse is active (display.x and display.y are set). If so, we move the emitter to the mouse position. This makes the particles follow the mouse.
Line 30: We call ps.update() to update all particles — moving them, fading them, and removing dead ones. This is called every frame to keep the system running.
What You Just Built
You created a particle system that follows the mouse, creating a beautiful trail of colorful particles. This is the foundation of countless interactive art installations.
This is perfect for:
- Gallery installations
- Music visualizations
- Interactive projections
- Creative coding projects
🗺️ 3. Interactive Maps and Floor Plans
What It Is
Interactive maps and floor plans allow users to explore physical spaces digitally. They combine visual representation with interactivity — users can click on rooms, zoom in and out, and pan around the space.
What We're Going to Build
We're going to build a zoomable, draggable floor plan with clickable rooms. Each room will be a different color, display a room number, and respond to hover and click events.
Here's what it will look like:
- A large world (2000x2000 pixels) with 8 randomly placed rooms
- Rooms are colored rectangles with room numbers
- Hovering over a room highlights it in white
- Clicking a room triggers a camera shake
- Arrow keys pan the camera
- Z and X keys zoom in and out
Why This Matters
Interactive maps are used everywhere:
- Real estate websites with property floor plans
- Museums and galleries with interactive maps
- Event venues with seating charts
- Office layout tools for space management
The Code
const display = new Display();
display.perform();
display.start(800, 600);
display.backgroundColor("#1a1a2e");
// ── WORLD SETUP ──
display.camera.worldWidth = 2000;
display.camera.worldHeight = 2000;
display.camera.x = 0;
display.camera.y = 0;
// ── CREATE ROOMS ──
const rooms = [];
const roomColors = ["#e94560", "#0f3460", "#16213e", "#533483", "#ffd93d"];
for (let i = 0; i < 8; i++) {
const x = 100 + Math.random() * 1800;
const y = 100 + Math.random() * 1800;
const w = 100 + Math.random() * 200;
const h = 100 + Math.random() * 200;
const room = new Component(w, h, roomColors[i % roomColors.length], x, y, "rect");
room.roomId = i;
room.originalColor = roomColors[i % roomColors.length];
room.isHovered = false;
// ── CUSTOM DRAWING WITH LABEL ──
room.update = function(ctx) {
// Shadow for depth
ctx.shadowColor = "rgba(0,0,0,0.3)";
ctx.shadowBlur = 10;
// Main rectangle
ctx.fillStyle = this.isHovered ? "#ffffff" : this.originalColor;
ctx.fillRect(this.x, this.y, this.width, this.height);
// Border
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.lineWidth = 2;
ctx.strokeRect(this.x, this.y, this.width, this.height);
// Room number
ctx.fillStyle = this.isHovered ? "#000" : "#fff";
ctx.font = "20px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Room " + (this.roomId + 1), this.x + this.width/2, this.y + this.height/2);
};
display.add(room);
rooms.push(room);
}
// ── CLICK DETECTION ──
let selectedRoom = null;
// ── UPDATE LOOP ──
function update(dt) {
// Hover detection
rooms.forEach(room => {
room.isHovered = false;
if (display.x && display.y) {
const worldX = display.x + display.camera.x;
const worldY = display.y + display.camera.y;
if (worldX > room.x && worldX < room.x + room.width &&
worldY > room.y && worldY < room.y + room.height) {
room.isHovered = true;
}
}
});
// Click detection (simplified)
if (display.x && display.y) {
rooms.forEach(room => {
if (room.isHovered) {
selectedRoom = room;
display.camera.shake(3, 3);
}
});
}
// Camera controls
if (display.keys[37]) display.camera.x -= 5;
if (display.keys[39]) display.camera.x += 5;
if (display.keys[38]) display.camera.y -= 5;
if (display.keys[40]) display.camera.y += 5;
// Zoom with Z and X
if (display.keys[90]) {
display.camera.setZoom(1.2);
}
if (display.keys[88]) {
display.camera.setZoom(0.8);
}
// HUD
const hud = new Tctxt("14px", "Arial", "white", 20, 20);
hud.setText("Arrow keys to pan | Z to zoom in | X to zoom out");
// Note: In a full implementation, you'd manage HUD text properly
}
What's Happening Line by Line
Line 1-4: Standard Display setup.
Line 7-9: We set the camera's world size to 2000x2000 pixels — this is the size of our map. The world size tells the camera the boundaries of the map. We also set the camera's starting position to (0,0), which shows the top-left corner of the map.
Line 12: We create an array to store all rooms and a set of five room colors. The colors are a mix of vibrant and darker shades to create visual variety.
Line 15-20: We loop eight times to create eight rooms. Each room gets a random position and size within the 2000x2000 world. The position is generated using 100 + Math.random() * 1800, which ensures rooms don't appear right at the edges.
Line 21-23: We add custom properties to each room — roomId (0-7) for identification, originalColor to store the room's base color, and isHovered (initially false) to track whether the mouse is over the room.
Line 26-47: We override the update method of each room to draw it with custom styling:
- Line 28-29: Adds a shadow for depth, making the rooms look like they're floating above the background.
- Line 32-33: Draws the main rectangle — white if hovered, otherwise its original color. The hover effect creates immediate visual feedback.
- Line 36-40: Adds a subtle border with semi-transparent white. The border gives rooms a defined edge.
- Line 43-47: Draws the room number ("Room 1", "Room 2", etc.) centered in the room. The text color changes from white to black when hovered for better contrast.
Line 55-67: In the update loop, we check each room for hover:
- First, we set
isHoveredto false for all rooms. - Then, if the mouse is active (
display.xanddisplay.yare set), we convert the mouse position from screen space to world space by adding the camera offset (display.x + display.camera.x). This is crucial — without this, the mouse position would be in screen space (relative to the screen), while the rooms are in world space (relative to the map). The camera offset adjusts for any panning or zooming. - We then check if the world-space mouse position is inside the room's rectangle using four simple comparisons.
- If so, we set
isHoveredto true.
Line 70-77: Click detection — if the mouse is clicked and a room is hovered, we select that room and trigger a camera shake for feedback. The camera shake is display.camera.shake(3, 3) which displaces the camera by 3 pixels in each direction and then returns it to normal.
Line 80-87: Camera controls — arrow keys pan the camera by 5 pixels per frame. This creates smooth, responsive panning.
Line 90-97: Zoom controls — Z zooms in by setting the zoom to 1.2x, X zooms out by setting the zoom to 0.8x. The zoom is applied to the camera's context, scaling everything drawn.
What You Just Built
You built an interactive floor plan with zoom, pan, hover effects, and clickable rooms. The camera system handles all the complex math — you just focus on the content.
This is perfect for:
- Real estate websites
- Museum floor plans
- Event venue maps
- Office layout tools
📊 4. Real-Time Dashboards
What It Is
Real-time dashboards display live data in a visual format. They're used in system monitoring, business intelligence, IoT, and many other fields. The key requirement is that they update continuously without interrupting the user experience.
What We're Going to Build
We're going to build a live dashboard with four animated gauges. Each gauge will display a percentage value that smoothly animates to random targets.
Here's what it will look like:
- Four circular gauges arranged in a row
- Each gauge has a colored arc that fills from bottom to top
- The percentage value is displayed in the center
- Each gauge has a label (CPU, Memory, Disk, Network)
- Values change randomly and animate smoothly
Why This Matters
Real-time dashboards are everywhere:
- System monitoring (CPU, memory, disk usage)
- Business intelligence (sales, revenue, conversions)
- IoT dashboards (temperature, humidity, pressure)
- Sports score displays
The Code
const display = new Display();
display.perform();
display.start(800, 600);
display.backgroundColor("#0a0a0a");
// ── CREATE GAUGES ──
const gauges = [];
const gaugeColors = ["#ff6b6b", "#ffd93d", "#6bcb77", "#4d96ff"];
for (let i = 0; i < 4; i++) {
const x = 100 + i * 180;
const y = 150;
const gauge = {
x: x,
y: y,
width: 120,
height: 120,
color: gaugeColors[i % gaugeColors.length],
value: 0.5 + Math.random() * 0.4,
targetValue: 0.5 + Math.random() * 0.4,
label: ["CPU", "Memory", "Disk", "Network"][i]
};
// ── CUSTOM GAUGE DRAWING ──
gauge.draw = function(ctx) {
const cx = this.x + this.width/2;
const cy = this.y + this.height/2;
const radius = 50;
const startAngle = Math.PI * 0.75;
const endAngle = Math.PI * 2.25;
const valueAngle = startAngle + (endAngle - startAngle) * this.value;
// Background arc
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, endAngle);
ctx.strokeStyle = "#333";
ctx.lineWidth = 12;
ctx.stroke();
// Value arc
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, valueAngle);
ctx.strokeStyle = this.color;
ctx.lineWidth = 12;
ctx.lineCap = "round";
ctx.stroke();
// Value text
ctx.fillStyle = "#fff";
ctx.font = "32px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(Math.floor(this.value * 100) + "%", cx, cy - 10);
// Label
ctx.fillStyle = "#888";
ctx.font = "14px Arial";
ctx.fillText(this.label, cx, cy + 40);
};
gauges.push(gauge);
}
// ── UPDATE LOOP ──
function update(dt) {
// Randomly update targets
gauges.forEach(gauge => {
if (Math.random() < 0.02) {
gauge.targetValue = 0.2 + Math.random() * 0.6;
}
// Smooth animation toward target
gauge.value += (gauge.targetValue - gauge.value) * 0.05;
});
// ── RENDER ──
const ctx = display.context;
gauges.forEach(gauge => {
gauge.draw(ctx);
});
}
What's Happening Line by Line
Line 1-4: Standard Display setup. The dark background (#0a0a0a) creates contrast for the brightly colored gauges.
Line 7-8: We create arrays for gauges and colors. The colors are distinct and vibrant: red (#ff6b6b), yellow (#ffd93d), green (#6bcb77), and blue (#4d96ff).
Line 11-19: We create four gauges, each positioned in a row. Each gauge has:
- Position (x, y) — spaced 180 pixels apart
- Size (width, height) — 120x120 pixels
- A color from the gaugeColors array
- A current value (random 50-90%)
- A target value (random 50-90%)
- A label ("CPU", "Memory", "Disk", "Network")
Line 22-61: We define a draw method for each gauge:
- Line 23-27: Calculate the center, radius, and angles for the gauge. The
startAngleandendAngleare set to 0.75π and 2.25π, creating a 270-degree arc starting from the bottom-left and going clockwise to the bottom-right. This gives a speedometer-like appearance. - Line 30-36: Draw a dark gray background arc. This represents the "empty" part of the gauge.
- Line 39-46: Draw the colored value arc — from the start angle to the value angle. The
lineCap: "round"makes the ends of the arc rounded, giving a polished look. - Line 49-55: Draw the percentage value as text in the center. The
Math.floor(this.value * 100)converts the decimal value (0.0-1.0) to a percentage (0-100). - Line 58-61: Draw the label below the gauge in a muted gray color.
Line 68-73: In the update loop, we randomly change the target value (2% chance per frame). Then we smoothly animate the current value toward the target using a "lerp" calculation: current += (target - current) * 0.05. The 0.05 value controls the animation speed — smaller numbers mean slower, smoother animation.
Line 76-79: We render all gauges by iterating through the gauges array and calling their draw method.
What You Just Built
You built a real-time dashboard with four animated gauges. Each gauge smoothly animates to random values, creating a live-updating display.
This is perfect for:
- System monitoring tools
- IoT dashboards
- Business intelligence dashboards
- Sports score displays
✍️ 5. Interactive Storytelling / Digital Comics
What It Is
Interactive storytelling combines narrative with interactivity. Digital comics, interactive fiction, and animated storybooks allow readers to engage with the story in ways that traditional media cannot.
What We're Going to Build
We're going to build a page-by-page interactive comic with animated panels. Each page has panels with text, and readers navigate through the story.
Here's what it will look like:
- Three pages with different panel layouts
- Each panel has a colored background and text
- Arrow keys navigate between pages
- A camera shake animates page transitions
Why This Matters
Interactive storytelling is growing rapidly:
- Digital comics with animated panels
- Educational content with interactive elements
- Interactive fiction and visual novels
- Presentation tools for storytelling
The Code
const display = new Display();
display.perform();
display.start(800, 600);
display.backgroundColor("#1a1a2e");
// ── PAGE MANAGER ──
let currentPage = 0;
const totalPages = 3;
const pages = [
{
background: "#16213e",
panels: [
{ x: 50, y: 50, w: 300, h: 200, color: "#e94560", text: "The journey begins..." },
{ x: 450, y: 50, w: 300, h: 200, color: "#0f3460", text: "A hero rises..." }
]
},
{
background: "#0a0a0a",
panels: [
{ x: 100, y: 100, w: 600, h: 200, color: "#533483", text: "A challenge appears..." },
{ x: 100, y: 350, w: 600, h: 150, color: "#e94560", text: "Will they succeed?" }
]
},
{
background: "#16213e",
panels: [
{ x: 150, y: 150, w: 500, h: 300, color: "#ffd93d", text: "The End... or is it?" }
]
}
];
// ── PANEL COMPONENT ──
function createPanel(panelData, pageIndex) {
const panel = new Component(panelData.w, panelData.h, panelData.color, panelData.x, panelData.y, "rect");
panel.text = panelData.text;
panel.isVisible = (pageIndex === 0);
panel.pageIndex = pageIndex;
panel.update = function(ctx) {
if (!this.isVisible) return;
// Panel background with shadow
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.shadowBlur = 20;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
ctx.shadowBlur = 0;
// Border
ctx.strokeStyle = "rgba(255,255,255,0.2)";
ctx.lineWidth = 2;
ctx.strokeRect(this.x, this.y, this.width, this.height);
// Text
ctx.fillStyle = "#fff";
ctx.font = "20px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(this.text, this.x + this.width/2, this.y + this.height/2);
// Page indicator
ctx.fillStyle = "rgba(255,255,255,0.3)";
ctx.font = "14px Arial";
ctx.fillText("Page " + (this.pageIndex + 1), this.x + this.width/2, this.y + this.height + 30);
};
display.add(panel);
return panel;
}
// ── CREATE ALL PANELS ──
const allPanels = [];
pages.forEach((page, index) => {
page.panels.forEach(panelData => {
const panel = createPanel(panelData, index);
allPanels.push(panel);
});
});
// ── PAGE CONTROL ──
function showPage(pageIndex) {
currentPage = pageIndex;
allPanels.forEach(panel => {
panel.isVisible = (panel.pageIndex === pageIndex);
});
// Animate transition
display.camera.shake(2, 2);
}
// ── UPDATE LOOP ──
function update(dt) {
// Page navigation
if (display.keys[39] || display.keys[68]) { // Right arrow or D
if (currentPage < totalPages - 1) {
showPage(currentPage + 1);
display.keys[39] = false; // Prevent rapid firing
display.keys[68] = false;
}
}
if (display.keys[37] || display.keys[65]) { // Left arrow or A
if (currentPage > 0) {
showPage(currentPage - 1);
display.keys[37] = false;
display.keys[65] = false;
}
}
// ── HUD ──
const ctx = display.context;
ctx.fillStyle = "rgba(255,255,255,0.5)";
ctx.font = "14px Arial";
ctx.textAlign = "center";
ctx.fillText("← → to navigate", 400, 580);
}
What's Happening Line by Line
Line 1-4: Standard Display setup. The dark blue background (#1a1a2e) gives the comic a moody, cinematic feel.
Line 7-8: We set up a page manager — currentPage starts at 0, and there are 3 pages total.
Line 10-32: We define three pages, each with panels. Each panel has a position (x, y), size (w, h), color, and text. The first page has two smaller panels, the second page has two larger panels, and the third page has one large panel — creating visual variety.
Line 35-63: We create a createPanel function that turns a panel definition into a Limn Engine Component:
- Line 36: Creates the Component with the panel's size, color, and position. The parameters are: width, height, color, x position, y position, and type ("rect" for rectangle).
- Line 37-39: Adds custom properties — text, visibility (only page 0 is visible at start), and page index.
- Line 41-60: Overrides the
updatemethod to draw the panel with:- A shadow (Lines 43-44): Adds depth and makes the panel look like it's floating.
- The panel background (Line 45-46): Fills the panel with its color.
- A subtle border (Line 49-51): Adds definition to the panel edges.
- The panel text centered (Line 54-58): Draws the story text in white.
- A page indicator at the bottom (Line 61-63): Shows which page the panel belongs to.
Line 66-72: We create all panels by looping through the pages and calling createPanel. This creates 5 panels total (2 + 2 + 1).
Line 75-83: We define a showPage function that:
- Sets the current page
- Updates the visibility of all panels (only panels on the current page are visible)
- Triggers a camera shake for transition feedback. The shake is
display.camera.shake(2, 2)which creates a subtle, satisfying transition effect.
Line 86-103: In the update loop:
- Line 87-95: Right arrow or D key advances to the next page (if not on the last page).
- Line 96-103: Left arrow or A key goes back to the previous page (if not on the first page).
- We set
display.keys[39] = falseand similar to prevent rapid firing — this ensures the user can't skip multiple pages in one keypress.
Line 106-110: Draws navigation instructions at the bottom of the screen in semi-transparent white.
What You Just Built
You built an interactive comic with animated panels and page navigation. Each panel is a component that can be animated, clicked, or triggered independently.
This is perfect for:
- Interactive storytelling
- Educational content
- Digital comics
- Presentation tools
📊 Comparison: Traditional Web vs. Limn Engine
| Task | Traditional Web | Limn Engine |
|---|---|---|
| Data Visualization | D3.js, Chart.js (rigid) | Full control, custom animations |
| Interactive Art | Canvas API (low-level) | Particle system, camera effects |
| Floor Plans | SVG, CSS (static) | Zoom, pan, click detection |
| Dashboards | React, charts (complex) | 60fps, smooth animations |
| Digital Comics | HTML/CSS (static) | Animated panels, interactions |
🔧 When to Use Limn Engine vs. Other Tools
| Use Case | Limn Engine | Traditional Tool |
|---|---|---|
| Simple static charts | Overkill | Chart.js, D3.js |
| Complex real-time dashboards | ✅ Perfect | React + libraries |
| Interactive art | ✅ Perfect | P5.js, Three.js |
| Floor plans with zoom/pan | ✅ Perfect | SVG + JavaScript |
| Digital comics with animations | ✅ Perfect | HTML + CSS + JS |
| Game development | ✅ Perfect | — |
🚀 Try It Yourself
All the code examples in this article can be tested directly in Limn Studio:
👉 limn-engine-doc.vercel.app/editor
Just copy and paste the code into the editor, click Run, and see it come to life. No downloads, no setup, no hassle.
📊 What You've Learned
| Concept | Why It Matters |
|---|---|
| Camera System | Zoom, pan, and follow for maps and large visualizations |
| Particle System | Stunning visual effects for art and dashboards |
| 60fps Loop | Smooth real-time updates for data and animations |
| Component System | Reusable visual elements for any purpose |
| Input Handling | Keyboard, mouse, and touch for interactivity |
| Custom Drawing | Full control over how objects are rendered |
🚀 What's Next?
Now that you've seen the possibilities, try building:
- A live Twitter sentiment visualizer
- A interactive museum map
- A music visualizer
- A custom dashboard for your own data
🔗 Resources
| Resource | Link |
|---|---|
| Limn Engine Docs | limn-engine-doc.vercel.app |
| Limn Studio (Editor) | limn-engine-doc.vercel.app/editor |
| Particle System Docs | limn-engine-doc.vercel.app/particles |
| Camera System Docs | limn-engine-doc.vercel.app/camera |
| GitHub Repository | github.com/terracodes004/limn-engine-doc |
| Report Bugs | GitHub Issues |
🎯 The One-Line Summary
"Limn Engine isn't just for games — it's a powerful 2D canvas engine for data visualization, interactive art, maps, dashboards, and digital storytelling." 🎨🚀
Draw your world into existence — one frame at a time. 🎨🚀



Top comments (0)