<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="favicon.png">
<title></title>
<meta name="description" content="" />
<style>
body {
margin: 0;
}
#mySvg {
position: absolute;
top: 3rem;
left: 0rem;
width: 64rem;
height: 36rem;
border: 1px solid black;
display: inline-block;
background: url("");
background-size: cover;
}
.curve {
fill: none;
stroke: black;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.tempCurve {
fill: none;
stroke: red;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
</style>
</head>
<body>
<div>
<div id="menu">
BG:<input type="color" id="bgColor" value="#000000" /> <input type="range" min="0" max="20" value="10" id="bgWidth" />
FG:<input type="color" id="fgColor" value="#FFFFFF" /> <input type="range" min="0" max="20" value="5" id="fgWidth" />
Tol: <input type="range" min="1" max="50" value="5" id="tolerance" />
<button onclick="deleteLast()">Delete Last Curve</button>
</div>
<svg id="mySvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path id="temp-path-bg" d="" class="tempCurve" />
<path id="temp-path-fg" d="" class="tempCurve" />
</svg>
</div>
<script src="../simplify.js"></script>
<script src="script.js"></script>
</body>
</html>
var svg = document.getElementById('mySvg');
var tempPathBG = document.getElementById('temp-path-bg');
var tempPathFG = document.getElementById('temp-path-fg');
svg.addEventListener('pointerdown',pointerDown);
svg.addEventListener('pointermove',pointerMove);
svg.addEventListener('pointerup',pointerUp);
svg.addEventListener('touchstart',function(evt){evt.preventDefault();});
svg.style.webkitTouchCallout = "none";
svg.style.khtmlUserSelect = "none";
svg.style.MozUserSelect = "none";
svg.style.msUserSelect = "none";
svg.style.userSelect = "none";
document.body.addEventListener('pointerleave',pointerEnd);
document.body.addEventListener('pointerup',pointerEnd);
document.body.addEventListener('pointercancel',pointerEnd);
var svgX = 100;
var svgY = 100;
var rect = svg.getBoundingClientRect();
var points = [];
var tempD = "";
var isDown = false;
var settings = {bg:{color:"#AAFFFF",width:widthToWidth(10)},fg:{color:"#440000",width:widthToWidth(5)},tolerance:1,precision:2};
settings.bg.color = document.getElementById('bgColor').value;
settings.bg.width = widthToWidth(document.getElementById('bgWidth').value);
settings.fg.color = document.getElementById('fgColor').value;
settings.fg.width = widthToWidth(document.getElementById('fgWidth').value);
var pathIdx = 0;
function resizeSvg(setViewBox=true){
rect = svg.getBoundingClientRect();
var w = 100*rect.width/rect.height;
if (setViewBox){
svg.setAttribute('viewBox','0 0 '+w+' 100');
}
svgY = 100;
svgX = w;
}
resizeSvg();
function toX(input){
return svgX*(input-rect.x)/rect.width;
}
function toY(input){
return svgY*(input-rect.y)/rect.height;
}
function widthToWidth(input){
return Math.pow(3,parseInt(input)/5-1);
}
function toTolerance(input){
return parseInt(input)/5;
}
function pointerDown(evt){
if (evt.pointerType == "touch"){isDown = false; return;}
isDown = true;
resizeSvg(false);
var x = toX(evt.clientX);
var y = toY(evt.clientY);
points = [{x:x,y:y}];
settings.bg.color = document.getElementById('bgColor').value;
settings.bg.width = widthToWidth(document.getElementById('bgWidth').value);
settings.fg.color = document.getElementById('fgColor').value;
settings.fg.width = widthToWidth(document.getElementById('fgWidth').value);
settings.tolerance = toTolerance(document.getElementById('tolerance').value)
tempPathBG.setAttributeNS(null,"d","");
tempPathBG.style.strokeWidth = settings.bg.width;
tempPathBG.style.stroke = settings.bg.color;
tempPathFG.setAttributeNS(null,"d","");
tempPathFG.style.strokeWidth = settings.fg.width;
tempPathFG.style.stroke = settings.fg.color;
tempD = "M"+x+","+y+" ";
}
function pointerMove(evt){
if (!isDown){return}
var evts = false;
if (evt.getCoalescedEvents){
evts = evt.getCoalescedEvents();
}
var x; var y;
if (evts){
for (var i=0;i<evts.length;i++){
x = toX(evts[i].clientX);
y = toY(evts[i].clientY);
points.push({x:x,y:y});
tempD += x+","+y+" ";
}
}
else {
x = toX(evt.clientX);
y = toY(evt.clientY);
points.push({x:x,y:y});
tempD += x+","+y+" ";
}
tempPathBG.setAttributeNS(null,"d",tempD);
tempPathFG.setAttributeNS(null,"d",tempD);
}
function pointerUp(evt){
if (!isDown){return}
isDown = false;
var x = toX(evt.clientX);
var y = toY(evt.clientY);
points.push({x:x,y:y});
addPath(points);
tempPathBG.setAttributeNS(null,"d","");
tempPathFG.setAttributeNS(null,"d","");
}
function pointerEnd(evt){
if (!evt.target || evt.target.tagName != "BODY"){return;}
if (!isDown){return}
isDown = false;
addPath(points);
tempPathBG.setAttributeNS(null,"d","");
tempPathFG.setAttributeNS(null,"d","");
}
function addPath(pts){
if (pts.length < 1){return;}
var simplified = simplifySvgPath(pts,{tolerance: settings.tolerance, precision: settings.precision});
var d = simplified;
if (settings.bg.width >= settings.fg.width){
var path = document.createElementNS("http://www.w3.org/2000/svg","path");
path.setAttributeNS(null,"d",d);
path.classList.add("curve");
path.style.strokeWidth = settings.bg.width;
path.style.stroke = settings.bg.color;
path.id = "path-"+pathIdx+"-bg";
tempPathBG.before(path);
}
if (settings.fg.width > 0){
var path = document.createElementNS("http://www.w3.org/2000/svg","path");
path.setAttributeNS(null,"d",d);
path.classList.add("curve");
path.style.strokeWidth = settings.fg.width;
path.style.stroke = settings.fg.color;
path.id = "path-"+pathIdx+"-fg";
tempPathFG.before(path);
}
pathIdx++;
}
function deletePath(id){
var el = document.getElementById("path-"+id+"-bg");
if (el){el.parentNode.removeChild(el)};
el = document.getElementById("path-"+id+"-fg");
if (el){el.parentNode.removeChild(el)};
}
function deleteLast(){
var els = document.querySelectorAll("#mySvg > path");
var maxId = -1;
els.forEach((el) => {
if (el.id && el.id.substring(0,5) == "path-"){
var idx = parseInt(el.id.split("-")[1]);
if (idx > maxId){maxId = idx;}
}
})
if (maxId > -1){
deletePath(maxId);
}
}
const EPSILON = 1e-12;
const MACHINE_EPSILON = 1.12e-16;
const isMachineZero = (val) => val >= -MACHINE_EPSILON && val <= MACHINE_EPSILON;
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
_negate() {
return new Point(-this.x, -this.y);
}
_normalize(length = 1) {
return this._multiply(length / (this._getLength() || Infinity));
}
_add(p) {
return new Point(this.x + p.x, this.y + p.y);
}
_subtract(p) {
return new Point(this.x - p.x, this.y - p.y);
}
_multiply(n) {
return new Point(this.x * n, this.y * n);
}
_dot(p) {
return this.x * p.x + this.y * p.y;
}
_getLength() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
_getDistance(p) {
const dx = this.x - p.x;
const dy = this.y - p.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
class Segment {
constructor(_point, _handleIn) {
this._point = _point;
this._handleIn = _handleIn;
}
}
const fit = (points, closed, error) => {
if (closed) {
points.unshift(points[points.length - 1]);
points.push(points[1]);
}
const length = points.length;
if (length === 0) {
return [];
}
const segments = [new Segment(points[0])];
fitCubic(points, segments, error, 0, length - 1,
points[1]._subtract(points[0]),
points[length - 2]._subtract(points[length - 1]));
if (closed) {
segments.shift();
segments.pop();
}
return segments;
};
const fitCubic = (points, segments, error, first, last, tan1, tan2) => {
if (last - first === 1) {
const pt1 = points[first], pt2 = points[last], dist = pt1._getDistance(pt2) / 3;
addCurve(segments, [pt1, pt1._add(tan1._normalize(dist)), pt2._add(tan2._normalize(dist)), pt2]);
return;
}
const uPrime = chordLengthParameterize(points, first, last);
let maxError = Math.max(error, error * error), split, parametersInOrder = true;
for (let i = 0; i <= 4; i++) {
const curve = generateBezier(points, first, last, uPrime, tan1, tan2);
const max = findMaxError(points, first, last, curve, uPrime);
if (max.error < error && parametersInOrder) {
addCurve(segments, curve);
return;
}
split = max.index;
if (max.error >= maxError)
break;
parametersInOrder = reparameterize(points, first, last, uPrime, curve);
maxError = max.error;
}
const tanCenter = points[split - 1]._subtract(points[split + 1]);
fitCubic(points, segments, error, first, split, tan1, tanCenter);
fitCubic(points, segments, error, split, last, tanCenter._negate(), tan2);
};
const addCurve = (segments, curve) => {
const prev = segments[segments.length - 1];
prev._handleOut = curve[1]._subtract(curve[0]);
segments.push(new Segment(curve[3], curve[2]._subtract(curve[3])));
};
const generateBezier = (points, first, last, uPrime, tan1, tan2) => {
const epsilon = EPSILON, abs = Math.abs, pt1 = points[first], pt2 = points[last],
C = [
[0, 0],
[0, 0],
], X = [0, 0];
for (let i = 0, l = last - first + 1; i < l; i++) {
const u = uPrime[i], t = 1 - u, b = 3 * u * t, b0 = t * t * t, b1 = b * t, b2 = b * u, b3 = u * u * u, a1 = tan1._normalize(b1), a2 = tan2._normalize(b2), tmp = points[first + i]._subtract(pt1._multiply(b0 + b1))._subtract(pt2._multiply(b2 + b3));
C[0][0] += a1._dot(a1);
C[0][1] += a1._dot(a2);
C[1][0] = C[0][1];
C[1][1] += a2._dot(a2);
X[0] += a1._dot(tmp);
X[1] += a2._dot(tmp);
}
const detC0C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
let alpha1;
let alpha2;
if (abs(detC0C1) > epsilon) {
const detC0X = C[0][0] * X[1] - C[1][0] * X[0], detXC1 = X[0] * C[1][1] - X[1] * C[0][1];
alpha1 = detXC1 / detC0C1;
alpha2 = detC0X / detC0C1;
}
else {
const c0 = C[0][0] + C[0][1], c1 = C[1][0] + C[1][1];
alpha1 = alpha2 = abs(c0) > epsilon ? X[0] / c0 : abs(c1) > epsilon ? X[1] / c1 : 0;
}
const segLength = pt2._getDistance(pt1), eps = epsilon * segLength;
let handle1, handle2;
if (alpha1 < eps || alpha2 < eps) {
alpha1 = alpha2 = segLength / 3;
}
else {
const line = pt2._subtract(pt1);
handle1 = tan1._normalize(alpha1);
handle2 = tan2._normalize(alpha2);
if (handle1._dot(line) - handle2._dot(line) > segLength * segLength) {
alpha1 = alpha2 = segLength / 3;
handle1 = handle2 = null;
}
}
return [pt1, pt1._add(handle1 || tan1._normalize(alpha1)), pt2._add(handle2 || tan2._normalize(alpha2)), pt2];
};
const reparameterize = (points, first, last, u, curve) => {
for (let i = first; i <= last; i++) {
u[i - first] = findRoot(curve, points[i], u[i - first]);
}
for (let i = 1, l = u.length; i < l; i++) {
if (u[i] <= u[i - 1])
return false;
}
return true;
};
const findRoot = (curve, point, u) => {
const curve1 = [], curve2 = [];
for (let i = 0; i <= 2; i++) {
curve1[i] = curve[i + 1]._subtract(curve[i])._multiply(3);
}
for (let i = 0; i <= 1; i++) {
curve2[i] = curve1[i + 1]._subtract(curve1[i])._multiply(2);
}
const pt = evaluate(3, curve, u), pt1 = evaluate(2, curve1, u), pt2 = evaluate(1, curve2, u), diff = pt._subtract(point), df = pt1._dot(pt1) + diff._dot(pt2);
return isMachineZero(df) ? u : u - diff._dot(pt1) / df;
};
const evaluate = (degree, curve, t) => {
const tmp = curve.slice();
for (let i = 1; i <= degree; i++) {
for (let j = 0; j <= degree - i; j++) {
tmp[j] = tmp[j]._multiply(1 - t)._add(tmp[j + 1]._multiply(t));
}
}
return tmp[0];
};
const chordLengthParameterize = (points, first, last) => {
const u = [0];
for (let i = first + 1; i <= last; i++) {
u[i - first] = u[i - first - 1] + points[i]._getDistance(points[i - 1]);
}
for (let i = 1, m = last - first; i <= m; i++) {
u[i] /= u[m];
}
return u;
};
const findMaxError = (points, first, last, curve, u) => {
let index = Math.floor((last - first + 1) / 2), maxDist = 0;
for (let i = first + 1; i < last; i++) {
const P = evaluate(3, curve, u[i - first]);
const v = P._subtract(points[i]);
const dist = v.x * v.x + v.y * v.y;
if (dist >= maxDist) {
maxDist = dist;
index = i;
}
}
return {
error: maxDist,
index: index,
};
};
const getSegmentsPathData = (segments, closed, precision) => {
const length = segments.length;
const precisionMultiplier = 10 ** precision;
const round = precision < 16 ? (n) => Math.round(n * precisionMultiplier) / precisionMultiplier : (n) => n;
const formatPair = (x, y) => round(x) + ',' + round(y);
let first = true;
let prevX, prevY, outX, outY;
const parts = [];
const addSegment = (segment, skipLine) => {
const curX = segment._point.x;
const curY = segment._point.y;
if (first) {
parts.push('M' + formatPair(curX, curY));
first = false;
}
else {
const inX = curX + (segment._handleIn?.x ?? 0);
const inY = curY + (segment._handleIn?.y ?? 0);
if (inX === curX && inY === curY && outX === prevX && outY === prevY) {
if (!skipLine) {
const dx = curX - prevX;
const dy = curY - prevY;
parts.push(dx === 0 ? 'v' + round(dy) : dy === 0 ? 'h' + round(dx) : 'l' + formatPair(dx, dy));
}
}
else {
parts.push('c' +
formatPair(outX - prevX, outY - prevY) +
' ' +
formatPair(inX - prevX, inY - prevY) +
' ' +
formatPair(curX - prevX, curY - prevY));
}
}
prevX = curX;
prevY = curY;
outX = curX + (segment._handleOut?.x ?? 0);
outY = curY + (segment._handleOut?.y ?? 0);
};
if (!length)
return '';
for (let i = 0; i < length; i++)
addSegment(segments[i]);
if (closed && length > 0) {
addSegment(segments[0], true);
parts.push('z');
}
return parts.join('');
};
const simplifySvgPath = (points, options = {}) => {
if (points.length === 0) {
return '';
}
return getSegmentsPathData(fit(points.map(typeof points[0].x === 'number' ? (p) => new Point(p.x, p.y) : (p) => new Point(p[0], p[1])), options.closed, options.tolerance ?? 2.5), options.closed, options.precision ?? 5);
};