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

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

Open
Filyus opened this issue Oct 22, 2021 · 11 comments
Open

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

Filyus opened this issue Oct 22, 2021 · 11 comments
Assignees
Milestone

Comments

@Filyus
Copy link

Filyus commented Oct 22, 2021

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
Copy link
Author

Filyus commented Oct 22, 2021

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
Copy link
Owner

jonobr1 commented Oct 22, 2021

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
Copy link
Author

Filyus commented Oct 22, 2021

@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
Copy link
Author

Filyus commented Oct 23, 2021

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
Copy link
Author

Filyus commented Oct 23, 2021

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
Copy link
Author

Filyus commented Oct 23, 2021

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
Copy link
Owner

jonobr1 commented Oct 26, 2021

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 jonobr1 self-assigned this Oct 26, 2021
@jonobr1 jonobr1 added this to the v0.8.0 milestone Oct 26, 2021
@Filyus
Copy link
Author

Filyus commented Dec 7, 2021

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

@Filyus
Copy link
Author

Filyus commented Dec 7, 2021

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
Copy link
Author

Filyus commented Dec 7, 2021

@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
Copy link
Owner

jonobr1 commented Dec 7, 2021

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

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

No branches or pull requests

2 participants