JCGE Documentation

A lightweight 2D game engine built with vanilla JavaScript and HTML5 Canvas. No build tools, no dependencies.

Getting Started

Create an HTML file, include the engine script, and write your game inside an engineReady listener.

<!-- index.html -->
<script src="path/to/engine/engine-core.js"></script>
<canvas id="canvas"></canvas>
<script src="game.js"></script>
// game.js
window.addEventListener("engineReady", function() {
    var canvas = document.getElementById("canvas");
    var engine = new Engine(canvas);

    engine.jumpEngineIntro = true;
    engine.displayFPS = true;

    engine.OnCreate = function() {
        var scene = new MyScene(engine);
        engine.registerScene(scene);
    };

    engine.start();
});
The engine loads its scripts asynchronously. Always define your classes inside the engineReady event listener so that engine classes like Scene, Vec2, etc. are available.

Project Structure

src/
├── engine/
│   ├── engine-core.js          — script loader (include this)
│   ├── engine-parts/
│   │   ├── engine.js           — Engine class
│   │   ├── scene.js            — Scene class
│   │   └── layer.js            — Layer class
│   ├── buffer/                 — Drawer, Sprite, SpriteSheet, SpriteAtlas
│   ├── cameras/                — FixedCamera, WorldCamera
│   ├── input/                  — InputManager
│   ├── audio/                  — Sound, SoundManager, SpatialLoop
│   ├── animations/             — Tween, Easing, Animation
│   ├── ui/                     — UILayer, UIButton, UILabel, UIPanel, UICircle
│   ├── particles/              — ParticleEmitter, Presets
│   ├── physics/                — Collision helpers
│   ├── tilemap/                — Tileset, Tilemap
│   ├── isometric/              — IsometricUtils, IsometricMap, PathFinder
│   ├── lights/                 — Lighting, LightSpot
│   ├── shadows/                — ShadowSystem, ShadowCaster
│   ├── assets/                 — AssetManager
│   ├── debug/                  — DebugOverlay
│   └── util/                   — Vec2, Size, RGB, buttons, EventEmitter
├── samples/                    — demo projects
└── empty-game-project/         — starter template

Engine

The main class. Creates the game loop with requestAnimationFrame, manages scenes, and provides access to all subsystems.

var engine = new Engine(canvas);
Property Description
engine.drawer The Drawer instance for rendering
engine.input The InputManager for keyboard/mouse/touch/gamepad
engine.sound The SoundManager for audio
engine.tweens The TweenManager for animations
engine.assets The AssetManager for preloading
engine.debug The DebugOverlay for debug visuals
engine.displayFPS Show FPS counter (boolean)
engine.jumpEngineIntro Skip the engine splash screen (boolean)
Method Description
start() Start the engine game loop
stop() Stop the engine
registerScene(scene) Add a scene to the engine
goToScene(scene, data?, transition?, duration?) Switch to a scene with optional data and transition
setCanvasSize(w, h) Set canvas dimensions
setFullScreen() Fill the browser window and auto-resize on window resize
screenSize() Returns Size of the canvas
mouseOnTopOf(gameObject) Check if mouse is over a registered GameObject or Element
mouseOnTopOfPosition(pos, size) Check if mouse is inside a rectangle area

Canvas Sizing

engine.setCanvasSize(960, 540);  // fixed dimensions
engine.setFullScreen();             // fill browser window, auto-resize

GPU Acceleration

The engine automatically enables GPU acceleration when available. The canvas context is created with alpha: false (opaque canvas, skips page compositing) and desynchronized: true (lower input latency). The canvas element is promoted to its own GPU-composited layer via will-change: transform.

Transitions

Use goToScene with a transition type:

engine.goToScene(scene, { level: 2 }, 'fade', 0.5);

Types: 'none', 'fade', 'slide-left', 'slide-right'

Scene

Scenes organize your game into screens. Override the lifecycle methods.

class GameScene extends Scene {
    constructor(engine) {
        super('GameScene', engine);
    }

    OnCreate() {
        // Runs once when scene becomes active
    }

    OnUpdate(elapsedTime) {
        // Runs every frame (elapsedTime = seconds since last frame)
    }

    OnDestroy() {
        // Runs when leaving this scene
    }
}
Method Description
registerGameObject(obj) Add a GameObject (auto-drawn by engine)
registerLayer(layer) Add a rendering layer
setCamera(camera) Set the scene's camera
getIncomingData() Get data passed via goToScene
ended() End the scene and move to next

Drawer

Handles all canvas rendering: shapes, text, sprites, and gradients. Access via engine.drawer.

Shapes

