Adventures in SVG


Introduction

I'm a big fan of the SVG image format in web applications. They allow developers to resize images without the loss of quality you'll get when doing the same with raster images. Not only that, but the XML that makes up an SVG image can be added to the DOM allowing elements of the SVG can be addressed in the same way as other HTML elements within the DOM. Once the SVG has been added to the DOM in this way, it's possible to manipulate it via Javascript and CSS, creating interesting animations.

Seeing Stars

Take this SVG image as an example. The image was created using Inkscape with two separate layers. The first layer contains the text "PRESENTING" which is arched using the text on path method of curving texts.


Next I added a new layer to the image, gave it the name "stars" and placed varying sizes of the star image randomly on the layer. This can be seen in the screenshot below where I changed the star colour to black so that they're easier to see.



Codeflow
the "stars" layer with multiple images in Inkscape


Once the SVG image has been saved, it's now ready to embed in a webpage but just using an <image> tag won't allow you use it in the way you want. Instead we need to wait until the page renders and then fetch the image via a simple rest call. In this example I use the Axios library to make that call.


  // The PresentingAnimator class is explained later
  const presentingAnimator = new PresentingAnimator();
  const initialized = useRef(false);

  useEffect(() => {
    if (!initialized.current) {
      fetchPresentingSVG();
      initialized.current = true;
      presentingAnimator.animate();
    }
  }, []);

  async function fetchPresentingSVG() {
    const resp = await axios.get("./dotspot/svg/presenting.svg");
    var anchor = document.getElementById("svgpresentinganchor");
    var svgEle = document.createElement("div");
    svgEle.style.margin = "0 auto";
    svgEle.style.maxWidth = "350px";
    svgEle.innerHTML = resp.data;

    anchor?.appendChild(svgEle);

  }

  ...
  
  <div>
	<div id="svgpresentinganchor"
    style={{ 
      width: "100%", padding: "50px", marginLeft: "30px", borderRadius: "20px", 
      background: "#666", float: "right", width: "400px" 
    }}
	></div>
  </div>

Now that the SVG has been created, fetched and loaded into the webpage we can start to animate it. The PresentingAnimator class takes care of this task...


export default class PresentingAnimator {
    running: boolean = false;
    timer: any;
    parent: any;
    stars: Array<any> = [];
    starCount = 0;
    currentStars: any = []

    // 1. Entry Point
    animate() {
        var _this = this;
        // 2. Find the graphics element that has the inkscape:label attribute value of "stars"
        var elements = document.querySelectorAll("svg > g");
        elements.forEach(g => {            
            if (g.getAttribute("inkscape:label") == "stars")    {
                _this.parent = g;
            }
        }); 

        this.init();
        this.start();
    }

    init() {
        // 3. Find all children in the "stars" parent element and make them invisible 
        this.stars = this.parent.querySelectorAll("g");
        this.starCount = this.stars.length;
        this.stars.forEach(g => {        
            g.style.opacity = 0;
        });
    }

    start() {

        // 4. Start to make a random star visible every 200 milliseconds 
        // and record it in the currentStars array 
        this.timer = setInterval(() => {
            var index = Math.floor(Math.random() * this.starCount);

            if (this.currentStars.indexOf(index) == -1) {
                var starAnimator = new StarAnimator()
                starAnimator.start(this.stars[index], index, this)
                this.currentStars.push(index);
            }
        }, 200);

    }

    // 5. Called when the target star has been faded out fully 
    complete(index: any)  {
        var i = this.currentStars.indexOf(index);
        this.currentStars.splice(i, 1); 
    }

}

class StarAnimator {

    star: any
    timer: any;
    opacity = 0;
    fadeout = false;
    index = -1;
    parent: any

