Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="element"></div>
<div id="controls">
  <svg id="brush" class="active" enable-background="new 0 0 430.51 430.51" version="1.1" viewBox="0 0 430.51 430.51" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m296 284.52-52.542-30.805c-2.865 0.345-5.734 0.913-8.59 1.73-13.717 3.937-33.557 15.227-50.186 46.668-13.202 24.961-8.33 46.331-4.031 65.187 4.108 18.023 7.354 32.259-4.718 46.5-1.187 1.399-1.511 3.336-0.845 5.047 0.419 1.076 1.188 1.952 2.15 2.512 0.571 0.33 1.21 0.551 1.885 0.637 30.535 3.836 76.235-14.17 104.42-62.771 12.802-22.074 18.3-43.299 15.902-61.38-0.624-4.717-1.796-9.17-3.446-13.325zm-51.496 3.33c-0.941 2.5-2.8 4.482-5.234 5.584-17.741 8.03-20.915 22.067-21.042 22.662-0.702 3.69-3.553 6.751-7.244 7.732-0.327 0.087-0.659 0.157-0.995 0.212-1.14 0.182-2.299 0.163-3.441-0.055-5.418-1.033-8.985-6.277-7.954-11.694 0.19-1.001 4.985-24.655 32.431-37.078 0.506-0.229 1.027-0.414 1.557-0.556 4.743-1.261 9.654 1.07 11.678 5.542 1.098 2.434 1.186 5.151 0.244 7.651z"/><path d="m418.94 11.153c-10.538-6.111-24.009-3.033-30.847 7.05-4.299 6.338-105.41 155.47-118.43 177.92-6.509 11.224-13.845 29.443-19.947 46.1l52.542 30.805c11.443-13.576 23.674-29.061 30.275-40.443 13.17-22.708 92.25-184.28 95.609-191.15 5.357-10.945 1.337-24.166-9.204-30.277z"/><path d="m166.59 341.07c-4.498-1.502-8.881-2.384-13.327-2.657-0.747-0.046-1.549-0.069-2.383-0.069-6.69 0-15.097 1.466-23.227 2.884-7.245 1.265-14.089 2.457-18.864 2.457-3.359 0-4.52-0.616-4.895-0.936-4.059-5.095 1.807-18.923 4.318-24.849 0.642-1.512 1.148-2.706 1.44-3.559 4.072-11.981 7.063-24.737 1.425-36.843-4.687-10.065-15.643-18.076-27.824-20.391-3.548-0.838-7.62-1.228-12.816-1.228-4.277 0-8.854 0.271-13.281 0.534-4.54 0.27-9.235 0.548-13.731 0.548-15.464 0-24.366-3.528-29.749-11.769-9.992-15.865 2.009-44.283 14.332-56.117 0.758-0.728 0.823-1.918 0.149-2.725-0.675-0.807-1.857-0.955-2.708-0.337-15.781 11.433-26.229 32.205-25.406 50.514 0.439 9.764 4.197 23.327 19.612 32.506 8.298 4.94 21.718 5.98 34.696 6.986 12.768 0.989 25.971 2.013 30.04 6.977 1.066 1.301 1.472 2.876 1.233 4.859-0.888 9.078-5.293 18.018-9.554 26.661-8.047 16.327-16.368 33.209-1.986 51.944 12.631 16.451 29.399 16.451 46.625 16.331 1.41-0.01 2.823-0.02 4.235-0.02 7.515 0 16.196 0.23 24.068 2.972 5.602 2.171 8.419 6.662 9.737 15.571 1.009 5.462 3.436 9.762 6.657 13 0.471-2.511 1.577-4.904 3.29-6.927 8.458-9.986 6.803-19.319 2.577-37.812-1.962-8.595-4.137-18.122-4.683-28.505z"/></svg>
