A Pen by Andreas Borgen on CodePen.
Last active
December 6, 2018 20:08
-
-
Save Sphinxxxx/e8100d76e820fc26adc91a3614f33503 to your computer and use it in GitHub Desktop.
Draw on an image
This file contains hidden or 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
<script> | |
window.onerror = function(msg, url, line) { alert('Error: '+msg+'\nURL: '+url+'\nLine: '+line); }; | |
console.clear(); | |
</script> | |
<script src="https://unpkg.com/[email protected]"></script> | |
<script src="https://unpkg.com/[email protected]"></script> | |
<script src="https://unpkg.com/vanilla-picker@2"></script> | |
<script src="https://unpkg.com/@sphinxxxx/[email protected]"></script> | |
<script src="https://unpkg.com/drag-tracker@1"></script> | |
<script src="https://unpkg.com/simplify-js@1"></script> | |
<script src="https://unpkg.com/undo-manager@1"></script> | |
<section id="app"> | |
<header> | |
<div class="help" :class="{show: viewState.showHelp}" @click="viewState.showHelp = !viewState.showHelp"> | |
<div class="content"> | |
<h1>Draw on an image</h1> | |
<span> | |
Touch or drag to draw,<br/> | |
pinch or scroll-wheel to zoom,<br/> | |
two fingers or right-click to pan. | |
</span> | |
</div> | |
</div> | |
</header> | |
<div id="tools"> | |
<label id="image-input" class="tool-button"> | |
<span>Select image</span> | |
<input type="file" accept="image/*" style="display:none" /> | |
</label> | |
<button id="rotater" class="tool-button" @click.prevent="rotate">Rotate</button> | |
<button id="eraser" class="tool-button" :class="{ active: editor.erasing }" @click.prevent="editor.erasing = !editor.erasing">Erase</button> | |
<div id="stroke-color" class="picker_sample"></div> | |
<label id="stroke-width" class="tool-button"> | |
<span>Stroke</span> | |
<input type="number" v-model.number="svg.stroke.width" @focus="$event.target.select()"/> | |
</label> | |
<button id="undoer" class="tool-button" @click.prevent="undo">Undo</button> | |
<button id="clearer" class="tool-button" @click.prevent="clear">Clear</button> | |
<button id="downloader" class="tool-button" @click.prevent="download">Download</button> | |
</div> | |
<div id="image"> | |
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |
<g id="container"> | |
<g id="image" :transform="`translate(${svg.img.pos})`"> | |
<image :xlink:href="svg.img.url" :width="svg.img.size[0]" :height="svg.img.size[1]" | |
x="0" y="0" :transform="`rotate(${svg.img.rot * 90})`"/> | |
</g> | |
<g id="doodles" stroke-linejoin="round" stroke-linecap="round" fill="none"> | |
<doodle v-for="(d, i) in svg.doodles" v-if="!d.isErased" :dood="d" :di="i"></doodle> | |
</g> | |
</g> | |
</svg> | |
</div> | |
</section> |
This file contains hidden or 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
(function() { | |
"use strict"; | |
const [$, $$] = ABOUtils.DOM.selectors(), | |
_cl = ABOUtils.Debug.log2; | |
//Global state model. Can be changed from within Vue or from the outside. | |
const _svgState = { | |
img: { | |
url: 'https://picsum.photos/900/600?image=836', | |
filename: 'test.jpg', | |
size: [900, 600], | |
pos: [0, 0], | |
rot: 0, | |
}, | |
stroke: { | |
width: 20, | |
color: 'red', | |
}, | |
doodles: [], | |
}, | |
_editorState = { | |
erasing: false, | |
cleared: [], | |
}, | |
_undoer = new UndoManager(); | |
let _svg, _zoomer, | |
_currDoodle, _eraseState; | |
Vue.component('doodle', { | |
template: '<path :d="pathData" :stroke="dood.color" :stroke-width="dood.strokeWidth" :data-index="di" />', | |
props: { | |
dood: Object, | |
di: Number, | |
}, | |
computed: { | |
pathData() { return 'M' + this.dood.points; } | |
}, | |
methods: { | |
}, | |
}); | |
new Vue({ | |
el: '#app', | |
data: { | |
svg: _svgState, | |
editor: _editorState, | |
viewState: { | |
showHelp: false, | |
}, | |
}, | |
computed: { | |
}, | |
watch: { | |
//If the stroke settings are changed, we probably want to stop erasing and draw a new doodle: | |
//https://stackoverflow.com/questions/42133894/vue-js-how-to-properly-watch-for-nested-data | |
'svg.stroke': { | |
handler: function (val, oldVal) { this.editor.erasing = false; }, | |
deep: true, | |
}, | |
}, | |
methods: { | |
rotate() { | |
const img = this.svg.img; | |
console.log('Rot', img.rot); | |
//Rotate everything around the center of what's currently visible: | |
const [left, top, width, height] = _zoomer.getViewBox(), | |
center = [left + width/2, top + height/2]; | |
let allDoods = this.svg.doodles; | |
//Also rotate the cleared doodles, in case the clearing is undone later: | |
_editorState.cleared.forEach(doods => allDoods = allDoods.concat(doods)); | |
allDoods.forEach(d => { | |
console.log(d); | |
d.points = d.points.map(p => rotate90deg(p, center)); | |
}); | |
img.pos = rotate90deg(img.pos, center); | |
img.rot = (img.rot + 1) % 4; | |
}, | |
undo() { | |
_undoer.undo(); | |
}, | |
clear() { | |
const doods = this.svg.doodles; | |
if(doods && doods.length) { | |
const doIt = () => { | |
this.editor.cleared.push(doods); | |
this.svg.doodles = []; | |
} | |
doIt(); | |
_undoer.add({ | |
undo() { _svgState.doodles = _editorState.cleared.pop(); }, | |
redo: doIt, | |
}); | |
} | |
}, | |
download() { exportJPG(); } | |
}, | |
}); | |
function init() { | |
_svg = $('#image svg'); | |
//_img = $('#image svg image'); | |
//Avoid hiding the toolbar behind the browser address bar on mobile: | |
function maxHeight() { | |
document.body.style.maxHeight = window.innerHeight + 'px'; | |
} | |
maxHeight(); | |
setInterval(maxHeight, 1000); | |
/* Image to draw on */ | |
function handleImage(result) { | |
loadImg(result.url, function() { actuallyHandleImage(this, result.url, result.file); }); | |
} | |
function actuallyHandleImage(img, url, file) { | |
_svgState.img = { | |
url: url, | |
filename: file.name, | |
//https://stackoverflow.com/questions/16080273/doesnt-svg-support-auto-width-and-height-for-images | |
size: [ | |
img.naturalWidth || img.width, | |
img.naturalHeight || img.height, | |
], | |
pos: [0, 0], | |
rot: 0, | |
}; | |
_cl('img-load', _svgState.img.size); | |
} | |
ABOUtils.DOM.dropImage(document, handleImage); | |
ABOUtils.DOM.dropImage($('#image-input input'), handleImage); | |
/* Stroke color */ | |
const pickerElm = $('#stroke-color'), | |
picker = new Picker({ | |
parent: pickerElm, | |
popup: 'bottom', | |
onChange: color => { | |
pickerElm.style.color = _svgState.stroke.color = color.rgbaString; | |
}, | |
}); | |
picker.setColor('#f0ba'); | |
/* Zoom/pan */ | |
_zoomer = (function initSvgZoom() { | |
const imgContainer = $('#image'), | |
zoomer = zoomableSvg('#app svg', { | |
container: imgContainer, | |
}); | |
//Mouse interaction: Left-click draws a doodle, so we must pan with right-click | |
let panStart, didPan; | |
function isRightButton(e) { return (e.buttons === 2) || (e.which === 3); } | |
imgContainer.addEventListener('mousedown', e => { | |
if(!isRightButton(e)) { return; } | |
panStart = [e.clientX, e.clientY]; | |
didPan = false; | |
}); | |
imgContainer.addEventListener('mousemove', e => { | |
if(!isRightButton(e)) { return; } | |
const panPos = [e.clientX, e.clientY], | |
panDiff = new zoomer.Coord(panPos[0] - panStart[0], panPos[1] - panStart[1]); | |
zoomer.moveViewport(panDiff); | |
panStart = panPos; | |
if(panDiff.x || panDiff.y) { didPan = true; } | |
}); | |
imgContainer.addEventListener('contextmenu', e => { | |
if(didPan) { e.preventDefault(); } | |
}); | |
return zoomer; | |
})(); | |
/* Draw doodles */ | |
(function initDraw() { | |
let cancelledByClick = false; | |
dragTracker({ | |
container: _svg, | |
//propagateEvents: true, | |
callbackDragStart: (_, pos) => { | |
//Remove focus from toolbar: | |
if(picker.domElement) { picker.closeHandler({ type: 'mousedown' }); }; | |
document.activeElement.blur(); | |
if(_editorState.erasing) { | |
eraseStart(pos); | |
} | |
else { | |
doodleStart(pos); | |
} | |
}, | |
callback: (_, pos) => { | |
if(_editorState.erasing) { | |
eraseMove(pos); | |
} | |
else { | |
doodleAppend(pos); | |
} | |
}, | |
callbackDragEnd: (_, pos, start, cancelled) => { | |
//We do allow clicking to erase a single doodle: | |
if(cancelledByClick && _editorState.erasing) { cancelled = false; } | |
cancelledByClick = false; | |
_eraseState = null; | |
//Multi-touch (pinch-to-zoom) | |
//Cancel current drawing or erasing session: | |
if(cancelled) { | |
console.log('Cancelled', cancelled); | |
_undoer.undo(); | |
} | |
else if(_editorState.erasing) { | |
} | |
else { | |
doodleEnd(pos); | |
} | |
}, | |
callbackClick: () => { | |
cancelledByClick = true; | |
//TODO: Trigger a click on a random element to remove focus from the toolbar | |
//https://gomakethings.com/how-to-simulate-a-click-event-with-javascript/ | |
// $$1('header').click(); | |
}, | |
}); | |
})(); | |
} | |
function getSvgPos(pos, doodle) { | |
const coord = _zoomer.vp2vb({ | |
x: pos[0], | |
y: pos[1], | |
}); | |
return [coord.x, coord.y]; | |
} | |
function doodleStart(pos) { | |
const curr = _currDoodle = { | |
color: _svgState.stroke.color, | |
strokeWidth: _svgState.stroke.width, | |
isErased: false, | |
}; | |
curr.points = [getSvgPos(pos, curr)]; | |
const doods = _svgState.doodles; | |
doods.push(curr); | |
_undoer.add({ | |
undo() { doods.pop(); }, | |
redo() { doods.push(curr); } | |
}); | |
} | |
function doodleAppend(pos) { | |
_currDoodle.points.push(getSvgPos(pos, _currDoodle)); | |
} | |
function doodleEnd(pos) { | |
const curr = _currDoodle; | |
//http://mourner.github.io/simplify-js/ | |
const ptsXY = curr.points.map(p => ({ x: p[0], y: p[1] }) ), | |
ptsSimpl = simplify(ptsXY, Math.sqrt(curr.strokeWidth/2)).map(p => [p.x, p.y]); | |
curr.points = ptsSimpl; | |
_cl('simplified', ptsXY.length, curr.points.length); | |
} | |
function erasePoint(pos) { | |
const svgPos = _svg.getBoundingClientRect(), | |
x = pos[0] + svgPos.left, | |
y = pos[1] + svgPos.top; | |
//https://stackoverflow.com/questions/3918842/how-to-find-out-the-actual-event-target-of-touchmove-javascript-event | |
const currHover = document.elementFromPoint(x, y); | |
if(currHover && (currHover.nodeName === 'path')) { | |
const i = currHover.dataset.index, | |
dood = _svgState.doodles[i]; | |
if(dood) { | |
dood.isErased = true; | |
_eraseState.erased.push(dood); | |
} | |
} | |
} | |
function eraseStart(pos) { | |
const state = _eraseState = { | |
prevPos: pos, | |
erased: [], | |
} | |
_undoer.add({ | |
undo() { state.erased.forEach(d => d.isErased = false); }, | |
redo() { state.erased.forEach(d => d.isErased = true); } | |
}); | |
erasePoint(pos); | |
} | |
function eraseMove(pos) { | |
const [x0, y0] = _eraseState.prevPos.map(Math.round), | |
[x1, y1] = pos.map(Math.round); | |
_eraseState.prevPos = pos; | |
//Use Bresenham's line algorithm to check all screen pixels we moved across since the last time. | |
//If we don't do this, it's easy to skip doodles when erasing fast: | |
//http://members.chello.at/easyfilter/bresenham.html | |
const dx = Math.abs(x1 - x0), | |
dy = -Math.abs(y1 - y0), | |
sx = (x0 < x1) ? 1 : -1, | |
sy = (y0 < y1) ? 1 : -1; | |
let [x, y] = [x0, y0], | |
err = dx + dy, | |
e2; | |
for (;;) { | |
erasePoint([x, y]); | |
if (x === x1 && y === y1) { break; } | |
e2 = 2 * err; | |
if (e2 >= dy) { err += dy; x += sx; } | |
if (e2 <= dx) { err += dx; y += sy; } | |
} | |
} | |
function rotate90deg(point, center) { | |
const [cx, cy] = center, | |
dx = cy - point[1], | |
dy = point[0] - cx; | |
const newPoint = [cx + dx, cy + dy]; | |
return newPoint; | |
} | |
//Export JPG: | |
function exportJPG() { | |
_cl('export'); | |
const [xMin, yMin, w, h] = findViewBox(); | |
_cl('..svg-vb', xMin, yMin, w, h); | |
/* Serialize the SVG and create a URL for it */ | |
const tmpSvg = _svg.cloneNode(true); | |
tmpSvg.setAttribute('viewBox', '' + [xMin, yMin, w, h]); | |
tmpSvg.setAttribute('width', w); | |
tmpSvg.setAttribute('height', h); | |
//The SVG's embedded source image doesn't transfer to 'img', | |
//so we must paint the canvas in two steps | |
const svgImg = tmpSvg.querySelector('image'); | |
svgImg.parentNode.removeChild(svgImg); | |
const serializer = new XMLSerializer(), | |
svgCode = serializer.serializeToString(tmpSvg), | |
svgBlob = new Blob([svgCode], { type: 'image/svg+xml' }), | |
svgUrl = URL.createObjectURL(svgBlob); | |
_cl('..svg-blob', /*svgCode,*/ svgUrl); | |
/* Create a blank canvas, and paint the SVG on it */ | |
const canvas = document.createElement('canvas'), | |
ctx = canvas.getContext('2d'); | |
canvas.width = w; | |
canvas.height = h; | |
//https://stackoverflow.com/questions/27736288/how-to-fill-the-whole-canvas-with-specific-color | |
ctx.fillStyle = 'white'; | |
ctx.fillRect(0, 0, w, h); | |
function handleImg() { | |
const img = this; | |
_cl('..loaded', img.src); | |
//The SVG's embedded source image doesn't transfer to 'img', | |
//so we must paint the canvas in two steps. | |
//In the first step, where we only paint the source image, | |
//we must place it correctly within the viewBox we found above: | |
const step0 = (img.src !== svgUrl); | |
if(step0) { | |
//If the drawing has been rotated, we must calculate the correct position for the image | |
const state = _svgState.img, | |
x = state.pos[0] - xMin, | |
y = state.pos[1] - yMin; | |
//https://stackoverflow.com/questions/17411991/html5-canvas-rotate-image | |
ctx.translate(x, y); | |
ctx.rotate(state.rot * Math.PI/2); | |
ctx.drawImage(img, 0,0); | |
ctx.setTransform(1,0,0,1,0,0); | |
//We have painted the source image. | |
//Now load and paint the doodles on top of that: | |
loadImg(svgUrl, handleImg); | |
} | |
else { | |
//Paint the doodles.. | |
ctx.drawImage(img, 0,0); | |
//..and export the JPG | |
const quality = .9, //JPG at 90% quality | |
name = _svgState.img.filename; | |
canvas.toBlob(b => downloadBlob(b, name), 'image/jpeg', quality); | |
} | |
} | |
//The SVG's embedded source image doesn't transfer to 'img', | |
//so we must paint the canvas in two steps | |
loadImg(_svgState.img.url /*svgUrl*/, handleImg); | |
//document.body.appendChild(tmpSvg); | |
//document.body.appendChild(img); | |
//document.body.appendChild(canvas); | |
} | |
function findViewBox() { | |
const extent = $('#container').getBBox(); | |
return [extent.x, extent.y, extent.width, extent.height].map(Math.round); | |
} | |
function loadImg(url, callback) { | |
const img = new Image(); | |
//Avoids error "Failed to execute 'toBlob' on 'HTMLCanvasElement': Tainted canvases may not be exported." | |
//https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image | |
img.crossOrigin = "Anonymous"; | |
//https://stackoverflow.com/questions/28545619/javascript-which-parameters-are-there-for-the-onerror-event-with-image-objects | |
img.onerror = function(e) { | |
alert(`ERROR loading image (${url}): ${e.message}`); | |
} | |
img.onload = callback; | |
img.src = url; | |
} | |
function downloadBlob(blob, filename) { | |
_cl('download'); | |
const url = (typeof blob === 'string') ? blob : URL.createObjectURL(blob), | |
link = document.createElement('a'); | |
link.setAttribute('href', url); | |
link.setAttribute('download', filename); | |
link.style.display = 'none'; | |
document.body.appendChild(link); | |
_cl('..click'); | |
link.click(); | |
document.body.removeChild(link); | |
//Edge needs a little time before the blob goes away.. | |
if(url !== blob) { setTimeout(() => URL.revokeObjectURL(url), 1000); } | |
} | |
window.addEventListener('load', init); | |
})(); |
This file contains hidden or 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
//https://robots.thoughtbot.com/sasss-content-directive | |
@mixin mobile-header() { | |
@media(max-width: 420px) { | |
font-size: 3.8vw; | |
@content; | |
} | |
} | |
body { | |
margin: 0; | |
height: 100vh; | |
font-family: Georgia, sans-serif; | |
//background: skyblue; | |
h1, h2 { | |
margin: .5em 0; | |
} | |
h1 { | |
font-size: 1.3em; | |
//text-align: center; | |
} | |
h2 { | |
font-size: 1.1em; | |
} | |
button { | |
padding: .5em; | |
font: inherit; | |
} | |
input { | |
font: inherit; | |
box-sizing: border-box; | |
&[type="number"] { | |
width: 3em; | |
text-align: right; | |
} | |
} | |
} | |
body, #app { | |
display: flex; | |
flex-flow: column nowrap; | |
#app { | |
flex: 1 1 auto; | |
//margin: 1em; | |
margin-top: 0; | |
//background: lightskyblue; | |
@media (max-width: 600px) { | |
margin: 0; | |
} | |
} | |
} | |
header { | |
background: #444; | |
//width: 100vw; | |
overflow: hidden; | |
h1 { | |
margin-top: 0; | |
//color: white; | |
line-height: 1; | |
} | |
.help { | |
position: absolute; | |
top:0; right:0; | |
z-index: 99; | |
margin: 0; | |
padding: .5em 1em; | |
padding-right: 3em; | |
box-sizing: border-box; | |
min-width: 2.4em; | |
min-height: 2.6em; | |
overflow: hidden; | |
@include mobile-header() { | |
.content { | |
font-size: 1.2em; | |
} | |
} | |
background: yellow; | |
cursor: pointer; | |
&:not(.show) { | |
padding: 0; | |
background: transparent; | |
h1, span { | |
display: none; | |
} | |
} | |
&::after { | |
content: '?'; | |
display: block; | |
position: absolute; | |
top: -.8em; right: -1.2em; | |
font-size: 1.5em; | |
width: 1em; | |
height: 1em; | |
line-height: 1; | |
text-align: center; | |
box-sizing: content-box; | |
padding: .5em .6em .2em .1em; | |
border-radius: 100%; | |
border: .5em solid yellow; | |
background: orange; | |
} | |
} | |
} | |
#tools, #image { | |
outline: 1px solid silver; | |
} | |
#tools { | |
display: flex; | |
flex-flow: row wrap; | |
align-items: center; | |
//padding-top: .5em; | |
//padding-left: .5em; | |
background: #ccc; | |
$size: 2.7em; | |
$group-spacing: .5em; | |
@include mobile-header() { | |
.picker_wrapper { | |
font-size: 3vw; | |
} | |
} | |
> * { | |
flex: 0 0 auto; | |
//margin-right: .5em; | |
//margin-bottom: .5em; | |
&:last-child { | |
margin-right: 0; | |
} | |
} | |
.tool-button { | |
display: inline-block; | |
position: relative; | |
width: $size; | |
height: $size; | |
padding: 0; | |
box-sizing: border-box; | |
border: none; | |
border-left: 1px solid silver; | |
//outline: none; | |
text-indent: $size * 2; | |
color: #333; | |
background-color: whitesmoke; | |
background-position: center; | |
background-size: contain; | |
background-repeat: no-repeat; | |
//box-shadow: inset 0 0 10px 5px rgba(gray, .2); | |
transition: opacity .2s; | |
box-sizing: border-box; | |
overflow: hidden; | |
cursor: pointer; | |
&:hover/*, &.active*/ { | |
background-color: paleturquoise; | |
//color: white; | |
} | |
} | |
#image-input { | |
line-height: $size; | |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none' stroke='black' stroke-width='4'%3E %3Crect x='9' y='9' width='82' height='72' stroke-width='2'/%3E %3Cpath d='M10,75 l30,-30 l30,30 m-10,-10 l30,-30 M25,60 a27,27 0 1,1 50,-10'/%3E %3C/svg%3E"); | |
} | |
#rotater { | |
margin-right: $group-spacing; | |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none' stroke='black' stroke-width='12'%3E %3Cpath d='M20,70 v-60' stroke-dasharray='12'/%3E %3Cpath d='M30,80 h60'/%3E %3Cpath d='M35,20 a40,40 0 0,1 40,40 m-20,-13 20,13 13,-20'/%3E %3C/svg%3E"); | |
} | |
#eraser { | |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none' stroke='black' stroke-width='12'%3E %3Cpath d='M15,17 c90,-20 0,100, 70,60'/%3E%3C/svg%3E"); | |
&.active { | |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none' stroke='black' stroke-width='12'%3E %3Cpath d='M15,17 c90,-20 0,100, 70,60' stroke-dasharray='50 40 999' /%3E %3Cpath d='M37,30 l40,40 m0,-40 -40,40' /%3E %3C/svg%3E"); | |
} | |
} | |
#stroke-color { | |
display: inline-block; | |
width: $size; | |
height: $size; | |
//.picker_sample overrides: | |
min-height: 0; | |
order: 0; | |
} | |
#stroke-width { | |
$pad: .2em; | |
width: 3.5em; | |
padding: $pad; | |
margin-right: $group-spacing; | |
span { | |
display: block; | |
//font-size: .5em; | |
color: transparent; //Hide the text | |
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='70'%3E%3Cpath fill='black' d='M10,30 c120,-50 240,50, 360,-30 v50 c-120,60 -240,-50, -360,-20'/%3E%3C/svg%3E") top center / contain no-repeat; | |
} | |
input { | |
position: absolute; | |
bottom: $pad; left:0; right:0; | |
margin: 0 auto; | |
} | |
} | |
#undoer { | |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none' stroke='black' stroke-width='12'%3E %3Cpath d='M20,13 v30 h30 m-30,0 a35,35 0 1,1 50,44'/%3E %3C/svg%3E"); | |
} | |
#clearer { | |
margin-right: $group-spacing; | |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none' stroke='black' stroke-width='12'%3E %3Cpath d='M20,20 l60,60 M20,80 l60,-60'/%3E %3C/svg%3E"); | |
} | |
#downloader { | |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none' stroke='black' stroke-width='12'%3E %3Cpath d='M50,15 v45 m-25,-25 l25,25 25,-25 M10,80 h80'/%3E %3C/svg%3E"); | |
} | |
} | |
#image { | |
flex: 1 1 auto; | |
position: relative; | |
min-height: 50vh; | |
svg { | |
display: block; | |
position: absolute; | |
top:0; left:0; | |
width: 100%; | |
height: 100%; | |
background: white; | |
//Must be inlined so it's included in the downloaded images | |
// #doodles { | |
// stroke-linecap: round; | |
// stroke-linejoin: round; | |
// } | |
} | |
} | |
pre { | |
flex: 0 0 auto; | |
height: 20em; | |
overflow: auto; | |
background: white; | |
color: #888; | |
border: 1px solid gainsboro; | |
} | |
/* Color picker overrides */ | |
#tools .popup.popup_bottom { | |
& { | |
left: -12em; | |
} | |
.picker_arrow { | |
left: 11.5em; | |
transform: none; | |
&::before { | |
transform-origin: 100% 0; | |
transform: rotate(45deg); | |
} | |
&::after { | |
left: -70%; | |
width: 200%; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment