Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#30 Use native SVG Quadratic and Cubic Beziers where possible
- Loading branch information
Ben Nortier
committed
Jul 12, 2019
1 parent
2ea3dc8
commit d557fb0
Showing
12 changed files
with
3,107 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/** | ||
* Knot insertion is known as "Boehm's algorithm" | ||
* | ||
* https://math.stackexchange.com/questions/417859/convert-a-b-spline-into-bezier-curves | ||
* code adapted from http://preserve.mactech.com/articles/develop/issue_25/schneider.html | ||
*/ | ||
export default (k, controlPoints, knots, newKnot) => { | ||
const x = knots | ||
const b = controlPoints | ||
const n = controlPoints.length | ||
let i = 0 | ||
let foundIndex = false | ||
for (let j = 0; j < n + k; j++) { | ||
if (newKnot > x[j] && newKnot <= x[j + 1]) { | ||
i = j | ||
foundIndex = true | ||
break | ||
} | ||
} | ||
if (!foundIndex) { | ||
throw new Error('invalid new knot') | ||
} | ||
|
||
const xHat = [] | ||
for (let j = 0; j < n + k + 1; j++) { | ||
if (j <= i) { | ||
xHat[j] = x[j] | ||
} else if (j === i + 1) { | ||
xHat[j] = newKnot | ||
} else { | ||
xHat[j] = x[j - 1] | ||
} | ||
} | ||
|
||
let alpha | ||
const bHat = [] | ||
for (let j = 0; j < n + 1; j++) { | ||
if (j <= i - k + 1) { | ||
alpha = 1 | ||
} else if (i - k + 2 <= j && j <= i) { | ||
if (x[j + k - 1] - x[j] === 0) { | ||
alpha = 0 | ||
} else { | ||
alpha = (newKnot - x[j]) / (x[j + k - 1] - x[j]) | ||
} | ||
} else { | ||
alpha = 0 | ||
} | ||
|
||
if (alpha === 0) { | ||
bHat[j] = b[j - 1] | ||
} else if (alpha === 1) { | ||
bHat[j] = b[j] | ||
} else { | ||
bHat[j] = { | ||
x: (1 - alpha) * b[j - 1].x + alpha * b[j].x, | ||
y: (1 - alpha) * b[j - 1].y + alpha * b[j].y | ||
} | ||
} | ||
} | ||
return { controlPoints: bHat, knots: xHat } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import insertKnot from './insertKnot' | ||
|
||
/** | ||
* For a pinned spline, the knots have to be repeated k times | ||
* (where k is the order), at both the beginning and the end | ||
*/ | ||
export const checkPinned = (k, knots) => { | ||
// Pinned at the start | ||
for (let i = 1; i < k; ++i) { | ||
if (knots[i] !== knots[0]) { | ||
throw Error(`not pinned. order: ${k} knots: ${knots}`) | ||
} | ||
} | ||
// Pinned at the end | ||
for (let i = knots.length - 2; i > knots.length - k - 1; --i) { | ||
if (knots[i] !== knots[knots.length - 1]) { | ||
throw Error(`not pinned. order: ${k} knots: ${knots}`) | ||
} | ||
} | ||
} | ||
|
||
const multiplicity = (knots, index) => { | ||
let m = 1 | ||
for (let i = index + 1; i < knots.length; ++i) { | ||
if (knots[i] === knots[index]) { | ||
++m | ||
} else { | ||
break | ||
} | ||
} | ||
return m | ||
} | ||
|
||
/** | ||
* https://saccade.com/writing/graphics/KnotVectors.pdf | ||
* A quadratic piecewise Bézier knot vector with seven control points | ||
* will look like this [0 0 0 1 1 2 2 3 3 3]. In general, in a | ||
* piecewise Bézier knot vector the first k knots are the same, | ||
* then each subsequent group of k-1 knots is the same, | ||
* until you get to the end. | ||
*/ | ||
export const computeInsertions = (k, knots) => { | ||
const inserts = [] | ||
let i = k | ||
while (i < knots.length - k) { | ||
const knot = knots[i] | ||
const m = multiplicity(knots, i) | ||
for (let j = 0; j < k - m - 1; ++j) { | ||
inserts.push(knot) | ||
} | ||
i = i + m | ||
} | ||
return inserts | ||
} | ||
|
||
export default (k, controlPoints, knots) => { | ||
checkPinned(k, knots) | ||
const insertions = computeInsertions(k, knots) | ||
return insertions.reduce((acc, tNew) => { | ||
return insertKnot(k, acc.controlPoints, acc.knots, tNew) | ||
}, { controlPoints, knots }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<!doctype html> | ||
<html> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
<style> | ||
body { | ||
background-color: #eee; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div id="contents"></div> | ||
<script src="toBezier.test.bundle.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import React from 'react' | ||
import { render } from 'react-dom' | ||
import { HashRouter } from 'react-router-dom' | ||
|
||
import { interpolateBSpline } from '../../src/entityToPolyline' | ||
import toPiecewiseBezier from '../../src/util/toPiecewiseBezier' | ||
import { piecewiseToPaths } from '../../src/toSVG' | ||
|
||
const controlPoints = [ | ||
{ x: 0, y: 0 }, | ||
{ x: 10, y: 0 }, | ||
{ x: 10, y: 10 }, | ||
{ x: 0, y: 10 }, | ||
{ x: 0, y: 20 }, | ||
{ x: 10, y: 20 } | ||
] | ||
const k = 4 | ||
const knots = [0, 0, 0, 0, 1, 2, 3, 3, 3, 3] | ||
const viewBox = '-1 -21 12 22' | ||
|
||
// const controlPoints = [ | ||
// { x: 0, y: 0 }, | ||
// { x: 122.4178296875701, y: -38.53600688262475 }, | ||
// { x: 77.52934654015353, y: 149.4771453152231 }, | ||
// { x: 200, y: 100 } | ||
// ] | ||
// const k = 3 | ||
// const knots = [0, 0, 0, 0.5, 1, 1, 1] | ||
// const viewBox = '-1 -160 200 200' | ||
|
||
const interpolated0 = interpolateBSpline(controlPoints, k - 1, knots) | ||
|
||
const polylineToPath = (polyline) => { | ||
const d = polyline.reduce(function (acc, point, i) { | ||
acc += (i === 0) ? 'M' : 'L' | ||
acc += point[0] + ',' + point[1] | ||
return acc | ||
}, '') | ||
return <path d={d} /> | ||
} | ||
|
||
const result = toPiecewiseBezier(k, controlPoints, knots) | ||
const interpolated1 = interpolateBSpline(result.controlPoints, k - 1, result.knots) | ||
const paths = piecewiseToPaths(k, result.controlPoints, k.knots) | ||
|
||
render(<HashRouter> | ||
<div> | ||
<svg | ||
preserveAspectRatio='xMinYMin meet' | ||
viewBox={viewBox} | ||
width='200' | ||
height='400' | ||
> | ||
<g stroke='#000' fill='none' strokeWidth='0.1' transform='matrix(1,0,0,-1,0,0)'> | ||
{polylineToPath(interpolated0)} | ||
</g> | ||
</svg> | ||
<svg | ||
preserveAspectRatio='xMinYMin meet' | ||
viewBox={viewBox} | ||
width='200' | ||
height='400' | ||
> | ||
<g stroke='#000' fill='none' strokeWidth='0.1' transform='matrix(1,0,0,-1,0,0)'> | ||
{polylineToPath(interpolated1)} | ||
</g> | ||
</svg> | ||
<svg | ||
preserveAspectRatio='xMinYMin meet' | ||
viewBox={viewBox} | ||
width='200' | ||
height='400' | ||
> | ||
<g stroke='#000' fill='none' strokeWidth='0.1' transform='matrix(1,0,0,-1,0,0)' | ||
dangerouslySetInnerHTML={{ __html: paths }} | ||
/> | ||
</svg> | ||
</div> | ||
</HashRouter>, document.getElementById('contents')) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.