Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Why melonJS

melonJS is designed so you can **focus on making games, not on graphics plumbing**.

- **Canvas2D-inspired rendering API** — If you've used the HTML5 Canvas, you already know melonJS. The rendering API (`save`, `restore`, `translate`, `rotate`, `setColor`, `fillRect`, ...) follows the same familiar patterns — no render graphs, no shader pipelines, no instruction sets to learn.
- **[Canvas2D-inspired rendering API](https://github.com/melonjs/melonJS/wiki/Rendering-API)** — If you've used the HTML5 Canvas, you already know melonJS. The rendering API (`save`, `restore`, `translate`, `rotate`, `setColor`, `fillRect`, ...) follows the same familiar patterns — no render graphs, no shader pipelines, no instruction sets to learn.

- **True renderer abstraction** — Write your game once, run it on WebGL or Canvas2D with zero code changes. The engine handles all GPU complexity behind a unified API, with automatic fallback when WebGL is not available. Designed to support future backends (WebGPU) without touching game code.

Expand Down
230 changes: 230 additions & 0 deletions packages/examples/src/examples/gradients/ExampleGradients.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import {
Application as App,
type Application,
type CanvasRenderer,
type Gradient,
Polygon,
Renderable,
Stage,
state,
type WebGLRenderer,
} from "melonjs";
import { createExampleComponent } from "../utils";

type Renderer = CanvasRenderer | WebGLRenderer;

/**
* A renderable that showcases linear and radial gradients.
* Gradients are created once and reused across frames.
*/
class GradientShowcase extends Renderable {
private sky!: Gradient;
private ground!: Gradient;
private sun!: Gradient;
private glow!: Gradient;
private health!: Gradient;
private mana!: Gradient;
private btn!: Gradient;
private circleGrad!: Gradient;
private pillGrad!: Gradient;
private rainbow!: Gradient;
private vig!: Gradient;
private star!: Polygon;

constructor() {
super(0, 0, 1024, 768);
this.anchorPoint.set(0, 0);
}

onActivateEvent() {
const w = this.width;
const h = this.height;
const renderer = this.parentApp.renderer;

// sky
this.sky = renderer.createLinearGradient(0, 0, 0, h * 0.6);
this.sky.addColorStop(0, "#0B0B3B");
this.sky.addColorStop(0.4, "#1a1a6e");
this.sky.addColorStop(0.7, "#4a2080");
this.sky.addColorStop(1, "#FF6B35");

// ground
this.ground = renderer.createLinearGradient(0, h * 0.6, 0, h);
this.ground.addColorStop(0, "#2d5016");
this.ground.addColorStop(0.5, "#1a3a0a");
this.ground.addColorStop(1, "#0d1f05");

// sun
const sunX = w * 0.75;
const sunY = h * 0.35;
const sunR = 60;
this.sun = renderer.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunR);
this.sun.addColorStop(0, "#FFFFFF");
this.sun.addColorStop(0.3, "#FFEE88");
this.sun.addColorStop(0.7, "#FFAA33");
this.sun.addColorStop(1, "rgba(255, 100, 0, 0)");

// sun glow
const glowR = 150;
this.glow = renderer.createRadialGradient(
sunX,
sunY,
sunR * 0.5,
sunX,
sunY,
glowR,
);
this.glow.addColorStop(0, "rgba(255, 200, 100, 0.3)");
this.glow.addColorStop(1, "rgba(255, 100, 0, 0)");

// health bar
const barX = 30;
const barW = 200;
this.health = renderer.createLinearGradient(barX, 0, barX + barW, 0);
this.health.addColorStop(0, "#00FF00");
this.health.addColorStop(0.5, "#FFFF00");
this.health.addColorStop(1, "#FF0000");

// mana bar
this.mana = renderer.createLinearGradient(barX, 0, barX + barW, 0);
this.mana.addColorStop(0, "#0044FF");
this.mana.addColorStop(0.5, "#00BBFF");
this.mana.addColorStop(1, "#00FFFF");

// metallic button
const btnY = 110;
const btnH = 40;
this.btn = renderer.createLinearGradient(0, btnY, 0, btnY + btnH);
this.btn.addColorStop(0, "#EEEEEE");
this.btn.addColorStop(0.5, "#AAAAAA");
this.btn.addColorStop(0.51, "#888888");
this.btn.addColorStop(1, "#CCCCCC");

// circle
this.circleGrad = renderer.createRadialGradient(65, 190, 0, 65, 190, 25);
this.circleGrad.addColorStop(0, "#FF6600");
this.circleGrad.addColorStop(1, "#CC0000");

// pill
this.pillGrad = renderer.createLinearGradient(110, 0, 270, 0);
this.pillGrad.addColorStop(0, "#00CC88");
this.pillGrad.addColorStop(1, "#0066FF");

// rainbow star
const outerR = 45;
const innerR = 20;
const points = 5;
const starVerts: { x: number; y: number }[] = [];
for (let i = 0; i < points * 2; i++) {
const angle = (i * Math.PI) / points - Math.PI / 2;
const r = i % 2 === 0 ? outerR : innerR;
starVerts.push({ x: Math.cos(angle) * r, y: Math.sin(angle) * r });
}
this.star = new Polygon(330, 190, starVerts);
this.rainbow = renderer.createRadialGradient(0, 0, 0, 0, 0, outerR);
this.rainbow.addColorStop(0, "#FF0000");
this.rainbow.addColorStop(0.2, "#FF8800");
this.rainbow.addColorStop(0.4, "#FFFF00");
this.rainbow.addColorStop(0.6, "#00FF00");
this.rainbow.addColorStop(0.8, "#0088FF");
this.rainbow.addColorStop(1, "#8800FF");

// vignette
const vigR = Math.max(w, h) * 0.7;
this.vig = renderer.createRadialGradient(
w / 2,
h / 2,
vigR * 0.3,
w / 2,
h / 2,
vigR,
);
this.vig.addColorStop(0, "rgba(0, 0, 0, 0)");
this.vig.addColorStop(1, "rgba(0, 0, 0, 0.5)");
}

draw(renderer: Renderer) {
const w = this.width;
const h = this.height;
const sunX = w * 0.75;
const sunY = h * 0.35;
const sunR = 60;
const glowR = 150;
const barX = 30;
const barY = 30;
const barW = 200;
const barH = 24;
const healthPct = 0.7;
const manaY = barY + barH + 10;

// sky
renderer.setColor(this.sky);
renderer.fillRect(0, 0, w, h * 0.6);

// ground
renderer.setColor(this.ground);
renderer.fillRect(0, h * 0.6, w, h * 0.4);

// sun
renderer.setColor(this.sun);
renderer.fillRect(sunX - sunR, sunY - sunR, sunR * 2, sunR * 2);

// sun glow
renderer.setColor(this.glow);
renderer.fillRect(sunX - glowR, sunY - glowR, glowR * 2, glowR * 2);

// health bar
renderer.setColor("#333333");
renderer.fillRect(barX - 2, barY - 2, barW + 4, barH + 4);
renderer.setColor(this.health);
renderer.fillRect(barX, barY, barW * healthPct, barH);

// mana bar
renderer.setColor("#333333");
renderer.fillRect(barX - 2, manaY - 2, barW + 4, barH + 4);
renderer.setColor(this.mana);
renderer.fillRect(barX, manaY, barW * 0.5, barH);

// metallic button
renderer.setColor(this.btn);
renderer.fillRect(30, 110, 160, 40);
renderer.setColor("#666666");
renderer.strokeRect(30, 110, 160, 40);

// gradient shapes
renderer.setColor(this.circleGrad);
renderer.fillEllipse(65, 190, 25, 25);

renderer.setColor(this.pillGrad);
renderer.fillRoundRect(110, 175, 160, 30, 15);

renderer.setColor(this.rainbow);
renderer.fill(this.star);

// vignette
renderer.setColor(this.vig);
renderer.fillRect(0, 0, w, h);

renderer.setColor("#FFFFFF");
}
}

