-
-
Save artrayd/989fdb9718aaffa16a4346aaa89c9bac to your computer and use it in GitHub Desktop.
Draw <svg> inside the browser with finger or mouse. Adopted for vuejs. Demo: https://artrayd.com/freehand-svg/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div id="app"> | |
<FreehandDrawSVG | |
v-bind:title="drawProps.title" | |
v-bind:colors="drawProps.colors" | |
v-bind:bgColors="drawProps.bgColors" | |
v-bind:lineColor = "drawProps.lineColor" | |
v-bind:bgColor = "drawProps.bgColor" | |
v-on:changeLineColor = "changeLineColor" | |
v-on:changeBgColor = "changeBgColor" | |
v-on:drawSubmit="saveDrawing" | |
> | |
</FreehandDrawSVG> | |
</div> | |
</template> | |
<script> | |
import FreehandDrawSVG from './components/FreehandDrawSVG.vue' | |
export default { | |
name: 'app', | |
components: { | |
FreehandDrawSVG | |
}, | |
data() { | |
return { | |
drawProps: | |
{ | |
title: "Freehand SVG Draw", | |
colors: ['#EAEAEA','#292929', '#00A0FF', '#FF29AA', '#FFE500', '#FF3030', '#12EB0A'], | |
bgColors: ['#EAEAEA','#292929', '#00A0FF', '#FF29AA', '#FFE500', '#FF3030', '#12EB0A'], | |
lineColor: "#292929", | |
bgColor: "#EAEAEA" | |
} | |
} | |
}, | |
methods: { | |
changeLineColor: function(color){ | |
this.drawProps.lineColor = color; | |
}, | |
changeBgColor: function(color){ | |
this.drawProps.bgColor = color; | |
}, | |
saveDrawing: function(drawSvg){ | |
console.log (drawSvg) | |
} | |
}, | |
mounted: function() { | |
} | |
} | |
</script> | |
<style> | |
body, html{ | |
font-family: 'Helvetica Neue', Helvetica, arial, sans-serif; | |
background: #131415; | |
margin: 0; | |
min-height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
width: 100% | |
} | |
#app { | |
font-family: 'Avenir', Helvetica, Arial, sans-serif; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
text-align: center; | |
color: #2c3e50; | |
min-height: 100%; | |
width: 100%; | |
display: flex; | |
justify-content: center; | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.canvas{ | |
width: 720px; | |
height: 480px; | |
position: relative; | |
cursor: none; | |
border: 1px solid rgba(255, 255, 255, 0.5); | |
overflow: hidden; | |
} | |
.canvas h2{ | |
position: absolute; | |
font-weight: 400; | |
width: 100%; | |
text-align: center; | |
z-index: 2; | |
top: 50%; | |
transform: translateY(-50%); | |
pointer-events: none; | |
} | |
.canvas .drawSvg{ | |
width: 100%; | |
height: 100%; | |
} | |
.draw .buttons{ | |
width: 100%; | |
left: 0; | |
top: 0; | |
margin-top: 24px; | |
display: flex; | |
justify-content: flex-start; | |
opacity: 0; | |
transition: all .2s; | |
} | |
.draw .showButtons{ | |
opacity: 1; | |
} | |
.draw .btn{ | |
color: #fff; | |
background: none; | |
position: relative; | |
border: none; | |
margin-left: 0; | |
font-size: 16px; | |
margin-bottom: 12px; | |
cursor: pointer; | |
opacity: 0.75; | |
transition: opacity .2s; | |
margin-right: 8px; | |
} | |
.draw .btn:hover{ | |
opacity: 1; | |
} | |
.draw .btn:focus{ | |
border: none; | |
outline: none; | |
} | |
.draw .undoBtn{ | |
justify-self: flex-end; | |
margin-left: auto; | |
} | |
.draw .toolbar{ | |
position: fixed; | |
/* right: 0;*/ | |
display: flex; | |
background: #2E2F30; | |
border-radius: 6px 0 0 6px; | |
padding: 12px; | |
top: 52%; | |
transform: translateY(-52%); | |
transition: 0.3s ease; | |
} | |
.draw .toolbar:hover{ | |
right: 0!important; | |
} | |
.draw .toolbar .allColors ul{ | |
display: flex; | |
flex-direction: row; | |
padding-left: 24px; | |
} | |
.draw .toolbar li{ | |
width: 40px; | |
height: 40px; | |
list-style: none; | |
margin-top: 18px; | |
margin-right: 12px; | |
border-radius: 50%; | |
border: 1px solid rgba( 255, 255, 255, .2); | |
cursor: pointer; | |
transition: all .3s ease-out; | |
} | |
.draw .toolbar .bgColor li{ | |
border-radius: 8px; | |
} | |
.draw .toolbar .activeColor{ | |
border: 1px solid rgba( 255, 255, 255, .5); | |
transform: scale(1.15); | |
margin-right: 60px; | |
} | |
.draw .toolbar li::after{ | |
content: ''; | |
position: absolute; | |
right: 0; | |
display: block; | |
height: 40px; | |
width: 0; | |
background: #434345; | |
transition: all .3s ease-out; | |
} | |
.draw .toolbar .activeColor::after{ | |
content: ''; | |
position: absolute; | |
right: -30px; | |
display: block; | |
height: 40px; | |
width: 1px; | |
background: #434345; | |
} | |
#cursor{ | |
top: 50%; | |
left: 50%; | |
} | |
#cursor, .dot { | |
display: block; | |
width: 5px; | |
height: 5px; | |
border-radius: 100%; | |
position: absolute; | |
opacity: .5; | |
pointer-events: none; | |
transition: opacity .1s ease-in-out; | |
} | |
.drawSvg rect{ | |
transition: fill .2s; | |
} | |
.draw ul li.bgColor{ | |
border-radius: 4px; | |
} | |
.mobile{ | |
display: none; | |
} | |
@media only screen and (max-width: 720px) { | |
body, html{ | |
background: #2E2F30 !important; | |
align-items: flex-start !important; | |
} | |
.draw{ | |
height: 100%; | |
width: 100%; | |
display: flex; | |
flex-direction: column-reverse; | |
} | |
.canvas{ | |
top: 70px; | |
} | |
.drawSvg{ | |
top: 0; | |
} | |
.mobile{ | |
display: flex; | |
} | |
.draw .buttons{ | |
position: fixed; | |
top: 0; | |
height: 56px; | |
width: 100%; | |
padding: 0; | |
padding-top: 12px; | |
margin: 0; | |
justify-content: flex-start; | |
background: #2E2F30; | |
z-index: 2; | |
} | |
.draw .buttons .btn{ | |
margin-left: 12px; | |
} | |
.draw .toolbar{ | |
position: relative; | |
top: auto; | |
transform: translateY(0); | |
right: 0 !important; | |
width: 100%; | |
margin-top: 60px; | |
padding: 0; | |
} | |
.draw .toolbar ul{ | |
margin-top: 20px; | |
padding-top: 20px; | |
border-top: 1px solid #434345; | |
} | |
.draw .toolbar li{ | |
margin: auto; | |
} | |
.allColors{ | |
width: 100%; | |
margin-top: 100px; | |
} | |
.draw .toolbar .allColors ul{ | |
flex-wrap: wrap; | |
width: 100%; | |
justify-content: center; | |
padding-left: 0; | |
} | |
.draw .toolbar .activeColor{ | |
position: absolute; | |
top: 40px; | |
margin-right: 12px; | |
} | |
.lineColor .activeColor{ | |
right: 35%; | |
} | |
.bgColor .activeColor{ | |
left: 35%; | |
} | |
.draw .toolbar .activeColor::after{ | |
display: none; | |
} | |
.wrap .draw .buttons .btn{ | |
margin-top: 0; | |
border: none; | |
font-size: 18px; | |
margin: auto; | |
margin-left: 12px; | |
} | |
.draw .buttons .undoBtn{ | |
top: -4px; | |
margin: auto; | |
margin-right: 24px; | |
} | |
#cursor{ | |
display: none; | |
} | |
.drawSvg, .canvas{ | |
width: 100%; | |
height: 360px; | |
box-sizing: border-box; | |
} | |
.action .canvas .drawSvg{ | |
width: 100%; | |
height: 360px; | |
max-height: 100%; | |
} | |
.drawUi{ | |
width: 100%; | |
flex-direction: column-reverse; | |
} | |
.drawUi ul{ | |
display: flex; | |
margin: 0; | |
margin-top: 24px; | |
flex-wrap: wrap; | |
} | |
.drawUi ul li{ | |
margin: 6px; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div class="draw"> | |
<div class="toolbar" :style="{ right: toolBarRight }"> | |
<div class="allColors"> | |
<div class="panelUp mobile"> | |
</div> | |
<transition-group class="lineColor" name="color-list" tag="ul" > | |
<li v-for="(color, $index) in colors" v-bind:style='{ "background-color": color }' v-on:click="changeLineColor(color, $index)" v-bind:key="color"></li> | |
</transition-group> | |
<transition-group class="bgColor" name="color-list" tag="ul" > | |
<li v-for="(color, $index) in bgColors" v-bind:key="color" v-bind:style='{ "background-color": color }' @click="changeBg(color, $index)"></li> | |
</transition-group> | |
</div> | |
</div> | |
<div class="buttons" v-bind:class="{ showButtons: undo }" > | |
<button v-on:click="download()" class="btn submitBtn">Download</button> | |
<button v-on:click="clean()" class="btn clearBtn">Clear</button> | |
<button v-on:click="undoLine()" class="btn undoBtn" >Undo</button> | |
</div> | |
<div class="canvas"> | |
<h2 v-bind:style='{ "color": lineColor }' class="noselect" v-if="!undo"> {{ title }} </h2> | |
<!-- Canvas size is defined in CSS, search for ".canvas" --> | |
<svg xmlns=http://www.w3.org/2000/svg version="1.1" class="drawSvg" :width="canvasWidth" :height="canvasHeight" | |
@mousedown="linestart()" | |
@touchstart="linestart()" | |
@mousemove="lineMove()" | |
@touchmove="lineMove()" | |
@mouseup="lineEnd()" | |
@touchend="lineEnd()" | |
@onmouseleave="outOfCanvas()" | |
@touchcancel="outOfCanvas()" | |
> | |
<rect id="bg" width="100%" height="100%" v-bind:fill="bgColor"></rect> | |
</svg> | |
<div id="cursor" v-bind:style='{ "background-color": lineColor }'></div> | |
</div> | |
</div> | |
</template> | |
<script> | |
require('../assets/css/freehandDraw.css') | |
// hello jquery | |
const $ = document.querySelector.bind(document); | |
const $$ = document.querySelectorAll.bind(document); | |
export default { | |
name: 'Draw', | |
props: [ | |
'title', | |
'colors', | |
'bgColors', | |
'lineColor', | |
'bgColor', | |
], | |
data () { | |
return { | |
board: '', | |
cursor: '', | |
colorNum: 0, | |
gesture: false, | |
line: '', | |
radius: 2.5, | |
width: 8, | |
undo: false, | |
onCanvas: false // mouseout event is not firing, dunno why | |
} | |
}, | |
computed: { | |
canvasWidth: function(){ | |
return this.board.clientWidth | |
}, | |
canvasHeight: function(){ | |
return this.board.clientHeight | |
}, | |
toolBarRight:{ | |
get: function(){ | |
return -this.colors.length * 51.5 + 'px' | |
}, | |
set: function(newVal){ | |
// this.toolBarRight = newVal // This is more right way to do that, but it won't work( Did the same with css hover | |
} | |
} | |
// requuires some setters, I don't understand it. | |
// rect: function(){ | |
// return this.board.getBoundingClientRect(); | |
// }, | |
// cursorX: function(){ | |
// let rect = this.board.getBoundingClientRect(); | |
// return Math.round( event.clientX - this.rect.x ) || Math.round(event.changedTouches[0].clientX - this.rect.x) | |
// }, | |
// cursorY: function(){ | |
// return Math.round( event.clientY - this.rect.y ) || Math.round(event.changedTouches[0].clientY - this.rect.y) | |
// } | |
}, | |
methods:{ | |
initBoard: function(){ | |
this.board = $('.drawSvg') | |
this.cursor = $('#cursor') | |
this.gesture = false | |
}, | |
linestart: function(){ | |
this.undo = true; | |
let e = event | |
// this.line += 'M' + this.cursorX + ',' + this.cursorY | |
// I prefer to use code above with computed valuse, but it drops some errors about setter, and I didn't succeed to solve it in 15 minutes. | |
let rect = this.board.getBoundingClientRect(); | |
let cursorX = Math.round( e.clientX - rect.x ) || Math.round(e.changedTouches[0].clientX - rect.x) | |
let cursorY = Math.round( e.clientY - rect.y ) || Math.round(e.changedTouches[0].clientY - rect.y) | |
this.line += 'M' + cursorX + ',' + cursorY | |
this.cursor.style.opacity = 1 | |
this.gesture = true | |
e.preventDefault() | |
}, | |
lineMove: function(){ | |
let e = event | |
let rect = this.board.getBoundingClientRect(); | |
let cursorX = Math.round( e.clientX - rect.x ) || Math.round(e.changedTouches[0].clientX - rect.x) | |
let cursorY = Math.round( e.clientY - rect.y ) || Math.round(e.changedTouches[0].clientY - rect.y) | |
if ( this.gesture == true ){ | |
this.line += 'L' + cursorX + ',' + cursorY | |
// this.line += 'L'+(e.clientX||e.touches[0].clientX)+','+(e.clientY||e.touches[0].clientY)+' ' | |
this.trace((e.clientX||e.touches[0].clientX), (e.clientY||e.touches[0].clientY)) | |
} | |
this.cursor.style.top = e.clientY - rect.y - this.radius+'px' | |
this.cursor.style.left = e.clientX - rect.x - this.radius+'px' | |
this.cursorX = e.clientX - rect.x | |
this.cursorY = e.clientY - rect.y | |
this.onCanvas = true | |
}, | |
trace: function(x,y){ | |
let dot = document.createElement('div'); | |
dot.classList.add('dot') | |
dot.style.top = y-this.radius+'px'; | |
dot.style.left = x-this.radius+'px'; | |
dot.style.background = this.lineColor; | |
dot.style.width = dot.style.height = this.radius*2+'px'; | |
document.body.appendChild(dot); | |
setTimeout(function(){dot.style.opacity=0},500); | |
setTimeout(function(){document.body.removeChild(dot)},1000); | |
}, | |
lineEnd: function(){ | |
let e = event; | |
let rect = this.board.getBoundingClientRect(); | |
let cursorX = Math.round( e.clientX - rect.x ) || Math.round(e.changedTouches[0].clientX - rect.x); | |
let cursorY = Math.round( e.clientY - rect.y ) || Math.round(e.changedTouches[0].clientY - rect.y); | |
this.line += 'L' + cursorX + ',' + cursorY; | |
// this.line += 'L'+(e.clientX||e.changedTouches[0].clientX)+','+(e.clientY||e.changedTouches[0].clientY) | |
this.cursor.style.opacity = .5 | |
let path = document.createElementNS('http://www.w3.org/2000/svg','path'); | |
path.setAttributeNS(null,'d',this.line); | |
path.setAttributeNS(null,'fill','none'); | |
path.setAttributeNS(null,'stroke-linecap','round'); | |
path.setAttributeNS(null,'stroke-linejoin','round'); | |
path.setAttributeNS(null,'stroke', this.lineColor); | |
path.setAttributeNS(null,'stroke-width', this.width); | |
this.board.appendChild(path); | |
// this.board.innerHTML = this.board.innerHTML // force SVG repaint after DOM change | |
this.gesture = false; | |
this.line = ''; | |
// saving to local storage | |
// localStorage.svg = new XMLSerializer().serializeToString(this.board) | |
}, | |
// For changing color positions inside toolbar | |
arrayMove: function (arr, fromIndex, toIndex) { | |
var element = arr[fromIndex]; | |
arr.splice(fromIndex, 1); | |
arr.splice(toIndex, 0, element); | |
}, | |
rgb2hex: function(rgb) { | |
if ( rgb.search("rgb") == -1 ) { | |
return rgb; | |
} else { | |
rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))?\)$/); | |
function hex(x) { | |
return ("0" + parseInt(x).toString(16)).slice(-2); | |
} | |
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); | |
} | |
}, | |
setActiveColor: function(Li){ | |
let allColors = $$(Li) | |
let target = event.target // for set time out | |
for (let color of allColors) { | |
color.classList.remove('activeColor') | |
} | |
setTimeout (function(){ | |
target.classList.add('activeColor') | |
}, 10) | |
}, | |
setActiveColorMounted: function(li, arr, color){ | |
let allLi = $$(li) | |
let colorIndex = arr.indexOf(color) | |
for (let li of allLi) { | |
let liColor = this.rgb2hex(li.style.backgroundColor).toUpperCase() | |
if( liColor === color){ | |
li.classList.add('activeColor') | |
this.arrayMove( arr, colorIndex, 0 ) | |
} | |
} | |
}, | |
changeLineColor(color, index){ | |
window.scrollTo({top: 0, behavior: 'smooth'}); | |
this.$emit( 'changeLineColor', color ) | |
this.setActiveColor('.lineColor li') | |
// changing circle position | |
this.arrayMove( this.colors, index, 0 ) | |
}, | |
changeBg(color, index){ // just change backgrounds colors one after another | |
window.scrollTo({top: 0, behavior: 'smooth'}); | |
this.$emit( 'changeBgColor', color ) | |
this.setActiveColor('.bgColor li') | |
$('.drawSvg #bg').setAttribute('fill', this.bgColor ) | |
this.arrayMove( this.bgColors, index, 0 ) | |
// this.board.innerHTML = this.board.innerHTML // force SVG repaint after DOM change | |
}, | |
undoLine: function(){ | |
let paths = $$('.drawSvg path'); | |
$('.drawSvg').removeChild(paths[paths.length-1]) | |
}, | |
clean: function (){ | |
this.line = ''; | |
this.undo = false; | |
let paths = $$('.drawSvg path').length | |
let i = 0 | |
for (i=0;i<paths;i++){ | |
this.board.removeChild(this.board.querySelectorAll('path')[0]) | |
} | |
// this.board.innerHTML = ''; more effective, but removes bg also | |
}, | |
download: function(){ | |
var dl = document.createElement("a"); | |
let drawing = this.svgDataURL( $('.drawSvg') ) | |
console.log (drawing) | |
document.body.appendChild(dl); // This line makes it work in Firefox. | |
dl.setAttribute("href", drawing) | |
dl.setAttribute("download", "freehand-svg-drawing.svg"); | |
dl.click(); | |
}, | |
svgDataURL: function(){ | |
// let drawSvg = new XMLSerializer().serializeToString(this.board) | |
let serialSvg = (new XMLSerializer).serializeToString(this.board); | |
return "data:image/svg+xml," + encodeURIComponent(serialSvg); | |
// this.$emit('drawSubmit', drawSvg); | |
}, | |
}, | |
mounted: function () { | |
this.initBoard() | |
this.colors | |
this.setActiveColorMounted('.lineColor li', this.colors, this.lineColor) | |
this.setActiveColorMounted('.bgColor li', this.bgColors, this.bgColor) | |
}, | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment