9am / 9am.github.io

9am Blog πŸ•˜ ㍑
https://9am.github.io/
MIT License
3 stars 0 forks source link

Seeing SVG Filter #9

Open 9am opened 1 year ago

9am commented 1 year ago
A visualization tool that helps to understand complicated SVG filter effects.
pedal hits
9am commented 1 year ago

I love SVG, especially the fascinating filter effects. But it's hard to learn and make some of your own effects. Usually, you learn by looking into the code made by others with the documentation. But there are 25 filter effect SVG elements, and each of them has its own attributes, not to mention the combination and composition of them.

Check out the amazing effect made by Dirk Weber, even with the comments, I have no clue about how it works. filter ```html ```

So I decide to make a small tool to help me understand the structure of the filter and how each of them works. (If you don't know what SVG filters can do, check out the further reading section[^1][^2])

The idea

  1. Take an SVG filter as input.
  2. Parse the filter to some data structure.
  3. Visualize each of the filter elements and the connection between them with canvas or SVG.

The Data Structure

First, we need to find a proper data structure to represent the filter and the connections between them.

A filter is composed of one or several SVG filter effect elements, the atomic part, which is kinda like a function in computer language. It takes inputs from the output of other effect elements, modifies the graphic, and passes the output to the next one. The output of the last effect element will be the result of this filter.

It's like the pedal board in front of a guitar player. The input signal is from the guitar, then each pedal modifies the signal to the next one. They can be arranged in different ways. Then the last pedal sends the output signal to the AMP.

pedal-2

So is it a tree? Wait, there are some elements doesn't work like a pure function. They can produce output by themselves, like feImage. So if it's a tree, they'll be the leaf nodes. The problem is that their output can be used by more than one element. So not a tree. it's a graph, to be more specific, a Directed acyclic graph.

Let's group the filter elements by the way they take inputs:

Group Thumbnail Tag Description
source βŽ•β†’ SourceGraphic``SourceAlpha
BackgroundImage``BackgroundAlpha
FillPaint``StrokePaint
Not filter element. But can be used as input for other elements.
noInput βŽ•β†’ feImage``feImage``feTurbulence Take no inputs.
withInput δΈ€βŽ•β†’ feColorMatrix``feComponentTransfer
feConvolveMatrix``feDiffuseLighting
feSpecularLighting``feDropShadow
feGaussianBlur``feMergeNode
feMorphology``feOffset``feTile
Take 1 inputs.
withInput2 δΊŒβŽ•β†’ feBlend``feComposite``feDisplacementMap Take 2 inputs.
withInputs δΈ‰βŽ•β†’ feMerge Take 1 or more inputs.

We need to implement a parseFilter function:

interface parseFilter {
    (filter: SVGFilterElement): Graph;
}
interface Graph {
    nodes: { id: string };
    links: { source: string, target: string };
}

Based on what we know about the Filter Effect Group, we can loop through the children of \<filter> to find inputs for each item. Here is the implementation:

const parseFilter = (filter) => {
    const [nodes, links] = [...filter.children].reduce(
        ([nodeMemo, linkMemo], child, i) => {
            child.id = `${child.tagName}${ID_JOIN}${i}`;
            const linkSet = getLinks(child, filter)
                .filter((link) => link)
                .reduce((memo, link) => new Set([...memo, link]), new Set());
            return [
                [...nodeMemo, child.id],
                new Set([...linkMemo, ...linkSet])
            ];
        },
        [[...FE.SOURCE].map(([feName]) => feName), new Set()]
    );
    // [[source, target]]
    const linksTuple = [...links].map((link) => link.split(LINK_JOIN));
    const nodesInFilter = new Set(linksTuple.flatMap((item) => item));
    return {
        nodes: nodes
            .filter((id) => nodesInFilter.has(id))
            .map((id) => ({ id })),
        links: linksTuple.map(([source, target]) => ({ source, target }))
    };
};
FE ```javascript const source = new Map([ ['SourceGraphic', ''], ['SourceAlpha', ''], ['BackgroundImage', ''], ['BackgroundAlpha', ''], ['FillPaint', ''], ['StrokePaint', ''] ]); const noInput = new Map([ ['feImage', 'orange'], ['feTurbulence', 'drakorange'], ['feFlood', 'orangered'] ]); const withInput2 = new Map([ ['feBlend', 'blueviolet'], ['feComposite', 'royalblue'], ['feDisplacementMap', 'darkslateblue'] ]); const withInput = new Map([ ...withInput2, ['feColorMatrix', 'blue'], ['feComponentTransfer', 'steelblue'], ['feConvolveMatrix', 'slateblue'], ['feDiffuseLighting', 'skyblue'], ['feSpecularLighting', 'lightblue'], ['feDropShadow', 'powderblue'], ['feGaussianBlur', 'midnightblue'], ['feMergeNode', 'drakblue'], ['feMorphology', 'dodgerblue'], ['feOffset', 'cadetblue'], ['feTile', 'cornflowerblue'] ]); const withInputs = new Map([['feMerge', 'yellowgreen']]); export const FE = { SOURCE: source, NO_INPUT: noInput, WITH_INPUT: withInput, WITH_INPUT2: withInput2, WITH_INPUTS: withInputs, ALL: new Map([...source, ...noInput, ...withInput, ...withInputs]) }; ```
getLink() ```javascript const getLinks = (node, container) => { if (FE.NO_INPUT.has(node.tagName)) { return [null]; } if (FE.WITH_INPUTS.has(node.tagName)) { return [...node.children].map((child) => { const inStr = child.getAttribute('in'); const source = container.querySelector(`[result="${inStr}"]`); return `${source?.id || inStr}${LINK_JOIN}${node.id}`; }); } if (FE.WITH_INPUT.has(node.tagName)) { const ins = [ getInputs(node, 'in'), FE.WITH_INPUT2.has(node.tagName) ? getInputs(node, 'in2') : null ].filter((item) => item !== undefined && item !== null); return ins.map((inStr) => { const source = container.querySelector(`[result="${inStr}"]`); return `${source?.id || inStr}${LINK_JOIN}${node.id}`; }); } throw new Error(`no links found ${node.id}`); }; const getInputs = (node, attr = 'in') => { const val = node.getAttribute(attr); if (val) { return val; } if (node.isSameNode(node.parentNode.firstElementChild)) { return 'SourceGraphic'; } const children = [...node.parentNode.children]; const index = children.findIndex((item) => item.isSameNode(node)); return children[index - 1].id; }; ```
graph-data

Edit step-1

Visualization

The Sankey graph is a perfect way to demonstrate this data structure. I choose d3-sankey because it doesn't render the graph directly, just offers the data to render. So I can take control of the details of the graph. After giving different colors to each type of filter element and adjusting the connection line between them, this is what I got:

sankey

Edit step-2

Meet seeing-svg-filter

After adding some other features, seeing-svg-filter is born. It can:

  1. Take an SVG filter from a file or paste it in.
  2. Visualize it with a Sankey graph.
  3. Hover on a node to see the inputs that compose it.
  4. Click on a node to see the detail.

sankey

Now I have the perfect tool to learn more about SVG filters. Maybe I'll try to implement some of them to understand better. And maybe move this tool further, to make another tool to easily generate a new filter effect. Hope you enjoy it, I'll see you next time.


@9am πŸ•˜

Further reading

[^1]: Sophisticated Effects created with SVG Filters [^2]: SVG Filter Effects: Outline Text with