jonobr1 / two.js

A renderer agnostic two-dimensional drawing api for the web.
https://two.js.org
MIT License
8.29k stars 454 forks source link

[Enhancement] Using <mask> instead of <clipPath> #563

Open Filyus opened 2 years ago

Filyus commented 2 years ago

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 tag inside of <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 tag

  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');
        }
      }
Filyus commented 2 years ago

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.

jonobr1 commented 2 years ago

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?

Filyus commented 2 years ago

@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)";

http://jsfiddle.net/FGcrq/1/

Filyus commented 2 years ago

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

Filyus commented 2 years ago

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.

Filyus commented 2 years ago

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.

jonobr1 commented 2 years ago

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

Filyus commented 2 years ago

Workers can be used to process ImageData asynchronously, and WebAssembly can be used for speedup. Some useful links:

Filyus commented 2 years ago

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;
  }
}
Filyus commented 2 years ago

@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.

jonobr1 commented 2 years ago

Thanks for the input and resources. The article about image styling and filters in WebAssembly is particularly helpful!