drawer.rectangle(position, size, filled, lineWidth, color, opacity, camera);
drawer.circle(position, radius, startAngle, endAngle, filled, lineWidth, color, opacity, camera);
drawer.triangle(position, size, filled, lineWidth, color, opacity, camera);
drawer.line(point1, point2, lineWidth, color, opacity, camera);
drawer.dashedLine(point1, point2, lineWidth, color, opacity, dashPattern, camera);
drawer.dashedPath(points, lineWidth, color, opacity, dashPattern, camera);
drawer.polygon(points, filled, lineWidth, color, opacity, camera);
drawer.gradient(position, size, startPoint, stopPoint, startColor, stopColor, opacity, camera);

Text

drawer.text(text, position, fontSize, font, style, color, opacity, camera, align, baseline);
drawer.textWidth(text, fontSize, font, style); // returns number

align: 'left' (default), 'center', 'right'. baseline: 'alphabetic' (default), 'top', 'middle'.

Sprites

drawer.sprite(sprite, position, opacity, camera);
drawer.spriteSheet(spriteSheet, position, opacity, camera);
drawer.spriteAtlas(atlas, position, opacity, camera);
drawer.gameObject(gameObject, opacity, camera);
drawer.image(image, sx, sy, sw, sh, dx, dy, dw, dh);

Transforms

drawer.drawRotated(sprite, position, angle, opacity);
drawer.drawScaled(sprite, position, scaleX, scaleY, opacity);
drawer.drawFlipped(sprite, position, flipX, flipY, opacity);
drawer.drawRegionRotated(image, sx, sy, sw, sh, position, dw, dh, angle, opacity);

Camera Transform Block

// Draw in world space without passing camera to every call
drawer.beginCamera(camera);
drawer.rectangle(new Vec2(100, 50), new Size(32, 32), true, 1, 'red');
drawer.line(new Vec2(0,0), new Vec2(100,100), 2, 'yellow');
drawer.endCamera();

Utility

drawer.clearWithColor('#000');
drawer.clear();
Use beginCamera/endCamera for batched world-space drawing — all draw calls inside the block use world coordinates with zoom and offset applied via the canvas transform. Alternatively, pass a camera parameter to individual draw calls for per-call offset (no zoom support).

InputManager

Provides per-frame edge detection for keyboard, mouse, touch, and gamepad. Access via engine.input.

Keyboard

input.isKeyDown(Keys.W)        // held down right now
input.isKeyPressed(Keys.Space)  // just pressed this frame
input.isKeyReleased(Keys.Escape) // just released this frame

Mouse

input.isMouseDown(0)       // 0=left, 1=middle, 2=right
input.isMousePressed(0)    // just clicked this frame
input.isMouseReleased(0)   // just released this frame
input.getMousePosition()   // Vec2 relative to canvas

Touch

input.isTouchActive()     // is there a touch on screen?

Touch events are automatically mapped to mouse events (left button).

Gamepad

input.isGamepadButtonDown(buttonIndex, padIndex)
input.getGamepadAxis(axisIndex, padIndex) // returns -1 to 1
input.getGamepad(index)                   // raw Gamepad object

Vec2

2D vector class. Point and Position are aliases for Vec2. Properties are .X and .Y.

var v = new Vec2(10, 20);
Method Returns Description
add(v) Vec2 Component-wise addition
sub(v) Vec2 Component-wise subtraction
scale(s) Vec2 Multiply both components by scalar
length() number Magnitude
normalize() Vec2 Unit vector
distance(v) number Distance to another vector
dot(v) number Dot product
lerp(v, t) Vec2 Linear interpolation (t: 0-1)
clone() Vec2 Copy
equals(v) boolean Exact equality

Static helpers: Vec2.zero(), Vec2.up(), Vec2.down(), Vec2.left(), Vec2.right()

GameObject

A positioned sprite with movement and collision.

var player = new GameObject(new Sprite(64, 64), new Vec2(100, 100));
player.sprite.loadImage('player.png');
player.velocity = new Vec2(100, 0);
player.move(elapsedTime);

if (player.collisionWith(enemy)) { /* AABB collision */ }

Parent-Child Attachments

Attach any GameObject or Element to a parent so it automatically follows the parent's position with an offset. Useful for turrets on tanks, HP bars above characters, etc.

// Turret follows tank body with offset
turret.attachTo(tankBody, 10, -15);

// HP bar (UILabel) follows player
hpBar.attachTo(player, 0, -20);

// Detach later
turret.detach();

The engine automatically updates child positions each frame before rendering. Works across layers — a UI element can follow a game object in a different layer.

Methods

MethodDescription
attachTo(parent, offsetX, offsetY)Attach to a parent object with X/Y offset
detach()Remove attachment, stop following
updateAttachment()Manually update position (auto-called by engine)

Sprite & SpriteSheet

// Static sprite
var spr = new Sprite(width, height);
spr.loadImage('image.png');

// Animated spritesheet
// SpriteSheet(name, frameW, frameH, frameSpeed, startFrame, endFrame, imagePath)
var sheet = new SpriteSheet('run', 64, 64, 5, 0, 7, 'sheet.png');