<svg id="eraser" enable-background="new 0 0 360 360" version="1.1" viewBox="0 0 360 360" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
    <path d="m348.99 102.95-98.954-98.953c-5.323-5.323-13.954-5.324-19.277 0l-153.7 153.7 118.23 118.23 153.7-153.7c5.323-5.322 5.323-13.953 0-19.278z"/>
    <path d="m52.646 182.11-41.64 41.64c-5.324 5.322-5.324 13.953 0 19.275l98.954 98.957c5.323 5.322 13.954 5.32 19.277 0l41.639-41.641-118.23-118.23z"/>
    <polygon points="150.13 360 341.77 360 341.77 331.95 182.81 331.95"/>
</svg>
<svg id="undo" class="disabled" enable-background="new 0 0 52.502 52.502" version="1.1" viewBox="0 0 52.502 52.502" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g><path d="m21.524 16.094v-12.048l-20.108 19.952 20.108 20.143v-12.047s17.598-4.355 29.712 16c0 0 3.02-15.536-10.51-26.794 1e-3 -1e-3 -5.991-5.604-19.202-5.206z"/><path d="m51.718 50.857-1.341-2.252c-10.214-17.164-24.401-16.203-27.853-15.68v13.634l-22.524-22.564 22.524-22.351v13.431c12.728-0.103 18.644 5.268 18.886 5.494 13.781 11.465 10.839 27.554 10.808 27.715l-0.5 2.573zm-26.073-20.155c5.761 0 16.344 1.938 24.854 14.376 0.128-4.873-0.896-15.094-10.41-23.01-0.099-0.088-5.982-5.373-18.533-4.975l-1.03 0.03v-10.676l-17.694 17.554 17.692 17.724v-10.414l0.76-0.188c0.07-0.018 1.73-0.421 4.361-0.421z"/></g></svg>  
<svg id="redo" class="disabled" enable-background="new 0 0 52.495 52.495" version="1.1" viewBox="0 0 52.495 52.495" xml:space=preserve" xmlns="http://www.w3.org/2000/svg">
<g><path d="m31.113 16.038v-12.048l19.971 20.08-19.971 20.08v-12.048s-17.735-4.292-29.849 16.064c0 0-3.02-15.536 10.51-26.794 0-1e-3 6.129-5.732 19.339-5.334z"/><path d="m0.783 50.929-0.5-2.573c-0.031-0.161-2.974-16.25 10.852-27.753 0.202-0.191 6.116-5.585 18.674-5.585 0.102 0 0.203 0 0.305 1e-3v-13.453l22.381 22.504-22.382 22.504v-13.637c-0.662-0.098-1.725-0.213-3.071-0.213-5.761 0-16.657 2.073-24.918 15.953l-1.341 2.252zm29.025-33.911c-11.776 0-17.297 5.033-17.352 5.084-9.545 7.944-10.578 18.172-10.452 23.047 12.361-18.058 29.123-14.072 29.344-14.019l0.765 0.185v10.411l17.561-17.656-17.561-17.657v10.654l-1.03-0.03c-0.433-0.013-0.857-0.019-1.275-0.019z"/></g></svg>
<svg id="download" class="disabled" enable-background="new 0 0 512 512" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m382.56 233.38c-2.592-5.728-8.288-9.376-14.56-9.376h-64v-208c0-8.832-7.168-16-16-16h-64c-8.832 0-16 7.168-16 16v208h-64c-6.272 0-11.968 3.68-14.56 9.376-2.624 5.728-1.6 12.416 2.528 17.152l112 128c3.04 3.488 7.424 5.472 12.032 5.472s8.992-2.016 12.032-5.472l112-128c4.16-4.704 5.12-11.424 2.528-17.152z"/><path d="M432,352v96H80v-96H16v128c0,17.696,14.336,32,32,32h416c17.696,0,32-14.304,32-32V352H432z"/></svg>
</div>
              
            
!

CSS

              
                body {
  background: #252525;
  padding: 0;
  margin: 0;
  overflow: hidden;
}

#controls {
  position: absolute;
  top: 0;
  left: 0;
  padding: 12px;
  height: 100%;
  width: 32px;
}

