S   G

Paweł Grabarz

">

Recap:

<html>
  <svg></svg>
</html>

Recap:

<svg width="300" height="300">
  <circle r="100" cx="150" cy="150"/>
</svg>

Recap:

<svg width="300" height="300">
  <circle r="100" cx="150" cy="150"/>
</svg>
circle {
    fill: $rainbow;
}

Vue

new Vue({
  el: '#app',
  template: `
    <svg width="300" height="300">
      <circle r="100" cx="150" cy="150"/>
    </svg>
  `
})

Bindings

<div id="app">
  <div>
    radius: <input type="range" v-model="r" min="10" max="150"/>
  </div>
  <svg width="300" height="300">
    <circle :r="r" cx="150" cy="150"/>
  </svg>
</div>
new Vue({
  el: '#app',
  data: { r: 25 }
})

React

const App = () => (
  <svg width="300" height="300">
    <circle r="100" cx="150" cy="150"/>
  </svg>
)

ReactDOM.render(<App/>, document.querySelector('#app'))

Pick your poison

Data driven SVG

D3

=

Layout

Utilities

DOM Mapping

Lots of modules

export {version} from "./build/package";
export * from "d3-array";
export * from "d3-axis";
export * from "d3-brush";
export * from "d3-chord";
export * from "d3-collection";
export * from "d3-color";
export * from "d3-dispatch";
export * from "d3-drag";
export * from "d3-dsv";
export * from "d3-ease";
export * from "d3-force";
export * from "d3-format";
export * from "d3-geo";
export * from "d3-hierarchy";
export * from "d3-interpolate";
export * from "d3-path";
export * from "d3-polygon";
export * from "d3-quadtree";
export * from "d3-queue";
export * from "d3-random";
export * from "d3-request";
export * from "d3-scale";
export * from "d3-selection";
export * from "d3-shape";
export * from "d3-time";
export * from "d3-time-format";
export * from "d3-timer";
export * from "d3-transition";
export * from "d3-voronoi";
export * from "d3-zoom";
computed: {
    chartPath() {
      return generateCurve(
        this.chartWidth,
        this.chartHeight,
        this.dataRangeX,
        this.dataRangeY,
        this.data);
    }
}
<path :d="chartPath" />

Using D3 modules

Using D3 modules

import { curveMonotoneX } from "d3-shape";

export function generateCurve(width, height, rangeX, rangeY, data) {
  const scaleY = height / rangeY;

  const segments = [];
  const curve = curveMonotoneX({
    moveTo(x, y) {
      segments.push(`M${x},${y}`);
    },
    closePath() {},
    lineTo(x, y) {
      segments.push(`L${x},${y}`);
    },
    bezierCurveTo(x1, y1, x2, y2, x, y) {
      segments.push(`C${x1},${y1} ${x2},${y2} ${x},${y}`);
    },
  });

  curve.areaStart();
  curve.lineStart();
  curve.point(-50, 0);

  data.forEach((point) => {
    const x = dataToOffset(rangeX, width, 0, point.x);
    const y = point.y * scaleY;
    curve.point(x, y);
  });
  curve.point(width + 50, 0);
  curve.lineEnd();
  curve.areaEnd();
  return segments.join(" ");
}

teach D3 to

generate SVG path

Feed it with data

import necessary module

Data driven SVG

Layout

Utilities

DOM Mapping

Reactive by default

D3 Parts
Custom JS

}

D3 to Vue

var dataSet = [10, 20, 30];

d3.select('svg').selectAll()
  .data(dataSet)
  .enter()
  .append('circle')
  .attr({
    r: d => d,
    cx: (d, i) => i * 100 + 50,
    cy: 50,
    fill: '#9b59b6'
  });
<svg></svg>

D3 to Vue

<svg>
  <circle
    v-for="(d, i) in dataSet"
    :r="d"
    :cx="i * 100 + 50"
    cy="50"
    fill="#9b59b6"
  />
</svg>
new Vue({
  el: 'svg',
  data: { dataSet: [10, 20, 30] }
})
d3.select('svg').selectAll()
  .data(dataSet)
  .enter()
  .append('circle')
  .attr({
    r: d => d,
    cx: (d, i) => i * 100 + 50,
    cy: 50,
    fill: '#9b59b6'
  });
<circle
  v-for="(d, i) in dataSet"
  :r="d"
  :cx="i * 100 + 50"
  cy="50"
  fill="#9b59b6"
 />

Animations

data: { numbers: [] },

mounted () {
  this.randomize()
  setInterval(this.randomize, 1000)
},

methods: {
  randomize () {
    this.numbers =
      Array.apply(null, Array(10))
      .map(() => 0.1 + 0.9 * Math.random())
  }
}

Every second...

...change the data

<g v-for="(num, i) in numbers"
   :style="barTransform(num, i)">
  <rect x="0" y="0" width="40" :height="num * 300"/>
  <text :x="20" :y="20">{{ ~~(num * 100) }}</text>
</g>

For every data point:

  • draw a bar and label
  • and setup it's size based on data value
barTransform(num, i) {
  const x = 15 + i * 50
  const y = 300 - num * 300
  return `transform: translate(${x}px, ${y}px)`
}

Custom behaviours

  • very easy to add in Vue's computed
horizontalTicks() {
  if (window.innerWidth < 640) {
    return this.generatedTicks.filter(
      (n, index) => (index % 2 === 0));
  }

  return this.generatedTicks;
},

Draw less ticks

for small screens

And hey, it's SVG! We can do @events with that too!

<svg>
  <path
    :d="myCalculatedPath"
    @click="sendMissionToMars"
  />
<svg>

Serious stuff...

How about...

...running a 60fps animation

interactive

Just a bunch of circles

<circle v-for="(c, i) in circles"
  :r="(1 - i / 60) * 0.07" :cx="c.x" :cy="c.y" />
followMouse() {
  let x = this.mouseX
  let y = this.mouseY
  const circles = this.circles
  const force = this.force
  for (let i = 0; i < circles.length; i++) {
    const circle = circles[i]
    x = circle.x = lerp(force, x, circle.x)
    y = circle.y = lerp(force, y, circle.y)
  }
}

Following mouse and each other

Every frame...

Gather

input

Simulate

Display

Simulation loop

Gather input

mounted () {
  document.addEventListener('mousemove', this.capturePos)
},

methods: {
   capturePos ($ev) {
      const size = Math.min(innerWidth, innerHeight)
      const offsetX = (size - innerWidth) / 2
      const offsetY = (size - innerHeight) / 2
      this.mouseX = (offsetX + $ev.clientX) / size
      this.mouseY = (offsetY + $ev.clientY) / size
    }
}

Bind some events

Protip: @event also work well for local stuff

Math voodoo to convert event position to uniform scale-independent svg space

<svg viewBox="0 0 1 1">
svg {
  position: absolute;
  width: 100%;
  height: 100%;
}

Simulate

60FPS?

observed data update => render

requestAnimationFrame(render)
requestAnimationFrame(mutateData)

Simulate

const timestep = 1000 / 120
let accumulator = 0
let lastTime = performance.now()

const tick = (t) => {
  accumulator += t - lastTime
  lastTime = t
  if(accumulator > 250)
    accumulator = 0
  
  while(accumulator > timestep) {
    this.followMouse()
    accumulator -= timestep
  }
  requestAnimationFrame(tick)
}
requestAnimationFrame(tick)

Every frame:

calculate new state

Keep track of real time

In fixed timesteps

until we kept up with real time

schedule next frame

escape the spiral of death

(ignore too much accumulated time)

Display?

followMouse() {
  let x = this.mouseX
  let y = this.mouseY
  const circles = this.circles
  const force = this.force
  for (let i = 0; i < circles.length; i++) {
    const circle = circles[i]
    x = circle.x = lerp(force, x, circle.x)
    y = circle.y = lerp(force, y, circle.y)
  }
}

reactive!

<circle v-for="(c, i) in circles"
  :r="(1 - i / 60) * 0.07" :cx="c.x" :cy="c.y" />

Simulation loop

Gather

input

Simulate

Display

Is it fast enough?

if you stick to vector:

SVG > canvas

Physics?

Is it fast enough for...

Is it fast like...

more than enough?

Thank You!

SVG in Vue.js

By Pawel Grabarz

SVG in Vue.js

  • 2,517