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
134 changes: 134 additions & 0 deletions packages/examples/src/examples/blendModes/ExampleBlendModes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { game, Renderable, Text, video } from "melonjs";
import { createExampleComponent } from "../utils";

const BLEND_MODES = [
"normal",
"add",
"multiply",
"screen",
"overlay",
"darken",
"lighten",
"color-dodge",
"color-burn",
"hard-light",
"soft-light",
"difference",
"exclusion",
];

const COLS = 5;
const CELL_W = 180;
const CELL_H = 170;
const CIRCLE_R = 45;

const createGame = () => {
const rows = Math.ceil(BLEND_MODES.length / COLS);
const canvasW = COLS * CELL_W;
const HEADER_H = 40;
const canvasH = rows * CELL_H + HEADER_H;

if (
!video.init(canvasW, canvasH, {
parent: "screen",
renderer: video.AUTO,
preferWebGL1: false,
})
) {
alert("Your browser does not support HTML5 canvas.");
return;
}

// detect which modes are actually supported by probing setBlendMode
const renderer = video.renderer as any;
const supported: Record<string, boolean> = {};
for (const mode of BLEND_MODES) {
const applied = renderer.setBlendMode(mode);
supported[mode] = applied === mode;
}
renderer.setBlendMode("normal");

// single renderable that draws the entire blend mode grid
class BlendModeGrid extends Renderable {
constructor() {
super(0, 0, canvasW, canvasH);
this.anchorPoint.set(0, 0);
}

override draw(renderer: any) {
// dark background
renderer.setBlendMode("normal");
renderer.setGlobalAlpha(1.0);
renderer.setColor("#1a1a2e");
renderer.fillRect(0, 0, canvasW, canvasH);

for (let i = 0; i < BLEND_MODES.length; i++) {
const col = i % COLS;
const row = Math.floor(i / COLS);
const cx = col * CELL_W + CELL_W / 2;
const cy = row * CELL_H + CELL_H / 2 - 10 + HEADER_H;
const mode = BLEND_MODES[i];

// blue circle (base) — always normal blend
renderer.setBlendMode("normal");
renderer.setGlobalAlpha(1.0);
renderer.setColor("#2266dd");
renderer.fillEllipse(
cx - CIRCLE_R * 0.35,
cy - CIRCLE_R * 0.2,
CIRCLE_R,
CIRCLE_R,
);

// red circle — apply the test blend mode
renderer.setBlendMode(mode);
renderer.setGlobalAlpha(0.85);
renderer.setColor("#dd4422");
renderer.fillEllipse(
cx + CIRCLE_R * 0.35,
cy + CIRCLE_R * 0.2,
CIRCLE_R,
CIRCLE_R,
);
}

// reset
renderer.setBlendMode("normal");
}
}

game.world.addChild(new BlendModeGrid(), 0);

// renderer type header
const header = new Text(canvasW / 2, 8, {
font: "Arial",
size: 16,
fillStyle: "#aaaaaa",
textAlign: "center",
text: renderer.type + " Supported Blend Modes",
});
header.anchorPoint.set(0, 0);
header.floating = true;
game.world.addChild(header, 2);

// add labels — green if supported, red if not
for (let i = 0; i < BLEND_MODES.length; i++) {
const col = i % COLS;
const row = Math.floor(i / COLS);
const x = col * CELL_W + CELL_W / 2;
const y = row * CELL_H + CELL_H - 25 + HEADER_H;
const mode = BLEND_MODES[i];

const label = new Text(x, y, {
font: "Arial",
size: 14,
fillStyle: supported[mode] ? "#44dd44" : "#dd4444",
textAlign: "center",
text: mode,
});
label.anchorPoint.set(0, 0);
game.world.addChild(label, 1);
}
};

