@svgdotjs/svg.filter.js
Version:
A plugin for svg.js adding filter functionality
458 lines (403 loc) • 12.1 kB
JavaScript
import {
Array as SVGArray,
Container,
Defs,
Element,
extend,
find,
namespaces as ns,
nodeOrNew,
utils,
wrapWithAttrCheck
} from '@svgdotjs/svg.js'
export default class Filter extends Element {
constructor (node) {
super(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 find('svg [filter*="' + this.id() + '"]')
}
toString () {
return 'url(#' + this.id() + ')'
}
}
// Create Effect class
class Effect extends 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 SVGArray(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, ns.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 = utils.capitalize(effect)
const fn = updateFunctions[effect]
Filter[name + 'Effect'] = class extends Effect {
constructor (node) {
super(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] = 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
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 = utils.capitalize(child)
Filter[name] = class extends Effect {
constructor (node) {
super(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[utils.capitalize(c)]
const fn = 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[utils.capitalize(light)]
const fn = wrapWithAttrCheck(function () {
return this.put(new _class())
})
Filter.DiffuseLightingEffect.prototype[light] = fn
Filter.SpecularLightingEffect.prototype[light] = fn
})
extend(Filter.MergeEffect, {
mergeNode (_in) {
return this.put(new Filter.MergeNode()).attr('in', _in)
}
})
// add .filter function
extend(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
}
})
extend(Container, {
// Define filter on defs
filter: function (block) {
return this.defs().filter(block)
}
})
extend(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)
}
}
extend(Effect, chainingEffects)
// Effect-specific extensions
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
}
})
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]
}