Various things we wrote

BLARG!

Dynamic SVG images with Vue

VueJS is a great way to manipulate SVG elements. With it’s reactive data and single file components creating dynamic data driven graphics is a breeze. These types of tools are very engaging to users and serve a functional purpose by increasing conversion rates. By engaging the user and getting them mentally invested, they are more apt to convert. One such tool is Flex-Specs, a flexible heater configuration tool. Created for our client to both increase conversion rates and ease the process of quoting. Customers use the tool to create and spec their flexible heater based on their process and easily get a quote. 

For our example we will be creating an iris from mathematical variations. I’m using Vuetify for the UI framework and the full VueJS import so I can embed my single file components right in the existing DOM of this blog. 

 

Inline SVG Vue Component

To start off, our SVG needs to be part of the page’s DOM, so instead of using an tag to import the SVG, we will add it inline with and that will be the basis of our template. We will call this component IrisSvg and import into our main app. 

We are going to pass some parameters to this component. The radius of the circle, the number of points and our multiplier. These will be tied to the sliders in the ui. Since all the SVG attributes can be dynamic or computed properties. For example, we can set up some changes in the image ratio based on the screen size. “style” will be a computed property and we can use Vuetify breaks points to change the ratio based on the breakpoint. 

Here's the start of our IrisSvg.vue component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
  <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 400 400" xml:space="preserve" :style="getResponsiveStyle">
   ...
  </svg>
</template>
 
<script>
  import g from './geometry_functions';
 
  export default {
    props : [
       'radius', //the radius of the circle on the screen
       'numPoints', //number of points around the circle in which to draw lines
       'multiplier' //our algorithm multiplier. This will be multiplied against the current point's degree/angle to find the next point. 
    ],
    data: () => ({
 
    })
  }
</script>

We will import this into our Vue app. Here's our vue app including the vuetify controls and importing IrisSvg. Checkout the full code at the bottom of the post for adding the rest of the UI. 

1
2
3
4
5
6
7
<template>
  <v-app>
    ...
          <iris-svg :num-points="numPoints" :radius="radius" :multiplier="multiplier"></iris-svg>
    ...
  </v-app>
</template>

Drawing Circles

Now let us draw a reference circle. Jump back into IrisSvg.vue... and add a circle element to the svg. The r (radius) will also be a dynamic parameter tied to the radius variable. We now have our first dynamic shape in our svg. The radius of the circle is tied to the radius slider. 

1
2
3
4
5
<template>
  <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 400 400" xml:space="preserve" :style="getResponsiveStyle">
    <circle :r="radius" cx="200" cy="200" class="circle"></circle>
  </svg>
</template>

Next we will use the v-for vue directive to create a series of points on the circumference. The center of these circles will be tied to a method in our component to compute the x and y position. We will place these evenly around the circle based on our numPoints slider value. These points will represent the lines of our iris. "radial_points" is a method within our component that will find the x/y point given i. See the full github code for this. 

1
<circle v-for="i in numPoints" r="2" :cx="radial_points(i).x" :cy="radial_points(i).y" class="circle"></circle>

Connect the Dots