svg {
  fill: #fff;
  width: 32px;
  height: 32px;
  margin-bottom: 24px;
  cursor: pointer;
}

svg:hover, svg.active {
  fill: #1b9cd8;
}

svg.disabled {
  opacity: 0.4;
  cursor: default;
}

svg.disabled:hover {
  fill: #fff;
}
              
            
!

JS

              
                class Sketch {
  constructor(container, drawingBoard, historyCount) {
    this._changeFn = null
    this._changeScope = null
    this._stage = new Stage(container, drawingBoard, historyCount, this._emitChange.bind(this))
  }
  onChange(scope, fn) {
    this._changeFn = fn
    this._changeScope = scope
  }
  canUndo() {
    return this._stage.canUndo()
  }
  canRedo() {
    return this._stage.canRedo()
  }
  undo() {
    this._stage.undo(() => {
      this._emitChange()
    })
  }
  redo() {
    this._stage.redo(() => {
      this._emitChange()
    })
  }
  setStrokeWidth(width) {
    this._stage.setStrokeWidth(width)
  }
  setStrokeColour(colour) {
    this._stage.setStrokeColour(colour)
  }
  selectPencil() {
    this._stage.selectPencil()
  }
  selectEraser() {
    this._stage.selectEraser()
  }
  export() {
    return this._stage.export()
  }
  update(container, drawingBoard) {
    this._stage.update(container, drawingBoard)
  }
  _emitChange() {
    if(this._changeFn) {
      this._changeFn(this._changeScope)
    }
  }
}

class Stage {
  constructor(container, drawingBoard, historyCount = 100, change) {

    container = Object.assign({
      element: null,
      dimensions: {
        width: 400,
        height: 400
      }
    }, container)

    drawingBoard = Object.assign({
      pathType: 'bezier',
      dimensions: {
        width: 400,
        height: 400
      },
      position: {
        x: 0,
        y: 0
      },
      scale: 1
    }, drawingBoard)

    this._stage = new Konva.Stage({
      container: container.element,
      width: container.dimensions.width,
      height: container.dimensions.height
    })

    this._init()
    this._drawingBoard = new DrawingBoard(this._stage, this._canvas, drawingBoard, historyCount, change)
  }
  canUndo() {
    return this._drawingBoard.canUndo()
  }
  canRedo() {
    return this._drawingBoard.canRedo()
  }
  undo(callback) {
    this._drawingBoard.undo(callback)
  }
  redo(callback) {
    this._drawingBoard.redo(callback)
  }
  setStrokeColour(colour) {
    this._drawingBoard.setStrokeColour(colour)
  }
  setStrokeWidth(width) {
    this._drawingBoard.setStrokeWidth(width)
  }
  selectPencil() {
    this._drawingBoard.selectPencil()
  }
  selectEraser() {
    this._drawingBoard.selectEraser()
  }
  export() {
    return this._drawingBoard.export()
  }
  update(container, drawingBoard) {

    if(container.dimensions && container.dimensions.width) {
      this._stage.setWidth(container.dimensions.width)
    }
    if(container.dimensions && container.dimensions.height) {
      this._stage.setHeight(container.dimensions.height)
    }

    this._drawingBoard.update(drawingBoard)

  }
  _init() {
    this._canvas = new Konva.Layer()
    this._stage.add(this._canvas)
    this._draw()
  }
  _draw() {
    this._stage.draw()
  }
}

