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

Seeing SVG Filter #9

Open
9am opened this issue Sep 21, 2022 · 1 comment
Open

Seeing SVG Filter #9

9am opened this issue Sep 21, 2022 · 1 comment
Assignees
Labels
svg SVG

Comments

@9am
Copy link
Owner

9am commented Sep 21, 2022

A visualization tool that helps to understand complicated SVG filter effects.

pedal

hits

@9am
Copy link
Owner Author

9am commented Sep 21, 2022

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
<filter>
    <!-- COLOR -->
    <feFlood flood-color="#73DCFF" flood-opacity="0.75" result="COLOR-blu" />
    <feFlood flood-color="#9673FF" flood-opacity="0.4" result="COLOR-red" />
    <!-- COLOR END -->

    <!-- Texture -->
    <feTurbulence baseFrequency=".05" type="fractalNoise" numOctaves="3" seed="0" result="Texture_10" />
    <feColorMatrix type="matrix" 
    values="0 0 0 0 0,
    0 0 0 0 0,
    0 0 0 0 0,
    0 0 0 -2.1 1.1" in="Texture_10"  result="Texture_20" />

    <feColorMatrix result="Texture_30" type="matrix" 
    values="0 0 0 0 0,
    0 0 0 0 0,
    0 0 0 0 0,
    0 0 0 -1.7 1.8" in="Texture_10" />
    <!-- Texture -->

    <!-- FILL --> 
    <feOffset dx="-3" dy="4" in="SourceAlpha" result="FILL_10"/>
    <feDisplacementMap scale="17" in="FILL_10" in2="Texture_10" result="FILL_20" />
    <feComposite operator="in" in="Texture_30" in2 = "FILL_20" result="FILL_40"/>
    <feComposite operator="in" in="COLOR-blu" in2="FILL_40" result="FILL_50" />
    <!-- FILL END-->

    <!-- OUTLINE -->  
    <feMorphology operator="dilate" radius="3" in="SourceAlpha" result="OUTLINE_10" />
    <feComposite operator="out" in="OUTLINE_10" in2 = "SourceAlpha" result="OUTLINE_20" />
    <feDisplacementMap scale="7" in="OUTLINE_20" in2="Texture_10" result="OUTLINE_30" />
    <feComposite operator="arithmetic" k2="-1" k3="1" in="Texture_20" in2="OUTLINE_30" result="OUTLINE_40" />
    <!-- OUTLINE END-->

    <!-- BEVEL OUTLINE -->
    <feConvolveMatrix order="8,8" divisor="1" kernelMatrix="1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 " in="SourceAlpha" result="BEVEL_10" />
    <feMorphology operator="dilate" radius="2" in="BEVEL_10" result="BEVEL_20" />
    <feComposite operator="out" in="BEVEL_20" in2="BEVEL_10" result="BEVEL_30"/>
    <feDisplacementMap scale="7" in="BEVEL_30" in2="Texture_10" result="BEVEL_40" />
    <feComposite operator="arithmetic" k2="-1" k3="1" in="Texture_20" in2="BEVEL_40" result="BEVEL_50" />
    <feOffset dx="-7" dy="-7" in="BEVEL_50" result="BEVEL_60"/>
    <feComposite operator="out" in="BEVEL_60" in2 = "OUTLINE_10" result="BEVEL_70" />
    <!-- BEVEL OUTLINE END -->

    <!-- BEVEL FILL -->
    <feOffset dx="-9" dy="-9" in="BEVEL_10" result="BEVEL-FILL_10"/>
    <feComposite operator="out" in="BEVEL-FILL_10" in2 = "OUTLINE_10" result="BEVEL-FILL_20" />
    <feDisplacementMap scale="17" in="BEVEL-FILL_20" in2="Texture_10" result="BEVEL-FILL_30" />
    <feComposite operator="in" in="COLOR-red" in2="BEVEL-FILL_30" result="BEVEL-FILL_50" />
    <!-- BEVEL FILL END-->

    <feMerge  result="merge2">
        <feMergeNode in="BEVEL-FILL_50" />
        <feMergeNode in="BEVEL_70" />
        <feMergeNode in="FILL_50" /> 
        <feMergeNode in="OUTLINE_40" />
    </feMerge>
</filter>

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 section12)

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
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()
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

Footnotes

  1. Sophisticated Effects created with SVG Filters

  2. SVG Filter Effects: Outline Text with

@9am 9am added the svg SVG label Sep 21, 2022
@9am 9am self-assigned this Apr 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
svg SVG
Projects
None yet
Development

No branches or pull requests

1 participant