Finally we want to draw a line from each point to it’s multiplier. In this instance I want to set up some duplication checking so we don’t redraw lines unnecessarily - will we use a computed property that returns an array of objects, each with two points defined x1,y1,x2,y2. These points are used to draw our lines with a simple svg line element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<line v-for="line in lines" :x1="line.x1" :y1="line.y1" :x2="line.x2" :y2="line.y2" class="line" />
...
     computed : {
      //creates an array of line points used for drawing. 
      lines : function(){
        var r = [];
        var dupes = [];
        var i = 1;
        //removes duplicates and also keeps the number of lines under a reasonable number. 
        while (i < 1000){
          var pts = this.line_points(i); // returns an object of {x1, y1, x2, y2}
          var a1 = Math.round(pts.a1 * 100) / 100;
          var a2 = Math.round(pts.a2 * 100) / 100;
          if (!dupes.includes(a1 + "--" + a2) &&  !dupes.includes(a2 + "--" + a1)){
            r.push(pts);
            dupes.push(a1 + "--" + a2);
          }
          i++;
        }
        return r; //returns the full array of line objects {x1, y1, x2, y2}
      },

We now have our fully reactive svg iris - fun mathematical structure to play around with. 

Below is the full code for App.vue or check it out on github.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
  <v-app>
    <v-container>
      <v-layout row wrap>
        <v-flex xs12 sm4>
          <h2>Controls</h2>
          <v-slider name="name" label="Number of Points" thumb-label="always" max="360" v-model="numPoints" ></v-slider>
          <v-slider name="name" step=".25" thumb-label="always" max="30" label="Multiplier" v-model="multiplier" ></v-slider>
          <v-slider name="name" step="1" thumb-label="always" max="300" label="Radius" v-model="radius" ></v-slider>
        </v-flex>
        <v-flex xs12 sm8 text-xs-center>
          <iris-svg :num-points="numPoints" :radius="radius" :multiplier="multiplier"></iris-svg>
        </v-flex>
      </v-layout>
    </v-container>
  </v-app>
</template>
 
<script>
import IrisSvg from './components/IrisSvg'
 
export default {
  name: 'App',
  components: {
    IrisSvg
  },
  data () {
    return {
      numPoints : 180,
      multiplier : 2,
      radius : 200,
    }
  },
  computed : {
 
  }
}
</script>

And the full code for IrisSvg.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<template>
  <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 400 400" xml:space="preserve" :style="getResponsiveStyle">
    <circle :r="radius" cx="200" cy="200" class="circle"></circle>
    <circle v-for="i in numPoints" r="2" :cx="radial_points(i).x" :cy="radial_points(i).y" class="circle"></circle>
    <line v-for="line in lines" :x1="line.x1" :y1="line.y1" :x2="line.x2" :y2="line.y2" class="line" />
  </svg>
</template>
 
<script>
  import g from './geometry_functions';
 
  export default {
    props : ['radius', 'numPoints', 'multiplier'],
    data: () => ({
      //radius : 190
    }),
    methods : {
      radial_points : function(i){
        var angle = (i / this.numPoints) * 360;
        return this.findPointOnCircle(200, 200, this.radius, angle);
      },
      line_points : function(i){
        var angle = (1 / this.numPoints) * 360;
        var p1 = this.findPointOnCircle(200, 200, this.radius, angle * i);
        var p2 = this.findPointOnCircle(200, 200, this.radius, (angle * i) * this.multiplier);
        return {
          x1 : p1.x, 
          y1 : p1.y, 
          x2 : p2.x, 
          y2 : p2.y,
          a1 : (angle * i) % 360,
          a2 : ((angle * i) * this.multiplier) % 360
        };
      },
      findPointOnCircle(originX, originY , radius, angleDegrees) {
        var angleRadians = Math.PI * 2 * angleDegrees / 360;
        var newX = (radius * Math.cos(angleRadians) + originX);
        var newY = (radius * Math.sin(angleRadians) + originY);
        return {"x" : newX, "y" : newY}
      },
    },
    computed : {
      //creates an array of line points used for drawing. 
      lines : function(){
        var r = [];
        var dupes = [];
        var i = 1;
        //removes duplicates and also keeps the number of lines under a reasonable number. 
        while (i < 1000){
          var pts = this.line_points(i);
          var a1 = Math.round(pts.a1 * 100) / 100;
          var a2 = Math.round(pts.a2 * 100) / 100;
          if (!dupes.includes(a1 + "--" + a2) &&  !dupes.includes(a2 + "--" + a1)){
            r.push(pts);
            dupes.push(a1 + "--" + a2);
          }
          i++;
        }
        return r;
      },
      getResponsiveStyle : function(){
        if (this.$vuetify.breakpoint.name != 'xs'){
          return "height:400px; max-width: 100%; max-height:450px; enable-background:new -50 -50 300 300";
        }else{
          return "height:300px; max-width: 100%; max-height:250px; enable-background:new -50 -50 300 300";
        }
      },
    }
  }
</script>
 
<style>
.circle {
  fill:none;
  stroke:#aaaaaa;
  stroke-width: .25px;
  stroke-miterlimit:10;
}
.line {
  fill:none;
  stroke:#3333ff;
  stroke-width: .25px;
  stroke-miterlimit:10;
}
</style>

Comments (0) Write a comment

Leave a Comment

828 582 4975

info@paleosun.com

70 Woodfin Place Suite 312
Asheville, NC, 28801