class DrawingBoard {
  constructor(stage, parentCanvas, settings, historyCount, change) {
    this._stage = stage
    this._parentCanvas = parentCanvas

    this._pathType = settings.pathType

    this._dimensions = settings.dimensions
    this._position = settings.position
    this._scale = settings.scale

    this._offsets = this._calculateOffsets()

    this._canvas = null
    this._board = null
    this._ctx = null

    this._strokeStyle = '#1b9cd8'
    this._lineWidth   = 4

    this._lineJoin    = 'round'
    this._lineCap     = 'round'
    this._shadowColor = 'rgb(27, 156, 216)'
    this._shadowBlur  = 2

    this._build(() => {

      this._exportManager = new ExportManager(this._canvas)

      this._historyManager = new HistoryManager({
        maxLength: historyCount
      },this._exportManager)

      this.save()
    })

    this._lastPointerPosition = null
    this._isPainting = false
    this._currentPoints = [ ]

    this._drawingMode = 'brush'

    this._listen()

    this._boardNeedsRendering = false

    this._change = change
    this._animate()

  }
  setStrokeColour(strokeColour) {
    this._strokeStyle = strokeColour
    this._shadowColor = this._strokeStyle

    this._ctx.strokeStyle = this._strokeStyle
    this._ctx.shadowColor = this._shadowColor
  }
  setStrokeWidth(width) {
    this._lineWidth = width
    this._ctx.lineWidth = this._lineWidth
  }
  selectPencil() {
    this._drawingMode = 'brush'
  }
  selectEraser() {
    this._drawingMode = 'eraser'
  }
  undo(callback) {
    this._historyManager.getPrevious((image) => {
      this._ctx.clearRect(0, 0, this._ctx.canvas.width, this._ctx.canvas.height)
      this._ctx.globalCompositeOperation = 'source-over'

      this._ctx.drawImage(image, 0, 0)
      this._parentCanvas.draw()

      callback()
    })
  }
  canUndo() {
    return this._historyManager.canUndo()
  }
  canRedo() {
    return this._historyManager.canRedo()
  }
  redo(callback) {
    this._historyManager.getNext((image) => {
      this._ctx.clearRect(0, 0, this._ctx.canvas.width, this._ctx.canvas.height)
      this._ctx.globalCompositeOperation = 'source-over'

      this._ctx.drawImage(image, 0, 0)
      this._parentCanvas.draw()
      
      callback()
    })
  }
  save() {
    this._historyManager.add()
  }
  export() {
    return this._exportManager.cropCanvasAndConvertToBase64(this._canvas)
  }
  update(settings) {
    if(settings.dimensions) {
      this._dimensions = settings.dimensions
    }

    if(settings.position) {
      this._position = settings.position
    }

    if(settings.scale) {
      this._scale = settings.scale
    }

    if(settings.scale || settings.position || settings.dimensions) {
      this._offsets = this._calculateOffsets()
    }

    this._board.width(this._dimensions.width)
    this._board.height(this._dimensions.height)

    this._board.x(this._offsets.x)
    this._board.y(this._offsets.y)

    this._board.scaleX(this._scale)
    this._board.scaleY(this._scale)

  }
  _animate() {
    if(this._boardNeedsRendering) {
      this._ctx.stroke()
      this._stage.draw()
      this._boardNeedsRendering = false
    }

    requestAnimationFrame(this._animate.bind(this))
  }
  _listen() {
    this._stage.on('contentMousedown.proto contentTouchstart.proto', () => {

      this._isPainting = true
      this._lastPointerPosition = this._getPointerPosition()

      if (this._drawingMode === 'brush') {
        this._ctx.globalCompositeOperation = 'source-over'
      }
      if (this._drawingMode === 'eraser') {
        this._ctx.globalCompositeOperation = 'destination-out'
      }

      if(this._pathType === 'bezier') {
        this._currentPoints.push({ x: this._lastPointerPosition.x, y: this._lastPointerPosition.y })
      }

      let p1 = this._currentPoints[ 0 ]
      // let p2 = this._currentPoints[ 1 ]

      this._ctx.beginPath()
      this._ctx.lineTo(p1.x, p1.y)

      this._boardNeedsRendering = true
    })
    this._stage.on('contentMouseup.proto contentTouchend.proto', () => {
      this._isPainting = false

      if(this._pathType === 'bezier') {
        this._currentPoints.length = 0
      }

      this.save()
      this._change()
    })
    this._stage.on('contentMousemove.proto contentTouchmove.proto', () => {

      if (!this._isPainting) { return }

      this._lastPointerPosition = this._getPointerPosition()

      if(this._pathType === 'bezier') {

        this._currentPoints.push({ x: this._lastPointerPosition.x, y: this._lastPointerPosition.y })

        let p1 = this._currentPoints[ 0 ]
        let p2 = this._currentPoints[ 1 ]

        this._ctx.beginPath()
        this._ctx.moveTo(p1.x, p1.y)

        for(let i = 1; i < this._currentPoints.length; i++) {

          let midPoint = this._midPointBetween(p1, p2)

          this._ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y)

          p1 = this._currentPoints[ i ]
          p2 = this._currentPoints[ i + 1 ]
        }

        this._ctx.lineTo(p1.x, p1.y)

        this._boardNeedsRendering = true
      }
    })
  }
  _build(callback) {
    this._canvas = document.createElement('canvas')
    this._canvas.width = this._dimensions.width
    this._canvas.height = this._dimensions.height

    this._ctx = this._canvas.getContext('2d')

    this._ctx.strokeStyle = this._strokeStyle
    this._ctx.lineWidth = this._lineWidth

    this._ctx.lineJoin = this._lineJoin
    this._ctx.lineCap = this._lineCap
    this._ctx.shadowColor = this._shadowColor
    this._ctx.shadowBlur = this._shadowBlur

    this._board = new Konva.Image({
      image: this._canvas,
      ...this._offsets,
      width: this._dimensions.width * this._scale,
      height: this._dimensions.height * this._scale
    })

    this._parentCanvas.add(this._board)
    this._draw()

    callback()
  }
  _midPointBetween(p1, p2) {
    return {
      x: p1.x + (p2.x - p1.x) / 2,
      y: p1.y + (p2.y - p1.y) / 2
    }
  }
  _getPointerPosition() {
    let n = this._stage.getPointerPosition()

    return {
      x: (n.x - this._offsets.x) / this._scale,
      y: (n.y - this._offsets.y) / this._scale
    }
  }
  _calculateOffsets() {
    return {
      x: this._position.x + (this._dimensions.width - (this._dimensions.width * this._scale)) / 2,
      y: this._position.y + (this._dimensions.height - (this._dimensions.height * this._scale)) / 2
    }
  }
  _draw() {
    this._board.draw()
  }
}

class HistoryManager {
  constructor({ maxLength } = { }, exportManager) {
    if(!(exportManager instanceof ExportManager)) {
      throw new Error('Must provide ExportManager instance')
    }

    this._exportManager = exportManager
    this._maxLength = maxLength
    this._history = [ ]
    this._index = -1
  }
  add() {
    this._index = this._index + 1
    this._history = this._history.slice(0, this._index)

    this._history.push(this._exportManager.fromCanvas(null, {
      to: 'base64'
    }))
  }
  canUndo() {
    return (this._index > 0)
  }
  canRedo() {
    return (this._history.length > this._index + 1)
  }
  getPrevious(callback) {
    this._exportManager.toCanvas(this._history[ this._getIndex('prev') ], {
      from: 'base64'
    }, (image) => {
      callback(image)
    })
  }
  getNext(callback) {
    this._exportManager.toCanvas(this._history[ this._getIndex('next') ], {
      from: 'base64'
    }, (image) => {
      callback(image)
    })
  }
  _getIndex(type) {
    if(type === 'prev') {
      if (this.canUndo()) {
        this._index = this._index - 1 
      }

      return this._index
    } else {
      if (this.canRedo()) {
        this._index = this._index + 1
      }

      return this._index
    }
  }
}

class ExportManager {
  constructor(canvas) {
    if(typeof canvas.getContext !== 'function') {
      throw new Error('Must provide canvas to Export Manager')
    }

    this._canvas = canvas
  }
  fromCanvas(canvas = null, { to = 'base64' } = { }) {

    if(canvas === null) {
      canvas = this._canvas
    } else if(typeof canvas.getContext !== 'function') {
      throw new Error('Invalid canvas element')
    }

    switch(to) {
      case 'base64':
        return canvas.toDataURL()
      default:
        throw new Error('Unknown export type')
    }
  }
  toCanvas(payload, { from = 'base64' } = { }, callback) {

    switch(from) {
      case 'base64':

        let img = new Image()
        img.src = payload

        img.onload = () => {
          callback(img)
        }

        break;
      default:
        throw new Error('Unknown export type')
    }
  }
  cropCanvasAndConvertToBase64(canvas) {

    if(this._isBlank(canvas)) {
      console.warn('Empty Canvas')
      return false
    }

    canvas = this._cloneCanvas(canvas)

    let ctx = canvas.getContext('2d')

    let w = canvas.width
    let h = canvas.height
    let pix = { x:[ ], y:[ ] }
    let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)