// Attach to animation system
var anim = new Animation();
anim.registerAnimation(sheet);
player.registerAnimation(anim);
player.setAnimation('run');

SpriteAtlas

A sprite atlas holds named regions within a single image. Each region can have its own position and size, unlike a SpriteSheet which uses a uniform grid. Ideal for sprites with irregular layouts (e.g. a car with 8 directions at different positions and sizes in one image).

// Define regions: name → { x, y, width, height }
var atlas = new SpriteAtlas('assets/sprites/car.png', {
    'north':     { x: 0,   y: 0,   width: 48, height: 64 },
    'northeast': { x: 60,  y: 10,  width: 56, height: 60 },
    'east':      { x: 130, y: 5,   width: 64, height: 48 },
    'south':     { x: 270, y: 0,   width: 48, height: 64 }
}, 'north');

// Switch displayed region
atlas.setRegion('east');

// Get current region rect
var rect = atlas.getRegion();  // { x, y, width, height }

// List all region names
var names = atlas.getRegionNames();  // ['north', 'northeast', ...]

Constructor

new SpriteAtlas(imagePath, regions, defaultRegion)
ParamTypeDescription
imagePathstringPath to the atlas image file
regionsObjectMap of region names to {x, y, width, height}
defaultRegionstringInitial region to display (defaults to first key)

Methods

MethodDescription
setRegion(name)Switch to a named region. Updates width and height automatically.
getRegion()Returns the current region's {x, y, width, height} rectangle.
getRegionNames()Returns an array of all region name strings.
SpriteAtlas vs SpriteSheet: Use SpriteSheet when all frames are the same size arranged in a grid. Use SpriteAtlas when regions have different sizes or irregular positions within the image. SpriteAtlas extends Sprite and works with GameObject and Element — the engine's Drawer detects it automatically.

Chroma Key

Remove a background color from any sprite type (Sprite, SpriteSheet, SpriteAtlas). The engine processes the image on load, replacing pixels matching the chroma key color with transparency. Useful for sprites with solid-color backgrounds (green, blue, purple, etc.).

// Remove green background from a sprite
var spr = new Sprite(64, 64, 'player.png');
spr.setChromaKey('#00FF00');

// With custom tolerance (default is 30, range 0–255)
spr.setChromaKey('#00FF00', 50);

// Works on SpriteSheet too
var sheet = new SpriteSheet('run', 64, 64, 5, 0, 7, 'sheet.png');
sheet.setChromaKey('#FF00FF', 20);

// And on SpriteAtlas
atlas.setChromaKey('#0000FF');

Method

sprite.setChromaKey(color, tolerance)
ParamTypeDescription
colorstringHex color to remove, e.g. '#FF00FF' or '#f0f'
tolerancenumberColor matching tolerance (0–255). Default: 30. Higher values remove more similar colors.
The chroma key is applied once when the image loads (not per-frame), so there is no performance cost during rendering. The original image is preserved internally — calling setChromaKey again re-processes from the original. Pass null as color to remove chroma key.

Layers

Z-ordered rendering groups within a scene.

var bgLayer = new Layer('background');
bgLayer.registerElement(bgElement);
scene.registerLayer(bgLayer);

var uiLayer = new Layer('ui');
uiLayer.registerGameObject(cursor);
scene.registerLayer(uiLayer);

Layers are drawn in registration order (first = behind, last = in front).

UILayer

A screen-space layer that is not affected by camera movement. Use it for HUD elements, menus, and overlays that should stay fixed on screen.

var hud = new UILayer('hud');
hud.registerElement(button);
hud.registerElement(label);
hud.registerElement(panel);
scene.registerLayer(hud);
UILayer extends Layer with isUI = true. The engine renders UI layers after world layers, ignoring any camera offset. Elements in a UILayer are automatically updated and drawn by the engine.

UIButton

An interactive button element. Supports sprite-based or color-fallback rendering, hover/press states, and click callbacks.

var btn = new UIButton(
    new Vec2(100, 400),
    new Size(160, 45),
    {
        label:        'Play',
        fontSize:     16,
        fontFamily:   'monospace',
        fontColor:    'white',
        normalColor:  '#2a4a6a',
        hoverColor:   '#3a6a8a',
        pressedColor: '#1a3a5a',
        onClick: function() { /* start game */ }
    }
);
hud.registerElement(btn);
OptionDescription
labelButton text
fontSizeText font size (default 14)
fontFamilyFont family (default 'monospace')
fontColorText color (default 'white')
normalColorBackground color at rest
hoverColorBackground color on hover
pressedColorBackground color when pressed
normalSpriteSprite for normal state (optional)
hoverSpriteSprite for hover state (optional)
pressedSpriteSprite for pressed state (optional)
onClickCallback when clicked
onHoverCallback on hover

