SVG Effect - Shiny Metal

I’ve been playing around a bit with SVG recently as I’ve worked on my SVG CSS Backgrounds Generator.

One idea I had was of “layers” of material that could catch the light independently:

Here’s how I created the effect.

Libraries

I’m using svg.js and svg.filters.js to generate SVG on-the-fly from a little bit of Javascript. Svg.js provides an API for creating and manipulating SVG, and adding it into the DOM. Svg.filters.js adds in support for SVG filters, which I’m using to generate the drop shadows.

Markup

The markup here is very simply, just a div that will hold the SVG element:

<style>
#svg-container {
    width: 640px;
    height: 240px;
}
</style>

<div id="svg-container"></div>

In real life you probably wouldn’t hard-code values like that. This is just a demo. There are going to be plenty more arbitrary constants in this demo…

Basic SVG generation

My script loads once jQuery is ready and creates the SVG drawing element based one some hard-coded sizes. I allow a little bit of space all around to display the drop shadows a bit more nicely.


$(function() {

    const offset = 20;
    const blockSize = 20;
    const widthInBlocks = 30;
    const heightInBlocks = 10;

    const draw = SVG().addTo('#svg-container').size(
        widthInBlocks * blockSize + offset * 2,
        heightInBlocks * blockSize + offset * 2
    );

    ...

});

Adding paths

There are three layers in this demo, each one being an SVG <path> element. I iterate the blocks in the display and randomly assign each block to a layer. When drawing a block, I append an absolute “move” command (capital M) followed by four relative drawing commands (lowercase h and v commands).

    let paths = ['', '', ''];

    for (let by = 0; by < heightInBlocks; ++by) {
        for (let bx = 0; bx < widthInBlocks; ++bx) {
            let pathIndex = 0;
            const rnd = Math.random();
            const edgeCutoff = 2;
            const isEdge = (bx < edgeCutoff) || (by < edgeCutoff) || (bx >= widthInBlocks-edgeCutoff) || (by >= heightInBlocks-edgeCutoff);

            if (rnd < 0.333) {
                pathIndex = 0;
            }
            else if (rnd < 0.66) {
                pathIndex = 1;
            } else {
                pathIndex = 2;
            }

            if (isEdge && Math.random() < 0.4) continue;

            paths[pathIndex] += ` M${bx*blockSize+offset},${by*blockSize+offset} h${blockSize} v${blockSize} h${-blockSize} v${-blockSize}`;
        }
    }

I wanted gaps at the edges of the rectangle, so there is an extra check to see if blocks in the outer two rows or columns should be skipped.

Styling the blocks

At this point, the blocks are not styled in any way and won’t be visible. There are three components I need to add now:

  • Shading/fill color - this is actually a linear gradient arranged horitontally. Most of the linear gradient is the basic color of the layer, but there is a small section in the middle that shaded to a lighter color.
  • Each layer has a drop-shadow filter applied, this gives the visual appearance of the layers being at different heights.
  • An animation transform is applied to each layer’s linear gradient to animate it from side to side. The timing and offset for each layer is slightly different to give the appearance of the light catching the different layer heights at different times.
    let colors     = ['#262e64', '#414b90', '#676fa8'];
    let highlights = ['#9ca1bf', '#9ca0bf', '#dadbe1']

    for (let pi = 0; pi < paths.length; ++pi) {
        let path = draw.path(paths[pi]);
        const color = colors[pi];
        const highlight = highlights[pi];
        const spread = (3 - pi) * 0.05;
        const gradient = draw.gradient('linear', function(add) {
            add.stop(0, color);
            add.stop(0.5-spread, color);
            add.stop(0.5, highlight);
            add.stop(0.5+spread, color);
            add.stop(1, color);
        });

        gradient.attr({gradientUnits: 'userSpaceOnUse'});
        gradient.attr({gradientTransform: 'rotate(15)'});

        const offset = -30 * pi;
        const animate = SVG(`<animateTransform attributeName="gradientTransform" attributeType="XML" type="translate" values="${offset-500},0; ${-offset+500},0" repeatCount="indefinite" dur="4s" additive="sum" />`);
        gradient.add(animate);

        path.fill(gradient);

        path.filterWith(function(add) {
            const blur = add.offset(0, 2).in(add.$sourceAlpha).gaussianBlur(2);

            add.blend(add.$source, blur)
        });
    }

(Unfortunately I couldn’t find a way of creating the <animateTransform> element using svg.js, so I had to drop down to raw SVG XML.)

Final thoughts

I think it turned out quite well – a pretty simple but striking effect. The main downside of this approach is that it’s pretty CPU-heavy. If you were using this for real you’d probably want to either reduce the size of the graphic, or only run the animation once.

You’re welcome to use the code presented on this page in your own projects. (I’d recommend cleaning it up a bit first however…)

Published: Monday, August 10, 2020

Pinterest
Reddit
Hacker News

You may be interested in...

Hackification.io is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com. I may earn a small commission for my endorsement, recommendation, testimonial, and/or link to any products or services from this website.

Comments? Questions?