Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alpha blending #231

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

Add alpha blending #231

wants to merge 2 commits into from

Conversation

LeaVerou
Copy link
Member

Closes #230

Currently WIP, opening a draft PR so we can iterate.

@facelessuser
Copy link
Collaborator

I don't know if it was intentional, but pre-multiplication was left out. This is actually what browsers do when overlaying colors. If this is not desired, feel free to disregard it.

If another compositing is desired in the future, additionally you would likely want to clamp the resultant alpha, but source over shouldn't yield an alpha greater than 1 or less than zero unless the input alpha were somehow out of range (I assume Colorjs.io doesn't allow this, but maybe I'm wrong).

diff --git a/src/over.js b/src/over.js
index 20b4a1e..40851b6 100644
--- a/src/over.js
+++ b/src/over.js
@@ -20,13 +20,14 @@ export default function over (source, backdrop, {
        else {
                let source_xyz = to(source, space);
                let backdrop_xyz = to(backdrop, space);
+               let alpha = source.alpha + backdrop.alpha * (1 - source.alpha)
                result = {
                        space,
-                       coords: source_xyz.map((s, i) => {
-                               let b = backdrop_xyz[i];
-                               return s + b * (1 - source.alpha);
+                       coords: source_xyz.coords.map((s, i) => {
+                               let b = backdrop_xyz.coords[i];
+                               return (s * source.alpha + b * backdrop.alpha * (1 - source.alpha)) / (alpha ? alpha : 1);
                        }),
-                       alpha: source.alpha + backdrop.alpha * (1 - source.alpha)
+                       alpha: alpha
                }
        }

Additionally, over seems to not return a Color object which I imagine was a mistake. I think it is due to how to() returns stuff, but I didn't dig much deeper than that.

Makes chaining over impossible currently.

@LeaVerou LeaVerou mentioned this pull request Oct 29, 2022
@LeaVerou
Copy link
Member Author

This is why this PR is draft :) Yes, there should absolutely be a premultiplied option, not sure whether it should be the default though. I agree with your proposed patch, but it doesn't show me the button to commit it, did you post it as a suggestion or just as a diff?

@facelessuser
Copy link
Collaborator

but it doesn't show me the button to commit it, did you post it as a suggestion or just as a diff?

I honestly don't know how to submit a diff that is auto-committable 😅. That is just a code block, sorry.

Yes, there should absolutely be a premultiplied option, not sure whether it should be the default though.

Everything I've read about compositing usually suggests premultiplying, but I see no harm in making it optional.


On a side note, there probably should be some accounting for none values as well. I imagine just translating them to zero should suffice. I wouldn't think there would be a need to preserve them across the alpha blending, but if that is desired, then I guess you'd need some additional logic to do that as well.

@LeaVerou
Copy link
Member Author

I honestly don't know how to submit a diff that is auto-committable 😅. That is just a code block, sorry.

This button:
image

Everything I've read about compositing usually suggests premultiplying, but I see no harm in making it optional.

👍🏼 @svgeesus ?

On a side note, there probably should be some accounting for none values as well.

Good point!

@svgeesus
Copy link
Member

There should be code for premultiplication, and it should be the default because CSS Color 4 requires premultiplication when interpolating non-opaque colors.

@svgeesus
Copy link
Member

On a side note, there probably should be some accounting for none values as well.

Handling interpolation with missing values is also defined in CSS Color 4

I imagine just translating them to zero should suffice.

No, because

Thus, the first stage in interpolating two colors is to classify any missing components in the input colors, and compare them to the components of the interpolation color space. If any analogous missing components are found, they will be carried forward and re-inserted in the converted color before linear interpolation takes place.

@svgeesus
Copy link
Member

@LeaVerou why is this re-implementing (a subset of) interpolation instead of calling the interpolation function?

@svgeesus why didn't you finish implementing premultiplication?? :)

if (premultiplied) {
		// not coping with polar spaces yet
		color1.coords = color1.coords.map(c => c * color1.alpha);
		color2.coords = color2.coords.map(c => c * color2.alpha);
	}

@LeaVerou
Copy link
Member Author

@svgeesus It’s implementing alpha blending, not interpolation (or at least trying to).

@facelessuser
Copy link
Collaborator

@LeaVerou @svgeesus I've had some time to revisit this to see if we can push this conversation forward.

Locally, I checked out this branch, rebased it, and made the following changes.

diff --git a/src/color.js b/src/color.js
index d78f6cf..cc25c80 100644
--- a/src/color.js
+++ b/src/color.js
@@ -17,6 +17,7 @@ import {
 	set,
 	setAll,
 	display,
+	over,
 } from "./index-fn.js";
 
 
@@ -185,6 +186,7 @@ Color.defineFunctions({
 	inGamut,
 	toGamut,
 	distance,
+	over,
 	toString: serialize,
 });
 
diff --git a/src/over.js b/src/over.js
index 22193fb..36e4874 100644
--- a/src/over.js
+++ b/src/over.js
@@ -1,12 +1,32 @@
 import getColor from "./getColor.js";
 import ColorSpace from "./space.js";
+import * as util from "./util.js";
 import xyz_d65 from "./spaces/xyz-d65.js";
 import to from "./to.js";
 
+function porterDuffSourceOver (cba, csa) {
+	// The normal way in which two colors are overlaid.
+
+	let fa = 1;
+	let fb = 1 - csa;
+
+	return {
+		co: (cb, cs) => {
+			return csa * fa * cs + cba * fb * cb;
+		},
+		ao: () => {
+			return csa * fa + cba * fb;
+		}
+	}
+}
+
+
 export default function over (source, backdrop, {
 	space = xyz_d65,
 	outputSpace = source.space,
 } = {}) {
+	// Colors should be an RGB like space.
+
 	source = getColor(source);
 	backdrop = getColor(backdrop);
 
@@ -17,31 +37,32 @@ export default function over (source, backdrop, {
 		throw new Error("Compositing in polar color spaces is not supported.");
 	}
 
-	let result;
-
-	if (source.alpha === 0) {
-		result = backdrop;
-	}
-	else if (source.alpha === 1 || backdrop.alpha === 0) {
-		result = source;
-	}
-	else {
-		let source_xyz = to(source, space);
-		let backdrop_xyz = to(backdrop, space);
-
-		result = {
-			space,
-			coords: source_xyz.map((s, i) => {
-				let b = backdrop_xyz[i];
-				return s + b * (1 - source.alpha);
-			}),
-			alpha: source.alpha + backdrop.alpha * (1 - source.alpha)
-		}
-	}
+	// This could be changed to use other methods if desired.
+	let compositor = porterDuffSourceOver(backdrop.alpha, source.alpha);
+	let cra = util.clamp(0.0, compositor.ao(), 1.0);
+	let source_xyz = to(source, space);
+	let backdrop_xyz = to(backdrop, space).coords;
+	let result = {
+		space,
+		coords: source_xyz.coords.map((s, i) => {
+			// Only normal blending is supported, so the result is the source.
+			// A blend function for different blend modes could be used.
+			// ```
+			// let cr = util.clamp(0.0, blend(backdrop_xyz[i], s), 1.0);
+			// ```
+			// The result of the mixing formula should be clamped, per
+			// https://www.w3.org/TR/compositing/#blending
+			let cr = util.clamp(0.0, s, 1.0);
+			// Apply compositing
+			cr = compositor.co(backdrop_xyz[i], cr);
+			if (cra !== 0 || cra !== 1) {
+				cr /= cra;
+			}
+			// Alpha blended color
+			return cr;
+		}),
+		alpha: cra
+	};
 
 	return to(result, outputSpace);
 }
-
-export function register (Color) {
-	Color.defineFunction("over", over, {returns: "color"});
-}
\ No newline at end of file

This should give results like what I have here, but we are only implementing source over compositing with normal blending, this provides simple alpha blending. This is expected to be performed in RGB gamuts. This would give results like I have here: https://facelessuser.github.io/coloraide/compositing/.

I'm willing run with this if this is what is desired, if not , we would need to clearly define what is wanted for me to push this forward. I've left it open to providing other blend modes and even other compositing approaches, but realize none of that may be wanted, but it is written in such a way that it could be if desired in the future.

Anyways, at the very least, hopefully, this can get discussions moving again.

@facelessuser
Copy link
Collaborator

I assume it may be reasonable to enforce gamut mapping of colors entering the function as well. I assume if this was extended beyond RGB spaces, then some of the channel clamping rules may not apply in those scenarios. Also, if expanded beyond RGB gamuts, not all blending modes and composition operators would make sense, but as mentioned earlier, these may not be desired.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add alpha blending
3 participants