export const ExampleBlendModes = createExampleComponent(createGame);
9 changes: 9 additions & 0 deletions packages/examples/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const sourceFiles = import.meta.glob("./examples/**/*.{ts,tsx,js}", {

import { ExampleAseprite } from "./examples/aseprite/ExampleAseprite";
import { ExampleBenchmark } from "./examples/benchmark/ExampleBenchmark";
import { ExampleBlendModes } from "./examples/blendModes/ExampleBlendModes";
import { ExampleCompressedTextures } from "./examples/compressedTextures/ExampleCompressedTextures";
import { ExampleDeviceTest } from "./examples/deviceTest/ExampleDeviceTest";
import { ExampleDragAndDrop } from "./examples/dragAndDrop/ExampleDragAndDrop";
Expand Down Expand Up @@ -61,6 +62,14 @@ const examples: {
description:
"Stress test rendering thousands of animated sprites to measure engine performance.",
},
{
component: <ExampleBlendModes />,
label: "Blend Modes",
path: "blend-modes",
sourceDir: "blendModes",
description:
"Visual comparison of all supported blend modes (normal, multiply, screen, overlay, darken, lighten, etc.).",
},
{
component: <ExampleCompressedTextures />,
label: "Compressed Textures",
Expand Down
2 changes: 2 additions & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
- TMX: support Tiled 1.8+ class-type custom properties (`type="class"` with nested properties)
- TMX: use Tiled 1.10+ `isCollection` tileset flag when available (fallback to image detection for older maps)
- TMX: support Tiled 1.12+ per-object opacity and visibility
- CanvasRenderer: add support for additional CSS blend modes (`overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, `exclusion`) when supported by the browser
- WebGLRenderer: add `darken` and `lighten` blend modes via `gl.MIN`/`gl.MAX` (WebGL2 only)

### Changed
- TypeScript: convert leaf modules to TypeScript — plugin, camera, particles emitter, state, audio
Expand Down
64 changes: 49 additions & 15 deletions packages/melonjs/src/video/canvas/canvas_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,41 +109,75 @@ export default class CanvasRenderer extends Renderer {

/**
* set a blend mode for the given context. <br>
* Supported blend mode between Canvas and WebGL renderer : <br>
* - "normal" : this is the default mode and draws new content on top of the existing content <br>
* <img src="../images/normal-blendmode.png" width="510"/> <br>
* - "multiply" : the pixels of the top layer are multiplied with the corresponding pixel of the bottom layer. A darker picture is the result. <br>
* <img src="../images/multiply-blendmode.png" width="510"/> <br>
* - "additive or lighter" : where both content overlap the color is determined by adding color values. <br>
* <img src="../images/lighter-blendmode.png" width="510"/> <br>
* - "screen" : The pixels are inverted, multiplied, and inverted again. A lighter picture is the result (opposite of multiply) <br>
* <img src="../images/screen-blendmode.png" width="510"/> <br>
* All renderers support: <br>
* - "normal" : draws new content on top of the existing content <br>
* <img src="../images/normal-blendmode.png" width="180"/> <br>
* - "add", "additive", or "lighter" : color values are added together <br>
* <img src="../images/add-blendmode.png" width="180"/> <br>
* - "multiply" : pixels are multiplied, resulting in a darker picture <br>
* <img src="../images/multiply-blendmode.png" width="180"/> <br>
* - "screen" : pixels are inverted, multiplied, and inverted again (opposite of multiply) <br>
* <img src="../images/screen-blendmode.png" width="180"/> <br>
* Canvas (browser-dependent) and WebGL2: <br>
* - "darken" : retains the darkest pixels of both layers <br>
* <img src="../images/darken-blendmode.png" width="180"/> <br>
* - "lighten" : retains the lightest pixels of both layers <br>
* <img src="../images/lighten-blendmode.png" width="180"/> <br>
* Canvas only, browser-dependent (falls back to "normal" if unsupported or in WebGL): <br>
* - "overlay" <br>
* <img src="../images/overlay-blendmode.png" width="180"/> <br>
* - "color-dodge" <br>
* <img src="../images/color-dodge-blendmode.png" width="180"/> <br>
* - "color-burn" <br>
* <img src="../images/color-burn-blendmode.png" width="180"/> <br>
* - "hard-light" <br>
* <img src="../images/hard-light-blendmode.png" width="180"/> <br>
* - "soft-light" <br>
* <img src="../images/soft-light-blendmode.png" width="180"/> <br>
* - "difference" <br>
* <img src="../images/difference-blendmode.png" width="180"/> <br>
* - "exclusion" <br>
* <img src="../images/exclusion-blendmode.png" width="180"/> <br>
* @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
* @param {string} [mode="normal"] - blend mode : "normal", "multiply", "lighter, "additive", "screen"
* @param {string} [mode="normal"] - blend mode
* @param {CanvasRenderingContext2D} [context]
* @returns {string} the blend mode actually applied (may differ if the requested mode is unsupported)
*/
setBlendMode(mode = "normal", context) {
context = context || this.getContext();
this.currentBlendMode = mode;
switch (mode) {
case "screen":
context.globalCompositeOperation = "screen";
break;

case "lighter":
case "additive":
case "add":
context.globalCompositeOperation = "lighter";
break;

case "multiply":
context.globalCompositeOperation = "multiply";
case "screen":
case "overlay":
case "darken":
case "lighten":
case "color-dodge":
case "color-burn":
case "hard-light":
case "soft-light":
case "difference":
case "exclusion":
context.globalCompositeOperation = mode;
// verify the browser accepted the mode
if (context.globalCompositeOperation !== mode) {
context.globalCompositeOperation = "source-over";
this.currentBlendMode = "normal";
}
break;

default: // normal
context.globalCompositeOperation = "source-over";
this.currentBlendMode = "normal";
break;
}
return this.currentBlendMode;
}

/**
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/melonjs/src/video/images/multiply-blendmode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/melonjs/src/video/images/normal-blendmode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/melonjs/src/video/images/screen-blendmode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 47 additions & 10 deletions packages/melonjs/src/video/webgl/webgl_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -725,18 +725,27 @@ export default class WebGLRenderer extends Renderer {

/**
* set a blend mode for the given context. <br>
* Supported blend mode between Canvas and WebGL renderer : <br>
* - "normal" : this is the default mode and draws new content on top of the existing content <br>
* <img src="../images/normal-blendmode.png" width="510"/> <br>
* - "multiply" : the pixels of the top layer are multiplied with the corresponding pixel of the bottom layer. A darker picture is the result. <br>
* <img src="../images/multiply-blendmode.png" width="510"/> <br>
* - "additive or lighter" : where both content overlap the color is determined by adding color values. <br>
* <img src="../images/lighter-blendmode.png" width="510"/> <br>
* - "screen" : The pixels are inverted, multiplied, and inverted again. A lighter picture is the result (opposite of multiply) <br>
* <img src="../images/screen-blendmode.png" width="510"/> <br>
* All renderers support: <br>
* - "normal" : draws new content on top of the existing content <br>
* <img src="../images/normal-blendmode.png" width="180"/> <br>
* - "add", "additive", or "lighter" : color values are added together <br>
* <img src="../images/add-blendmode.png" width="180"/> <br>
* - "multiply" : pixels are multiplied, resulting in a darker picture <br>
* <img src="../images/multiply-blendmode.png" width="180"/> <br>
* - "screen" : pixels are inverted, multiplied, and inverted again (opposite of multiply) <br>
* <img src="../images/screen-blendmode.png" width="180"/> <br>
* WebGL2 additionally supports: <br>
* - "darken" : retains the darkest pixels of both layers <br>
* <img src="../images/darken-blendmode.png" width="180"/> <br>
* - "lighten" : retains the lightest pixels of both layers <br>
* <img src="../images/lighten-blendmode.png" width="180"/> <br>
* Other CSS blend modes ("overlay", "color-dodge", "color-burn", "hard-light", "soft-light",
* "difference", "exclusion") may be supported by the Canvas renderer (browser-dependent)
* and will always fall back to "normal" in WebGL. <br>
* @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
* @param {string} [mode="normal"] - blend mode : "normal", "multiply", "lighter", "additive", "screen"
* @param {string} [mode="normal"] - blend mode
* @param {WebGLRenderingContext} [gl] - a WebGL context
* @returns {string} the blend mode actually applied (may differ if the requested mode is unsupported)
*/
setBlendMode(mode = "normal", gl = this.gl) {
if (this.currentBlendMode !== mode) {
Expand All @@ -746,24 +755,52 @@ export default class WebGLRenderer extends Renderer {

switch (mode) {
case "screen":
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_COLOR);
break;

case "lighter":
case "additive":
case "add":
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE);
break;

case "multiply":
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA);
break;

case "darken":
if (this.WebGLVersion > 1) {
gl.blendEquation(gl.MIN);
gl.blendFunc(gl.ONE, gl.ONE);
} else {
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
this.currentBlendMode = "normal";
}
break;

case "lighten":
if (this.WebGLVersion > 1) {
gl.blendEquation(gl.MAX);
gl.blendFunc(gl.ONE, gl.ONE);
} else {
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
this.currentBlendMode = "normal";
}
break;

default:
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
this.currentBlendMode = "normal";
break;
}
}
return this.currentBlendMode;
}

/**
Expand Down
Loading