two.js
two.js copied to clipboard
[Enhancement] Using <mask> instead of <clipPath>
Is your feature request related to a problem? Please describe.
<mask>
allows you to do more things than <clipPath>
. It can be used even to create an eraser functionality.
Describe the solution you'd like
Change the .mask
property to .clipPath
.
.mask
will be used to create <mask>
tag.
It will be possible to specify a group or path that will be added to the <defs>
.
Describe alternatives you've considered More complex algorithms can be used to create the eraser, but they will be slower and require more code.
Additional context Below the parts of code that I have used for SVG.
Properties:
Object.defineProperty(object, 'eraserMask', {
enumerable: true,
get: function() {
return this._eraserMask;
},
set: function(v) {
this._eraserMask = v;
this._flagEraserMask = true;
if (!v.eraser) {
v.eraser = true;
}
}
Object.defineProperty(object, 'eraser', {
enumerable: true,
get: function() {
return this._eraser;
},
set: function(v) {
this._eraser = v;
this._flagEraser = true;
}
});
Init:
_eraserMask: null,
_eraser: false,
_flagEraser: false,
Reset:
this._flagVertices = this._flagLength = this._flagFill = this._flagStroke =
this._flagLinewidth = this._flagOpacity = this._flagVisible =
this._flagCap = this._flagJoin = this._flagMiter =
this._flagClip = this._flagEraser = false;
this._flagValue = this._flagFamily = this._flagSize =
this._flagLeading = this._flagAlignment = this._flagFill =
this._flagStroke = this._flagLinewidth = this._flagOpacity =
this._flagVisible = this._flagClip = this._flagEraser = this._flagDecoration =
this._flagClassName = this._flagBaseline = this._flagWeight =
this._flagStyle = false;
Create the
getEraserMask: function(shape, domElement) {
var eraserMask = shape._renderer.eraserMask;
if (!eraserMask) {
eraserMask = shape._renderer.eraserMask = svg.createElement('mask');
eraserMask.setAttribute("maskUnits", "userSpaceOnUse");
domElement.defs.appendChild(eraserMask);
}
return eraserMask;
},
Some checks
if (!tag || /(radial|linear)gradient/i.test(tag) || object._clip || object._eraser) {
return;
}
if (object._clip || object._eraser) {
return;
}
Main code:
if (this._flagEraser) {
var eraserMask = svg.getEraserMask(this, domElement);
var elem = this._renderer.elem;
if (this._eraser) {
elem.removeAttribute('id');
eraserMask.setAttribute('id', this.id);
eraserMask.appendChild(elem);
} else {
eraserMask.removeAttribute('id');
elem.setAttribute('id', this.id);
this.parent._renderer.elem.appendChild(elem); // TODO: should be insertBefore
}
}
if (this._flagEraserMask) {
if (this._eraserMask) {
svg[this._eraserMask._renderer.type].render.call(this._eraserMask, domElement);
this._renderer.elem.setAttribute('mask', 'url(#' + this._eraserMask.id + ')');
}
else {
this._renderer.elem.removeAttribute('mask');
}
}
I need to say that the idea with the eraser needs some work, because of it is necessary to create a new <mask>
and parent group every time you erase something or make double drawing.
Super cool. This is a great idea. The reason I haven't implemented <mask />
usage so far is because there isn't a way to achieve the same effect (that I researched) in Canvas 2D. Any ideas of how we might go about that?
@jonobr1 I found something, but here you have to change the transparency, not the luminance.
context.globalCompositeOperation = "destination-out"; //"xor" also can be used
context.strokeStyle = "rgba(0, 0, 0, 1.0)";
Most likely this formula is used in SVG mask for transparency:
const alpha = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
with only gray colors this formula becomes more simple:
const alpha = red; //same as "alpha = green" and "alpha = blue"
links: https://developer.mozilla.org/en-US/docs/Web/CSS/mask-type - don't use it, just read https://en.wikipedia.org/wiki/Relative_luminance
Example of computing alpha with getImageData
:
https://jsfiddle.net/c73fLkzn/
for (let i = 0; i < data.length; i += 4) {
const red = data[i];
const green = data[i + 1];
const blue = data[i + 2];
const alpha = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
data[i + 3] = alpha;
}
Perhaps someone can come up with a more fast code, but I don't see this yet.
Well, I figured out the problem withglobalCompositeOperation
.
Itersections will change transparency:
http://jsfiddle.net/7gkdn6ha/1/
But converting colors to alpha with getImageData
as described above solves the problem.
Thanks for exploring this. This is super helpful! I think I can add this once I'm done with the ES6 branch. I'll add this to the milestones
Workers can be used to process ImageData asynchronously, and WebAssembly can be used for speedup. Some useful links:
C++ code for the Worker function:
void updateAlpha(unsigned char* data, int len) {
for (int i = 0; i < len; i += 4) {
int red = data[i];
int green = data[i + 1];
int blue = data[i + 2];
int alpha = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
data[i + 3] = alpha;
}
}
Optimized version:
void updateAlpha(unsigned char* data, int len) {
int i = 0;
while (i < len) {
int red = data[i++];
int green = data[i++];
int blue = data[i++];
int alpha = (2126 * red + 7152 * green + 722 * blue) / 10000;
data[i++] = alpha;
}
}
Optimized and compact version:
void updateAlpha(unsigned char* data, int len) {
int i = 0;
while (i < len) {
data[i++] = (2126 * data[i++] + 7152 * data[i++] + 722 * data[i++]) / 10000;
}
}
@jonobr1 Frankly, it is worth considering abandoning Canvas 2D altogether, as it is an obsolete technology that slows down progress. Such useful effects as glow, blur and shadows will also work faster in WebGL than in Canvas 2D.
Thanks for the input and resources. The article about image styling and filters in WebAssembly is particularly helpful!