Use btn.showIt = false to hide/show the button dynamically. Properties isHovered, isPressed, and isDisabled are available for state checks.

When no sprites are provided, UIButton renders a colored rectangle with centered text. This is the recommended approach for dark-themed UIs.

UILabel

A text element for screen-space text display. Update text dynamically with setText().

var label = new UILabel('Score: 0', new Vec2(20, 30), {
    fontSize:   14,
    fontFamily: 'monospace',
    fontStyle:  'bold',
    color:      '#44aaff'
});
hud.registerElement(label);

// Update text each frame:
label.setText('Score: ' + score);

UIPanel

A rectangular background panel for grouping UI elements visually.

var panel = new UIPanel(
    new Vec2(10, 10),
    new Size(200, 80),
    {
        fillColor:   'rgba(10, 10, 30, 0.85)',
        borderColor: '#334455',
        borderWidth: 1
    }
);
hud.registerElement(panel);
Register panels before labels and buttons in the UILayer so they render behind the interactive elements.

UICircle

A circle shape element. Extends Element.

var circle = new UICircle(
    new Vec2(100, 100),
    {
        radius:      30,
        fillColor:   '#3498db',
        borderColor: '#2980b9',
        borderWidth: 2
    }
);
layer.registerElement(circle);
OptionTypeDefaultDescription
radiusnumber20Circle radius in pixels
fillColorstring'white'Fill color
borderColorstringnullBorder/stroke color (null = no border)
borderWidthnumber1Border line width
The position is the center of the circle (same as the engine's drawer.circle() method).

Camera

var cam = new Camera(screenW, screenH, worldW, worldH);
cam.addOffset = true;  // enable camera-relative drawing
scene.setCamera(cam);

WorldCamera

Extends Camera with smooth follow, shake, zoom, and coordinate conversion.

var cam = new WorldCamera(screenW, screenH, worldW, worldH);
cam.addOffset = true;
cam.smoothFollow = true;
cam.followLerp = 0.1;      // 0 = no follow, 1 = instant

// Follow a game object:
cam.setPositionTo(gameObject, elapsedTime);
cam.update(elapsedTime);

// Or set camera center directly (free-form pan):
cam.setCenter(400, 300);
Method Description
setPositionTo(gameObject, dt) Follow a target with smooth lerp
setCenter(x, y) Set camera center to a world position (updates position and offset automatically)
shake(intensity, duration) Camera shake effect
setZoom(level) Set zoom (1.0 = normal, 2.0 = 2x zoom in, 0.5 = zoom out)
getZoom() Returns current zoom level
screenToWorld(screenX, screenY) Convert screen coordinates to world coordinates (zoom-aware). Returns Vec2
applyTransform(ctx) Apply zoom transform to canvas context. Call before drawing world-space content
resetTransform(ctx) Restore canvas context after applyTransform()
update(dt) Update shake and effects (call each frame)

Zoom Rendering

Use applyTransform / resetTransform to draw world content at the current zoom level. UI should be drawn outside the transform block so it stays in screen space.

// Zoom with scroll wheel
var wheel = input.getWheelDelta();
if (wheel !== 0) {
    var z = cam.getZoom();
    cam.setZoom(wheel > 0 ? z * 1.1 : z / 1.1);
}

// Convert mouse to world coords (accounts for zoom)
var world = cam.screenToWorld(mousePos.X, mousePos.Y);

// Draw world content with zoom
cam.applyTransform(ctx);
map.draw(drawer, cam);       // world-space drawing
cam.resetTransform(ctx);

// Draw UI after (screen-space, no zoom)
drawer.text('HUD', new Vec2(10, 20), 14, 'monospace', 'bold', '#fff');

Tilemap

var tileset = new Tileset('tileset.png', 32, 32);
tileset.displayWidth = 48;  // render at different size than source
tileset.displayHeight = 48;

var mapData = [[0,1,2], [3,4,5]];
var tilemap = new Tilemap(tileset, mapData, cols, rows);

// Per-tile rotation (for auto-tiling, road curves, etc.)
tilemap.setTile(5, 3, 7);               // set tile ID
tilemap.setTileRotation(5, 3, 90);      // rotate 90°
tilemap.setTileSource(5, 3, 0, 0, 512, 512); // custom source rect
Method Description
draw(drawer, camera) Render visible tiles (zoom-aware culling, WorldCamera support)
setTile(col, row, tileId) Set tile ID at grid position
setTileRotation(col, row, angleDeg) Set per-tile rotation (0, 90, 180, 270). 0 removes it.
clearTileRotation(col, row) Clear rotation for a tile
setTileSource(col, row, sx, sy, sw, sh) Override source rect (e.g. for multi-tile sprites)
clearTileSource(col, row) Revert to tileset grid lookup
setCollisionLayer(data) Set 2D array (1=solid, 0=empty)
isSolid(col, row) Check if a tile is solid
isSolidAt(worldX, worldY) Check if world position hits solid tile
getTileAt(worldX, worldY) Get tile ID at world position
addLayer(data) Add another tile layer
worldSize() Returns Size of the full map in pixels
displayWidth/displayHeight on the Tileset allow rendering tiles at a different size than the source (e.g. 256px source tiles drawn at 48px). Per-tile rotation uses ctx.save/translate/rotate/restore only for rotated tiles — unrotated tiles use the fast direct drawImage path.

Particles

Built-in Presets

var fire      = ParticleEmitter.fire(engine, position);
var smoke     = ParticleEmitter.smoke(engine, position);
var explosion = ParticleEmitter.explosion(engine, position);
var sparkle   = ParticleEmitter.sparkle(engine, position);

// In OnUpdate:
fire.draw(elapsedTime);

Custom Emitter

var emitter = new ParticleEmitter(engine, {
    position:   new Vec2(100, 100),
    rate:       10,           // particles per emission
    minSpeed:   20,  maxSpeed:  100,
    minAngle:   0,   maxAngle:  Math.PI * 2,
    minLife:    0.5, maxLife:   2,
    minSize:    2,   maxSize:   6,
    startColor: new RGB(255, 200, 50),
    endColor:   new RGB(255, 50, 0, 0),
    gravity:    new Vec2(0, 100),
    shape:      'circle'    // or 'rectangle'
});
Method Description
draw(elapsedTime) Update and render all particles
emit(count) Emit particles manually
stop() Stop continuous emission
clear() Remove all particles

Tweens & Easing

Animate any numeric property on any object. Managed by engine.tweens.

// Animate position over 1 second
engine.tweens.to(player.position, { X: 300, Y: 200 }, 1.0, Easing.easeInOut)
    .onComplete(function() { /* done */ });

// Chain tweens
engine.tweens.to(obj, { X: 100 }, 0.5, Easing.easeIn)
    .then(obj, { Y: 200 }, 0.5, Easing.easeOut);

Easing Functions

linear easeIn easeOut easeInOut easeInCubic easeOutCubic easeInOutCubic bounce elastic

Collision

Static utility functions for collision detection.

Collision.rectRect(posA, sizeA, posB, sizeB);
Collision.circleCircle(posA, radiusA, posB, radiusB);
Collision.circleRect(circlePos, radius, rectPos, rectSize);
Collision.pointRect(point, rectPos, rectSize);
Collision.pointCircle(point, circlePos, radius);

All return true if collision detected.

EventEmitter

Pub/sub events for decoupled communication.

var events = new EventEmitter();
events.on('scoreChange', function(score) { /* ... */ });
events.once('gameOver', function() { /* fires once */ });
events.emit('scoreChange', 100);
events.off('scoreChange', callback);
events.removeAll('scoreChange'); // or removeAll() for all events

Sound

// Direct playback
var jump = new Sound('jump.mp3');
jump.play();

// Via SoundManager (recommended)
engine.sound.setMasterVolume(0.8);
engine.sound.setMusicVolume(0.5);
engine.sound.setSFXVolume(1.0);
engine.sound.playMusic(bgMusic);   // looping, stops previous
engine.sound.playSFX(jumpSound);    // one-shot
engine.sound.muteAll();
engine.sound.unmuteAll();

Spatial Audio

Distance-based audio for world-space sounds. Volume attenuates with distance from camera using quadratic falloff. Zoom affects perceived distance (zoomed in = louder).

One-shot Spatial SFX

// Play a sound at a world position — auto volume from distance
engine.sound.playSpatialSFX(sound, worldX, worldY, camera, maxDistance);

// Get the volume a sound would have at a position (0-1)
var vol = engine.sound.getSpatialVolume(worldX, worldY, camera, maxDistance);
ParameterDescription
soundSound object to play
worldX, worldYSound source world position
cameraCamera with location and getZoom()
maxDistanceMax audible distance in world pixels (default 500)

SpatialLoop — Persistent Looping Sounds

For continuous sounds (engine hum, factory noise, ambient). Smoothly lerps volume each frame to avoid audio clicks.

var loop = new SpatialLoop('engine.ogg', 0.3); // path, baseVolume (0-1)
loop.start();                                   // begin looping (starts silent)
loop.setPosition(worldX, worldY);                // update source position

// Call every frame to update volume from distance:
loop.updateVolume(camera, engine.sound, maxDistance, dt);

loop.isSilent();                                // true if volume < 0.005
loop.stop();                                    // stop looping
PropertyDescription
baseVolumeMaximum volume at closest range (0-1, default 0.3)
worldX, worldYCurrent world position
playingWhether the loop is active
baseVolume caps the maximum volume even at point-blank range. A truck engine at baseVolume = 0.15 will never exceed 15% volume. The spatial calculation is a multiplier: finalVolume = spatialFalloff * baseVolume.

AssetManager

Preload assets before the game starts. Access via engine.assets.

engine.assets
    .loadImage('player', 'sprites/player.png')
    .loadSound('jump', 'audio/jump.mp3')
    .loadJSON('mapData', 'maps/level1.json')
    .onProgress(function(loaded, total) { })
    .onComplete(function() { engine.start(); });

engine.assets.startLoading();

// Later:
var img = engine.assets.getImage('player');
var snd = engine.assets.getSound('jump');
var data = engine.assets.getJSON('mapData'); // parsed Object

Lighting

2D lighting system using an offscreen canvas with radial gradients. Supports colored lights, flickering, and camera integration.

var lighting = new Lighting(engine);
lighting.ambientAlpha = 0.85;   // darkness level (0 = no dark, 1 = pitch black)
lighting.ambientColor = 'black';

// Add lights
// Point light (circle)
var torch = new LightSpot(new Vec2(200, 150), new RGB(255, 160, 50), 180, 0.9);
torch.flicker = 0.25;
lighting.addLightSpot(torch);

// Cone light (directional, e.g. headlight)
var headlight = new LightSpot(new Vec2(100, 100), new RGB(255, 240, 200), 40, 0.5);
headlight.shape = 'cone';        // 'circle' (default) or 'cone'
headlight.direction = 0;         // radians — direction cone points
headlight.spread = Math.PI / 6; // half-angle (60° total cone)
lighting.addLightSpot(headlight);

// In OnUpdate (draw AFTER scene, BEFORE UI):
lighting.draw(elapsedTime, camera); // supports WorldCamera zoom
LightSpot PropertyDescription
positionVec2 world position
colorRGB light color (default white)
radiusLight reach in pixels (default 150)
intensityBrightness 0-1 (default 1.0)
flickerFlicker amount 0-1 (default 0)
activeEnable/disable (default true)
shape'circle' (default) or 'cone' (directional triangle)
directionCone direction in radians (0 = right)
spreadCone half-angle in radians (default π/3)
Lighting MethodDescription
addLightSpot(light)Add a light source
removeLightSpot(light)Remove a light source
clearLights()Remove all lights
draw(elapsedTime, camera?)Render lighting (zoom-aware, frustum culled, skips if ambientAlpha < 0.01)

Shadows

2D directional shadow system using an offscreen canvas. Shadows are projected based on a global light angle and rendered with configurable blur and opacity. Overlapping shadows do not stack darker.

var shadows = new ShadowSystem(engine);
shadows.lightAngle   = 225;             // degrees, where light comes FROM
shadows.shadowLength = 80;              // projection distance in pixels
shadows.shadowOpacity = 0.4;            // composite alpha
shadows.blur          = 4;              // soft shadow blur radius
shadows.simple        = false;           // true = fast flat shadows (no offscreen canvas)

// Add casters
var crate = new ShadowCaster(
    new Vec2(100, 200),       // position (can share reference with a GameObject)
    new Size(50, 50),        // footprint size
    'rectangle',              // type: 'rectangle' or 'ellipse'
    1.0                       // heightScale (taller objects = longer shadow)
);
shadows.addCaster(crate);

// Tall pillar with longer shadow
shadows.addCaster(new ShadowCaster(new Vec2(300, 150), new Size(20, 20), 'rectangle', 2.5));

// Tree with ellipse shadow
shadows.addCaster(new ShadowCaster(new Vec2(500, 120), new Size(40, 40), 'ellipse', 2.0));

// In OnUpdate (draw BEFORE objects so shadows appear behind):
shadows.draw(camera);
ShadowSystem PropertyDescription
lightAngleDegrees, direction light comes from (default 225)
shadowLengthBase projection distance in pixels (default 40)
shadowOpacityAlpha when compositing onto canvas (default 0.4)
shadowColorShadow fill color (default 'black')
blurBlur radius in pixels (default 4, ignored in simple mode)
simpleFast flat shadows mode — no offscreen canvas/blur (default false)
ShadowSystem MethodDescription
addCaster(caster)Add a shadow caster
removeCaster(caster)Remove a shadow caster
clearCasters()Remove all casters
draw(camera?)Render shadows (zoom-aware, frustum culled, skips if opacity < 0.01)
ShadowCaster PropertyDescription
positionVec2 world position (shared reference for auto-tracking)
sizeSize footprint
type'rectangle' or 'ellipse'
heightScaleShadow length multiplier (default 1.0)
rotationRotation in radians (default 0). For rotating objects like vehicles.
activeEnable/disable (default true)
Simple mode (simple = true) draws flat shadow shapes directly via the drawer — no offscreen canvas, no blur, much faster for many casters. Use for games with lots of dynamic objects (vehicles, units). The standard mode uses an offscreen canvas with projected hexagons/ellipses and Gaussian blur.

Debug Overlay

Toggle debug visuals via engine.debug.

engine.debug.showCollisionBoxes = true;
engine.debug.showCameraBounds   = true;
engine.debug.showObjectCount    = true;

IsometricUtils

Static utility class for isometric coordinate conversion. No instance needed — all methods are static.

MethodDescription
toScreen(col, row, tileW, tileH, height, heightStep, offset) Convert grid coordinates to screen position. Returns Vec2. Formula: x = (col - row) * tileW/2, y = (col + row) * tileH/2 - height * heightStep
toGrid(screenX, screenY, tileW, tileH, offset) Convert screen position to grid coordinates (ignores height). Returns Vec2 with floating-point col/row
getDiamondVertices(screenX, screenY, tileW, tileH) Get the four diamond vertices [top, right, bottom, left] as Vec2 array
// Grid to screen
var screen = IsometricUtils.toScreen(5, 3, 64, 32, 2, 16, offset);

// Screen to grid (approximate, ignores height)
var grid = IsometricUtils.toGrid(mouseX, mouseY, 64, 32, offset);

// Diamond vertices for hit testing
var verts = IsometricUtils.getDiamondVertices(sx, sy, 64, 32);

IsometricMap

Height-aware isometric tilemap with collision detection, depth-sorted rendering, and height-based mouse picking.

Constructor

var map = new IsometricMap(cols, rows, tileWidth, tileHeight, heightStep);

Properties

PropertyTypeDescription
cols, rowsnumberGrid dimensions
tileWidth, tileHeightnumberTile diamond size in pixels
heightStepnumberPixels per height unit (default 16)
tiles[][]numberTile type IDs indexed as [row][col]
heightMap[][]numberHeight values indexed as [row][col]
collisionMap[][]number0 = walkable, 1 = blocked. Indexed as [row][col]
offsetVec2Screen offset for centering the map
tileSpritesobjectMap of tile type ID → Image. Registered via setTileSprite()

Methods

MethodDescription
toScreen(col, row)Grid to screen with automatic height lookup. Returns Vec2
toGrid(screenX, screenY)Screen to grid with height-aware diamond hit testing. Returns Vec2 (col, row) or (-1, -1)
getHeight(col, row)Returns height value at tile
isWalkable(col, row)Returns true if tile is not blocked
canMoveTo(fromCol, fromRow, toCol, toRow)Walkable AND height difference ≤ 1
inBounds(col, row)Returns true if coordinates are within grid
setTileSprite(tileType, image)Register a sprite image for a tile type. draw() renders the sprite instead of a procedural diamond
draw(drawer, camera, entityCallback)Render all tiles back-to-front with optional entity callback
drawTileHighlight(drawer, col, row, color, camera)Draw a highlighted diamond overlay on a tile

Tile Sprites

Register Image objects per tile type. When a sprite is registered for a tile type, draw() renders it instead of the procedural colored diamond. Tiles without a registered sprite fall back to the default procedural rendering.

var grassImg = new Image();
grassImg.src = 'tiles/grass.png';
var roadImg = new Image();
roadImg.src = 'tiles/road.png';

map.setTileSprite(0, grassImg);  // tile type 0 uses grass sprite
map.setTileSprite(1, roadImg);   // tile type 1 uses road sprite
// tile type 2 has no sprite → draws procedural diamond

Entity Callback

The draw() method accepts an optional callback for depth-sorted entity rendering. The callback is called once per tile during the back-to-front traversal:

map.draw(drawer, camera, function(col, row, screenX, screenY) {
    // Draw entities that belong to this tile
    if (player.col === col && player.row === row) {
        drawPlayer(ctx, screenX, screenY);
    }
});
Default procedural tile colors are derived from tile type: 0 = grass (green), 1 = dirt (brown), 2 = stone (grey), 3 = water (blue). Elevated tiles render left and right side faces with darkened shading. When a tile sprite is registered via setTileSprite(), it replaces the procedural top face.

PathFinder

Generic A* pathfinding that works with any grid-based map. Supports both IsometricMap and rectangular grids. The grid must have cols, rows, and isWalkable(col, row).

MethodDescription
PathFinder.findPath(grid, startCol, startRow, endCol, endRow, options?) Returns array of {col, row} waypoints. Empty array if no path found

Options

OptionDefaultDescription
allowStartfalseAllow start tile even if not walkable (e.g. building)
allowEndfalseAllow end tile even if not walkable
includeStarttrueInclude start position in returned path

Grid Interface

The grid object must have:

// Works with any grid that has isWalkable
var path = PathFinder.findPath(myGrid, 0, 0, 10, 10, {
    allowStart: true,   // start at a building tile
    allowEnd: true,     // end at a building tile
    includeStart: true
});
if (path.length > 0) {
    for (var i = 0; i < path.length; i++) {
        console.log(path[i].col, path[i].row);
    }
}

Game Loader

For games with many script files, use the loader to load them sequentially after the engine is ready.

<script src="engine/engine-core.js"></script>
<script src="engine/game-loader.js"></script>
<script>
    loader([
        { name: "Player", path: "player.js" },
        { name: "Scene",  path: "scenes/game.js" }
    ]);
</script>

Then listen for gameReady instead of engineReady:

window.addEventListener("gameReady", function() { ... });

RGB

var color = new RGB(255, 100, 50, 0.8); // r, g, b, alpha
color.toString();             // "rgb(255, 100, 50, 0.8)"
color.toStringWithoutAlpha(); // "rgb(255, 100, 50)"

Size

var s = new Size(100, 50); // s.width, s.height

Keys & MouseButton

Constants for input detection. Uses KeyboardEvent.code values.

Group Constants
Arrows Keys.ArrowUp, ArrowDown, ArrowLeft, ArrowRight
Letters Keys.A through Keys.Z
Digits Keys.Digit0 through Keys.Digit9
Function Keys.F1 through Keys.F12
Special Keys.Enter, Space, Escape, Tab, Backspace, Delete
Modifiers Keys.ShiftLeft, ControlLeft, AltLeft, MetaLeft (+ Right)
Mouse MouseButton.LEFT (0), MIDDLE (1), RIGHT (2)

Visual Editor

JCGE comes with a standalone visual editor built with Vite + React + Electron. It lets you visually compose scenes, manage layers, place game objects, configure cameras, paint isometric tilemaps, and export playable games — all without writing boilerplate.

Scene editor view
Scene & Layer management
Object properties panel
Object properties & transforms
Isometric map editor
Isometric tilemap painting
Live preview
Live preview & export

Features

Running the editor

cd jcge_editor
npm install
npm run electron:dev    # dev mode with hot reload
npm run electron:build  # build standalone app

Source code and documentation: github.com/slient-commit/js-canvas-game-engine

Tests

The engine and editor both include unit test suites powered by Vitest. Tests cover the pure-logic parts of the codebase: math utilities, collision detection, animations, pathfinding, event system, and the editor's state management.

Zero-dependency architecture

The engine itself is pure vanilla JavaScript with no npm dependencies. It loads via a single <script> tag and runs directly in the browser. The node_modules folder and package.json inside src/engine/ exist only for the test runner — they are not used at runtime and do not affect the engine in any way. You can safely delete them and the engine will continue to work exactly the same. They are only needed if you want to run the tests.

The engine has zero runtime dependencies. The npm packages (vitest) are devDependencies used exclusively for testing. No build step, bundler, or node_modules is required to use the engine in a game.

How the engine tests work

Engine source files declare global classes (class Vec2, class Tween, etc.) without export statements, since they are designed for <script> tag loading in browsers. To test them in Node.js, the test setup file reads each source file with fs.readFileSync, transforms the declarations into globalThis assignments, and evaluates them with vm.runInThisContext. This makes all engine classes available as globals inside tests, exactly as they would be in a browser.

Running the tests

# Engine tests (181 tests across 8 files)
cd src/engine
npm install     # one-time: installs vitest
npm test        # runs all tests
npm run test:watch  # re-runs on file changes

# Editor tests (92 tests across 2 files)
cd jcge_editor
npm install     # one-time: installs vitest + other deps
npm test
npm run test:watch

Test coverage

FileTestsWhat it covers
vec2.test.js 36 Constructor, add/sub/scale, length, distance, dot, normalize, lerp, clone, equals, statics, aliases
rgb.test.js 8 Constructor defaults, custom values, toString, toStringWithoutAlpha
collision.test.js 26 rectRect, circleCircle, circleRect, pointRect, pointCircle — overlap, touching, separated, edge cases
easing.test.js 31 All 9 easing functions: boundary values, curve shapes, bounce range, elastic oscillation
eventEmitter.test.js 17 on/emit, once, off, removeAll, argument passing, chaining
tween.test.js 22 Interpolation, easing, completion, onComplete/onUpdate callbacks, chaining, TweenManager
isometricUtils.test.js 20 toScreen, toGrid round-trip, height offset, getDiamondVertices shape
pathFinder.test.js 21 A* pathfinding: straight/diagonal paths, obstacles, height cost, out of bounds, waypoint format
Editor tests (jcge_editor/)
projectReducer.test.js 70 All 30+ reducer actions, createEmptyProject, scene/layer/object CRUD, asset renaming, selectors
fileHandlers.test.js 22 isPathSafe and isSafeFilename security validation — path traversal, null inputs, edge cases

Adding new tests

For the engine: create a .test.js file in src/engine/tests/. If your test uses a class that isn't loaded yet, add its source file path to the files array in tests/setup.js. The class will then be available as a global in your test.

For the editor: place test files in a __tests__/ directory next to the module being tested, or anywhere Vitest can discover them. Editor modules use standard ES imports, so no special setup is needed.