Skip to content

Instantly share code, notes, and snippets.

@mutoo
Created August 24, 2018 01:08
Show Gist options
  • Select an option

  • Save mutoo/ab286334e22208d1982a55f866fda8fc to your computer and use it in GitHub Desktop.

Select an option

Save mutoo/ab286334e22208d1982a55f866fda8fc to your computer and use it in GitHub Desktop.
Svg Path Inspector
<div id="app">
<div ref="canvas">
</div>
<div class="caption">
{{pathString}}
</div>
</div>
// This pen demonstrate how svg path works
const SVG_PATH = `M40 60L60 140C100 240 100 80 140 60S220 100 160 220Q260 240 300 140T340 260`;
new Vue({
el: '#app',
data() {
return {
pathString: '',
hammers_: [],
};
},
methods: {
setup(width, height) {
this.canvas = SVG(this.$refs['canvas']).size(width, height);
this.createStage(400, 300);
this.createGrid(20);
this.createFrame();
this.createDefs();
this.createMovingHandle();
this.createPathWithHandles();
this.translate(120, 90); // center the stage
},
createStage(width, height) {
this.stage = this.canvas.group();
this.stage.rect(width, height).
fill('white').
stroke({width: 1}).
addClass('stage');
this.canvas.add(this.stage);
},
createGrid(gridSize) {
this.gridPattern = this.canvas.pattern(gridSize, gridSize, add => {
add.polyline([0, gridSize, 0, 0, gridSize, 0]).
fill('none').
stroke({width: 1, opacity: 0.25});
});
this.grid = this.canvas.rect(this.canvas.width(),
this.canvas.height()).
fill(this.gridPattern.fill()).
addClass('grid');
},
createFrame() {
this.canvas.rect(640, 480).
fill('none').
stroke({width: 1}).
addClass('frame');
},
createDefs() {
let defs = this.canvas.defs();
this.rectHandleDef = defs.rect(10, 10).stroke({
width: 1,
}).translate(-5, -5);
this.circleHandleDef = defs.circle(10).stroke({
width: 1,
}).translate(-5, -5);
},
translate(x, y) {
this.gridPattern.translate(
x % this.gridPattern.width(),
y % this.gridPattern.height(),
);
this.stage.translate(x, y);
},
createMovingHandle() {
let movingHandle = this.stage.use(this.rectHandleDef).
center(390, 10).
addClass('handle').
addClass('handle--movable');
let hammer = this.createHammer(movingHandle.node);
let panstartTX;
let panstartTY;
hammer.on('panstart', ev => {
let transform = this.stage.transform();
panstartTX = transform.x;
panstartTY = transform.y;
});
hammer.on('panmove', ev => {
this.translate(panstartTX + ev.deltaX, panstartTY + ev.deltaY);
});
},
createPathWithHandles() {
this.path = this.stage.path(SVG_PATH).
fill('none').
stroke({width: 2});
// console.log(this.path.array());
let pathArray = this.path.array();
this.pathString = pathArray.toString();
let handlesLayer = this.stage.group();
let gizmo = handlesLayer.group();
let updateGizmo = () => {
gizmo.clear();
let lastSecondPoint;
let lastPoint;
let ghostPoint;
for (let i = 0; i < pathArray.value.length; i++) {
let component = pathArray.value[i];
switch (component[0]) {
case 'M':
lastSecondPoint = null;
lastPoint = [component[1], component[2]];
break;
case 'L':
lastSecondPoint = lastPoint;
lastPoint = [component[1], component[2]];
break;
case 'C':
gizmo.line(
lastPoint[0],
lastPoint[1],
component[1],
component[2],
).stroke({
width: 1,
}).addClass('gizmo-line');
gizmo.line(
component[3],
component[4],
component[5],
component[6],
).stroke({
width: 1,
}).addClass('gizmo-line');
lastSecondPoint = [component[3], component[4]];
lastPoint = [component[5], component[6]];
break;
case 'S':
if (lastSecondPoint) {
ghostPoint = [
2 * lastPoint[0] - lastSecondPoint[0],
2 * lastPoint[1] - lastSecondPoint[1],
];
gizmo.line(
lastPoint[0],
lastPoint[1],
ghostPoint[0],
ghostPoint[1],
).stroke({
width: 1,
}).addClass('gizmo-line');
}
gizmo.line(
component[1],
component[2],
component[3],
component[4],
).stroke({
width: 1,
}).addClass('gizmo-line');
if (lastSecondPoint) {
gizmo.use(this.rectHandleDef).
center(ghostPoint[0], ghostPoint[1]).
addClass('handle--kinect');
}
lastSecondPoint = [component[1], component[2]];
lastPoint = [component[3], component[4]];
break;
case 'Q':
gizmo.line(
lastPoint[0],
lastPoint[1],
component[1],
component[2],
).stroke({
width: 1,
}).addClass('gizmo-line');
gizmo.line(
component[1],
component[2],
component[3],
component[4],
).stroke({
width: 1,
}).addClass('gizmo-line');
lastSecondPoint = [component[1], component[2]];
lastPoint = [component[3], component[4]];
break;
case 'T':
if (!lastSecondPoint) {
lastSecondPoint = lastPoint;
}
ghostPoint = [
2 * lastPoint[0] - lastSecondPoint[0],
2 * lastPoint[1] - lastSecondPoint[1],
];
gizmo.line(
lastPoint[0],
lastPoint[1],
ghostPoint[0],
ghostPoint[1],
).stroke({
width: 1,
}).addClass('gizmo-line');
gizmo.line(
ghostPoint[0],
ghostPoint[1],
component[1],
component[2],
).stroke({
width: 1,
}).addClass('gizmo-line');
gizmo.use(this.rectHandleDef).
center(ghostPoint[0], ghostPoint[1]).
addClass('handle--kinect');
lastSecondPoint = [ghostPoint[0], ghostPoint[1]];
lastPoint = [component[1], component[2]];
break;
}
}
};
let updatePath = () => {
updateGizmo();
this.path.plot(pathArray);
this.pathString = pathArray.toString();
};
updateGizmo();
pathArray.value.forEach(component => {
switch (component[0]) {
case 'M':
case 'L':
this.createHandleForPoint(
handlesLayer,
this.circleHandleDef,
component,
1,
2,
updatePath,
);
break;
case 'C':
this.createHandleForPoint(
handlesLayer,
this.rectHandleDef,
component,
1,
2,
updatePath,
);
this.createHandleForPoint(
handlesLayer,
this.rectHandleDef,
component,
3,
4,
updatePath,
);
this.createHandleForPoint(
handlesLayer,
this.circleHandleDef,
component,
5,
6,
updatePath,
);
break;
case 'S':
case 'Q':
this.createHandleForPoint(
handlesLayer,
this.rectHandleDef,
component,
1,
2,
updatePath,
);
this.createHandleForPoint(
handlesLayer,
this.circleHandleDef,
component,
3,
4,
updatePath,
);
break;
case 'T':
this.createHandleForPoint(
handlesLayer,
this.circleHandleDef,
component,
1,
2,
updatePath,
);
break;
}
});
},
createHandleForPoint(
container,
def,
component,
xIdx,
yIdx,
updateCallback,
) {
let movingHandle = container.use(def).
center(component[xIdx], component[yIdx]).
addClass('handle').
addClass('handle--movable');
let hammer = this.createHammer(movingHandle.node);
let panstartX;
let panstartY;
hammer.on('panstart', ev => {
panstartX = movingHandle.x();
panstartY = movingHandle.y();
});
hammer.on('panmove', ev => {
component[xIdx] = this.snapToGrid(panstartX + ev.deltaX);
component[yIdx] = this.snapToGrid(panstartY + ev.deltaY);
movingHandle.center(component[xIdx], component[yIdx]);
updateCallback();
});
},
snapToGrid(n) {
let gridSize = this.gridPattern.width();
return Math.round(n / gridSize) * gridSize;
},
createHammer(node) {
let hammer = new Hammer(node);
this.hammers_.push(hammer);
return hammer;
},
destroyHammers() {
this.hammers_.forEach(hammer => {
hammer.destroy();
});
},
},
mounted() {
this.setup(640, 480);
},
beforeDestroy() {
this.destroyHammers();
},
});
<script src="//unpkg.com/vue"></script>
<script src="//unpkg.com/svg.js"></script>
<script src="//unpkg.com/hammerjs"></script>
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.caption {
width: 640px;
padding: 20px;
box-sizing: border-box;
background-color: #efefef;
font-family: 'Source Code Pro', Menlo, Consolas, Monaco, monospace;
}
.grid {
pointer-events: none;
}
.handle {
cursor: pointer;
fill: white;
}
.handle--movable {
cursor: move;
}
.handle--kinect {
cursor: no-drop;
fill: #999999;
}
.gizmo-line {
opacity: 0.5;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment