doctype html

// Point of entry for Vue
#root

// App template
template#app
  rough-icon(icon='vuejs' prefix='fab' :stroke='green' :fill='green')
  rough-icon(icon='thumbs-up' prefix='fal' :stroke='blue' :fill='blue' :roughness='0.6')
  rough-icon(icon='money-bill' prefix='far' :stroke='green' :fill='green')
  rough-icon(icon='quidditch' :stroke='brown300' :fill='brown400' :roughness='1.5')
  rough-icon(icon='magnet' stroke='white' :fill='red' :roughness='1.5')
  rough-icon(icon='coffee' stroke='white' fill='white' :roughness='1.5')
  rough-icon(icon='heart' :stroke='red' :fill='red' :roughness='2' :bowing='4')
  rough-icon(icon='dolly' stroke='white' :fill='brown')
  rough-icon(icon='chess-rook' stroke='white' fill='white')
  rough-icon(icon='gamepad' stroke='white' :fill='grey')
  rough-icon(icon='lightbulb' stroke='white' :fill='yellow')
  rough-icon(icon='eye' prefix='far' :stroke='blue300' :fill='blue800' :roughness='1.2')
  rough-icon(icon='snowflake' :stroke='lightBlue100' :fill='lightBlue100')
  rough-icon(icon='futbol' stroke='white' fill='white' :roughness='1.3')
  rough-icon(icon='football-ball' stroke='white' fill='#795548')
  rough-icon(icon='flask' stroke='white' fill='#BA68C8' :roughness='1.3')
  rough-icon(icon='battery-full' prefix='far' :stroke='green' :fill='green' :roughness='0.9')
  rough-icon(icon='browser' stroke='white' fill='white')
  rough-icon(icon='sliders-h-square' prefix='far' stroke='white' fill='white')
  rough-icon(icon='signal' stroke='white' :fill='green')
  rough-icon(icon='trophy' stroke='white' :fill='yellow')
  rough-icon(icon='watch' stroke='white' fill='white')
  rough-icon(icon='pencil' stroke='white' fill='white')

// Component Templates
template#roughIcon
  canvas(ref='canvas')

// Profile link styling for self-shilling
a.shill(href='https://codepen.io/milesmanners' target='_blank' title='Made by Miles Manners')
View Compiled
@include theme($bg-dark-blue, $blue);

body {
  display: flex;
}

#root {
  margin: auto;
  display: flex;
  flex-flow: row wrap;
  align-items: center;
  justify-content: space-around;
}

canvas {
  display: block;
  margin: 10px;
}
View Compiled
/* jshint esversion: 9, asi: true, boss: true */
import SvgPath from "https://cdn.skypack.dev/svgpath@2.3.0";

console.clear()

// Point of entry
const app = Vue.createApp({
  template: '#app',

  data: () => ({
    red: '#F44336',
    yellow: '#FFEB3B',
    green: '#4CAF50',
    blue300: '#64B5F6',
    blue: '#2196F3',
    blue800: '#1565C0',
    lightBlue100: '#B3E5FC',
    brown300: '#A1887F',
    brown400: '#8D6E63',
    brown: '#795548',
    grey: '#9E9E9E',
    white: '#FFFFFF'
  })
})

// Component Definitions
app.component('rough-icon', {
  template: '#roughIcon',

  props: {
    icon: { type: String, default: 'unknown' },
    prefix: { type: String, default: 'fas' },
    buffer: { type: Number, default: 30 },
    scale: { type: Number, default: 0.15 },
    roughness: { type: Number, default: 1 },
    bowing: { type: Number, default: 1 },
    stroke: { type: String, default: 'white' },
    strokeWidth: { type: Number, default: 1 },
    fill: { type: String, default: 'white' },
    fillStyle: { type: String, default: 'hachure' },
    fillWeight: { type: Number, default: -1 },
    hachureAngle: { type: Number, default: -41 },
    hachureGap: { type: Number, default: -1 },
    curveStepCount: { type: Number, default: 9 },
    simplification: { type: Number, default: 0 }
  },
  
  data: () => ({
    rc: null,
    ctx: null,
    drawable: null
  }),
  
  computed: {
    options () {
      return {
        roughness: this.roughness,
        bowing: this.bowing,
        stroke: this.stroke,
        strokeWidth: this.strokeWidth,
        fill: this.fill,
        fillStyle: this.fillStyle,
        fillWeight: this.fillWeight,
        hachureAngle: this.hachureAngle,
        hachureGap: this.hachureGap,
        curveStepCount: this.curveStepCount,
        simplification: this.simplification
      }
    },
    
    path () {
      let iconDef = window.FontAwesome.findIconDefinition({ prefix: this.prefix, iconName: this.icon })
      let icon = window.FontAwesome.icon(iconDef)
      let path = icon.abstract[0].children[0].attributes.d
      let svgPath = new SvgPath(path).abs()
      
      let left = Infinity
      let top = Infinity
      
      svgPath.iterate((seg, i, x, y) => {
        if (!i) return
        if (x < left) {
          left = x
        }
        if (y < top) {
          top = y
        }
      })
      
      return svgPath.translate(-left + this.buffer, -top + this.buffer).scale(this.scale).toString()
    },
    
    bounds () {
      let width = 0
      let height = 0
      
      new SvgPath(this.path).iterate((seg, i, x, y) => {
        if (x > width) {
          width = x
        }
        if (y > height) {
          height = y
        }
      })
      
      return { width, height }
    }
  },
  
  mounted () {
    const canvas = this.$refs.canvas
    this.rc = rough.canvas(canvas)
    this.ctx = canvas.getContext('2d')
    
    // DPR stuff for mobile and retina displays
    let scale = window.devicePixelRatio
    let width = this.bounds.width + this.buffer * this.scale
    let height = this.bounds.height + this.buffer * this.scale
    
    canvas.width = width * scale
    canvas.height = height * scale
    canvas.style.width = width + 'px'
    canvas.style.height = height + 'px'
    this.ctx.scale(scale, scale)
    
    // A simple dynamic sizing method
    window.addEventListener('resize', this.resize, { passive: true })
    
    this.generate()
    this.draw()
  },
  
  watch: {
    icon (val) {
      this.resize()
      this.generate()
      this.draw()
    },
    prefix (val) {
      this.resize()
      this.generate()
      this.draw()
    },
    buffer (val) {
      this.resize()
      this.generate()
      this.draw()
    },
    scaling (val) {
      this.resize()
      this.generate()
      this.draw()
    },
    roughness (val) {
      this.drawable.options.roughness = val
      this.generate()
      this.draw()
    },
    bowing (val) {
      this.drawable.options.bowing = val
      this.generate()
      this.draw()
    },
    stroke (val) {
      this.drawable.options.stroke = val
      this.draw()
    },
    strokeWidth (val) {
      this.drawable.options.strokeWidth = val
      this.generate()
      this.draw()
    },
    fill (val) {
      this.drawable.options.fill = val
      this.draw()
    },
    fillStyle (val) {
      this.drawable.options.fillStyle = val
      this.generate()
      this.draw()
    },
    fillWeight (val) {
      this.drawable.options.fillWeight = val
      this.draw()
    },
    hachureAngle (val) {
      this.drawable.options.hachureAngle = val
      this.generate()
      this.draw()
    },
    hachureGap (val) {
      this.drawable.options.hachureGap = val
      this.generate()
      this.draw()
    },
    curveStepCount (val) {
      this.drawable.options.curveStepCount = val
      this.generate()
      this.draw()
    },
    simplification (val) {
      this.drawable.options.simplification = val
      this.generate()
      this.draw()
    }
  },
  
  methods: {
    generate () {
      this.drawable = this.rc.generator.path(this.path, this.options)
    },
    
    draw () {
      const canvas = this.$refs.canvas
      this.ctx.clearRect(0, 0, canvas.width, canvas.height)
      this.rc.draw(this.drawable)
    },
    
    resize () {
      let scale = window.devicePixelRatio

      const canvas = this.$refs.canvas
      let oldWidth = canvas.width
      let oldHeight = canvas.height
      let newWidth = (this.bounds.width + this.buffer * this.scale) * scale
      let newHeight = (this.bounds.height + this.buffer * this.scale) * scale

      if (Math.abs(oldWidth - newWidth) >= 1 || Math.abs(oldHeight - newHeight) >= 1) {
        canvas.width = newWidth
        canvas.height = newHeight
        canvas.style.width = newWidth + 'px'
        canvas.style.height = newHeight + 'px'
        this.ctx.scale(scale, scale)

        this.generate()
        this.draw()
      }
    }
  }
})

app.mount('#root')

External CSS

  1. https://codepen.io/milesmanners/pen/JLPRaG.scss
  2. https://fonts.googleapis.com/css?family=Open+Sans

External JavaScript

  1. https://pro.fontawesome.com/releases/v5.15.2/js/all.js
  2. https://unpkg.com/vue@3.0.5/dist/vue.global.js
  3. https://unpkg.com/roughjs@4.3.1