    start(star: any, index: any, parent: any) {

        this.star = star;
        this.index = index;
        this.parent = parent;

        this.timer = setInterval(() => {
            if (this.fadeout)   {
                this.opacity -= 0.01;
                if (this.opacity < 0)   {
                    clearInterval(this.timer);
                    this.parent.complete(this.index)
                }
            } else {
                this.opacity += 0.01;
            }
            if (this.opacity > 1)   {
                this.fadeout = true; 
            }
            this.star.style.opacity = this.opacity
        }, 10);
        
    }

}

  1. The entry point called from the page Javascript when the DOM has mounted
  2. This gets called from the page Javascript when the DOM has mounted
  3. The "init" function is called before any animation begins. It finds all child elements in the parent "stars" element and makes them invisible using the opacity attribute.
  4. The "start" function is called when we want to start animating the stars. We start a new timer every 200 milliseconds, get a random star index in the child elements, check to see if it's in the currentStars array and then animate it. A new StarAnimator class is created with then index and it takes care of fading in to full visibility and then fading out to invisibility once more.
  5. The "complete" function is called from the StarAnimator instance and the target star is removed from the currentStars array.

The jsfiddle snippet below shows how this SVG is rendered...


Oh YEAH Baby!!!

This is another simple, text based SVG image that has been given a 1960's, Austin Powers style makeover and doesn't require a great deal of graphic design skills.




Once again, I created this image in Inkscape using a size 48pt Bauhaus 93 font which I then converted to path elements, applied a long shadow and rotated slightly. It's embedded into the webpage in the same way as the "Presenting" image above using an Axios rest call.


export default class PresentingAnimator {
  running: boolean = false;
  timer: any;

  // 1. Variables
  colourParams = {
    r: { value: 152, descending: false },
    g: { value: 0, descending: false },
    b: { value: 200, descending: false },
  };

  isRunning() {
    return this.running;
  }

  // 2. the entry point
  animate() {
    console.log("SVGLogoAnimator.animate")    
    this.running = true
    this.timer = setInterval(() => {
        this.changeElements();
    }, 30);
  }

  stop()    {
    clearInterval(this.timer);
  }

  // 3. Change the colour
  changeElements() {
    this.getNextRGB(this.colourParams.r, 1);
    this.getNextRGB(this.colourParams.g, 2);
    this.getNextRGB(this.colourParams.b, 3);
    var rgb =
      "rgb(" +
      this.colourParams.r.value +
      "," +
      this.colourParams.g.value +
      "," +
      this.colourParams.b.value +
    ")";
    this.changeElementList(document.querySelectorAll(".ds-fill"), rgb);
    this.changeElementList(
      document.querySelectorAll("svglogoanchor > svg > g > g > path"),
      rgb
    );
    this.changeElementList(
      document.querySelectorAll("svg > g > g > g > path"),
      rgb
    );
  }

  // 4. Change colours for all elements at a specific level
  changeElementList(elements: any, rgb: any) {
    for (var ele in elements) {
      if (elements[ele] != null && elements[ele].style != null) {
        elements[ele].style.fill = rgb;
      }
    }
  }

  // 5. Determine the next colour
  getNextRGB(param: any, inc: any) {
    var ret = param.value;
    if (param.descending) {
      ret -= inc;
    } else {
      ret += inc;
    }
    if (ret < 0) {
      ret = 0;
      param.descending = false;
    } else if (ret > 255) {
      ret = 255;
      param.descending = true;
    }
    param.value = ret;
  }
}
  1. The colour variables that store the current values as they change
  2. The entry point called from the page Javascript when the DOM has mounted. It calls the changeElements function every 30 milliseconds.
  3. This function calls the getNextRGB function for each of the r,g & b variables with a different increment value for each. All of the child graphic elements are then set to that colour.
  4. The changeElementList accepts a list of graphic elements to be changed. Having it in a function allows different levels of elements to be handled separately.
  5. The getNextRGB checks to see if the passed rgb element is currently incrementing or decrementing. When the value reaches 255 or 0 respectively, the descending flag is reset.

The jsfiddle snippet below shows how this SVG is rendered...


And there you have it. Like I said at the start, I like the flexibility afforded by SVG images. They can really add a bit of pizzazz that you might not otherwise get using animated gifs. They can also add interactivity by hooking up to mouse events, etc. I didn't cover that here so I'll come back to that again.