@svgdotjs/svg.filter.js
Version:
A plugin for svg.js adding filter functionality
394 lines (368 loc) • 12.5 kB
JavaScript
/*!
* @svgdotjs/svg.filter.js - A plugin for svg.js adding filter functionality
* @version 3.0.9
* https://github.com/svgdotjs/svg.filter.js
*
* @copyright Wout Fierens
* @license MIT
*
* BUILT: Mon Feb 24 2025 17:15:33 GMT+0100 (Central European Standard Time)
*/;
'use strict';
var svg_js = require('@svgdotjs/svg.js');
class Filter extends svg_js.Element {
constructor(node) {
super(svg_js.nodeOrNew('filter', node), node);
this.$source = 'SourceGraphic';
this.$sourceAlpha = 'SourceAlpha';
this.$background = 'BackgroundImage';
this.$backgroundAlpha = 'BackgroundAlpha';
this.$fill = 'FillPaint';
this.$stroke = 'StrokePaint';
this.$autoSetIn = true;
}
put(element, i) {
element = super.put(element, i);
if (!element.attr('in') && this.$autoSetIn) {
element.attr('in', this.$source);
}
if (!element.attr('result')) {
element.attr('result', element.id());
}
return element;
}
// Unmask all masked elements and remove itself
remove() {
// unmask all targets
this.targets().each('unfilter');
// remove mask from parent
return super.remove();
}
targets() {
return svg_js.find('svg [filter*="' + this.id() + '"]');
}
toString() {
return 'url(#' + this.id() + ')';
}
}
// Create Effect class
class Effect extends svg_js.Element {
constructor(node, attr) {
super(node, attr);
this.result(this.id());
}
in(effect) {
// Act as getter
if (effect == null) {
const _in = this.attr('in');
const ref = this.parent() && this.parent().find(`[result="${_in}"]`)[0];
return ref || _in;
}
// Avr as setter
return this.attr('in', effect);
}
// Named result
result(result) {
return this.attr('result', result);
}
// Stringification
toString() {
return this.result();
}
}
// This function takes an array with attr keys and sets for every key the
// attribute to the value of one paramater
// getAttrSetter(['a', 'b']) becomes this.attr({a: param1, b: param2})
const getAttrSetter = params => {
return function (...args) {
for (let i = params.length; i--;) {
if (args[i] != null) {
this.attr(params[i], args[i]);
}
}
};
};
const updateFunctions = {
blend: getAttrSetter(['in', 'in2', 'mode']),
// ColorMatrix effect
colorMatrix: getAttrSetter(['type', 'values']),
// Composite effect
composite: getAttrSetter(['in', 'in2', 'operator']),
// ConvolveMatrix effect
convolveMatrix: function (matrix) {
matrix = new svg_js.Array(matrix).toString();
this.attr({
order: Math.sqrt(matrix.split(' ').length),
kernelMatrix: matrix
});
},
// DiffuseLighting effect
diffuseLighting: getAttrSetter(['surfaceScale', 'lightingColor', 'diffuseConstant', 'kernelUnitLength']),
// DisplacementMap effect
displacementMap: getAttrSetter(['in', 'in2', 'scale', 'xChannelSelector', 'yChannelSelector']),
// DropShadow effect
dropShadow: getAttrSetter(['in', 'dx', 'dy', 'stdDeviation']),
// Flood effect
flood: getAttrSetter(['flood-color', 'flood-opacity']),
// Gaussian Blur effect
gaussianBlur: function (x = 0, y = x) {
this.attr('stdDeviation', x + ' ' + y);
},
// Image effect
image: function (src) {
this.attr('href', src, svg_js.namespaces.xlink);
},
// Morphology effect
morphology: getAttrSetter(['operator', 'radius']),
// Offset effect
offset: getAttrSetter(['dx', 'dy']),
// SpecularLighting effect
specularLighting: getAttrSetter(['surfaceScale', 'lightingColor', 'diffuseConstant', 'specularExponent', 'kernelUnitLength']),
// Tile effect
tile: getAttrSetter([]),
// Turbulence effect
turbulence: getAttrSetter(['baseFrequency', 'numOctaves', 'seed', 'stitchTiles', 'type'])
};
const filterNames = ['blend', 'colorMatrix', 'componentTransfer', 'composite', 'convolveMatrix', 'diffuseLighting', 'displacementMap', 'dropShadow', 'flood', 'gaussianBlur', 'image', 'merge', 'morphology', 'offset', 'specularLighting', 'tile', 'turbulence'];
// For every filter create a class
filterNames.forEach(effect => {
const name = svg_js.utils.capitalize(effect);
const fn = updateFunctions[effect];
Filter[name + 'Effect'] = class extends Effect {
constructor(node) {
super(svg_js.nodeOrNew('fe' + name, node), node);
}
// This function takes all parameters from the factory call
// and updates the attributes according to the updateFunctions
update(args) {
fn.apply(this, args);
return this;
}
};
// Add factory function to filter
// Allow to pass a function or object
// The attr object is catched from "wrapWithAttrCheck"
Filter.prototype[effect] = svg_js.wrapWithAttrCheck(function (fn, ...args) {
const effect = new Filter[name + 'Effect']();
if (fn == null) return this.put(effect);
// For Effects which can take children, a function is allowed
if (typeof fn === 'function') {
fn.call(effect, effect);
} else {
// In case it is not a function, add it to arguments
args.unshift(fn);
}
return this.put(effect).update(args);
});
});
// Correct factories which are not that simple
svg_js.extend(Filter, {
merge(arrayOrFn) {
const node = this.put(new Filter.MergeEffect());
// If a function was passed, execute it
// That makes stuff like this possible:
// filter.merge((mergeEffect) => mergeEffect.mergeNode(in))
if (typeof arrayOrFn === 'function') {
arrayOrFn.call(node, node);
return node;
}
// Check if first child is an array, otherwise use arguments as array
const children = arrayOrFn instanceof Array ? arrayOrFn : [...arguments];
children.forEach(child => {
if (child instanceof Filter.MergeNode) {
node.put(child);
} else {
node.mergeNode(child);
}
});
return node;
},
componentTransfer(components = {}) {
const node = this.put(new Filter.ComponentTransferEffect());
if (typeof components === 'function') {
components.call(node, node);
return node;
}
// If no component is set, we use the given object for all components
if (!components.r && !components.g && !components.b && !components.a) {
const temp = components;
components = {
r: temp,
g: temp,
b: temp,
a: temp
};
}
for (const c in components) {
// components[c] has to hold an attributes object
node.add(new Filter['Func' + c.toUpperCase()](components[c]));
}
return node;
}
});
const filterChildNodes = ['distantLight', 'pointLight', 'spotLight', 'mergeNode', 'FuncR', 'FuncG', 'FuncB', 'FuncA'];
filterChildNodes.forEach(child => {
const name = svg_js.utils.capitalize(child);
Filter[name] = class extends Effect {
constructor(node) {
super(svg_js.nodeOrNew('fe' + name, node), node);
}
};
});
const componentFuncs = ['funcR', 'funcG', 'funcB', 'funcA'];
// Add an update function for componentTransfer-children
componentFuncs.forEach(function (c) {
const _class = Filter[svg_js.utils.capitalize(c)];
const fn = svg_js.wrapWithAttrCheck(function () {
return this.put(new _class());
});
Filter.ComponentTransferEffect.prototype[c] = fn;
});
const lights = ['distantLight', 'pointLight', 'spotLight'];
// Add light sources factories to lightining effects
lights.forEach(light => {
const _class = Filter[svg_js.utils.capitalize(light)];
const fn = svg_js.wrapWithAttrCheck(function () {
return this.put(new _class());
});
Filter.DiffuseLightingEffect.prototype[light] = fn;
Filter.SpecularLightingEffect.prototype[light] = fn;
});
svg_js.extend(Filter.MergeEffect, {
mergeNode(_in) {
return this.put(new Filter.MergeNode()).attr('in', _in);
}
});
// add .filter function
svg_js.extend(svg_js.Defs, {
// Define filter
filter: function (block) {
const filter = this.put(new Filter());
/* invoke passed block */
if (typeof block === 'function') {
block.call(filter, filter);
}
return filter;
}
});
svg_js.extend(svg_js.Container, {
// Define filter on defs
filter: function (block) {
return this.defs().filter(block);
}
});
svg_js.extend(svg_js.Element, {
// Create filter element in defs and store reference
filterWith: function (block) {
const filter = block instanceof Filter ? block : this.defs().filter(block);
return this.attr('filter', filter);
},
// Remove filter
unfilter: function (remove) {
/* remove filter attribute */
return this.attr('filter', null);
},
filterer() {
return this.reference('filter');
}
});
// chaining
const chainingEffects = {
// Blend effect
blend: function (in2, mode) {
return this.parent() && this.parent().blend(this, in2, mode); // pass this as the first input
},
// ColorMatrix effect
colorMatrix: function (type, values) {
return this.parent() && this.parent().colorMatrix(type, values).in(this);
},
// ComponentTransfer effect
componentTransfer: function (components) {
return this.parent() && this.parent().componentTransfer(components).in(this);
},
// Composite effect
composite: function (in2, operator) {
return this.parent() && this.parent().composite(this, in2, operator); // pass this as the first input
},
// ConvolveMatrix effect
convolveMatrix: function (matrix) {
return this.parent() && this.parent().convolveMatrix(matrix).in(this);
},
// DiffuseLighting effect
diffuseLighting: function (surfaceScale, lightingColor, diffuseConstant, kernelUnitLength) {
return this.parent() && this.parent().diffuseLighting(surfaceScale, diffuseConstant, kernelUnitLength).in(this);
},
// DisplacementMap effect
displacementMap: function (in2, scale, xChannelSelector, yChannelSelector) {
return this.parent() && this.parent().displacementMap(this, in2, scale, xChannelSelector, yChannelSelector); // pass this as the first input
},
// DisplacementMap effect
dropShadow: function (x, y, stdDeviation) {
return this.parent() && this.parent().dropShadow(this, x, y, stdDeviation).in(this); // pass this as the first input
},
// Flood effect
flood: function (color, opacity) {
return this.parent() && this.parent().flood(color, opacity); // this effect dont have inputs
},
// Gaussian Blur effect
gaussianBlur: function (x, y) {
return this.parent() && this.parent().gaussianBlur(x, y).in(this);
},
// Image effect
image: function (src) {
return this.parent() && this.parent().image(src); // this effect dont have inputs
},
// Merge effect
merge: function (arg) {
arg = arg instanceof Array ? arg : [...arg];
return this.parent() && this.parent().merge(this, ...arg); // pass this as the first argument
},
// Morphology effect
morphology: function (operator, radius) {
return this.parent() && this.parent().morphology(operator, radius).in(this);
},
// Offset effect
offset: function (dx, dy) {
return this.parent() && this.parent().offset(dx, dy).in(this);
},
// SpecularLighting effect
specularLighting: function (surfaceScale, lightingColor, diffuseConstant, specularExponent, kernelUnitLength) {
return this.parent() && this.parent().specularLighting(surfaceScale, diffuseConstant, specularExponent, kernelUnitLength).in(this);
},
// Tile effect
tile: function () {
return this.parent() && this.parent().tile().in(this);
},
// Turbulence effect
turbulence: function (baseFrequency, numOctaves, seed, stitchTiles, type) {
return this.parent() && this.parent().turbulence(baseFrequency, numOctaves, seed, stitchTiles, type).in(this);
}
};
svg_js.extend(Effect, chainingEffects);
// Effect-specific extensions
svg_js.extend(Filter.MergeEffect, {
in: function (effect) {
if (effect instanceof Filter.MergeNode) {
this.add(effect, 0);
} else {
this.add(new Filter.MergeNode().in(effect), 0);
}
return this;
}
});
svg_js.extend([Filter.CompositeEffect, Filter.BlendEffect, Filter.DisplacementMapEffect], {
in2: function (effect) {
if (effect == null) {
const in2 = this.attr('in2');
const ref = this.parent() && this.parent().find(`[result="${in2}"]`)[0];
return ref || in2;
}
return this.attr('in2', effect);
}
});
// Presets
Filter.filter = {
sepiatone: [0.343, 0.669, 0.119, 0, 0, 0.249, 0.626, 0.130, 0, 0, 0.172, 0.334, 0.111, 0, 0, 0.000, 0.000, 0.000, 1, 0]
};
module.exports = Filter;
//# sourceMappingURL=svg.filter.node.cjs.map