class GradientScreen extends Stage {
override onResetEvent(app: Application) {
app.world.backgroundColor.parseCSS("#000000");
app.world.addChild(new GradientShowcase());
}
}

const createGame = () => {
const _app = new App(1024, 768, {
parent: "screen",
scale: "auto",
scaleMethod: "flex-width",
});

state.set(state.PLAY, new GradientScreen());
state.change(state.PLAY);
};

export const ExampleGradients = createExampleComponent(createGame);
6 changes: 6 additions & 0 deletions packages/examples/src/examples/graphics/ExampleGraphics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,18 @@ const createGame = () => {
renderer.lineTo(840, 55);
renderer.lineTo(940, 30);

renderer.stroke();

// dashed zigzag line
renderer.setLineDash([10, 6]);
renderer.beginPath();
renderer.moveTo(540, 50);
renderer.lineTo(640, 75);
renderer.lineTo(740, 50);
renderer.lineTo(840, 75);
renderer.lineTo(940, 50);
renderer.stroke();
renderer.setLineDash([]);

renderer.setColor("#ff69b4");
renderer.fill(this.roundRect1);
Expand Down
13 changes: 13 additions & 0 deletions packages/examples/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ const ExampleDragAndDrop = lazy(() =>
default: m.ExampleDragAndDrop,
})),
);
const ExampleGradients = lazy(() =>
import("./examples/gradients/ExampleGradients").then((m) => ({
default: m.ExampleGradients,
})),
);
const ExampleGraphics = lazy(() =>
import("./examples/graphics/ExampleGraphics").then((m) => ({
default: m.ExampleGraphics,
Expand Down Expand Up @@ -187,6 +192,14 @@ const examples: {
description:
"Interactive drag-and-drop with pointer events, collision detection, and drop zones.",
},
{
component: <ExampleGradients />,
label: "Gradients",
path: "gradients",
sourceDir: "gradients",
description:
"Linear and radial gradients for sky backgrounds, health bars, UI buttons, and lighting effects.",
},
{
component: <ExampleGraphics />,
label: "Graphics",
Expand Down
2 changes: 2 additions & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## [18.3.0] (melonJS 2)

### Added
- Renderer: `setLineDash()` and `getLineDash()` methods — set dash patterns for stroke operations, matching the Canvas 2D API. Works on both Canvas and WebGL renderers. Dash state is saved/restored with `save()`/`restore()`.
- Renderer: `createLinearGradient()` and `createRadialGradient()` methods — create gradient fills that can be passed to `setColor()`, matching the Canvas 2D API. Works on both Canvas and WebGL renderers with all fill methods (`fillRect`, `fillEllipse`, `fillArc`, `fillPolygon`, `fillRoundRect`). Gradient state is saved/restored with `save()`/`restore()`.
- Tiled: extensible object factory registry for `TMXTileMap.getObjects()` — object creation is now dispatched through a `Map`-based registry (like `loader.setParser`), with built-in factories for text, tile, and shape objects, plus class-based factories for Entity, Collectable, Trigger, Light2d, Sprite, NineSliceSprite, ImageLayer, and ColorLayer
- Tiled: new public `registerTiledObjectFactory(type, factory)` and `registerTiledObjectClass(name, Constructor)` APIs allowing plugins to register custom Tiled object handlers by class name without modifying engine code
- Tiled: `detectObjectType()` now checks `settings.class` and `settings.name` against the factory registry before falling through to structural detection, enabling class-based dispatch for custom types
Expand Down
2 changes: 2 additions & 0 deletions packages/melonjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import save from "./system/save.ts";
import timer from "./system/timer.ts";
import Tween from "./tweens/tween.ts";
import CanvasRenderer from "./video/canvas/canvas_renderer.js";
import { Gradient } from "./video/gradient.js";
import Renderer from "./video/renderer.js";
import RenderState from "./video/renderstate.js";
import CanvasRenderTarget from "./video/rendertarget/canvasrendertarget.js";
Expand Down Expand Up @@ -119,6 +120,7 @@ export {
DropTarget,
Entity, // eslint-disable-line @typescript-eslint/no-deprecated
GLShader,
Gradient,
ImageLayer,
Light2d,
NineSliceSprite,
Expand Down
38 changes: 31 additions & 7 deletions packages/melonjs/src/video/canvas/canvas_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ONCONTEXT_RESTORED,
on,
} from "../../system/event.ts";
import { Gradient } from "./../gradient.js";
import Renderer from "./../renderer.js";
import TextureCache from "./../texture/cache.js";

Expand Down Expand Up @@ -525,6 +526,23 @@ export default class CanvasRenderer extends Renderer {
this.strokeLine(startX, startY, endX, endY);
}

/**
* Set the line dash pattern.
* @param {number[]} segments - dash pattern
*/
setLineDash(segments) {
super.setLineDash(segments);
this.getContext().setLineDash(this.renderState.lineDash);
}

/**
* Get the current line dash pattern.
* @returns {number[]} dash pattern
*/
getLineDash() {
return this.getContext().getLineDash();
}

/**
* Stroke the given me.Polygon on the screen
* @param {Polygon} poly - the shape to draw
Expand Down Expand Up @@ -669,7 +687,10 @@ export default class CanvasRenderer extends Renderer {
this.setBlendMode(result.blendMode);
}
// re-sync from the native context (which is authoritative for Canvas)
this.currentColor.copy(context.fillStyle);
// fillStyle may be a CanvasGradient/CanvasPattern — only sync if it's a color string
if (typeof context.fillStyle === "string") {
this.currentColor.copy(context.fillStyle);
}
this.currentColor.glArray[3] = context.globalAlpha;
// reset scissor cache so the next clipRect() won't skip
this.currentScissor[0] = 0;
Expand Down Expand Up @@ -724,16 +745,19 @@ export default class CanvasRenderer extends Renderer {
/**
* Set the current fill & stroke style color.
* By default, or upon reset, the value is set to #000000.
* @param {Color|string} color - css color value
* @param {Color|string|Gradient} color - css color value or a Gradient object
*/
setColor(color) {
const currentColor = this.currentColor;
const context = this.getContext();

currentColor.copy(color);
// globalAlpha is applied at rendering time by the canvas

context.strokeStyle = context.fillStyle = currentColor.toRGBA();
if (color instanceof Gradient) {
this.renderState.currentGradient = color;
context.strokeStyle = context.fillStyle = color.toCanvasGradient(context);
} else {
this.renderState.currentGradient = null;
this.currentColor.copy(color);
context.strokeStyle = context.fillStyle = this.currentColor.toRGBA();
}
}

/**
Expand Down
Loading
Loading