|
(function(undefined) { |
|
const ud = ABOUtils.DOM, |
|
ug = ABOUtils.Geom, |
|
us = ABOUtils.SVG; |
|
const [$$1, $$] = ud.selectors(); |
|
|
|
var svgInput = document.getElementById('svgInput'); |
|
var svgOutput = document.getElementById('svgOutput'); |
|
|
|
|
|
/*Sync editor and image*/ |
|
//http://stackoverflow.com/questions/2823733/textarea-onchange-detection |
|
svgInput.addEventListener('input', e => refreshOutput(true)); |
|
refreshOutput(); |
|
|
|
function refreshOutput(save) { |
|
const svg = svgInput.value |
|
//http://stackoverflow.com/questions/584751/inserting-html-into-a-div |
|
svgOutput.innerHTML = svg; |
|
console.log('refresh', save); |
|
if(save) { |
|
localStorage.ABO_svg_editor = svg; |
|
} |
|
|
|
const uri = svgURI(svg); |
|
document.querySelector('#uri').textContent = `${uri}\nurl("${uri}")`; |
|
} |
|
|
|
|
|
/*Output coordinates when clicking the SVG*/ |
|
svgOutput.onclick = function(e) { |
|
if(!$$1('#logCoords').checked) { return; } |
|
|
|
var svgBounds = svgOutput.querySelector('svg') |
|
.getBoundingClientRect(), |
|
svgPosX = e.clientX - svgBounds.left, |
|
svgPosY = e.clientY - svgBounds.top, |
|
svgPos = [Math.round(svgPosX), Math.round(svgPosY)]; |
|
|
|
//console.log(svgPos); |
|
editor.insert('<!-- ' + svgPos + '-->' ); |
|
} |
|
|
|
|
|
/*Implement "Swiped arc"*/ |
|
$('#arcSwipe').click(function() { |
|
function sq(x) { return x*x; } |
|
function sqrt(x) { return Math.sqrt(x); } |
|
|
|
var center = $('#arcCenter').val().split(','); |
|
var centerX = parseFloat(center[0]), centerY = parseFloat(center[1]); |
|
var start = $('#arcStart').val().split(','); |
|
var startX = parseFloat(start[0]), startY = parseFloat(start[1]); |
|
var degr = parseFloat($('#arcDegrees').val()); |
|
|
|
var big = (Math.abs(degr)>180) ? 1 : 0; |
|
var ccw = (degr>0) ? 1 : 0; |
|
|
|
var dx = startX-centerX; |
|
var dy = startY-centerY; |
|
var radStart = Math.atan(dy/dx); |
|
if(dx<0) { radStart += Math.PI; } |
|
var degrRad = degr * Math.PI/180; |
|
|
|
var r = sqrt( sq(dx) + sq(dy) ); |
|
var radEnd = radStart + degrRad; |
|
var endX = Math.cos(radEnd)*r + centerX; |
|
var endY = Math.sin(radEnd)*r + centerY; |
|
|
|
function coord(x, y, doRound) { |
|
if(doRound) { |
|
function round(num, digits) { |
|
var log10 = Math.log10(Math.abs(num)); |
|
var multiplier = Math.pow(10, digits-Math.floor(log10)-1); |
|
|
|
var normalized = Math.round(num*multiplier) / multiplier; |
|
return normalized; |
|
} |
|
x = round(x, 4); |
|
y = round(y, 4); |
|
} |
|
return x+','+y+' ' ; |
|
} |
|
var path = '<path d="M' + coord(centerX,centerY) + |
|
'L' + coord(startX,startY) + |
|
'A'+coord(r,r, true) + '0 ' + coord(big,ccw) + coord(endX,endY, true) + '" />\n'; |
|
|
|
//debugger |
|
//$('#arcPath').text(path); |
|
editor.insert(path); |
|
editor.focus(); |
|
}); |
|
|
|
|
|
/*Implement "Split SVG segments"*/ |
|
$('#splitSegs').click(function() { |
|
var cornerTol = Number($('#splitSegsCornerTolerance').val()) || 0; |
|
|
|
var path = getCurrentTag(), |
|
data = path.content.match(/ d="(.*?)"/)[1], |
|
parsed = us.absolutizePath(us.parsePath(data)), |
|
parts = [], currPart = [], |
|
control, seg, prevSeg, addSeg, joinAngle, |
|
svgGroup; |
|
//vecPrevOut, vecIn, vecOut; |
|
|
|
for(var i=1; i<parsed.length; i++) { |
|
prevSeg = parsed[i-1]; |
|
seg = parsed[i]; |
|
addSeg = false; |
|
|
|
switch(seg[0]) { |
|
case 'S': |
|
case 'T': |
|
//Mirrors previous control point - always smooth: |
|
addSeg = true; |
|
|
|
if(seg.length === 3) { |
|
control = { |
|
x: seg.startPoint.x + (prevSeg.__vecOut[1].x - prevSeg.__vecOut[0].x), |
|
y: seg.startPoint.y + (prevSeg.__vecOut[1].y - prevSeg.__vecOut[0].y), |
|
}; |
|
} |
|
else { |
|
control = { x: seg[1], y: seg[2] }; |
|
} |
|
seg.__vecOut = [control, seg.endPoint]; |
|
break; |
|
|
|
case 'Z': |
|
//Belongs to the current segment and must be added: |
|
addSeg = true; |
|
break; |
|
|
|
case 'H': |
|
case 'V': |
|
case 'L': |
|
//Straight lines: |
|
seg.__vecIn = seg.__vecOut = [seg.startPoint, seg.endPoint]; |
|
break; |
|
|
|
case 'C': |
|
case 'Q': |
|
//Bezier curves: |
|
control = { x: seg[1], y: seg[2] }; |
|
seg.__vecIn = [seg.startPoint, control,]; |
|
|
|
control = { x: seg[seg.length-4], y: seg[seg.length-3] }; |
|
seg.__vecOut = [control, seg.endPoint]; |
|
|
|
break; |
|
|
|
default: |
|
//M: Explicitly start a new segment |
|
//A: May calculate entry and exit direction at some point, but not now... |
|
break; |
|
} |
|
|
|
//See if the current join is smooth enough to include this segment: |
|
function point2Array(p) { return [p.x, p.y]; } |
|
if(prevSeg.__vecOut && seg.__vecIn) { |
|
joinAngle = ug.angleBetween(point2Array(prevSeg.__vecOut[0]), point2Array(prevSeg.__vecOut[1]), point2Array(seg.__vecIn[1])); |
|
//console.log(prevSeg, '->', seg, ':', joinAngle); |
|
|
|
addSeg = ((Math.PI-joinAngle) <= cornerTol); |
|
} |
|
|
|
if(addSeg) { |
|
currPart.push(seg); |
|
} |
|
else { |
|
if(currPart.length) { |
|
parts.push(currPart); |
|
} |
|
|
|
currPart = [ seg ]; |
|
if(seg[0] !== 'M') { |
|
currPart.unshift(['M', seg.startPoint.x, seg.startPoint.y]); |
|
} |
|
} |
|
} |
|
if(currPart.length) { |
|
parts.push(currPart); |
|
} |
|
|
|
if(parts.length) { |
|
svgGroup = |
|
'<g stroke-width="2" fill="none" >\n ' + |
|
parts.map(function(part, i) { |
|
var d = us.serializePath(part), |
|
c = ['tomato', 'lime', 'dodgerblue', 'gold'][i % 4]; |
|
return '<path d="' +d+ '" stroke="' +c+ '" />'; |
|
}).join('\n ') + |
|
'\n</g>'; |
|
|
|
console.log(svgGroup); |
|
|
|
editor.moveCursorToPosition(path.end); |
|
editor.clearSelection(); |
|
editor.insert('\n' + svgGroup); |
|
} |
|
}); |
|
|
|
function getCurrentTag() { |
|
var pos = editor.getCursorPosition(), |
|
rowIndex = pos.row, |
|
colIndex, |
|
tempLine; |
|
|
|
var startLine = editor.session.getLine(rowIndex), |
|
tagStart = { row: rowIndex }, |
|
tagEnd = { row: rowIndex }, |
|
tag = startLine; |
|
|
|
//< |
|
colIndex = pos.column; |
|
tempLine = startLine; |
|
while((tagStart.column = tempLine.lastIndexOf('<', colIndex)) < 0) { |
|
tempLine = editor.session.getLine(--tagStart.row); |
|
colIndex = tempLine.length; |
|
|
|
tag = tempLine + tag; |
|
} |
|
tag = tag.slice(tagStart.column); |
|
//console.log('<:', tag); |
|
|
|
//> |
|
colIndex = pos.column; |
|
tempLine = startLine; |
|
while((tagEnd.column = tempLine.indexOf('>', colIndex)) < 0) { |
|
tempLine = editor.session.getLine(++tagEnd.row); |
|
colIndex = 0; |
|
|
|
tag += tempLine; |
|
} |
|
tagEnd.column++; |
|
tag = tag.slice(0, (tagEnd.column - tempLine.length) || undefined ); |
|
//console.log('>:', tag); |
|
|
|
//editor.moveCursorToPosition(tagEnd); |
|
//editor.clearSelection(); |
|
//editor.insert('\n'); |
|
|
|
return { |
|
start: tagStart, |
|
end: tagEnd, |
|
content: tag |
|
}; |
|
} |
|
|
|
/*Drag and drop background image for tracing*/ |
|
ud.dropImage(svgOutput, function(data) { |
|
svgOutput.style.backgroundImage = "url('" + data.url + "')"; |
|
}); |
|
|
|
|
|
/*Download the completed image*/ |
|
var downloader = document.getElementById('downloader'); |
|
downloader.addEventListener('click', download); |
|
|
|
function download(e) { |
|
var svg = svgInput.value; |
|
|
|
//http://stackoverflow.com/questions/2483919/how-to-save-svg-canvas-to-local-filesystem |
|
|
|
//Option 1 - opens image in new tab: |
|
//e.preventDefault(); |
|
//open("data:image/svg+xml," + encodeURIComponent(svg)); |
|
|
|
//Option 2 - actual download: |
|
var b64 = btoa(unescape(encodeURIComponent(svg))); |
|
downloader.setAttribute('href', 'data:image/svg+xml;base64,' + b64); |
|
} |
|
|
|
//Export PNG: |
|
(function(link, inputW, inputAA) { |
|
var actuallyDownloading = false; |
|
link.onclick = function(e) { |
|
//console.log('Png clicked', actuallyDownloading) |
|
if(actuallyDownloading) { return; } |
|
|
|
e.preventDefault(); |
|
|
|
var img = new Image(), |
|
svgElm = document.querySelector('svg', svgOutput), |
|
//Firefox... |
|
// w = svgElm.clientWidth, |
|
// h = svgElm.clientHeight; |
|
size = svgElm.getBoundingClientRect(), |
|
w = size.width, |
|
h = size.height; |
|
|
|
//Width/anti-aliasing: |
|
//http://stackoverflow.com/a/37897818/1869660 |
|
//https://jsfiddle.net/uqfgs477/1/ |
|
var overrideW = Number(inputW.value); |
|
var antiAlias = inputAA.checked; |
|
if(overrideW || !antiAlias) { |
|
svgElm = $(svgElm).clone()[0]; |
|
|
|
if(overrideW) { |
|
//Replace width/height with only a viewBox to support resizing: |
|
if(!svgElm.hasAttribute('viewBox')) { |
|
svgElm.setAttribute('viewBox', '0,0 '+[w,h]); |
|
} |
|
svgElm.removeAttribute('width'); |
|
svgElm.removeAttribute('height'); |
|
|
|
h = h * (overrideW/w); |
|
w = overrideW; |
|
} |
|
|
|
if(!antiAlias) { |
|
//This doesn't take care of text and clip-paths. |
|
//See further filter-ing below. |
|
// |
|
// svgElm.setAttribute('shape-rendering', 'crispEdges'); |
|
|
|
const filters = $$('filter', svgElm); |
|
filters.forEach(f => ud.createElement( |
|
'feFuncA', |
|
ud.createElement('feComponentTransfer', f), |
|
{ type: 'discrete', tableValues: '0 1' }, |
|
)); |
|
} |
|
} |
|
|
|
//http://stackoverflow.com/a/20559830/1869660 |
|
var svgCode = $('<div>').append($(svgElm).clone()).html(); |
|
|
|
if(!antiAlias) { |
|
var svgContentPos = svgCode.indexOf( '<', svgCode.indexOf('<svg')+1 ); |
|
//http://stackoverflow.com/questions/35434315/how-to-get-crispedges-for-svg-text |
|
//(and also clip-path...) |
|
svgCode = |
|
svgCode.substring(0, svgContentPos) + ` |
|
<defs> |
|
<style> |
|
svg * { shape-rendering: crispEdges !important; } |
|
svg text { filter: url('#crispify') !important; } |
|
</style> |
|
<filter id="crispify"> |
|
<feComponentTransfer> |
|
<feFuncA type="discrete" tableValues="0 1"/> |
|
</feComponentTransfer> |
|
</filter> |
|
</defs> |
|
` + |
|
svgCode.substring(svgContentPos); |
|
console.log(svgCode); |
|
} |
|
//If text can be edited (e.g. if the SVG is inside a <div contenteditable>), |
|
//we may insert invalid SVG, such as and <br>. This must be removed, or else 'img' won't load: |
|
svgCode = svgCode.replace(/ /g, ' '); |
|
svgCode = svgCode.replace(/<br\W*?>/g, ''); |
|
//console.log(svgCode); |
|
|
|
img.width = w; |
|
img.height = h; |
|
|
|
//https://stackoverflow.com/questions/28545619/javascript-which-parameters-are-there-for-the-onerror-event-with-image-objects |
|
img.onerror = function(e) { |
|
console.log('ERROR loading image', e); |
|
} |
|
img.onload = function() { |
|
console.log('exporting - loaded'); |
|
|
|
var canvas = document.createElement('canvas'), |
|
ctx = canvas.getContext('2d'); |
|
|
|
canvas.width = w; |
|
canvas.height = h; |
|
ctx.drawImage(img, 0, 0); |
|
|
|
/* Note: |
|
For some colors, the "anti-anti-alias by filter" trick above |
|
still leaves shades that are really close to the original along the edges. |
|
When (!antiAlias), look into "Color quantization" on the canvas imageData? |
|
- https://www.reddit.com/r/javascript/comments/1ruh9m/wrote_a_color_quantizer_try_it_out/ |
|
http://palebluepixel.org/static/projects/colorpal/ |
|
https://github.com/leeoniya/RgbQuant.js |
|
- https://gist.github.com/nrabinowitz/1104622 |
|
- https://github.com/igor-bezkrovny/image-quantization |
|
*/ |
|
|
|
//Fails for large ~1MB svgs... |
|
// var pngData = canvas.toDataURL('image/png'); |
|
// link.setAttribute('href', pngData); |
|
// |
|
canvas.toBlob(function(blob) { |
|
var newImg = document.createElement("img"), |
|
url = URL.createObjectURL(blob); |
|
|
|
link.href = url; |
|
|
|
actuallyDownloading = true; |
|
link.click(); |
|
actuallyDownloading = false; |
|
}); |
|
}; |
|
|
|
//Fails for large ~1MB svgs... |
|
// img.src = 'data:image/svg+xml;base64,' + btoa(svgCode); |
|
// |
|
var svgBlob = new Blob([svgCode], { type: 'image/svg+xml' }), |
|
url = URL.createObjectURL(svgBlob); |
|
//console.log('img', /*svgCode,*/ svgBlob, url) |
|
img.src = url; |
|
}; |
|
})($$1('#exporter-png'), $$1('#export-png-width'), $$1('#export-png-antialias')); |
|
|
|
|
|
/*Add fancy-schmancy editor*/ |
|
//http://stackoverflow.com/questions/6440439/how-do-i-make-a-textarea-an-ace-editor |
|
var editor = (function(textarea) { |
|
var editID = textarea.attr('id') + '-proxy'; |
|
var editDiv = $('<div>', { id: editID, 'class': textarea.attr('class') }) |
|
.insertBefore(textarea); |
|
textarea.hide(); |
|
|
|
var editor = ace.edit(editID); |
|
editor.$blockScrolling = Infinity; |
|
editor.setOptions({ |
|
//enableBasicAutocompletion: true, |
|
enableLiveAutocompletion: true |
|
}); |
|
|
|
var session = editor.getSession(); |
|
session.setMode("ace/mode/xml"); |
|
session.setValue(textarea.val()); |
|
session.on('change', function() { |
|
textarea.val(session.getValue()); |
|
//This doesn't trigger the textarea's "input" event, so we need to push an update: |
|
refreshOutput(true); |
|
}); |
|
|
|
editDiv.resizable({ |
|
resize: function( event, ui ) { editor.resize(); }, |
|
//stop: function( event, ui ) { } |
|
}); |
|
|
|
return editor; |
|
})($(svgInput)); |
|
|
|
$('#prettify').click(function() { |
|
var cur = editor.getCursorPosition(); |
|
var svg = editor.getSession().getValue(); |
|
|
|
svg = vkbeautify.xml(svg); |
|
svg = svg.replace(/\s*xmlns([:|=])/g, ' xmlns$1'); |
|
|
|
editor.getSession().setValue(svg); |
|
editor.moveCursorToPosition(cur); |
|
editor.focus(); |
|
}); |
|
|
|
|
|
function svgURI(svg) { |
|
const REGEX = { |
|
whitespace: /\s+/g, |
|
urlHexPairs: /%[\dA-F]{2}/g, |
|
singleQuotes: /'/g, |
|
quotes: /"/g, |
|
} |
|
|
|
const MAGIC_QUOTES = 'd347070c-6710-4ebc-be38-f3a8ed79d7fd' + Date.now(), |
|
MAGIC_SINGLEQUOTES = '313b9ce4-16e9-4b21-9adf-748d9c331663' + Date.now(); |
|
|
|
|
|
function buildDOM(svg) { |
|
let dom; |
|
if((typeof svg) === 'string') { |
|
// Strip the Byte-Order Mark if the SVG has one |
|
if (svg.charCodeAt(0) === 0xfeff) { svg = svg.slice(1); } |
|
|
|
//Breaks on invalid XML, which the browser usually handles fine (e.g. unencoded "<" or ">" in text).. |
|
//svg = new DOMParser().parseFromString(svg, "image/svg+xml"); |
|
|
|
const wrapper = document.createElement('div'); |
|
wrapper.innerHTML = svg; |
|
dom = wrapper.children[0]; |
|
} |
|
else { |
|
dom = svg.cloneNode(true); |
|
} |
|
return dom; |
|
} |
|
|
|
function trimDOM(elm) { |
|
function white2Space(text) { |
|
return text.replace(REGEX.whitespace, ' '); |
|
} |
|
|
|
Array.from(elm.attributes).forEach(a => { |
|
//Remove unnecessary whitespace from attributes (e.g. multi-line path data): |
|
let val = white2Space(a.value).trim(); |
|
//Later, we'll change the attribute delimiters from `"` to `'` to create URI-friendly attributes. |
|
//To avoid malformed XML, we need to escape any single quotes in attribute values: |
|
val = val.replace(REGEX.singleQuotes, MAGIC_SINGLEQUOTES); |
|
|
|
a.value = val; |
|
}); |
|
|
|
Array.from(elm.childNodes).forEach(n => { |
|
if(n instanceof SVGElement) { |
|
trimDOM(n); |
|
} |
|
else { |
|
//If the parent element presents text data (<text>, <tspan> etc), we need to keep the actual text nodes: |
|
if(elm.textLength && (n.nodeName === '#text')) { |
|
//Remove unnecessary whitespace (which isn't rendered anyway). |
|
//Note that we don't `.trim()` here, because there may be several text nodes in a row, and in-between spaces *are* rendered. |
|
let text = white2Space(n.textContent); |
|
//Later, we'll replace all `"` with `'` to create URI-friendly attributes. |
|
//To keep double quotes in text, we need to escape them here: |
|
text = text.replace(REGEX.quotes, MAGIC_QUOTES); |
|
|
|
n.textContent = text; |
|
} |
|
else { |
|
//Comments, indentation etc.. |
|
n.remove(); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* mini-svg-data-uri by Taylor Hunt |
|
* https://github.com/tigt/mini-svg-data-uri/ |
|
* The MIT license |
|
*/ |
|
function encodeXML(xml) { |
|
function dataURIPayload(string) { |
|
function specialHexEncode(match) { |
|
//Browsers tolerate these characters, and they're frequent. |
|
//This kind of replacing is safe, because non-ASCII characters are only encoded using escape sequences from 0x80 and up. |
|
switch (match) { |
|
case '%20': return ' '; |
|
case '%3D': return '='; |
|
case '%3A': return ':'; |
|
case '%2F': return '/'; |
|
default: return match.toLowerCase(); // compresses better |
|
} |
|
} |
|
return encodeURIComponent(string).replace(REGEX.urlHexPairs, specialHexEncode); |
|
} |
|
|
|
//Replace `"` with `'` to create URI-friendly attributes. |
|
xml = xml.replace(REGEX.quotes, "'"); |
|
|
|
const body = dataURIPayload(xml); |
|
return 'data:image/svg+xml,' + body; |
|
} |
|
|
|
|
|
const dom = buildDOM(svg); |
|
|
|
//Create compact XML: |
|
trimDOM(dom); |
|
let xml = new XMLSerializer().serializeToString(dom); |
|
//Now we can finally replace our magic strings with actual escaped quotes. |
|
//If we did that before, the '&' escape character would be double-escaped by the XMLSerializer.. |
|
xml = xml.split(MAGIC_SINGLEQUOTES).join(''') |
|
.split(MAGIC_QUOTES).join('"'); |
|
|
|
//URI encode: |
|
const uri = encodeXML(xml); |
|
|
|
return uri; |
|
} |
|
|
|
|
|
})(); |