two.js icon indicating copy to clipboard operation
two.js copied to clipboard

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

Open Filyus opened this issue 3 years ago • 11 comments

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 avatar Oct 22 '21 09:10 Filyus

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.

Filyus avatar Oct 22 '21 14:10 Filyus

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 avatar Oct 22 '21 19:10 jonobr1

@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 avatar Oct 22 '21 19:10 Filyus

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 avatar Oct 23 '21 09:10 Filyus

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 avatar Oct 23 '21 09:10 Filyus

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.

Filyus avatar Oct 23 '21 09:10 Filyus

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

jonobr1 avatar Oct 26 '21 17:10 jonobr1

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

Filyus avatar Dec 07 '21 07:12 Filyus

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 avatar Dec 07 '21 08:12 Filyus

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

Filyus avatar Dec 07 '21 11:12 Filyus

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

jonobr1 avatar Dec 07 '21 18:12 jonobr1