xxxxxxxxxx
373
/*
GameMaker's official manual does an excellent job explaining what blendmodes do on the mathematical level, I still found it difficult to conceptualize, so I made this blendmode emulation along with the best explanation I was able to haphazardly put together.
GameMaker Manual:
https://manual.yoyogames.com/Additional_Information/Guide_To_Using_Blendmodes.htm
This website let's you play with equivelent blendmodes:
http://www.andersriggelsen.dk/glblendfunc.php
Note, every color value is represented from 0 to 1. (1, 1, 1, 1) is 1 red, 1 green, 1 blue, and 1 alpha, making a fully opaque white. There can't be a value above 1, and if an operation results in a value larger than 1, it gets capped to 1. This also applies if the value goes below 0, it gets capped to 0.
*/
/*
There are 4 basic inputs to blend modes:
The source color, or the color you are attempting to draw
The destination color, or the color that is already in that position
The source factor, how you transform the source color
The destination factor, how you transform the destination color
The source color is multiplied by the result of the source factor, the destination color is multiplied by the result of the destination factor, and then the results of both are added together.
Say we wanted to directly replace a color on the screen. How would we accomplish this with blendmodes?
Let's say that the color on the screen, the "destination color", is dark grey, or (0.2, 0.2, 0.2, 1).
The color we are trying to draw, the "source color", is light grey, or (0.8, 0.8, 0.8, 1).
We could try skipping factors and add the colors together,
(0.2, 0.2, 0.2, 1) << Destination Color
+ (0.8, 0.8, 0.8, 1) << Source Color
====
( 1, 1, 1, 1) << Final Color
That's white, not light grey. Since we ultimately add the two colors together, we need some way to cancel out the destination dark grey, to turn in to (0, 0, 0, 0).
That's where factors come in. We could multiply the destination color with zero on all of its feilds to cancel it out. We'll also add multiply the source color by one to keep it exactly the same.
(0.2, 0.2, 0.2, 1) << Destination Color
x [ 0, 0, 0, 0] << Destination Factor (bm_zero)
====
( 0, 0, 0, 0) << Destination Result
(0.8, 0.8, 0.8, 1) << Source Color
x [ 1, 1, 1, 1] << Source Factor (bm_one)
====
(0.8, 0.8, 0.8, 1) << Source Result
And then add both results together:
( 0, 0, 0, 0) << Destination Result
+ (0.8, 0.8, 0.8, 1) << Source Result
====
(0.8, 0.8, 0.8, 1) << Final Color
And just like that, we successfully changed the color. Obviously this is quite simplistic and redundant, but even here you can start to get a glimpse of it's power.
Say we wanted to get alpha working, how would we go about doing that? What we need is to use the alpha as a sort of weight, to say how much of the source color and how much of the destination color we want added together. This requires 2 new blend mode options, bm_src_alpha [src.A, src.A, src.A, src.A] (if the source alpha was 1, it would be identical to bm_one) and bm_inv_src_alpha [1-src.A, 1-src.A, 1-src.A, 1-src.A] (if the source alpha was 1, it would be identical to bm_zero).
Let's say we have the destination color as (0.2, 0.2, 0.2, 1) again, but the source color is (0.8, 0.8, 0.8, 0.6), so the alpha is 0.5. What this would mean is that we want 40% of the destination color, and 60% of the source color.
Last time we were able to cheat and not calculate the factor's results, but in this case we need to do that since it is going to be different.
First calculate the Factor Results,
dest = ( 0.2, 0.2, 0.2, 1) << Destination Color
src = ( 0.8, 0.8, 0.8, 0.6) << Source Color
[1-src.A, 1-src.A, 1-src.A, 1-src.A] << Destination Factor (bm_inv_src_alpha)
=>
( 0.4, 0.4, 0.4, 0.4) << Destination Factor Result
dest = ( 0.2, 0.2, 0.2, 1) << Destination Color
src = ( 0.8, 0.8, 0.8, 0.6) << Source Color
[src.A, src.A, src.A, src.A] << Source Factor (bm_src_alpha)
=>
( 0.6, 0.6, 0.6, 0.6) << Source Factor Result
Then do whatever
(0.2, 0.2, 0.2, 1) << Destination Color
x (0.4, 0.4, 0.4, 0.4) << Destination Factor Result
====
(0.08, 0.08, 0.08, 0.4) << Destination Result
( 0.8, 0.8, 0.8, 0.6) << Source Color
x ( 0.6, 0.6, 0.6, 0.6) << Source Factor Result
====
(0.48, 0.48, 0.48, 0.36) << Source Result
And then add both results together:
(0.08, 0.08, 0.08, 0.4 ) << Destination Result
+ (0.48, 0.48, 0.48, 0.36) << Source Result
====
(0.56, 0.56, 0.56, 0.76) << Final Color
Copying the GameMaker guide, let's recreate Photoshop's multiply. For this, we'll need a new blendmode option, bm_dest_colour (Just realized that this isn't the American spelling... sorry for the inconsistency. You'll have to just deal with me calling everything "color" instead of "colour"), which looks like [dest.R, dest.G, dest.B, dest.A]. This time we'll have the destination color as pink (1, 0.5, 1, 1) and the source color as teal (0.5, 1, 1, 1).
First calculate the Factor Results...
dest = ( 1, 0.5, 1, 1) << Destination Color
src = (0.5, 1, 1, 1) << Source Color
[ 0, 0, 0, 0] << Destination Factor (bm_zero)
=>
( 0, 0, 0, 0) << Destination Factor Result
dest = ( 1, 0.5, 1, 1) << Destination Color
src = ( 0.5, 1, 1, 1) << Source Color
[dest.R, dest.G, dest.B, dest.A] << Source Factor (bm_dest_colour)
=>
( 1, 0.5, 1, 1) << Source Factor Result
Then do whatever
( 1, 0.5, 1, 1) << Destination Color
x ( 0, 0, 0.4, 0.4) << Destination Factor Result
====
( 0, 0, 0, 0) << Destination Result
(0.5, 1, 1, 1) << Source Color
x (1, 0.5, 1, 1) << Source Factor Result
====
(0.5, 0.5, 1, 1) << Source Result
And then add both results together:
(0, 0, 0, 0) << Destination Result
+ (0.5, 0.5, 1, 1) << Source Result
====
(0.5, 0.5, 1, 1) << Final Color
Or more simply: (src * dest) + (dest * 0)
Hopefully that clears up what these can do, and helps you come up with ways of using them.
*/
/*
GameMaker also offers "gpu_set_blendmode_ext_sepalpha()", which let's you apply blend modes exactly as gpu_set_blendmode_ext() does, but you can apply different blend modes to alpha seperately. This can be useful if you wanted to subtract using [bm_zero, bm_inv_src_color], but without the alpha channels canceling each other out.
https://manual.yoyogames.com/GameMaker_Language/GML_Reference/Drawing/GPU_Control/gpu_set_blendmode_ext_sepalpha.htm
*/
// Here are all of GameMaker's blend modes(excluding its built in combinations, which are bm_normal, bm_add, bm_subtract, and bm_max), in the form of functions.
const blend = {
// I went over these two above.
zero:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [0, 0, 0, 0],
one:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [1, 1, 1, 1],
src_color:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [srcR, srcG, srcB, srcA],
invert_src_color:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [1-srcR, 1-srcG, 1-srcB, 1-srcA],
src_alpha:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [srcA, srcA, srcA, srcA],
invert_src_alpha:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [1-srcA, 1-srcA, 1-srcA, 1-srcA],
dest_color:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [destR, destG, destB, destA],
invert_dest_color:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [1-destR, 1-destG, 1-destB, 1-destA],
dest_alpha:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [destA, destA, destA, destA],
invert_dest_alpha:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => [1-destA, 1-destA, 1-destA, 1-destA],
src_alpha_sat:
(srcR, srcG, srcB, srcA, destR, destG, destB, destA) => {
const f = Math.min(srcA, 1-destA);
return [f, f, f, 1];
},
};
let sourceFactor = blend.src_alpha;
let destinationFactor = blend.invert_src_alpha;
function setblendmode(src, dest) {
sourceFactor = src;
destinationFactor = dest;
}
// various blend mode examples
// Note that you can't recreate every photoshop blendmode with GameMaker's blendmodes, you will need to recreate them with shaders.
// bm_normal
// (src * src.A) + (dest * (1 - src.A))
setblendmode(blend.src_alpha, blend.invert_src_alpha);
// bm_add
// (src * src.A) + (dest * 1)
//setblendmode(blend.src_alpha, blend.one);
// bm_subtract
// (src * 0) + (dest * (1 - src))
//setblendmode(blend.zero, blend.invert_src_color);
// multiply
// (src * dest) + (dest * 0)
//setblendmode(blend.dest_color, blend.zero);
// screen
// (src * (1 - dest)) + (dest * 1)
//setblendmode(blend.invert_dest_color, blend.one);
// use only source color
// (src * 1) + (dest * 0)
//setblendmode(blend.one, blend.zero);
// keep destination color
// (src * 0) + (dest * 1)
//setblendmode(blend.zero, blend.one);
// linear dodge (sometimes also called "add", however gamemaker uses a different combination for its bm_add)
// (src * 1) + (dest * 1)
//setblendmode(blend.one, blend.one);
// set to black
// (src * 0) + (dest * 0)
//setblendmode(blend.zero, blend.zero);
// invert? (doesn't seem to work in gamemaker... not sure why.)
// edit: it does work, probably. I wrote the part where gamemaker sets the alpha of the application layer to 1 incorrectly.
setblendmode(blend.invert_dest_color, blend.invert_src_alpha);
// (src * (1 - dest)) + (dest * (1 - src.A))
// calculates the blend mode using the algorithm described above
function calculateBlend(src, dest) {
const destinationFactorResult =
destinationFactor(
src.r, src.g, src.b, src.a,
dest.r, dest.g, dest.b, dest.a
);
const sourceFactorResult =
sourceFactor(
src.r, src.g, src.b, src.a,
dest.r, dest.g, dest.b, dest.a
);
const destinationResult =
[
dest.r * destinationFactorResult[0],
dest.g * destinationFactorResult[1],
dest.b * destinationFactorResult[2],
dest.a * destinationFactorResult[3]
];
const sourceResult =
[
src.r * sourceFactorResult[0],
src.g * sourceFactorResult[1],
src.b * sourceFactorResult[2],
src.a * sourceFactorResult[3]
];
// Math.min and Math.max because values must be <=1 and >0
const finalColor = new Color(
Math.min(Math.max(destinationResult[0] + sourceResult[0], 0), 1),
Math.min(Math.max(destinationResult[1] + sourceResult[1], 0), 1),
Math.min(Math.max(destinationResult[2] + sourceResult[2], 0), 1),
Math.min(Math.max(destinationResult[3] + sourceResult[3], 0), 1)
);
return finalColor;
}
// other things...
class Color {
constructor(r, g = r, b = r, a = 1) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
}
const drawQueue = [
{ x: 20, y: 20, width: 90, height: 130, color: new Color(1, 0.2, 1, 1) },
{ x: 50, y: 60, width: 70, height: 110, color: new Color(0.2, 1, 1, 1) },
{ x: 140 + 20, y: 20, width: 90, height: 130, color: new Color(0.4, 0.5, 0.9, 1) },
{ x: 140 + 50, y: 60, width: 70, height: 110, color: new Color(1, 0.5, 0.9, 0.5) },
{ x: 0, y: 80, width: 360, height: 10, color: new Color(1, 1, 1, 0.5) },
{ x: 0, y: 90, width: 360, height: 10, color: new Color(1, 1, 1, 1) },
{ x: 0, y: 100, width: 360, height: 10, color: new Color(0, 0, 0, 1) },
{ x: 0, y: 110, width: 360, height: 10, color: new Color(0, 0, 0, 0.5) },
{ x: 290, y: 150, width: 40, height: 10, color: new Color(1, 0, 0, 1) },
{ x: 290, y: 150 + 10, width: 40, height: 10, color: new Color(0, 1, 0, 1) },
{ x: 290, y: 150 + 20, width: 40, height: 10, color: new Color(0, 0, 1, 1) },
{ x: 290 + 10, y: 150 - 10, width: 10, height: 40, color: new Color(1, 0, 0, 1) },
{ x: 290 + 20, y: 150 - 10, width: 10, height: 40, color: new Color(0, 1, 0, 1) },
{ x: 290 + 30, y: 150 - 10, width: 10, height: 40, color: new Color(0, 0, 1, 1) },
{ x: 280, y: 30, width: 5, height: 10, color: new Color(1, 1, 1, 1) },
{ x: 280 + 5, y: 30, width: 5, height: 10, color: new Color(7/8, 7/8, 7/8, 1) },
{ x: 280 + 10, y: 30, width: 5, height: 10, color: new Color(6/8, 6/8, 6/8, 1) },
{ x: 280 + 15, y: 30, width: 5, height: 10, color: new Color(5/8, 5/8, 5/8, 1) },
{ x: 280 + 20, y: 30, width: 5, height: 10, color: new Color(0.5, 0.5, 0.5, 1) },
{ x: 280 + 25, y: 30, width: 5, height: 10, color: new Color(3/8, 3/8, 3/8, 1) },
{ x: 280 + 30, y: 30, width: 5, height: 10, color: new Color(2/8, 2/8, 2/8, 1) },
{ x: 280 + 35, y: 30, width: 5, height: 10, color: new Color(1/8, 1/8, 1/8, 1) },
{ x: 280 + 40, y: 30, width: 5, height: 10, color: new Color(0, 0, 0, 1) },
]
// set up image array
const img = [];
for (let x = 0; x < 360; x++) {
img.push([])
for (let y = 0; y < 200; y++) {
img[x].push(new Color(x / 360, 0.4, y / 200));
}
}
// go through the queue and draw everything in it
for (let i = 0; i < drawQueue.length; i++) {
const p = drawQueue[i];
for (let x = p.x; x < p.x + p.width; x++) {
for (let y = p.y; y < p.y + p.height; y++) {
const destinationColor = img[x][y];
img[x][y] = calculateBlend(p.color, destinationColor);
}
}
}
let textE;
// actually go and render the image into a canvas element using p5.js
function setup() {
createCanvas(img.length, img[0].length);
background(0)
loadPixels();
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const c = img[x][y];
set(x, y, [c.r * 255, c.g * 255, c.b * 255, 255])
}
}
updatePixels();
textE = createP();
textE.style("color", '#fff')
}
function draw() {
let t = ''
if (mouseX > 0 && mouseX < width && 0 < mouseY && mouseY < height) {
const c = img[Math.floor(mouseX)][Math.floor(mouseY)];
const R = Math.floor(c.r * 255)
const G = Math.floor(c.g * 255)
const B = Math.floor(c.b * 255)
const A = Math.floor(c.a * 255)
t = `${R} ${G} ${B} ${A}`
}
textE.html(t)
}