Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Last active December 16, 2017 02:30
Show Gist options
  • Save Sphinxxxx/020e839f763195324ebc83efa02e6dd4 to your computer and use it in GitHub Desktop.
Save Sphinxxxx/020e839f763195324ebc83efa02e6dd4 to your computer and use it in GitHub Desktop.
This is not the SVG editor you're looking for
//- Challenge (nested transforms): https://a.hrc.onl/img/mark-h.svg
//-
//- Clean (zoom out to see controls...):
//- <svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
//- viewBox="0 0 336 288" height="288" width="336" >
//- <g transform="matrix(1.3333333,0,0,-1.3333333,0,288)" >
//- <g transform="scale(0.1)" style="fill:#254da3;fill-opacity:1;fill-rule:nonzero;stroke:none" >
//- <path d="M 720,0 0,0 l 0,720 90,360 -90,360 0,720 720,0 0,-2160" />
//- <path d="m 2160,0 -720,0 90,1080 -90,1080 720,0 0,-2160" />
//- <path d="m 2520,1080 -1080,1080 0,-720 -1440,0 0,-720 1440,0 0,-720 1080,1080" style="fill:#e51b2d" />
//- </g>
//- </g>
//- </svg>
//-
//- Possible tools to flatten transforms:
//- https://github.com/adobe-webplatform/Snap.svg/issues/199
//- https://gist.github.com/timo22345/9413158
//- http://jsfiddle.net/Nv78L/35/
//- http://stackoverflow.com/questions/5149301/baking-transforms-into-svg-path-element-commands
//- https://github.com/svg/svgo
//- https://github.com/svg/svgo/issues/344
//- https://github.com/RazrFalcon/SVGCleaner
//- http://stackoverflow.com/a/15113751/1869660
//-
#input
h2 SVG or path data:
textarea(rows=2) M90 150 c10 143 80-73 146 140 l-27-170-70 166 c113-205 121 31 171-115
//- M83.042,5.1183 h50v50 c-17.494-0.0693-32.193,12.866-40.115,27.573-15.016,28.085-14.637,64.08,1.47,91.619,8.2224,13.896,22.852,25.561,39.678,25.065,17.327-0.61091,31.829-13.477,39.421-28.33,13.688-25.755,13.954-58.007,1.7246-84.359-7.21-15.386-20.89-29.537-38.524-31.39-1.214-0.1219-2.434-0.1833-3.654-0.1823z
input#show-original(type='checkbox', checked)
label(for='show-original') Show original
input#show-controls(type='checkbox', checked)
label(for='show-controls') Show controls
#drawing-board
#original.layer
#drawing.layer
#controls.layer
svg(xmlns='http://www.w3.org/2000/svg')
#output
h2 Edited
//pre
textarea(disabled, rows=5)
window.onerror = function(msg, url, line) { alert('Error: '+msg+'\nURL: '+url+'\nLine: '+line); };
ABOUtils.log2screen();
"use strict";
function Coord(x, y) {
this.x = x;
this.y = y;
}
Coord.prototype.negate = function() {
return new Coord(-this.x, -this.y);
}
Coord.prototype.subtract = function(c2) {
return new Coord(this.x - c2.x, this.y - c2.y);
};
Coord.prototype.toArray = function() {
return [this.x, this.y];
};
function zoomableSvg(svg, options) {
if(typeof(svg) === 'string') { svg = document.querySelector(svg); }
options = options || {};
let _ui = options.container || svg,
_dragOffset,
_pinchState,
_zoom,
_viewport = {
width: _ui.clientWidth,
height: _ui.clientHeight
},
_viewBox = (function parseVB(vbAttr) {
const vb = vbAttr && vbAttr.split(/[ ,]/)
.filter(x => x.length)
.map(x => Number(x));
if(vb && (vb.length === 4)) {
return {
left: vb[0],
top: vb[1],
width: vb[2],
height: vb[3]
};
}
})(svg.getAttribute('viewBox'));
const _public = {
getViewBox: getViewBox,
getZoom: function() { return _zoom; },
vp2vb: vp2vb
};
if(_viewBox) {
//Adjust the zoom in case the SVG has been resized via CSS:
_zoom = _viewport.width/_viewBox.width;
//If the SVG is inside a container, adjust the SVG's viewBox to the container's aspect ratio,
//or else vp2vb() calculations won't be accurate:
if(options.container) {
changeZoom(0, new Coord(0,0));
}
}
else {
_zoom = 1;
_viewBox = {
left: 0,
top: 0,
width: _viewport.width,
height: _viewport.height
};
updateViewBox();
}
//Zoom
_ui.addEventListener('wheel', function(e) {
e.preventDefault();
changeZoom((e.deltaY > 0) ? -.1 : .1, ABOUtils.relativeMousePos(e, _ui));
});
_ui.addEventListener('touchmove', function(te) {
var touches = te.touches; //touchEvent.targetTouches;
if(touches.length !== 2) {
_pinchState = null;
return;
}
te.preventDefault();
//console.log(touches[0].identifier, touches[1].identifier);
const p1 = ABOUtils.relativeMousePos(touches[0], _ui),
p2 = ABOUtils.relativeMousePos(touches[1], _ui),
dx = p1.x - p2.x,
dy = p1.y - p2.y,
pinch = {
center: new Coord((p1.x + p2.x)/2, (p1.y + p2.y)/2),
dist: Math.sqrt(dx*dx + dy*dy),
};
if(_pinchState) {
moveViewport(pinch.center.subtract(_pinchState.center));
changeZoom((pinch.dist/_pinchState.dist) - 1, pinch.center);
}
_pinchState = pinch;
});
_ui.addEventListener('touchend', function (te) {
_pinchState = null;
});
//Drag
dragTracker({
container: _ui,
//selector: ...,
callbackDragStart: (_, pos) => {
_dragOffset = pos;
},
callback: (_, pos, start) => {
moveViewport(new Coord(pos[0] - _dragOffset[0], pos[1] - _dragOffset[1]));
_dragOffset = pos;
},
callbackDragEnd: () => {
_dragOffset = null;
}
});
function changeZoom(delta, viewportCenter) {
//console.log(delta, center);
_zoom *= 1 + delta;
setZoom(_zoom, viewportCenter);
}
function setZoom(zoom, viewportCenter) {
var newVBW = _viewport.width/zoom,
newVBH = _viewport.height/zoom,
newVBTopLeft;
var resizeFactor = newVBW/_viewBox.width,
newVPRect = {
w: _viewport.width * resizeFactor,
h: _viewport.height * resizeFactor,
t: viewportCenter.y - (viewportCenter.y * resizeFactor),
l: viewportCenter.x - (viewportCenter.x * resizeFactor),
};
newVBTopLeft = vp2vb( new Coord(newVPRect.l, newVPRect.t) );
_viewBox.top = newVBTopLeft.y;
_viewBox.left = newVBTopLeft.x;
_viewBox.width = newVBW;
_viewBox.height = newVBH;
//console.log(zoom, newVPRect, _viewBox);
updateViewBox();
}
function moveViewport(viewportDelta) {
var vbDelta = vp2vb(viewportDelta.negate());
_viewBox.top = vbDelta.y;
_viewBox.left = vbDelta.x;
//console.log(viewportDelta, vbDelta);
updateViewBox();
}
//Viewport coordinate -> viewBox coordinate:
function vp2vb(vpCoord) {
var relX = vpCoord.x/_viewport.width,
relY = vpCoord.y/_viewport.height,
vbX = _viewBox.width *relX + _viewBox.left,
vbY = _viewBox.height*relY + _viewBox.top,
vbCoord = new Coord(vbX, vbY);
//console.log(_viewBox, [relX, relY], '->', vbCoord);
return vbCoord;
}
function getViewBox() {
return [_viewBox.left, _viewBox.top, _viewBox.width, _viewBox.height];
}
function updateViewBox() {
const viewBox = getViewBox();
svg.setAttribute('viewBox', viewBox);
if(options.onChanged) { options.onChanged.call(_public); }
}
return _public;
}
(function(undefined) {
const RAD_CONTROL = 16,
RAD_END = 16,
ZOOM_MAX = 1000;
var _svg,
_origSvg,
_viewBox,
_viewport,
_zoomer,
_input = $$1('#input textarea'),
_output = $$1('#output textarea'),
_board = $$1('#drawing-board'),
_original = $$1('#original'),
_drawing = $$1('#drawing'),
_controls = $$1('#controls svg');
function PathKeeper(ui, segments) {
this.ui = ui;
this.segments = segments;
}
function UIDataBinding(keeper, segment, coordIndex) {
this.keeper = keeper;
this.segment = segment;
this.coordIndex = coordIndex;
}
const svgUI = {
createElement: function(parent, name, attributes) {
//http://stackoverflow.com/questions/16488884/add-svg-element-to-existing-svg-using-dom
var svgNS = 'http://www.w3.org/2000/svg';
var elm = document.createElementNS(svgNS, name);
for(var attr in attributes) {
elm.setAttributeNS(null, attr, attributes[attr]);
}
parent.appendChild(elm);
return elm;
},
getAttr: function(element, attr) {
return element.getAttributeNS(null, attr);
},
setAttr: function(element, attr, val) {
if(val || (val === 0)) {
element.setAttributeNS(null, attr, val);
}
else {
element.removeAttributeNS(null, attr);
}
},
unwrapVal: function(prop) {
//[SVGAnimatedLength]...
var val = prop.baseVal.value;
//console.log(prop, val);
return val;
},
};
function updatePath(keeper) {
var pathUI = keeper.ui,
segments = keeper.segments,
data = RosomanSVG.serialize(segments);
svgUI.setAttr(pathUI, 'd', data);
}
function updateHandles(pointUI) {
var handles = pointUI.__handles;
if(!handles) { return; }
handles.forEach(function(h) {
var p1 = h.__p1,
p2 = h.__p2;
//console.log(h, p1, p2);
var x1 = svgUI.unwrapVal(p1.cx),
y1 = svgUI.unwrapVal(p1.cy),
x2 = svgUI.unwrapVal(p2.cx),
y2 = svgUI.unwrapVal(p2.cy),
data = ['M', [x1,y1], 'L', [x2,y2]].join(' ');
svgUI.setAttr(h, 'd', data);
});
}
function movePoint(pointUI, viewportCoord) {
var coord = _zoomer.vp2vb(viewportCoord);
svgUI.setAttr(pointUI, 'cx', coord.x);
svgUI.setAttr(pointUI, 'cy', coord.y);
////[_bezier, 'c2'] => _bezier['c2'] = ...
//pointUI.dataBinding[0][pointUI.dataBinding[1]] = dragPos;
var binding = pointUI.dataBinding;
binding.segment[binding.coordIndex] = coord.x;
binding.segment[binding.coordIndex+1] = coord.y;
updatePath(binding.keeper);
updateHandles(pointUI);
}
function init(svml) {
_controls.innerHTML = '';
_drawing.innerHTML = svml;
_svg = $$1('svg', _drawing);
svgUI.setAttr(_svg, 'width', '');
svgUI.setAttr(_svg, 'height', '');
//http://stackoverflow.com/questions/12592417/outerhtml-of-an-svg-element
_original.innerHTML = _drawing.innerHTML; //_svg.outerHTML;
_origSvg = $$1('svg', _original);
//__updateViewBox();
function onZoomed() {
//Sync all SVGs:
const zoomer = this,
vbAttr = '' + zoomer.getViewBox();
svgUI.setAttr(_controls, 'viewBox', vbAttr);
svgUI.setAttr(_svg, 'viewBox', vbAttr);
svgUI.setAttr(_origSvg, 'viewBox', vbAttr);
//Adjust the controls UI to stay the same size:
let zoom = zoomer.getZoom();
_controls.style.fontSize = (1/zoom) + 'em';
$$('.bez-control').forEach(function(x) { svgUI.setAttr(x, 'r', RAD_CONTROL/zoom); });
$$('.endpoint').forEach(function(x) { svgUI.setAttr(x, 'r', RAD_END/zoom); });
outputSvg();
}
_zoomer = zoomableSvg(_svg, {
container: _board,
onChanged: onZoomed
});
//Create UI for Bezier end and control points:
function createControlPoint(keeper, segment, index) {
//console.log('control-point', segment);
const c = svgUI.createElement(_controls, 'circle', {
class: 'bez-control',
'data-draggable': '',
cx: segment[index],
cy: segment[index+1],
r: RAD_CONTROL
});
c.dataBinding = new UIDataBinding(keeper, segment, index);
return c;
}
function createEndPoint(keeper, segment, index) {
const c = svgUI.createElement(_controls, 'circle', {
class: 'endpoint',
'data-draggable': '',
cx: segment[index],
cy: segment[index+1],
r: RAD_END
});
c.dataBinding = new UIDataBinding(keeper, segment, index);
return c;
}
function createHandle(p1, p2) {
const handle = svgUI.createElement(_controls, 'path', { class: 'bez-handle' });
function addHandle(p) {
p.__handles = p.__handles || [];
p.__handles.push(handle);
}
handle.__p1 = p1;
handle.__p2 = p2;
addHandle(p1, handle);
addHandle(p2, handle);
updateHandles(p1);
}
const paths = $$('path', _svg);
paths.forEach(function(path) {
const data = svgUI.getAttr(path, 'd'),
segmentsRel = RosomanSVG.parse(data),
segments = RosomanSVG.absolutize(segmentsRel),
keeper = new PathKeeper(path, segments);
let prevEndPoint, prevEndCoord;
//console.log(data, segmentsRel, segments);
//For simplify.js:
//console.log('points', JSON.stringify(segments.map(s => s.endPoint)));
segments.forEach(function(seg) {
const segType = seg[0];
let c1, c2, end;
//First, transform H/V to L so they get proper endpoint coordinates:
switch(segType) {
case 'H':
seg[0] = 'L';
seg[2] = prevEndCoord.y;
break;
case 'V':
seg[0] = 'L';
seg[2] = seg[1];
seg[1] = prevEndCoord.x;
break;
}
switch(segType) {
case 'C':
c1 = createControlPoint(keeper, seg, 1);
c2 = createControlPoint(keeper, seg, 3);
end = createEndPoint(keeper, seg, 5);
createHandle(c1, prevEndPoint);
createHandle(c2, end);
prevEndPoint = end;
prevEndCoord = new Coord(seg[5], seg[6]);
break;
//Standalone quad, or continued cubic:
case 'Q':
case 'S':
c1 = createControlPoint(keeper, seg, 1);
end = createEndPoint(keeper, seg, 3);
if(segType === 'Q') {
createHandle(c1, prevEndPoint);
}
createHandle(c1, end);
prevEndPoint = end;
prevEndCoord = new Coord(seg[3], seg[4]);
break;
case 'Z':
break;
//"L"/"H"/"V" (see above)
//"T" (continued quad - doesn't control its control point)
//"A" (TODO: Usable editor UI?)
default:
prevEndPoint = createEndPoint(keeper, seg, seg.length-2);
prevEndCoord = new Coord(seg[seg.length-2], seg[seg.length-1]);
break;
}
});
});
}
_input.onclick = function() {
//If no current selection
if(_input.selectionStart === _input.selectionEnd) {
this.select();
}
}
_input.oninput = function() {
var svg = _input.value.trim();
if(svg[0] !== '<') {
svg = '<svg xmlns="http://www.w3.org/2000/svg" >\n' +
' <path d="' + svg + '" fill="none" stroke="black" stroke-width="2"></path>\n' +
'</svg>';
}
init(svg);
};
_input.oninput();
function outputSvg() {
_output.value/*textContent*/ = _drawing.innerHTML.trim();
}
/*User interaction (drag & drop)*/
dragTracker({
container: _controls,
selector: '[data-draggable]',
callback: (box, pos, start) => {
movePoint(box, new Coord(pos[0], pos[1]));
outputSvg();
},
});
})();
"use strict";
function Coord(x, y) {
this.x = x;
this.y = y;
this.negate = function() {
return new Coord(-this.x, -this.y);
}
}
Coord.prototype.toArray = function() {
return [this.x, this.y];
};
function zoomableSvg(svg, options) {
if(typeof(svg) === 'string') { svg = document.querySelector(svg); }
options = options || {};
let _ui = options.container || svg,
_dragOffset,
_zoom = 1,
_viewport = {
width: _ui.clientWidth,
height: _ui.clientHeight
},
_viewBox = (function parseVB(vbAttr) {
const vb = vbAttr && vbAttr.split(/[ ,]/)
.filter(x => x.length)
.map(x => Number(x));
if(vb && (vb.length === 4)) {
return {
left: vb[0],
top: vb[1],
width: vb[2],
height: vb[3]
};
}
})(svg.getAttribute('viewBox'));
const _public = {
getViewBox: getViewBox,
getZoom: function() { return _zoom; },
vp2vb: vp2vb
};
if(!_viewBox) {
_viewBox = {
left: 0,
top: 0,
width: _viewport.width,
height: _viewport.height
};
updateViewBox();
}
_ui.addEventListener('wheel', function(e) {
e.preventDefault();
changeZoom((e.deltaY > 0) ? -.1 : .1, ABOUtils.relativeMousePos(e, _ui));
});
_ui.addEventListener('mousedown', function(e) {
if(e.target.getAttribute('data-draggable')) {
//Don't interfere with other draggable elements:
return;
}
e.preventDefault();
const area = this.getBoundingClientRect();
_dragOffset = new Coord(e.clientX - area.left, e.clientY - area.top);
});
_ui.addEventListener('mousemove', function(e) {
//'mouseup' while out of window:
if(_dragOffset && (e.buttons !== undefined) && (e.buttons !== 1)) {
_dragOffset = null;
}
if(_dragOffset) {
e.preventDefault();
const dragPos = ABOUtils.relativeMousePos(e, _ui);
moveViewport(new Coord(dragPos.x - _dragOffset.x, dragPos.y - _dragOffset.y));
_dragOffset = dragPos;
}
});
function changeZoom(delta, viewportCenter) {
//console.log(delta, center);
_zoom *= 1 + delta;
setZoom(_zoom, viewportCenter);
}
function setZoom(zoom, viewportCenter) {
var newVBW = _viewport.width/zoom,
newVBH = _viewport.height/zoom,
newVBTopLeft;
var resizeFactor = newVBW/_viewBox.width,
newVPRect = {
w: _viewport.width * resizeFactor,
h: _viewport.height * resizeFactor,
t: viewportCenter.y - (viewportCenter.y * resizeFactor),
l: viewportCenter.x - (viewportCenter.x * resizeFactor),
};
newVBTopLeft = vp2vb( new Coord(newVPRect.l, newVPRect.t) );
_viewBox.top = newVBTopLeft.y;
_viewBox.left = newVBTopLeft.x;
_viewBox.width = newVBW;
_viewBox.height = newVBH;
//console.log(zoom, newVPRect, _viewBox);
updateViewBox();
}
function moveViewport(viewportDelta) {
var vbDelta = vp2vb(viewportDelta.negate());
_viewBox.top = vbDelta.y;
_viewBox.left = vbDelta.x;
//console.log(viewportDelta, vbDelta);
updateViewBox();
}
//Viewport coordinate -> viewBox coordinate:
function vp2vb(vpCoord) {
var relX = vpCoord.x/_viewport.width,
relY = vpCoord.y/_viewport.height,
vbX = _viewBox.width *relX + _viewBox.left,
vbY = _viewBox.height*relY + _viewBox.top,
vbCoord = new Coord(vbX, vbY);
//console.log(_viewBox, [relX, relY], '->', vbCoord);
return vbCoord;
}
function getViewBox() {
return [_viewBox.left, _viewBox.top, _viewBox.width, _viewBox.height];
}
function updateViewBox() {
const viewBox = getViewBox();
svg.setAttribute('viewBox', viewBox);
if(options.onChanged) { options.onChanged.call(_public); }
}
return _public;
}
(function(undefined) {
const RAD_CONTROL = 10,
RAD_END = 10,
ZOOM_MAX = 1000;
var _svg,
_origSvg,
_viewBox,
_viewport,
//_zoom,
_zoomer,
_input = $$1('#input textarea'),
_output = $$1('#output pre'),
_board = $$1('#drawing-board'),
_original = $$1('#original'),
_drawing = $$1('#drawing'),
_controls = $$1('#controls svg');
function PathKeeper(ui, segments) {
this.ui = ui;
this.segments = segments;
}
function UIDataBinding(keeper, segment, coordIndex) {
this.keeper = keeper;
this.segment = segment;
this.coordIndex = coordIndex;
}
var svgUI = {
createElement: function(parent, name, attributes) {
//http://stackoverflow.com/questions/16488884/add-svg-element-to-existing-svg-using-dom
var svgNS = 'http://www.w3.org/2000/svg';
var elm = document.createElementNS(svgNS, name);
for(var attr in attributes) {
elm.setAttributeNS(null, attr, attributes[attr]);
}
parent.appendChild(elm);
return elm;
},
getAttr: function(element, attr) {
return element.getAttributeNS(null, attr);
},
setAttr: function(element, attr, val) {
if(val || (val === 0)) {
element.setAttributeNS(null, attr, val);
}
else {
element.removeAttributeNS(null, attr);
}
},
unwrapVal: function(prop) {
//[SVGAnimatedLength]...
var val = prop.baseVal.value;
//console.log(prop, val);
return val;
},
};
/*
//Viewport coordinate -> viewBox coordinate:
function __vp2vb(vpCoord) {
var relX = vpCoord.x/_viewport.width,
relY = vpCoord.y/_viewport.height,
vbX = _viewBox.width *relX + _viewBox.left,
vbY = _viewBox.height*relY + _viewBox.top,
vbCoord = new Coord(vbX, vbY);
//console.log(_viewBox, [relX, relY], '->', vbCoord);
return vbCoord;
}
function __updateViewBox() {
var viewBoxAttr = '' + [_viewBox.left, _viewBox.top, _viewBox.width, _viewBox.height]
svgUI.setAttr(_controls, 'viewBox', viewBoxAttr);
svgUI.setAttr(_svg, 'viewBox', viewBoxAttr);
svgUI.setAttr(_origSvg, 'viewBox', viewBoxAttr);
}
*/
function updatePath(keeper) {
var pathUI = keeper.ui,
segments = keeper.segments,
data = RosomanSVG.serialize(segments);
svgUI.setAttr(pathUI, 'd', data);
}
function updateHandles(pointUI) {
var handles = pointUI.__handles;
if(!handles) { return; }
handles.forEach(function(h) {
var p1 = h.__p1,
p2 = h.__p2;
//console.log(h, p1, p2);
var x1 = svgUI.unwrapVal(p1.cx),
y1 = svgUI.unwrapVal(p1.cy),
x2 = svgUI.unwrapVal(p2.cx),
y2 = svgUI.unwrapVal(p2.cy),
data = ['M', [x1,y1], 'L', [x2,y2]].join(' ');
svgUI.setAttr(h, 'd', data);
});
}
function movePoint(pointUI, viewportCoord) {
var coord = _zoomer.vp2vb(viewportCoord);
svgUI.setAttr(pointUI, 'cx', coord.x);
svgUI.setAttr(pointUI, 'cy', coord.y);
////[_bezier, 'c2'] => _bezier['c2'] = ...
//pointUI.dataBinding[0][pointUI.dataBinding[1]] = dragPos;
var binding = pointUI.dataBinding;
binding.segment[binding.coordIndex] = coord.x;
binding.segment[binding.coordIndex+1] = coord.y;
updatePath(binding.keeper);
updateHandles(pointUI);
}
/*
function changeZoom(delta, viewportCenter) {
//console.log(delta, center);
_zoom *= 1 + delta;
setZoom(_zoom, viewportCenter);
}
function setZoom(zoom, viewportCenter) {
var newVBW = _viewport.width/zoom,
newVBH = _viewport.height/zoom,
newVBTopLeft;
var resizeFactor = newVBW/_viewBox.width,
newVPRect = {
w: _viewport.width * resizeFactor,
h: _viewport.height * resizeFactor,
t: viewportCenter.y - (viewportCenter.y * resizeFactor),
l: viewportCenter.x - (viewportCenter.x * resizeFactor),
};
newVBTopLeft = __vp2vb( new Coord(newVPRect.l, newVPRect.t) );
_viewBox.top = newVBTopLeft.y;
_viewBox.left = newVBTopLeft.x;
_viewBox.width = newVBW;
_viewBox.height = newVBH;
//console.log(zoom, newVPRect, _viewBox);
__updateViewBox();
//Adjust the controls UI to stay the same size:
_controls.style.fontSize = (1/zoom) + 'em';
$$('.bez-control').forEach(function(x) { svgUI.setAttr(x, 'r', RAD_CONTROL/zoom); })
$$('.endpoint').forEach(function(x) { svgUI.setAttr(x, 'r', RAD_END/zoom); })
}
function moveViewport(viewportDelta) {
var vbDelta = __vp2vb(viewportDelta.negate());
_viewBox.top = vbDelta.y;
_viewBox.left = vbDelta.x;
//console.log(viewportDelta, vbDelta);
__updateViewBox();
}
*/
function init(svml) {
var paths;
/*
_zoom = 1;
_viewport = {
width: _board.clientWidth,
height: _board.clientHeight
};
_viewBox = {
top: 0,
left: 0,
width: _viewport.width,
height: _viewport.height
};
*/
_controls.innerHTML = '';
_drawing.innerHTML = svml;
_svg = $$1('svg', _drawing);
svgUI.setAttr(_svg, 'width', '');
svgUI.setAttr(_svg, 'height', '');
//http://stackoverflow.com/questions/12592417/outerhtml-of-an-svg-element
_original.innerHTML = _drawing.innerHTML; //_svg.outerHTML;
_origSvg = $$1('svg', _original);
//__updateViewBox();
function onZoomed() {
//Sync all SVGs:
const zoomer = this,
vbAttr = '' + zoomer.getViewBox();
svgUI.setAttr(_controls, 'viewBox', vbAttr);
svgUI.setAttr(_svg, 'viewBox', vbAttr);
svgUI.setAttr(_origSvg, 'viewBox', vbAttr);
//Adjust the controls UI to stay the same size:
let zoom = zoomer.getZoom();
_controls.style.fontSize = (1/zoom) + 'em';
$$('.bez-control').forEach(function(x) { svgUI.setAttr(x, 'r', RAD_CONTROL/zoom); });
$$('.endpoint').forEach(function(x) { svgUI.setAttr(x, 'r', RAD_END/zoom); });
outputSvg();
}
_zoomer = zoomableSvg(_origSvg, {
container: _board,
onChanged: onZoomed
});
//Create UI for Bezier end and control points:
function createControlPoint(keeper, segment, index) {
//console.log('control-point', segment);
var c = svgUI.createElement(_controls, 'circle', {
class: 'bez-control',
'data-draggable': true,
cx: segment[index],
cy: segment[index+1],
r: RAD_CONTROL
});
c.dataBinding = new UIDataBinding(keeper, segment, index);
return c;
}
function createEndPoint(keeper, segment, index) {
var c = svgUI.createElement(_controls, 'circle', {
class: 'endpoint',
'data-draggable': true,
cx: segment[index],
cy: segment[index+1],
r: RAD_END
});
c.dataBinding = new UIDataBinding(keeper, segment, index);
return c;
}
function createHandle(p1, p2) {
var handle = svgUI.createElement(_controls, 'path', { class: 'bez-handle' });
function addHandle(p) {
p.__handles = p.__handles || [];
p.__handles.push(handle);
}
handle.__p1 = p1;
handle.__p2 = p2;
addHandle(p1, handle);
addHandle(p2, handle);
updateHandles(p1);
}
paths = $$('path', _svg);
paths.forEach(function(path) {
var data = svgUI.getAttr(path, 'd'),
segmentsRel = RosomanSVG.parse(data),
segments = RosomanSVG.absolutize(segmentsRel),
keeper = new PathKeeper(path, segments),
prevEndPoint, prevEndCoord;
//console.log(data, segmentsRel, segments);
segments.forEach(function(seg) {
var segType = seg[0],
c1, c2, end;
//First, transform H/V to L so they get proper endpoint coordinates:
switch(segType) {
case 'H':
seg[0] = 'L';
seg[2] = prevEndCoord.y;
break;
case 'V':
seg[0] = 'L';
seg[2] = seg[1];
seg[1] = prevEndCoord.x;
break;
}
switch(segType) {
case 'C':
c1 = createControlPoint(keeper, seg, 1);
c2 = createControlPoint(keeper, seg, 3);
end = createEndPoint(keeper, seg, 5);
createHandle(c1, prevEndPoint);
createHandle(c2, end);
prevEndPoint = end;
prevEndCoord = new Coord(seg[5], seg[6]);
break;
//Standalone quad, or continued cubic:
case 'Q':
case 'S':
c1 = createControlPoint(keeper, seg, 1);
end = createEndPoint(keeper, seg, 3);
if(segType === 'Q') {
createHandle(c1, prevEndPoint);
}
createHandle(c1, end);
prevEndPoint = end;
prevEndCoord = new Coord(seg[3], seg[4]);
break;
case 'Z':
break;
//"T" (continued quad - doesn't control its control point)
//"L"
//"A" (TODO: Usable editor UI?)
default:
prevEndPoint = createEndPoint(keeper, seg, seg.length-2);
prevEndCoord = new Coord(seg[seg.length-2], seg[seg.length-1]);
break;
}
});
});
}
_input.onclick = function() {
//If no current selection
if(_input.selectionStart === _input.selectionEnd) {
this.select();
}
}
_input.oninput = function() {
var svg = _input.value.trim();
if(svg[0] !== '<') {
svg = '<svg xmlns="http://www.w3.org/2000/svg" >\n' +
' <path d="' + svg + '" fill="none" stroke="black" stroke-width="2"></path>\n' +
'</svg>';
}
init(svg);
};
_input.oninput();
/*User interaction (drag & drop)*/
//http://stackoverflow.com/questions/18425089/simple-drag-and-drop-code
var dragged, dragOffset;
document.body.addEventListener('mouseup', function() {
dragged = undefined;
});
document.body.addEventListener('mousemove', function(e) {
//'mouseup' while out of window:
if(dragged && (e.buttons !== undefined) && (e.buttons !== 1)) {
dragged = undefined;
}
var dragPos;
if(dragged) {
/*
if(dragged === _board) {
dragPos = getBoardCoord(e);
moveViewport(new Coord(dragPos.x - dragOffset.x, dragPos.y - dragOffset.y));
dragOffset = dragPos;
}
else */{
dragPos = getBoardCoord(e, dragOffset, false);
movePoint(dragged, dragPos);
}
outputSvg();
}
});
//[p1, c1, c2, p2].forEach(function(c) {
// c.addEventListener('mousedown', function(e) {
ABOUtils.live('mousedown', '.endpoint, .bez-control', function(e) {
e.preventDefault();
dragged = this;
//console.log('mousedown', this);
var mousePos = new Coord(e.clientX, e.clientY);
var draggedPos = dragged.getBoundingClientRect();
var draggedCenter = new Coord(draggedPos.left + draggedPos.width/2,
draggedPos.top + draggedPos.height/2);
dragOffset = new Coord(mousePos.x-draggedCenter.x, mousePos.y-draggedCenter.y);
});
//Zoom & move
/*
_board.addEventListener('wheel', function(e) {
e.preventDefault();
changeZoom((e.deltaY > 0) ? -.1 : .1, getBoardCoord(e));
outputSvg();
});
_board.addEventListener('mousedown', function(e) {
e.preventDefault();
dragged = this;
var area = this.getBoundingClientRect();
dragOffset = new Coord(e.clientX - area.left, e.clientY - area.top);
//console.log(dragOffset);
});
*/
function getBoardCoord(mouseEvent, offset, restrict) {
/*
function respectBounds(value, min,max) {
return Math.max(min, Math.min(value, max));
}
offset = offset || { x:0, y:0 };
//Buggy in Firefox...
//Related? http://stackoverflow.com/questions/11334452/event-offsetx-in-firefox
// var dragPos = coord(e.offsetX+dragOffset.x, e.offsetY+dragOffset.y);
var svgBounds = _board.getBoundingClientRect();
var x = mouseEvent.clientX - svgBounds.left - offset.x,
y = mouseEvent.clientY - svgBounds.top - offset.y;
if(restrict) {
x = respectBounds(x, 0, svgBounds.width);
y = respectBounds(y, 0, svgBounds.height);
}
return new Coord(x, y);
*/
var pos = ABOUtils.relativeMousePos(mouseEvent, _board, restrict);
if(offset) {
pos.x -= offset.x;
pos.y -= offset.y;
}
return new Coord(pos.x, pos.y);
}
function outputSvg() {
_output.textContent = _drawing.innerHTML.trim();
}
})();
<script src="https://codepen.io/Sphinxxxx/pen/VejGLv"></script>
<script src="https://cdn.rawgit.com/Sphinxxxx/drag-tracker/v0.3/src/drag-tracker.js"></script>
$colorBG: rgba(0,0,0, 0);
$colorBGPattern: #cdf;
html, body { margin: 0; padding: 0; }
body { font-family: Georgia, sans-serif; }
h2 { margin:0; text-align:center; }
textarea {
width: 100%;
box-sizing: border-box;
white-space: pre;
}
#input {
background: lawngreen;
}
#drawing-board {
position: relative;
width: 601px;
height: 501px;
margin: 1em auto;
//background: linear-gradient(to right, $colorBG 48%, $colorBGPattern 48%, $colorBGPattern 50%, $colorBG 50%, $colorBG 100%),
// linear-gradient(to bottom, $colorBG 48%, $colorBGPattern 48%, $colorBGPattern 50%, $colorBG 50%, $colorBG 100%);
background: linear-gradient(to right, $colorBGPattern 1px, $colorBG 1px),
linear-gradient(to bottom, $colorBGPattern 1px, $colorBG 1px);
background-size: 20px 20px;
.layer {
position: absolute;
top:0; left:0; bottom:0; right:0;
}
#original {
opacity: .3;
}
#controls {
//Base for stroke-width of controls UI, which is 'em'-sized to make resizing easier.
//(when zooming, the child SVG changes its font-size +-1em).
font-size: 10px;
$stroke: .2em;
.endpoint {
fill: transparent;
stroke: blue;
stroke-width: $stroke;
}
.endpoint, .bez-control {
cursor: pointer;
}
.bez-control, .bez-handle {
fill: lightgreen;
stroke: green;
stroke-width: $stroke;
stroke-dasharray: $stroke * 2;
}
.bez-handle {
stroke: gold;
z-index: -1;
pointer-events: none;
}
}
}
#show-original:not(:checked) ~ #drawing-board #original {
display: none;
}
#show-controls:not(:checked) ~ #drawing-board #controls {
display: none;
}
#output {
background: deepskyblue;
padding-bottom: 0.5em;
//pre {
// width: 100%;
// min-height: 1em;
// max-height: 50vh;
// margin: 0;
// background: white;
// overflow: auto;
//}
textarea {
background: white;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment