<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./style.css">
<script src="./index.js">
</script>
</head>
<body>
<div class="main">
<div class="canvas">
<svg width="500" height="500" viewbox="0 0 500 500" id="svg1">
<defs>
<pattern id="Pattern" x="0" y="0" width="25" height="25" patternUnits="userSpaceOnUse">
<Line x1="0" y1="0" x2="25" y2="0" stroke="gray" stroke-width="1"></Line>
<Line x1="0" y1="0" x2="0" y2="25" stroke="gray" stroke-width="1"></Line>
</pattern>
</defs>
<rect id="gridContainer" width="500" height="500" fill="url(#Pattern)" />
<path id="path" stroke="black" fill="none" stroke-width="2" d="" />
<g id="guideContainer" class="guideContainer">
<path id="helperPath" stroke-dasharray="2 5" stroke="brown" fill="none" stroke-width="2" d="" />
</g>
</svg>
</div>
<div class="control">
<button id="btnClear">clear all</button>
<button id="btnRemove">remove selected point</button>
<br/>
<span>
<input id="radLine" type="radio" name="path" value="Line" checked/>
<label for="radLine">Line</label>
</span>
<span>
<input id="radQuad" type="radio" name="path" value="Quad"/>
<label for="radQuad">Quadratic Curve</label>
</span>
<span>
<input id="radCube" type="radio" name="path" value="Cube"/>
<label for="radCube">Bezier Curve </label>
</span>
<!-- <span>
<input id="radArc" type="radio" name="path" value="Arc"/>
<label for="radArc">Arc </label>
</span> -->
<br/>
<span>
<input id="showGuide" type="checkbox" checked/>
<label for="showGuide">Show Guides </label>
</span>
<span>
<input id="showGrid" type="checkbox" checked/>
<label for="showGrid">Show Grid </label>
</span>
<br>
<p>
Click on the grid to add a new node
</p>
<p>
Click on a node to delete
</p>
<p>
Drag a node to modify
</p>
</div>
</div>
<div class="pathValue">
<div id="div">
</div>
</div>
</body>
</html>
{
"name": "@plnkr/starter-angular",
"version": "1.0.3",
"description": "Angular starter template",
"dependencies": {
"@angular/common": "^6.0.9",
"@angular/compiler": "^6.0.9",
"@angular/core": "^6.0.9",
"@angular/platform-browser": "^6.0.9",
"@angular/platform-browser-dynamic": "^6.0.9",
"core-js": "^2.5.5",
"rxjs": "^6.1.0",
"zone.js": "^0.8.26"
},
"main": "./lib/main.ts",
"plnkr": {
"runtime": "system",
"useHotReload": true
}
}
* {
box-sizing: content-box;
padding: 0;
margin: 0;
}
svg {
border: solid 1px black;
}
.point {
fill: #0000ff10;
cursor: move;
}
.point.helperNode {
fill: #ffff0035;
}
.selectedNode:not(.helperNode):not(.baseNode) {
fill: green;
}
.main {
display: flex;
user-select: none;
}
.canvas {
padding: 10px;
height:500px;
flex: 0 0 500px;
}
.control {
padding: 10px;
flex: 0 0 150px;
flex-direction: column;
display: flex;
}
.control > * {
flex: 0 0 15px;
margin: 4px;
}
.pathValue {
clear: both;
}
p {
color: orangered;
font-size: 13px;
}
#div {
padding: 1px 10px;
font-size: 15px;
color: navy;
}
button {
border: solid 1px gray;
outline: none;
box-shadow: 2px 2px 2px black;
}
button:active {
border: solid 1px gray;
outline: none;
box-shadow: 2px 2px 0px black;
}
.baseNode{
fill: gray;
}
.hide{display: none;}
import { div, path, svg, btnClear, btnRemove, helperPath, guideContainer, showGuide, gridContainer, showGrid } from "./UIItems";
import { PointStore } from "./PointStore"
import { Point, Line, Quad, Cube, Factory } from "./Point"
PointStore.notify = () => {
update();
}
showGuide.onclick = function (e) {
if ((showGuide as HTMLInputElement).checked) {
guideContainer.classList.remove("hide");
} else {
guideContainer.classList.add("hide");
}
}
showGrid.onclick = function (e) {
if ((showGrid as HTMLInputElement).checked) {
gridContainer.classList.remove("hide");
} else {
gridContainer.classList.add("hide");
}
}
svg.onclick = function (e: MouseEvent) {
let offset = svg.getBoundingClientRect();
let x = e.x - offset.left;
let y = e.y - offset.top;
if (!PointStore.getLastPoint()) {
let p0 = new Point(svg, x, y);
PointStore.addNewPoint(p0);
let domNode = p0.getNode();
domNode.classList.add("baseNode");
guideContainer.appendChild(domNode);
return;
}
let radios = document.getElementsByName('path');
let p;
for (var i = 0; i < radios.length; i++) {
let rad: HTMLInputElement = radios[i] as HTMLInputElement;
if (rad.checked) {
p = Factory.createPath(rad.value, svg, x, y);
break;
}
}
PointStore.addNewPoint(p);
guideContainer.appendChild(p.getNode());
update();
}
btnClear.onclick = function () {
let nodes = PointStore.removeAllNodes();
nodes.forEach((x, i, k) => {
guideContainer.removeChild(x);
});
update();
}
btnRemove.onclick = function () {
let nodes = PointStore.removeSelectedNode();
nodes.forEach((x, i, k) => {
guideContainer.removeChild(x);
});
update();
}
function update() {
let p = PointStore.getPath();
div.innerText = p.path;
path.setAttribute("d", p.path);
helperPath.setAttribute("d", p.guide);
}
// Initialie -- Draw a heart
(function drawInitilPath() {
let p = Factory.createPath("P", svg, 125, 175);
PointStore.addNewPoint(p);
let domNode = p.getNode();
domNode.classList.add("baseNode");
guideContainer.appendChild(domNode);
p = Factory.createPath("C", svg, 250, 150);
p.metaPoint[0].x = 100;
p.metaPoint[0].y = 50;
p.metaPoint[1].x = 250;
p.metaPoint[1].y = 50;
PointStore.addNewPoint(p);
guideContainer.appendChild(p.getNode());
p = Factory.createPath("C", svg, 375, 175);
p.metaPoint[0].x = 250;
p.metaPoint[0].y = 50;
p.metaPoint[1].x = 400;
p.metaPoint[1].y = 50;
PointStore.addNewPoint(p);
guideContainer.appendChild(p.getNode());
p = Factory.createPath("C", svg, 250, 350);
p.metaPoint[0].x = 350;
p.metaPoint[0].y = 250;
p.metaPoint[1].x = 275;
p.metaPoint[1].y = 275;
PointStore.addNewPoint(p);
guideContainer.appendChild(p.getNode());
p = Factory.createPath("C", svg, 125, 175);
p.metaPoint[0].x = 225;
p.metaPoint[0].y = 275;
p.metaPoint[1].x = 150;
p.metaPoint[1].y = 250;
PointStore.addNewPoint(p);
guideContainer.appendChild(p.getNode());
update();
})()
let svg: any = document.getElementById("svg1");
let path = document.getElementById("path");
let div = document.getElementById("div");
let helperPath = document.getElementById("helperPath");
let btnClear = document.getElementById("btnClear");
let btnRemove = document.getElementById("btnRemove");
let guideContainer = document.getElementById("guideContainer");
let gridContainer = document.getElementById("gridContainer");
let showGuide = document.getElementById("showGuide");
let showGrid = document.getElementById("showGrid") as HTMLInputElement;
export { svg, path, div, btnRemove, btnClear, helperPath, guideContainer, showGuide, gridContainer ,showGrid }
import { PointStore } from "./PointStore"
export class Point {
x = 0;
y = 0;
elm: SVGCircleElement;
metaPoint: Point[] = [];
type = "M"
static points = [];
container: SVGElement;
guideContainer: SVGGElement;
constructor(container: SVGElement, x, y) {
this.container = container;
this.guideContainer = container.querySelector('#guideContainer');
this.x = x;
this.y = y;
this.elm = this.createPoint();
this.elm.onclick = e => {
e.stopPropagation();
PointStore.points.forEach(i => {
i.getNode().classList.remove("selectedNode");
});
PointStore.selectedPoint = this;
this.elm.classList.add("selectedNode");
};
let mmH = e => {
e.stopPropagation();
let offset = container.getBoundingClientRect();
let x = e.x - offset.left;
let y = e.y - offset.top;
if (e.which == 1) {
this.update(x, y);
PointStore.notify();
}
};
let muH = e => {
e.stopPropagation();
container.removeEventListener("mousemove", mmH);
container.removeEventListener("mouseup", muH);
};
this.elm.onmousedown = e => {
e.stopPropagation();
container.addEventListener("mousemove", mmH);
container.addEventListener("mouseup", muH);
};
}
createPoint() {
let point = document.createElementNS("http://www.w3.org/2000/svg", "circle");
point.setAttribute("stroke", "darkgray");
point.setAttribute("class", "point");
point.setAttribute("stroke-width", "2");
point.setAttribute("fill", "transparent");
point.setAttribute("r", "5");
return point;
}
update(x, y) {
this.x = x || this.x;
this.y = y || this.y;
this.elm.setAttribute("cx", this.x.toString());
this.elm.setAttribute("cy", this.y.toString());
}
getNode() {
this.update(this.x, this.y);
return this.elm;
}
getPath() {
return { path: `M${this.x},${this.y}`, guide: '' };
}
}
export class Line extends Point {
constructor(container, x, y) {
super(container, x, y);
this.type = "l"
}
getPath() {
let pp = PointStore.getPreciousPoint(this);
let path = `l${this.x - pp.x},${this.y - pp.y}`;
return { path, guide: '' };
}
}
export class Quad extends Point {
sx = 0;
sy = 0;
constructor(container: SVGElement, x, y) {
super(container, x, y);
let lp = PointStore.getLastPoint();
this.sx = (lp.x + x) / 2;
this.sy = 25 + (lp.y + y) / 2;
this.type = "q"
let meta = new Point(this.container, this.sx, this.sy)
this.metaPoint.push(meta);
let meta1Node = meta.getNode();
meta1Node.classList.add('helperNode');
this.guideContainer.appendChild(meta1Node);
}
update(x, y) {
super.update(x, y);
let q = this.metaPoint[0];
q && q.update(q.x, q.y);
}
getPath() {
let q = this.metaPoint[0];
let pp = PointStore.getPreciousPoint(this);
let xOff = pp.x;
let yOff = pp.y;
let path = `q${q.x - xOff},${q.y - yOff} ${this.x - xOff},${this.y - yOff}`;
let guide = `M${pp.x},${pp.y} L${q.x},${q.y} L${this.x},${this.y}`;
return {
path,
guide
};
}
}
export class Cube extends Point {
sxa = 0;
sya = 0;
sxb = 0;
syb = 0;
constructor(container: SVGElement, x, y) {
super(container, x, y);
let lp = PointStore.getLastPoint();
this.sxa = -5 + (lp.x + x) / 2;
this.sya = 25 + (lp.y + y) / 2;
this.sxb = 5 + (lp.x + x) / 2;
this.syb = -25 + (lp.y + y) / 2;
this.type = "c"
let meta1 = new Point(this.container, this.sxa, this.sya);
let meta2 = new Point(this.container, this.sxb, this.syb);
this.metaPoint.push(meta1);
let meta1Node = meta1.getNode();
this.guideContainer.appendChild(meta1Node);
meta1Node.classList.add('helperNode');
let meta2Node = meta2.getNode();
this.metaPoint.push(meta2);
this.guideContainer.appendChild(meta2Node);
meta2Node.classList.add('helperNode');
}
update(x, y) {
super.update(x, y);
let q1 = this.metaPoint[0];
let q2 = this.metaPoint[1];
q1 && q1.update(q1.x, q1.y);
q2 && q2.update(q2.x, q2.y);
}
getPath() {
let p1 = this.metaPoint[0];
let p2 = this.metaPoint[1];
let pp = PointStore.getPreciousPoint(this);
let xOff = pp.x;
let yOff = pp.y;
let path = `c${p1.x - xOff},${p1.y - yOff} ${p2.x - xOff},${p2.y - yOff} ${this.x - xOff},${this.y - yOff}`;
let guide = `M${p1.x},${p1.y} L${pp.x},${pp.y}` + `M${p2.x},${p2.y} L${this.x},${this.y}`;
return {
path,
guide
};
}
}
export class Arc extends Point {
rx = 0;
ry = 0;
constructor(container: SVGElement, x, y) {
super(container, x, y);
let lp = PointStore.getLastPoint();
this.rx = (lp.x + x) / 2;
this.ry = (lp.y + y) / 2;
this.type = "a"
let meta1 = new Point(this.container, this.rx, this.ry);
this.metaPoint.push(meta1);
let meta1Node = meta1.getNode();
this.guideContainer.appendChild(meta1Node);
meta1Node.classList.add('helperNode');
}
update(x, y) {
super.update(x, y);
let q1 = this.metaPoint[0];
q1 && q1.update(q1.x, q1.y);
}
getPath() {
let p1 = this.metaPoint[0];
let pp = PointStore.getPreciousPoint(this);
let xOff = pp.x;
let yOff = pp.y;
let x = (p1.x) / 2
let y = (p1.y) / 2
let path = `A${x},${y} 0 1 1 ${this.x - xOff},${this.y - yOff}`;
return {
path,
guide: ''
};
}
}
export class Factory {
static createPath(pathName, container, x, y) {
switch (pathName) {
case "Point":
case "P":
return new Point(container, x, y);
case "Line":
case "L":
return new Line(container, x, y);
break;
case "Cube":
case "C":
return new Cube(container, x, y);
break;
case "Quad":
case "Q":
return new Quad(container, x, y);
break;
case "Arc":
case "A":
return new Arc(container, x, y);
break;
}
}
}
import { Point } from "./Point"
class PS {
points: Point[] = [];
guidePoints = [];
selectedPoint: Point;
notify() { }
addNewPoint(point: Point) {
this.points.push(point);
}
getPath() {
let path = this.points.reduce((a, b, index) => {
return [...a, b.getPath().path];
}, []);
let guide = this.points.reduce((a, b, index) => {
return [...a, b.getPath().guide];
}, []);
return { path: path.join(" "), guide: guide.join(" ") };
}
removeSelectedNode() {
let i = this.points.indexOf(this.selectedPoint);
let removedNodes: SVGCircleElement[] = [];
if (i > 0) {
for (let sn of this.selectedPoint.metaPoint) {
removedNodes.push(sn.getNode());
}
removedNodes.push(this.selectedPoint.getNode());
this.selectedPoint.metaPoint.length = 0;
this.points.splice(i, 1);
}
return removedNodes;
}
removeAllNodes() {
let removedNodes: SVGCircleElement[] = [];
for (let i = this.points.length - 1; i >= 0; i--) {
let cr = this.points[i];
removedNodes.push(cr.getNode());
for (let sn of cr.metaPoint) {
removedNodes.push(sn.getNode());
}
cr.metaPoint.length = 0;
this.points.splice(i, 1);
}
this.points.length = 0;
return removedNodes;
}
getPreciousPoint(point: Point) {
let index = this.points.indexOf(point);
return this.points[index - 1];
}
getLastPoint() {
let index = this.points.length - 1;
return this.points[index];
}
}
let PointStore = new PS();
export { PointStore }