    let x
    let y
    let index

    for(y = 0; y < h; y++) {
      for(x = 0; x < w; x++) {
        index = (y * w + x) * 4
        if(imageData.data[ index + 3 ] > 0) {
          pix.x.push(x)
          pix.y.push(y)
        }
      }
    }

    pix.x.sort((a, b) => {
      return a - b
    })

    pix.y.sort((a, b) => {
      return a - b
    })

    let n = pix.x.length - 1

    w = pix.x[ n ] - pix.x[ 0 ]
    h = pix.y[ n ] - pix.y[ 0 ]

    let cut = ctx.getImageData(pix.x[ 0 ], pix.y[ 0 ], w, h)

    canvas.width = w
    canvas.height = h

    ctx.putImageData(cut, 0, 0)

    return {
      file: canvas.toDataURL(),
      x: pix.x[ 0 ],
      y: pix.y[ 0 ]
    }
  }
  _cloneCanvas(old) {

    let n = document.createElement('canvas')
    let ctx = n.getContext('2d')

    n.width = old.width
    n.height = old.height

    ctx.drawImage(old, 0, 0)

    return n
  }
  _isBlank(a) {
    let b = document.createElement('canvas')
    b.width = a.width
    b.height = a.height

    return a.toDataURL() === b.toDataURL()
  }
}

let sketch = new Sketch({
  element: 'element',
  dimensions: {
    width: window.innerWidth,
    height: window.innerHeight
  }
}, {
  pathType: 'bezier',
  dimensions: {
    width: window.innerWidth,
    height: window.innerHeight
  },
  position: {
    x: 0,
    y: 0
  },
  scale: 1,
}, 50)

window.onresize = function() {
  sketch = new Sketch({
    element: 'element',
    dimensions: {
      width: window.innerWidth,
      height: window.innerHeight
    }
  }, {
    pathType: 'bezier',
    dimensions: {
      width: window.innerWidth,
      height: window.innerHeight
    },
    position: {
      x: 0,
      y: 0
    },
    scale: 1,
  }, 50)
}

sketch.onChange(sketch, (scope) => {
  const canUndo = sketch.canUndo()
  const canRedo = sketch.canRedo()
  document.querySelector('#undo').setAttribute('class', (canUndo) ? '' : 'disabled')
  document.querySelector('#redo').setAttribute('class', (canRedo) ? '' : 'disabled')
  document.querySelector('#download').setAttribute('class', (canUndo || canRedo) ? '' : 'disabled')
})

document.getElementById('brush').onclick = function() {
  document.querySelector('#eraser').setAttribute('class', '')
  document.querySelector('#brush').setAttribute('class', 'active')  
  sketch.selectPencil()
}
document.getElementById('eraser').onclick = function() {
  document.querySelector('#eraser').setAttribute('class', 'active')
  document.querySelector('#brush').setAttribute('class', '')
  sketch.selectEraser()
}
document.getElementById('undo').onclick = function() {
  sketch.undo()
}
document.getElementById('redo').onclick = function() {
  sketch.redo()
}
document.getElementById('download').onclick = function() {
  const file = sketch.export()

  if (file) {
    console.log(file)
  }
}
              
            
!
999px

Console