<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>
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;
}
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)
}
}
This Pen doesn't use any external CSS resources.