samizdatco / skia-canvas

A GPU-accelerated 2D graphics environment for Node.js
MIT License
1.67k stars 63 forks source link

Applying an SVG filter to the canvas #157

Open velara3 opened 7 months ago

velara3 commented 7 months ago

The documentation says, "Skia canvas supports the full set of CSS filter image processing operators"

In the examples I've seen applying a filter is as simple as setting the filter property:

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

ctx.filter = "blur(4px)";

The CSS filter documentation mentions the URL() filter:

A CSS url(). Takes an IRI pointing to an SVG filter element, which may be embedded in an external XML file.

How would I do the same in Skia Canvas?

Could it be embedded? Will it load an external file?

Embedded:

canvas.filter = url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E");

External:

canvas.filter = url("filter.svg#hueRotate");

// filter.svg: 
<?xml version="1.0" standalone="no"?>
<svg width="1" height="1" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
    <filter id="hueRotate">
      <feColorMatrix type="hueRotate" values="270"/>
    </filter>
  </defs>
</svg>

I realize now I can probably test this. However, I didn't find any examples in the documentation.

mpaperno commented 7 months ago

There is currently no support for any SVG features at all, nor is there anything to handle url() types. The filters currently parsed can be found in the code here, for what it's worth... which I think is all of them except url().

Skia itself must support it, but I have no idea about skia-safe (the intermediate library layer used by skia-canvas), or what would be involved in implementing it. Just being able to load SVGs natively would be a great start.

velara3 commented 7 months ago

It looks like Skia itself has SVG support. I couldn't find a library with the term skia safe but I see the import statements in the code and a few links to Rust Skia.

I don't know that I know how to implement it either. It would be great to have.

Basically, I'm trying to apply filters to canvas elements server side and the filters Skia Canvas support are the CSS filters (except URL()) but they are somewhat primitive if IIUC.

The URL() CSS filter would allow SVG filters which seem to offer much wider range.

Although, if it was possible, having a way to apply multiple filters in code might solve the issue. Something like:

var filters = [];
var dropShadow = new DropShadow(3,3,1, rgba(0,0,0,.5));
filters.push(dropShadow);
var blur = new Blur(3,6);
filters.push(blur);
var displacementMap = new DisplacementMap();
filters.push(displacementMap);
canvas.filters = filters;
mpaperno commented 7 months ago

Yea, sorry, "Rust Skia" is what I meant (it's called "skia safe" in the Rust package manager for some reason). And from what I understand it support most, if not all, Skia features. But skia-canvas only implements a subset. There's PR #155 which would probably be a good place to start on updating.

Perhaps I misunderstand what you mean, but multiple filters can be added now like in the standard, by separating the filter functions with spaces in the ctx.filter = call, eg. ctx.filter = "contrast(1.4) sepia(1) drop-shadow(-9px 9px 3px #e81)".

Cheers, -Max

velara3 commented 7 months ago

I'm aware of adding multiple filters inline. What I meant by adding multiple filters manually was to do it longhand in JavaScript code rather than by string values. So instead of:

ctx.filter = "contrast(1.4) sepia(1) drop-shadow(-9px 9px 3px #e81)";

something like this:

var filters = [];
var aFoundationalFilter = new FoundationalFilter();
aFoundationalFilter.matrix = new Matrix(0,0,1,0,0,1);
aFoundationalFilter.offsetX = 10;
aFoundationalFilter.offsetY = 10;
filters.push(aFoundationalFilter);
var compositeFilter = new CompositeFilter();
compositeFilter.valueA = 20;
compositeFilter.valueB = 20;
filters.push(compositeFilter);
ctx.applyFilters(filters);

I'd rather create the filter objects and set values on them if that makes sense. Some of my past experience in image data filters make sense and some areas I've still got more to learn.

mpaperno commented 7 months ago

Well there's no "filter object type" in standard JS that I know of, and the context filter property only takes a string, so.... to be more imperative about adding filters like you suggest, you'd need to add those features somehow. Even if there already was a applyFilters() convenience method, currently it would still just be parsing filter strings, presumably... so I'm not sure what the benefit would be.

You could have your filter objects output a string as a result, collect them into an array (eg. filters.push(compositeFilter.toString())), then simply ctx.filter = filters.join(' ').

That's actually almost exactly what I've done in some code where the app's users can add arbitrary canvas filters as image layers.

mpaperno commented 7 months ago

Totally OT now, but if there was a built-in way to track added filters so that they can later be reset automatically, w/out having to specify the filter again with "no op" values (eg. blur(0)), that would be pretty slick. For example when one wants to apply filter(s) to a particular drawing layer/path within a larger composition. Can be added on top as a JS layer of course, but having that in the binary core would likely be a lot more efficient.

velara3 commented 6 months ago

Indeed. One would hope that there would be class references for all the HTML and SVG markup elements that exist. So that they could be applied and modified like you describe. I have to pass on this library for now but will check back periodically for this support. For now, I believe my only option is to try client side.

velara3 commented 6 months ago

If there's anything I can do to help push this along (besides writing the code - I'm unable to contribute at this moment) let me know.