├── .gitignore ├── .npmignore ├── .travis.yml ├── src ├── length.js ├── index.js ├── offset.js ├── reverse.js ├── boundingBox.js ├── helpers.js ├── remove.js ├── rotate.js ├── cubify.js ├── scale.js ├── decurve.js ├── add.js ├── position.js └── moveIndex.js ├── rollup.config.js ├── test ├── reverse.js ├── boundingBox.js ├── decurve.js ├── remove.js ├── rotate.js ├── offset.js ├── helpers.js ├── add.js ├── cubify.js ├── position.js ├── length.js ├── scale.js └── moveIndex.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | cjs/ 2 | dist/ 3 | modules/ 4 | node_modules/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .travis.yml 3 | rollup.config.js 4 | test/ 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 8 5 | notifications: 6 | email: false 7 | before_script: 8 | - npm prune 9 | script: 10 | - npm run lint 11 | - npm test 12 | jobs: 13 | include: 14 | - stage: release 15 | node_js: lts/* 16 | deploy: 17 | provider: script 18 | skip_cleanup: true 19 | script: 20 | - npx semantic-release 21 | -------------------------------------------------------------------------------- /src/length.js: -------------------------------------------------------------------------------- 1 | import decurve from './decurve' 2 | import { linearLength } from './helpers' 3 | 4 | const length = (shape, accuracy) => { 5 | const s = decurve(shape, accuracy) 6 | 7 | return s.reduce((currentLength, { x: x2, y: y2, moveTo }, i) => { 8 | if (!moveTo) { 9 | const { x: x1, y: y1 } = s[ i - 1 ] 10 | currentLength += linearLength(x1, y1, x2, y2) 11 | } 12 | 13 | return currentLength 14 | }, 0) 15 | } 16 | 17 | export default length 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import add from './add' 2 | import boundingBox from './boundingBox' 3 | import cubify from './cubify' 4 | import length from './length' 5 | import moveIndex from './moveIndex' 6 | import offset from './offset' 7 | import position from './position' 8 | import remove from './remove' 9 | import reverse from './reverse' 10 | import rotate from './rotate' 11 | import scale from './scale' 12 | 13 | export { 14 | add, 15 | boundingBox, 16 | cubify, 17 | length, 18 | moveIndex, 19 | offset, 20 | position, 21 | remove, 22 | reverse, 23 | rotate, 24 | scale 25 | } 26 | -------------------------------------------------------------------------------- /src/offset.js: -------------------------------------------------------------------------------- 1 | import { applyFuncToShapes } from './helpers' 2 | 3 | const offsetPoints = (shape, x, y) => shape.map(point => { 4 | const p = { ...point } 5 | 6 | p.x += x 7 | p.y += y 8 | 9 | if (p.curve) { 10 | p.curve = { ...p.curve } 11 | 12 | if (p.curve.type === 'quadratic' || p.curve.type === 'cubic') { 13 | p.curve.x1 += x 14 | p.curve.y1 += y 15 | } 16 | 17 | if (p.curve.type === 'cubic') { 18 | p.curve.x2 += x 19 | p.curve.y2 += y 20 | } 21 | } 22 | 23 | return p 24 | }) 25 | 26 | const offset = (s, x = 0, y = 0) => applyFuncToShapes(offsetPoints, s, x, y) 27 | 28 | export default offset 29 | -------------------------------------------------------------------------------- /src/reverse.js: -------------------------------------------------------------------------------- 1 | import cubify from './cubify' 2 | import { applyFuncToShapes } from './helpers' 3 | 4 | const reversePoints = shape => { 5 | let m 6 | let c 7 | 8 | return shape.reverse().map(({ x, y, moveTo, curve }, i) => { 9 | const point = { x, y } 10 | 11 | if (c) { 12 | const { x1: x2, y1: y2, x2: x1, y2: y1 } = c 13 | point.curve = { type: 'cubic', x1, y1, x2, y2 } 14 | } 15 | 16 | if (i === 0 || m) { 17 | point.moveTo = true 18 | } 19 | 20 | m = moveTo 21 | c = curve || null 22 | 23 | return point 24 | }) 25 | } 26 | 27 | const reverse = s => applyFuncToShapes(reversePoints, cubify(s)) 28 | 29 | export default reverse 30 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonJs from 'rollup-plugin-commonjs' 3 | import nodeResolve from 'rollup-plugin-node-resolve' 4 | import { uglify } from 'rollup-plugin-uglify' 5 | 6 | const config = { 7 | input: 'src/index.js', 8 | output: { 9 | name: 'Points', 10 | sourcemap: false, 11 | format: 'umd' 12 | }, 13 | plugins: [ 14 | babel({ exclude: 'node_modules/**' }), 15 | commonJs(), 16 | nodeResolve() 17 | ] 18 | } 19 | 20 | if (process.env.NODE_ENV === 'production') { 21 | config.output.file = 'dist/points.min.js' 22 | config.plugins.push(uglify()) 23 | } else { 24 | config.output.file = 'dist/points.js' 25 | } 26 | 27 | export default config 28 | -------------------------------------------------------------------------------- /src/boundingBox.js: -------------------------------------------------------------------------------- 1 | import decurve from './decurve' 2 | import { getShapeArray } from './helpers' 3 | 4 | const boundingBox = s => { 5 | let bottom 6 | let left 7 | let right 8 | let top 9 | 10 | const shapes = getShapeArray(s) 11 | 12 | shapes.map(shape => decurve(shape).map(({ x, y }) => { 13 | if (typeof bottom !== 'number' || y > bottom) { 14 | bottom = y 15 | } 16 | 17 | if (typeof left !== 'number' || x < left) { 18 | left = x 19 | } 20 | 21 | if (typeof right !== 'number' || x > right) { 22 | right = x 23 | } 24 | 25 | if (typeof top !== 'number' || y < top) { 26 | top = y 27 | } 28 | })) 29 | 30 | return { 31 | bottom, 32 | center: { 33 | x: left + ((right - left) / 2), 34 | y: top + ((bottom - top) / 2) 35 | }, 36 | left, 37 | right, 38 | top 39 | } 40 | } 41 | 42 | export default boundingBox 43 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | const angleFromSides = (a, b, c) => { 2 | const r = Math.acos( 3 | (Math.pow(a, 2) + Math.pow(b, 2) - Math.pow(c, 2)) / 4 | (2 * a * b) 5 | ) 6 | 7 | return r * (180 / Math.PI) 8 | } 9 | 10 | const applyFuncToShapes = (f, s, ...args) => { 11 | if (isShapeArray(s)) { 12 | return s.map(shape => f(shape, ...args)) 13 | } 14 | 15 | return f(s, ...args) 16 | } 17 | 18 | const getShapeArray = s => isShapeArray(s) ? s : [ s ] 19 | 20 | const linearLength = (x1, y1, x2, y2) => Math.sqrt( 21 | Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2) 22 | ) 23 | 24 | const isShapeArray = s => Array.isArray(s[ 0 ]) 25 | 26 | const numberAtInterval = (a, b, interval) => { 27 | const c = a === b ? 0 : Math.abs(b - a) 28 | return c === 0 ? a : (a < b ? a + c * interval : a - c * interval) 29 | } 30 | 31 | export { 32 | angleFromSides, 33 | applyFuncToShapes, 34 | getShapeArray, 35 | linearLength, 36 | isShapeArray, 37 | numberAtInterval 38 | } 39 | -------------------------------------------------------------------------------- /test/reverse.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import reverse from '../src/reverse' 4 | 5 | test('`reverse` should reverse order of points', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 100, y: 0 }, 9 | { x: 100, y: 100 }, 10 | { x: 0, y: 100 }, 11 | { x: 0, y: 0 } 12 | ] 13 | 14 | const expectedShape = [ 15 | { x: 0, y: 0, moveTo: true }, 16 | { x: 0, y: 100 }, 17 | { x: 100, y: 100 }, 18 | { x: 100, y: 0 }, 19 | { x: 0, y: 0 } 20 | ] 21 | 22 | expect(reverse(shape)).toEqual(expectedShape) 23 | }) 24 | 25 | test('`reverse` should maintain moveTo props in middle of points', () => { 26 | const shape = [ 27 | { x: 0, y: 0, moveTo: true }, 28 | { x: 100, y: 0 }, 29 | { x: 100, y: 100, moveTo: true }, 30 | { x: 0, y: 100 }, 31 | { x: 0, y: 0 } 32 | ] 33 | 34 | const expectedShape = [ 35 | { x: 0, y: 0, moveTo: true }, 36 | { x: 0, y: 100 }, 37 | { x: 100, y: 100 }, 38 | { x: 100, y: 0, moveTo: true }, 39 | { x: 0, y: 0 } 40 | ] 41 | 42 | expect(reverse(shape)).toEqual(expectedShape) 43 | }) 44 | -------------------------------------------------------------------------------- /src/remove.js: -------------------------------------------------------------------------------- 1 | import { applyFuncToShapes } from './helpers' 2 | 3 | const isBetween = (a, b, c) => { 4 | if (b.curve || c.curve) { 5 | return false 6 | } 7 | 8 | const crossProduct = 9 | (c.y - a.y) * 10 | (b.x - a.x) - 11 | (c.x - a.x) * 12 | (b.y - a.y) 13 | 14 | if (Math.abs(crossProduct) > Number.EPSILON) { 15 | return false 16 | } 17 | 18 | const dotProduct = 19 | (c.x - a.x) * 20 | (b.x - a.x) + 21 | (c.y - a.y) * 22 | (b.y - a.y) 23 | 24 | if (dotProduct < 0) { 25 | return false 26 | } 27 | 28 | const squaredLengthBA = 29 | (b.x - a.x) * 30 | (b.x - a.x) + 31 | (b.y - a.y) * 32 | (b.y - a.y) 33 | 34 | if (dotProduct > squaredLengthBA) { 35 | return false 36 | } 37 | 38 | return true 39 | } 40 | 41 | const removePoints = shape => { 42 | const s = [] 43 | 44 | for (let i = 0, l = shape.length; i < l; i++) { 45 | const a = s[ s.length - 1 ] 46 | const b = shape[ i + 1 ] 47 | const c = shape[ i ] 48 | 49 | if (!(a && b && c) || !(isBetween(a, b, c))) { 50 | s.push(c) 51 | } 52 | } 53 | 54 | return s 55 | } 56 | 57 | const remove = s => applyFuncToShapes(removePoints, s) 58 | 59 | export default remove 60 | -------------------------------------------------------------------------------- /src/rotate.js: -------------------------------------------------------------------------------- 1 | import { applyFuncToShapes } from './helpers' 2 | import boundingBox from './boundingBox' 3 | 4 | const rotatePoint = (x, y, c, s, about) => { 5 | const { x: offsetX, y: offsetY } = about 6 | const relativeX = x - offsetX 7 | const relativeY = y - offsetY 8 | 9 | return [ 10 | (relativeX * c - relativeY * s) + offsetX, 11 | (relativeX * s + relativeY * c) + offsetY 12 | ] 13 | } 14 | 15 | const rotatePoints = (shape, angle, about) => shape.map(point => { 16 | const r = angle * Math.PI / 180 17 | const c = Math.cos(r) 18 | const s = Math.sin(r) 19 | const [ x, y ] = rotatePoint(point.x, point.y, c, s, about) 20 | const p = { ...point, x, y } 21 | 22 | if (p.curve) { 23 | if (p.curve.type === 'quadratic' || p.curve.type === 'cubic') { 24 | const [ x1, y1 ] = rotatePoint(p.curve.x1, p.curve.y1, c, s, about) 25 | p.curve = { ...p.curve, x1, y1 } 26 | } 27 | 28 | if (p.curve.type === 'cubic') { 29 | const [ x2, y2 ] = rotatePoint(p.curve.x2, p.curve.y2, c, s, about) 30 | p.curve = { ...p.curve, x2, y2 } 31 | } 32 | } 33 | 34 | return p 35 | }) 36 | 37 | const rotate = (s, angle) => { 38 | const { center: about } = boundingBox(s) 39 | return applyFuncToShapes(rotatePoints, s, angle, about) 40 | } 41 | 42 | export default rotate 43 | -------------------------------------------------------------------------------- /src/cubify.js: -------------------------------------------------------------------------------- 1 | import arcToBezier from 'svg-arc-to-cubic-bezier' 2 | import { applyFuncToShapes } from './helpers' 3 | 4 | const cubifyShape = shape => { 5 | const s = [] 6 | 7 | for (let i = 0, l = shape.length; i < l; i++) { 8 | const point = shape[ i ] 9 | 10 | if (point.curve && point.curve.type !== 'cubic') { 11 | const { x: px, y: py } = shape[ i - 1 ] 12 | const { x: cx, y: cy } = point 13 | 14 | if (point.curve.type === 'arc') { 15 | const curves = arcToBezier({ 16 | px, 17 | py, 18 | cx, 19 | cy, 20 | rx: point.curve.rx, 21 | ry: point.curve.ry, 22 | xAxisRotation: point.curve.xAxisRotation, 23 | largeArcFlag: point.curve.largeArcFlag, 24 | sweepFlag: point.curve.sweepFlag 25 | }) 26 | 27 | curves.forEach(({ x1, y1, x2, y2, x, y }) => { 28 | s.push({ x, y, curve: { type: 'cubic', x1, y1, x2, y2 } }) 29 | }) 30 | } else if (point.curve.type === 'quadratic') { 31 | const x1 = px + (2 / 3 * (point.curve.x1 - px)) 32 | const y1 = py + (2 / 3 * (point.curve.y1 - py)) 33 | const x2 = cx + (2 / 3 * (point.curve.x1 - cx)) 34 | const y2 = cy + (2 / 3 * (point.curve.y1 - cy)) 35 | 36 | s.push({ x: cx, y: cy, curve: { type: 'cubic', x1, y1, x2, y2 } }) 37 | } 38 | } else { 39 | s.push(point) 40 | } 41 | } 42 | 43 | return s 44 | } 45 | 46 | const cubify = s => applyFuncToShapes(cubifyShape, s) 47 | 48 | export default cubify 49 | -------------------------------------------------------------------------------- /src/scale.js: -------------------------------------------------------------------------------- 1 | import boundingBox from './boundingBox' 2 | import { applyFuncToShapes } from './helpers' 3 | 4 | const scalePoint = (point, scaleFactor, anchorX, anchorY) => { 5 | const p = { ...point } 6 | 7 | p.x = anchorX - ((anchorX - p.x) * scaleFactor) 8 | p.y = anchorY - ((anchorY - p.y) * scaleFactor) 9 | 10 | if (point.curve) { 11 | p.curve = { ...p.curve } 12 | 13 | if (p.curve.type === 'arc') { 14 | if (p.curve.rx) { 15 | p.curve.rx = p.curve.rx * scaleFactor 16 | } 17 | 18 | if (p.curve.ry) { 19 | p.curve.ry = p.curve.ry * scaleFactor 20 | } 21 | } else { 22 | p.curve.x1 = anchorX - ((anchorX - p.curve.x1) * scaleFactor) 23 | p.curve.y1 = anchorY - ((anchorY - p.curve.y1) * scaleFactor) 24 | 25 | if (p.curve.type === 'cubic') { 26 | p.curve.x2 = anchorX - ((anchorX - p.curve.x2) * scaleFactor) 27 | p.curve.y2 = anchorY - ((anchorY - p.curve.y2) * scaleFactor) 28 | } 29 | } 30 | } 31 | 32 | return p 33 | } 34 | 35 | const scale = (s, scaleFactor, anchor = 'center') => { 36 | const { bottom, center, left, right, top } = boundingBox(s) 37 | 38 | let anchorX = center.x 39 | let anchorY = center.y 40 | 41 | switch (anchor) { 42 | case 'topLeft': 43 | anchorX = left 44 | anchorY = top 45 | break 46 | case 'topRight': 47 | anchorX = right 48 | anchorY = top 49 | break 50 | case 'bottomRight': 51 | anchorX = right 52 | anchorY = bottom 53 | break 54 | case 'bottomLeft': 55 | anchorX = left 56 | anchorY = bottom 57 | break 58 | } 59 | 60 | return applyFuncToShapes(shape => shape.map(point => { 61 | return scalePoint(point, scaleFactor, anchorX, anchorY) 62 | }), s) 63 | } 64 | 65 | export default scale 66 | -------------------------------------------------------------------------------- /test/boundingBox.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import boundingBox from '../src/boundingBox' 4 | 5 | test('`boundingBox` should return correct coordinates from shape', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 100, y: 0 }, 9 | { x: 100, y: 100 }, 10 | { x: 0, y: 100 }, 11 | { x: 0, y: 0 } 12 | ] 13 | 14 | const expectedCoordinates = { 15 | bottom: 100, 16 | center: { x: 50, y: 50 }, 17 | left: 0, 18 | right: 100, 19 | top: 0 20 | } 21 | 22 | expect(boundingBox(shape)).toEqual(expectedCoordinates) 23 | }) 24 | 25 | test('`boundingBox` should return correct coordinates from shape array', () => { 26 | const shapes = [ 27 | [ 28 | { x: 0, y: 0, moveTo: true }, 29 | { x: 100, y: 0 }, 30 | { x: 100, y: 100 }, 31 | { x: 0, y: 100 }, 32 | { x: 0, y: 0 } 33 | ], 34 | [ 35 | { x: 50, y: 50, moveTo: true }, 36 | { x: 150, y: 50 }, 37 | { x: 150, y: 150 }, 38 | { x: 50, y: 150 }, 39 | { x: 50, y: 50 } 40 | ] 41 | ] 42 | 43 | const expectedCoordinates = { 44 | bottom: 150, 45 | center: { x: 75, y: 75 }, 46 | left: 0, 47 | right: 150, 48 | top: 0 49 | } 50 | 51 | expect(boundingBox(shapes)).toEqual(expectedCoordinates) 52 | }) 53 | 54 | test('`boundingBox` should return correct coordinates from circle', () => { 55 | const shape = [ 56 | { x: 50, y: 30, moveTo: true }, 57 | { x: 50, y: 70, curve: { type: 'arc', rx: 20, ry: 20, sweepFlag: 1 } }, 58 | { x: 50, y: 30, curve: { type: 'arc', rx: 20, ry: 20, sweepFlag: 1 } } 59 | ] 60 | 61 | const expectedCoordinates = { 62 | bottom: 70, 63 | center: { x: 50, y: 50 }, 64 | left: 30, 65 | right: 70, 66 | top: 30 67 | } 68 | 69 | expect(boundingBox(shape)).toEqual(expectedCoordinates) 70 | }) 71 | -------------------------------------------------------------------------------- /src/decurve.js: -------------------------------------------------------------------------------- 1 | import { angleFromSides, linearLength } from './helpers' 2 | import cubify from './cubify' 3 | import { curvedPoints } from './add' 4 | 5 | const angle = triangle => { 6 | const [ ax, ay ] = triangle[ 0 ] 7 | const [ bx, by ] = triangle[ 1 ] 8 | const [ cx, cy ] = triangle[ 2 ] 9 | 10 | const a = linearLength(ax, ay, bx, by) 11 | const b = linearLength(bx, by, cx, cy) 12 | const c = linearLength(cx, cy, ax, ay) 13 | 14 | return angleFromSides(a, b, c) 15 | } 16 | 17 | const curved = shape => shape.reduce((c, { curve }) => curve ? true : c, false) 18 | 19 | const decurve = (shape, accuracy = 1) => { 20 | if (!curved(shape)) { 21 | return shape 22 | } 23 | 24 | const s = cubify(shape) 25 | const d = [] 26 | 27 | s.map((point, i) => { 28 | if (point.curve) { 29 | const prevPoint = s[ i - 1 ] 30 | 31 | if (prevPoint.x !== point.x || prevPoint.y !== point.y) { 32 | straighten(prevPoint, point, accuracy) 33 | .map(p => d.push(p)) 34 | } 35 | } else { 36 | d.push(point) 37 | } 38 | }) 39 | 40 | return d 41 | } 42 | 43 | const straight = (x1, y1, cx1, cy1, x2, y2, cx2, cy2, accuracy) => { 44 | const t1 = [[ cx1, cy1 ], [ x2, y2 ], [ x1, y1 ]] 45 | const t2 = [[ cx2, cy2 ], [ x1, y1 ], [ x2, y2 ]] 46 | return angle(t1) < accuracy && angle(t2) < accuracy 47 | } 48 | 49 | const straighten = (prevPoint, point, accuracy) => { 50 | const { x: x1, y: y1 } = prevPoint 51 | const { x: x2, y: y2, curve } = point 52 | const { x1: cx1, y1: cy1, x2: cx2, y2: cy2 } = curve 53 | 54 | if (straight(x1, y1, cx1, cy1, x2, y2, cx2, cy2, accuracy)) { 55 | return [ point ] 56 | } 57 | 58 | const [ midPoint, lastPoint ] = curvedPoints(prevPoint, point) 59 | 60 | return [ 61 | ...straighten(prevPoint, midPoint, accuracy), 62 | ...straighten(midPoint, lastPoint, accuracy) 63 | ] 64 | } 65 | 66 | export default decurve 67 | -------------------------------------------------------------------------------- /test/decurve.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import decurve from '../src/decurve' 4 | import { toPoints } from 'svg-points' 5 | 6 | test('`decurve` should not exceed call stack maximum when a curve is drawn to/from same location', () => { 7 | const d = 'M3337.1,5965.8c-26.4-9.2-64.5-15.1-87,4.6c3.7,0.8,7.5,1.9,11.1,3.3c2.9,1.1,4.7,4.3,4.4,8.2c-0.3,5-3.6,8.4-7.8,7.6c-3.3-0.6-6.4-1.9-9.7-2.4c-4.6-0.6-9.9,3.1-11.8,7.7c-2.1,5.3-0.5,12.5,3.8,16c3.5,2.8,7.8,3.4,11.8,1.8c3-1.2,6.2-2.9,8.3-5.2c3.9-4.3,7.9-5.5,13.3-3.1c5.2,2.4,9.4,0.3,13.8-2.9c7.6-5.5,9-5,10.7,4.5c1.5,8.3,3.9,16.2,7.7,23.7c0.6,1.1,2.6,1.6,4,2c0.4,0.1,1.6-0.9,1.7-1.6c1.6-10.7,2.9-21.4-0.7-32.1c-2.1-6.3-4-12.7-5.6-19.1c-0.3-1.1,1.2-2.7,2.1-3.8c0.4-0.5,1.7-0.6,2.4-0.3c3.8,1.6,7.7,3.3,11.3,5.3c4.1,2.3,4.4,6,1.3,10.3c-4.1,5.7-4.1,5.7,1.7,11.8l-0.1-0.1c-2.5,5.4-4.2,10.7-3.4,16.9c1.3,9.9,1.9,19.9,3.1,29.8c0.4,3.2,1.6,6.4,3.1,9.3c0.5,1,2.9,1.8,4.2,1.6c1.3-0.2,3.1-1.8,3.3-2.9c0.6-5,0.7-10,0.7-15c0-5.3,1.5-9.7,5.9-13c2.6-2,5-4.4,7.3-6.8c2.7-2.9,3.4-8.2,0.7-11.6c-4.7-6.2-7.8-12.8-7.3-20.7c0.1-0.8,0.5-2,1-2.2c1.5-0.4,3.8-1.1,4.5-0.4c7,7.1,13.8,14.4,20.3,21.9c2.1,2.4,3.7,5.4,4.8,8.4C3379.6,5993.2,3357.7,5972,3337.1,5965.8zM3258.8,6027.3c1-2.2,1.4-5.7,3.1-6.4c7.4-3.2,14.5-7.4,22.7-8.4c2.7-0.3,6.4,4.1,5.6,6.7c-2.3,7.4-7.7,11.4-14.5,10.6C3270,6029.1,3264.4,6028.1,3258.8,6027.3C3258.7,6027.2,3258.8,6027.3,3258.8,6027.3zM3349.6,6039.3c0.1,2.8-1,4.6-3.4,5.5c-3.1,1.2-6.6-1.3-6.2-4.6c0.3-2.7,2.1-4.1,4.6-4.5C3347,6035.4,3349.3,6037.2,3349.6,6039.3z' 8 | const shape = toPoints({ type: 'path', d }) 9 | decurve(shape) 10 | }) 11 | 12 | test('`decurve` should return same shape when shape has no curves', () => { 13 | const shape = [ 14 | { x: 525.667, y: 321.333, moveTo: true }, 15 | { x: 462.8335, y: 321.333 }, 16 | { x: 400, y: 321.333 }, 17 | { x: 274.333, y: 321.333 }, 18 | { x: 274.333, y: 200 }, 19 | { x: 274.333, y: 78.667 }, 20 | { x: 399.9995, y: 78.667 }, 21 | { x: 525.666, y: 78.667 }, 22 | { x: 525.666, y: 200 }, 23 | { x: 525.666, y: 321.333 }, 24 | { x: 525.6665, y: 321.333 }, 25 | { x: 525.667, y: 321.333 } 26 | ] 27 | 28 | expect(decurve(shape, 1)).toEqual(shape) 29 | }) 30 | -------------------------------------------------------------------------------- /src/add.js: -------------------------------------------------------------------------------- 1 | import cubify from './cubify' 2 | import { numberAtInterval } from './helpers' 3 | 4 | const linearPoints = (from, to) => [ 5 | { 6 | x: numberAtInterval(from.x, to.x, 0.5), 7 | y: numberAtInterval(from.y, to.y, 0.5) 8 | }, 9 | to 10 | ] 11 | 12 | const curvedPoints = (from, to) => { 13 | const { x1, y1, x2, y2 } = to.curve 14 | 15 | const A = { x: from.x, y: from.y } 16 | const B = { x: x1, y: y1 } 17 | const C = { x: x2, y: y2 } 18 | const D = { x: to.x, y: to.y } 19 | const E = { x: numberAtInterval(A.x, B.x, 0.5), y: numberAtInterval(A.y, B.y, 0.5) } 20 | const F = { x: numberAtInterval(B.x, C.x, 0.5), y: numberAtInterval(B.y, C.y, 0.5) } 21 | const G = { x: numberAtInterval(C.x, D.x, 0.5), y: numberAtInterval(C.y, D.y, 0.5) } 22 | const H = { x: numberAtInterval(E.x, F.x, 0.5), y: numberAtInterval(E.y, F.y, 0.5) } 23 | const J = { x: numberAtInterval(F.x, G.x, 0.5), y: numberAtInterval(F.y, G.y, 0.5) } 24 | const K = { x: numberAtInterval(H.x, J.x, 0.5), y: numberAtInterval(H.y, J.y, 0.5) } 25 | 26 | return [ 27 | { x: K.x, y: K.y, curve: { type: 'cubic', x1: E.x, y1: E.y, x2: H.x, y2: H.y } }, 28 | { x: D.x, y: D.y, curve: { type: 'cubic', x1: J.x, y1: J.y, x2: G.x, y2: G.y } } 29 | ] 30 | } 31 | 32 | const points = (from, to) => to.curve 33 | ? curvedPoints(from, to) 34 | : linearPoints(from, to) 35 | 36 | const addPoints = (shape, pointsRequired) => { 37 | if (isNaN(pointsRequired)) { 38 | throw Error('`add` function must be passed a number as the second argument') 39 | } 40 | 41 | const nextShape = [ ...shape ] 42 | 43 | for (let i = 1; i < nextShape.length;) { 44 | if (nextShape.length >= pointsRequired) { 45 | return nextShape 46 | } 47 | 48 | const to = nextShape[ i ] 49 | 50 | if (to.moveTo) { 51 | i++ 52 | } else { 53 | const from = nextShape[ i - 1 ] 54 | const [ midPoint, replacementPoint ] = points(from, to) 55 | 56 | nextShape.splice(i, 1, midPoint, replacementPoint) 57 | 58 | i += 2 59 | } 60 | } 61 | 62 | return addPoints(nextShape, pointsRequired) 63 | } 64 | 65 | const add = (shape, pointsRequired) => addPoints(cubify(shape), pointsRequired) 66 | 67 | export { curvedPoints } 68 | export default add 69 | -------------------------------------------------------------------------------- /test/remove.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import remove from '../src/remove' 4 | 5 | test('`remove` should remove midpoint', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 25, y: 0 }, 9 | { x: 50, y: 0 } 10 | ] 11 | 12 | const expectedShapes = [ 13 | { x: 0, y: 0, moveTo: true }, 14 | { x: 50, y: 0 } 15 | ] 16 | 17 | expect(remove(shape)).toEqual(expectedShapes) 18 | }) 19 | 20 | test('`remove` should remove multiple midpoints', () => { 21 | const shape = [ 22 | { x: 1, y: 1, moveTo: true }, 23 | { x: 2, y: 2 }, 24 | { x: 3, y: 3 }, 25 | { x: 4, y: 4 } 26 | ] 27 | 28 | const expectedShapes = [ 29 | { x: 1, y: 1, moveTo: true }, 30 | { x: 4, y: 4 } 31 | ] 32 | 33 | expect(remove(shape)).toEqual(expectedShapes) 34 | }) 35 | 36 | test('`remove` should not remove midpoint if curve', () => { 37 | const shape = [ 38 | { x: 0, y: 0, moveTo: true }, 39 | { x: 25, y: 0, curve: { type: 'arc', rx: 1, ry: 1 } }, 40 | { x: 50, y: 0 } 41 | ] 42 | 43 | expect(remove(shape)).toEqual(shape) 44 | }) 45 | 46 | test('`remove` should remove duplicate point', () => { 47 | const shape = [ 48 | { x: 0, y: 10, moveTo: true }, 49 | { x: 25, y: 0 }, 50 | { x: 25, y: 0 }, 51 | { x: 50, y: 50 } 52 | ] 53 | 54 | const expectedShapes = [ 55 | { x: 0, y: 10, moveTo: true }, 56 | { x: 25, y: 0 }, 57 | { x: 50, y: 50 } 58 | ] 59 | 60 | expect(remove(shape)).toEqual(expectedShapes) 61 | }) 62 | 63 | test('`remove` should remove multiple duplicate points', () => { 64 | const shape = [ 65 | { x: 0, y: 10, moveTo: true }, 66 | { x: 25, y: 0 }, 67 | { x: 25, y: 0 }, 68 | { x: 25, y: 0 }, 69 | { x: 50, y: 50 } 70 | ] 71 | 72 | const expectedShapes = [ 73 | { x: 0, y: 10, moveTo: true }, 74 | { x: 25, y: 0 }, 75 | { x: 50, y: 50 } 76 | ] 77 | 78 | expect(remove(shape)).toEqual(expectedShapes) 79 | }) 80 | 81 | test('`remove` should not remove duplicate point if curve', () => { 82 | const shape = [ 83 | { x: 0, y: 10, moveTo: true }, 84 | { x: 25, y: 0 }, 85 | { x: 25, y: 0, curve: { type: 'arc', rx: 1, ry: 1 } }, 86 | { x: 50, y: 50 } 87 | ] 88 | 89 | expect(remove(shape)).toEqual(shape) 90 | }) 91 | -------------------------------------------------------------------------------- /test/rotate.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import rotate from '../src/rotate' 4 | 5 | test('`rotate` should correctly calculate rotation of square', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 100, y: 0 }, 9 | { x: 100, y: 100 }, 10 | { x: 0, y: 100 }, 11 | { x: 0, y: 0 } 12 | ] 13 | 14 | const expectedShape = [ 15 | { x: 100, y: 0, moveTo: true }, 16 | { x: 100, y: 100 }, 17 | { x: 0, y: 100 }, 18 | { x: 0, y: 0 }, 19 | { x: 100, y: 0 } 20 | ] 21 | 22 | expect(rotate(shape, 90)).toEqual(expectedShape) 23 | }) 24 | 25 | test('`rotate` should correctly calculate rotation of triangle', () => { 26 | const shape = [ 27 | { x: 50, y: 0, moveTo: true }, 28 | { x: 100, y: 100 }, 29 | { x: 0, y: 100 }, 30 | { x: 50, y: 0 } 31 | ] 32 | 33 | const expectedShape = [ 34 | { x: 25, y: 93.3, moveTo: true }, 35 | { x: 31.7, y: -18.3 }, 36 | { x: 118.3, y: 31.7 }, 37 | { x: 25, y: 93.3 } 38 | ] 39 | 40 | const result = rotate(shape, 210).map(({ x, y, ...props }) => { 41 | return { 42 | ...props, 43 | x: Math.round(x * 10) / 10, 44 | y: Math.round(y * 10) / 10 45 | } 46 | }) 47 | 48 | expect(result).toEqual(expectedShape) 49 | }) 50 | 51 | test('`rotate` should correctly calculate rotation of curved path', () => { 52 | const shape = [ 53 | { x: 0, y: 0, moveTo: true }, 54 | { x: 100, y: 100, curve: { type: 'quadratic', x1: 50, y1: 50 } }, 55 | { x: 0, y: 200, curve: { type: 'quadratic', x1: 50, y1: 150 } }, 56 | { x: -100, y: 100, curve: { type: 'quadratic', x1: -50, y1: 150 } }, 57 | { x: 0, y: 0, curve: { type: 'quadratic', x1: -50, y1: 50 } } 58 | ] 59 | 60 | const expectedShape = [ 61 | { x: 100, y: 100, moveTo: true }, 62 | { x: 0, y: 200, curve: { type: 'quadratic', x1: 50, y1: 150 } }, 63 | { x: -100, y: 100, curve: { type: 'quadratic', x1: -50, y1: 150 } }, 64 | { x: 0, y: 0, curve: { type: 'quadratic', x1: -50, y1: 50 } }, 65 | { x: 100, y: 100, curve: { type: 'quadratic', x1: 50, y1: 50 } } 66 | ] 67 | 68 | const result = rotate(shape, 90).map(({ x, y, ...props }) => { 69 | return { 70 | ...props, 71 | x: parseInt(Math.round(x), 10), 72 | y: parseInt(Math.round(y), 10) 73 | } 74 | }) 75 | 76 | expect(result).toEqual(expectedShape) 77 | }) 78 | -------------------------------------------------------------------------------- /src/position.js: -------------------------------------------------------------------------------- 1 | import decurve from './decurve' 2 | import length from './length' 3 | import { angleFromSides, numberAtInterval, linearLength } from './helpers' 4 | 5 | const angle = (x1, y1, x2, y2, a) => { 6 | if (x1 === x2) { 7 | return y1 >= y2 ? 0 : 180 8 | } 9 | 10 | const b = 100 11 | const c = linearLength(x2, y2, x1, y1 - b) 12 | const ang = angleFromSides(a, b, c) 13 | 14 | return x1 < x2 ? ang : 360 - ang 15 | } 16 | 17 | const over = (shape, length, totalLength, desiredLength) => { 18 | const { x: x1, y: y1 } = shape[ length - 2 ] 19 | const { x: x2, y: y2 } = shape[ length - 1 ] 20 | const segmentLength = linearLength(x1, y1, x2, y2) 21 | const segmentInterval = (desiredLength - totalLength) / segmentLength + 1 22 | return { x1, y1, x2, y2, segmentInterval, segmentLength } 23 | } 24 | 25 | const position = (shape, interval, accuracy) => { 26 | const s = decurve(shape, accuracy) 27 | const l = s.length 28 | const t = length(s) 29 | const d = t * interval 30 | 31 | const { x1, y1, x2, y2, segmentInterval, segmentLength } = 32 | interval > 1 ? over(s, l, t, d) 33 | : (interval < 0 ? under(s, d) : within(s, l, d)) 34 | 35 | return { 36 | angle: angle(x1, y1, x2, y2, segmentLength), 37 | x: numberAtInterval(x1, x2, segmentInterval), 38 | y: numberAtInterval(y1, y2, segmentInterval) 39 | } 40 | } 41 | 42 | const under = (shape, desiredLength) => { 43 | const { x: x1, y: y1 } = shape[ 0 ] 44 | const { x: x2, y: y2 } = shape[ 1 ] 45 | const segmentLength = linearLength(x1, y1, x2, y2) 46 | const segmentInterval = desiredLength / segmentLength 47 | return { x1, y1, x2, y2, segmentInterval, segmentLength } 48 | } 49 | 50 | const within = (shape, length, desiredLength) => { 51 | let currentLength = 0 52 | 53 | for (let i = 0; i < length; i++) { 54 | const { moveTo } = shape[ i ] 55 | 56 | if (!moveTo) { 57 | const { x: x1, y: y1 } = shape[ i - 1 ] 58 | const { x: x2, y: y2 } = shape[ i ] 59 | 60 | const segmentLength = linearLength(x1, y1, x2, y2) 61 | 62 | if (currentLength + segmentLength >= desiredLength) { 63 | const segmentInterval = (desiredLength - currentLength) / segmentLength 64 | return { x1, y1, x2, y2, segmentInterval, segmentLength } 65 | } 66 | 67 | currentLength += segmentLength 68 | } 69 | } 70 | } 71 | 72 | export default position 73 | -------------------------------------------------------------------------------- /src/moveIndex.js: -------------------------------------------------------------------------------- 1 | import { applyFuncToShapes } from './helpers' 2 | 3 | const countLinePoints = lines => lines.reduce((count, points) => ( 4 | count + countPoints(points) 5 | ), 0) 6 | 7 | const countPoints = points => points.length - (isJoined(points) ? 1 : 0) 8 | 9 | const isJoined = points => { 10 | const firstPoint = points[ 0 ] 11 | const lastPoint = points[ points.length - 1 ] 12 | return firstPoint.x === lastPoint.x && firstPoint.y === lastPoint.y 13 | } 14 | 15 | const joinLines = lines => lines.reduce((shape, line) => ( 16 | [ ...shape, ...line ] 17 | ), []) 18 | 19 | const moveIndex = (s, offset) => applyFuncToShapes(movePointsIndex, s, offset) 20 | 21 | const movePointsIndex = (shape, offset) => { 22 | const lines = splitLines(shape) 23 | const count = countLinePoints(lines) 24 | const normalisedOffset = ((offset % count) + count) % count 25 | 26 | if (!normalisedOffset) { 27 | return shape 28 | } 29 | 30 | const { lineIndex, pointIndex } = nextIndex(lines, normalisedOffset) 31 | const reorderedLines = reorderLines(lines, lineIndex) 32 | const firstLine = reorderPoints(reorderedLines[ 0 ], pointIndex) 33 | const restOfLines = [ ...reorderedLines ].splice(1) 34 | 35 | return joinLines([ firstLine, ...restOfLines ]) 36 | } 37 | 38 | const nextIndex = (lines, offset) => { 39 | for (let i = 0, l = lines.length; i < l; i++) { 40 | const count = countPoints(lines[ i ]) 41 | 42 | if (offset <= count - 1) { 43 | return { 44 | lineIndex: i, 45 | pointIndex: offset 46 | } 47 | } 48 | 49 | offset -= count 50 | } 51 | } 52 | 53 | const reorderLines = (lines, offset) => [ ...lines ] 54 | .splice(offset) 55 | .concat([ ...lines ].splice(0, offset)) 56 | 57 | const reorderPoints = (points, offset) => { 58 | if (!offset) { 59 | return points 60 | } 61 | 62 | const nextPoints = [ 63 | { x: points[ offset ].x, y: points[ offset ].y, moveTo: true }, 64 | ...[ ...points ].splice(offset + 1) 65 | ] 66 | 67 | if (isJoined(points)) { 68 | return [ 69 | ...nextPoints, 70 | ...[ ...points ].splice(1, offset) 71 | ] 72 | } 73 | 74 | return [ 75 | ...nextPoints, 76 | ...[ ...points ].splice(0, offset + 1) 77 | ] 78 | } 79 | 80 | const splitLines = shape => shape.reduce((lines, point) => { 81 | if (point.moveTo) { 82 | lines.push([]) 83 | } 84 | 85 | lines[ lines.length - 1 ].push(point) 86 | 87 | return lines 88 | }, []) 89 | 90 | export default moveIndex 91 | -------------------------------------------------------------------------------- /test/offset.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import offset from '../src/offset' 4 | 5 | test('`offset` should correctly handle missing offsets', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 50, y: 25 }, 9 | { x: -10, y: -100 } 10 | ] 11 | 12 | expect(offset(shape)).toEqual(shape) 13 | }) 14 | 15 | test('`offset` should add correct offset', () => { 16 | const shape = [ 17 | { x: 0, y: 0, moveTo: true }, 18 | { x: 50, y: 25 }, 19 | { x: -10, y: -100 } 20 | ] 21 | 22 | const expectedShapes = [ 23 | { x: 10, y: -5, moveTo: true }, 24 | { x: 60, y: 20 }, 25 | { x: 0, y: -105 } 26 | ] 27 | 28 | expect(offset(shape, 10, -5)).toEqual(expectedShapes) 29 | }) 30 | 31 | test('`offset` should add correct offsets to arc curve', () => { 32 | const shape = [ 33 | { x: 0, y: 0, moveTo: true }, 34 | { 35 | x: 80, 36 | y: 35, 37 | curve: { 38 | type: 'arc', 39 | rx: 2, 40 | ry: 2, 41 | xAxisRotation: 45, 42 | sweepFlag: 1, 43 | largeArcFlag: 1 44 | } 45 | }, 46 | { x: -10, y: -100 } 47 | ] 48 | 49 | const expectedShapes = [ 50 | { x: 10, y: -5, moveTo: true }, 51 | { 52 | x: 90, 53 | y: 30, 54 | curve: { 55 | type: 'arc', 56 | rx: 2, 57 | ry: 2, 58 | xAxisRotation: 45, 59 | sweepFlag: 1, 60 | largeArcFlag: 1 61 | } 62 | }, 63 | { x: 0, y: -105 } 64 | ] 65 | 66 | expect(offset(shape, 10, -5)).toEqual(expectedShapes) 67 | }) 68 | 69 | test('`offset` should add correct offsets to quadratic curve', () => { 70 | const shape = [ 71 | { x: 0, y: 0, moveTo: true }, 72 | { 73 | x: 100, 74 | y: 200, 75 | curve: { 76 | type: 'quadratic', 77 | x1: 50, 78 | y1: 200 79 | } 80 | }, 81 | { x: -10, y: -100 } 82 | ] 83 | 84 | const expectedShapes = [ 85 | { x: 10, y: -5, moveTo: true }, 86 | { 87 | x: 110, 88 | y: 195, 89 | curve: { 90 | type: 'quadratic', 91 | x1: 60, 92 | y1: 195 93 | } 94 | }, 95 | { x: 0, y: -105 } 96 | ] 97 | 98 | expect(offset(shape, 10, -5)).toEqual(expectedShapes) 99 | }) 100 | 101 | test('`offset` should add correct offsets to cubic curve', () => { 102 | const shape = [ 103 | { x: 0, y: 0, moveTo: true }, 104 | { 105 | x: 5, 106 | y: 10, 107 | curve: { 108 | type: 'cubic', 109 | x1: 2, 110 | y1: 0, 111 | x2: 3, 112 | y2: 10 113 | } 114 | }, 115 | { x: -10, y: -100 } 116 | ] 117 | 118 | const expectedShapes = [ 119 | { x: 10, y: -5, moveTo: true }, 120 | { 121 | x: 15, 122 | y: 5, 123 | curve: { 124 | type: 'cubic', 125 | x1: 12, 126 | y1: -5, 127 | x2: 13, 128 | y2: 5 129 | } 130 | }, 131 | { x: 0, y: -105 } 132 | ] 133 | 134 | expect(offset(shape, 10, -5)).toEqual(expectedShapes) 135 | }) 136 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import { applyFuncToShapes, getShapeArray, isShapeArray } from '../src/helpers' 4 | 5 | test('`isShapeArray` should return true if array of arrays', () => { 6 | expect(isShapeArray([[], [], []])).toBe(true) 7 | }) 8 | 9 | test('`isShapeArray` should return false if array of objects', () => { 10 | expect(isShapeArray([{}, {}, {}])).toBe(false) 11 | }) 12 | 13 | test('`isShapeArray` should return false if empty array', () => { 14 | expect(isShapeArray([])).toBe(false) 15 | }) 16 | 17 | test('`getShapeArray` should return shapes array if array of shapes', () => { 18 | const shapes = [ 19 | [ 20 | { x: 1, y: 1, moveTo: true }, 21 | { x: 2, y: 2 } 22 | ], 23 | [ 24 | { x: 3, y: 3, moveTo: true }, 25 | { x: 4, y: 4 } 26 | ] 27 | ] 28 | 29 | expect(getShapeArray(shapes)).toEqual(shapes) 30 | }) 31 | 32 | test('`getShapeArray` should return shapes array if a single of shape', () => { 33 | const shape = [ 34 | { x: 1, y: 1, moveTo: true }, 35 | { x: 2, y: 2 } 36 | ] 37 | 38 | const expectedShapes = [ 39 | [ 40 | { x: 1, y: 1, moveTo: true }, 41 | { x: 2, y: 2 } 42 | ] 43 | ] 44 | 45 | expect(getShapeArray(shape)).toEqual(expectedShapes) 46 | }) 47 | 48 | test('`applyFuncToShapes` should apply function to single shape', () => { 49 | const shape = [ 50 | [ 51 | { x: 1, y: 1, moveTo: true }, 52 | { x: 2, y: 2 } 53 | ], 54 | [ 55 | { x: 3, y: 3, moveTo: true }, 56 | { x: 4, y: 4 } 57 | ] 58 | ] 59 | 60 | const func = s => s.map(p => ({ ...p, x: p.x + 1, y: p.y + 1 })) 61 | 62 | const expectedShape = [ 63 | [ 64 | { x: 2, y: 2, moveTo: true }, 65 | { x: 3, y: 3 } 66 | ], 67 | [ 68 | { x: 4, y: 4, moveTo: true }, 69 | { x: 5, y: 5 } 70 | ] 71 | ] 72 | 73 | expect(applyFuncToShapes(func, shape)).toEqual(expectedShape) 74 | }) 75 | 76 | test('`applyFuncToShapes` should apply function to shapes array', () => { 77 | const shapes = [ 78 | { x: 1, y: 1, moveTo: true }, 79 | { x: 2, y: 2 } 80 | ] 81 | 82 | const func = s => s.map(p => ({ ...p, x: p.x + 1, y: p.y + 1 })) 83 | 84 | const expectedShapes = [ 85 | { x: 2, y: 2, moveTo: true }, 86 | { x: 3, y: 3 } 87 | ] 88 | 89 | expect(applyFuncToShapes(func, shapes)).toEqual(expectedShapes) 90 | }) 91 | 92 | test('`applyFuncToShapes` should apply function with arguments', () => { 93 | const shape = [ 94 | [ 95 | { x: 1, y: 1, moveTo: true }, 96 | { x: 2, y: 2 } 97 | ], 98 | [ 99 | { x: 3, y: 3, moveTo: true }, 100 | { x: 4, y: 4 } 101 | ] 102 | ] 103 | 104 | const func = (s, n) => s.map(p => ({ ...p, x: p.x + n, y: p.y + n })) 105 | 106 | const expectedShape = [ 107 | [ 108 | { x: 6, y: 6, moveTo: true }, 109 | { x: 7, y: 7 } 110 | ], 111 | [ 112 | { x: 8, y: 8, moveTo: true }, 113 | { x: 9, y: 9 } 114 | ] 115 | ] 116 | 117 | expect(applyFuncToShapes(func, shape, 5)).toEqual(expectedShape) 118 | }) 119 | -------------------------------------------------------------------------------- /test/add.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import add from '../src/add' 4 | 5 | test('`add` should throw an error if not passed a second argument', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 50, y: 25 }, 9 | { x: -10, y: -100 } 10 | ] 11 | 12 | expect(() => add(shape)).toThrow() 13 | }) 14 | 15 | test('`add` should add correct number of extra points', () => { 16 | const shape = [ 17 | { x: 0, y: 0, moveTo: true }, 18 | { x: 50, y: 25 }, 19 | { x: -10, y: -100 } 20 | ] 21 | 22 | expect(add(shape, 5).length).toBe(5) 23 | }) 24 | 25 | test('`add` should add correct extra points at midpoints', () => { 26 | const shape = [ 27 | { x: 0, y: 0, moveTo: true }, 28 | { x: 50, y: 25 }, 29 | { x: -10, y: -100 } 30 | ] 31 | 32 | const expectedShape = [ 33 | { x: 0, y: 0, moveTo: true }, 34 | { x: 25, y: 12.5 }, 35 | { x: 50, y: 25 }, 36 | { x: 20, y: -37.5 }, 37 | { x: -10, y: -100 } 38 | ] 39 | 40 | expect(add(shape, 5)).toEqual(expectedShape) 41 | }) 42 | 43 | test('`add` should add correct extra midpoints at midpoints when less than one per join', () => { 44 | const shape = [ 45 | { x: 50, y: 50, moveTo: true }, 46 | { x: 150, y: 50 }, 47 | { x: 150, y: 150 }, 48 | { x: 50, y: 150 }, 49 | { x: 50, y: 50 } 50 | ] 51 | 52 | const expectedShape = [ 53 | { x: 50, y: 50, moveTo: true }, 54 | { x: 100, y: 50 }, 55 | { x: 150, y: 50 }, 56 | { x: 150, y: 150 }, 57 | { x: 50, y: 150 }, 58 | { x: 50, y: 50 } 59 | ] 60 | 61 | expect(add(shape, 6)).toEqual(expectedShape) 62 | }) 63 | 64 | test('`add` should add correct number of extra points when more than one per join', () => { 65 | const shape = [ 66 | { x: 0, y: 0, moveTo: true }, 67 | { x: 50, y: 25 }, 68 | { x: -10, y: -100 } 69 | ] 70 | 71 | expect(add(shape, 8).length).toBe(8) 72 | }) 73 | 74 | test('`add` should add correct extra midpoints at midpoints when more than one per join', () => { 75 | const shape = [ 76 | { x: 0, y: 0, moveTo: true }, 77 | { x: 50, y: 25 }, 78 | { x: -10, y: -100 } 79 | ] 80 | 81 | const expectedShape = [ 82 | { x: 0, y: 0, moveTo: true }, 83 | { x: 12.5, y: 6.25 }, 84 | { x: 25, y: 12.5 }, 85 | { x: 37.5, y: 18.75 }, 86 | { x: 50, y: 25 }, 87 | { x: 35, y: -6.25 }, 88 | { x: 20, y: -37.5 }, 89 | { x: -10, y: -100 } 90 | ] 91 | 92 | expect(add(shape, 8)).toEqual(expectedShape) 93 | }) 94 | 95 | test('`add` should add correct curve midpoint', () => { 96 | const shape = [ 97 | { x: 0, y: 0, moveTo: true }, 98 | { x: 100, y: 0, curve: { type: 'cubic', x1: 0, y1: 30, x2: 100, y2: 30 } } 99 | ] 100 | 101 | const expectedShape = [ 102 | { x: 0, y: 0, moveTo: true }, 103 | { x: 50, y: 22.5, curve: { type: 'cubic', x1: 0, y1: 15, x2: 25, y2: 22.5 } }, 104 | { x: 100, y: 0, curve: { type: 'cubic', x1: 75, y1: 22.5, x2: 100, y2: 15 } } 105 | ] 106 | 107 | expect(add(shape, 3)).toEqual(expectedShape) 108 | }) 109 | 110 | test('`add` should not add midpoint between a moveTo point', () => { 111 | const shape = [ 112 | { x: 10, y: 10, moveTo: true }, 113 | { x: 10, y: 20 }, 114 | { x: 20, y: 10, moveTo: true }, 115 | { x: 20, y: 20 } 116 | ] 117 | 118 | const expectedShape = [ 119 | { x: 10, y: 10, moveTo: true }, 120 | { x: 10, y: 15 }, 121 | { x: 10, y: 20 }, 122 | { x: 20, y: 10, moveTo: true }, 123 | { x: 20, y: 15 }, 124 | { x: 20, y: 20 } 125 | ] 126 | 127 | expect(add(shape, 6)).toEqual(expectedShape) 128 | }) 129 | -------------------------------------------------------------------------------- /test/cubify.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import cubify from '../src/cubify' 4 | 5 | test('`cubify` should return same shape with cubic beziers instead of arcs', () => { 6 | const shape1 = [ 7 | { x: 80, y: 80, moveTo: true }, 8 | { x: 125, y: 125, curve: { type: 'arc', rx: 45, ry: 45 } }, 9 | { x: 125, y: 80 }, 10 | { x: 80, y: 80 } 11 | ] 12 | 13 | const expectedShape1 = [ 14 | { x: 80, y: 80, moveTo: true }, 15 | { 16 | x: 125, 17 | y: 125, 18 | curve: { 19 | type: 'cubic', 20 | x1: 80, 21 | y1: 104.83617610223001, 22 | x2: 100.16382389777, 23 | y2: 125 24 | } 25 | }, 26 | { x: 125, y: 80 }, 27 | { x: 80, y: 80 } 28 | ] 29 | 30 | const shape2 = [ 31 | { x: 230, y: 80, moveTo: true }, 32 | { x: 275, y: 125, curve: { type: 'arc', rx: 45, ry: 45, largeArcFlag: 1 } }, 33 | { x: 275, y: 80 }, 34 | { x: 230, y: 80 } 35 | ] 36 | 37 | const expectedShape2 = [ 38 | { x: 230, y: 80, moveTo: true }, 39 | { 40 | x: 185, 41 | y: 125, 42 | curve: { 43 | type: 'cubic', 44 | x1: 205.16382389777002, 45 | y1: 80, 46 | x2: 185, 47 | y2: 100.16382389776999 48 | } 49 | }, 50 | { 51 | x: 230, 52 | y: 170, 53 | curve: { 54 | type: 'cubic', 55 | x1: 185, 56 | y1: 149.83617610222998, 57 | x2: 205.16382389777, 58 | y2: 170 59 | } 60 | }, 61 | { 62 | x: 275, 63 | y: 125.00000000000001, 64 | curve: { 65 | type: 'cubic', 66 | x1: 254.83617610222998, 67 | y1: 170, 68 | x2: 275, 69 | y2: 149.83617610223 70 | } 71 | }, 72 | { x: 275, y: 80 }, 73 | { x: 230, y: 80 } 74 | ] 75 | 76 | const shape3 = [ 77 | { x: 80, y: 230, moveTo: true }, 78 | { x: 125, y: 275, curve: { type: 'arc', rx: 45, ry: 45, sweepFlag: 1 } }, 79 | { x: 125, y: 230 }, 80 | { x: 80, y: 230 } 81 | ] 82 | 83 | const expectedShape3 = [ 84 | { x: 80, y: 230, moveTo: true }, 85 | { 86 | x: 125, 87 | y: 275, 88 | curve: { 89 | type: 'cubic', 90 | x1: 104.83617610223001, 91 | y1: 230, 92 | x2: 125, 93 | y2: 250.16382389777 94 | } 95 | }, 96 | { x: 125, y: 230 }, 97 | { x: 80, y: 230 } 98 | ] 99 | 100 | const shape4 = [ 101 | { x: 230, y: 230, moveTo: true }, 102 | { x: 275, y: 275, curve: { type: 'arc', rx: 45, ry: 45, largeArcFlag: 1, sweepFlag: 1 } }, 103 | { x: 275, y: 230 }, 104 | { x: 230, y: 230 } 105 | ] 106 | 107 | const expectedShape4 = [ 108 | { x: 230, y: 230, moveTo: true }, 109 | { 110 | x: 275, 111 | y: 185, 112 | curve: { 113 | type: 'cubic', 114 | x1: 230, 115 | y1: 205.16382389777002, 116 | x2: 250.16382389777, 117 | y2: 185 118 | } 119 | }, 120 | { 121 | x: 320, 122 | y: 230, 123 | curve: { 124 | type: 'cubic', 125 | x1: 299.83617610223, 126 | y1: 185, 127 | x2: 320, 128 | y2: 205.16382389777 129 | } 130 | }, 131 | { 132 | x: 275, 133 | y: 275, 134 | curve: { 135 | type: 'cubic', 136 | x1: 320, 137 | y1: 254.83617610222998, 138 | x2: 299.83617610223, 139 | y2: 275 140 | } 141 | }, 142 | { x: 275, y: 230 }, 143 | { x: 230, y: 230 } 144 | ] 145 | 146 | expect(cubify(shape1)).toEqual(expectedShape1) 147 | expect(cubify(shape2)).toEqual(expectedShape2) 148 | expect(cubify(shape3)).toEqual(expectedShape3) 149 | expect(cubify(shape4)).toEqual(expectedShape4) 150 | }) 151 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Colin Meinke", 4 | "email": "hello@colinmeinke.com", 5 | "url": "https://colinmeinke.com" 6 | }, 7 | "babel": { 8 | "env": { 9 | "cjs": { 10 | "plugins": [ 11 | "@babel/plugin-proposal-object-rest-spread" 12 | ], 13 | "presets": [ 14 | "@babel/preset-env" 15 | ] 16 | }, 17 | "modules": { 18 | "plugins": [ 19 | "@babel/plugin-proposal-object-rest-spread" 20 | ], 21 | "presets": [ 22 | [ 23 | "@babel/preset-env", 24 | { 25 | "modules": false 26 | } 27 | ] 28 | ] 29 | }, 30 | "test": { 31 | "plugins": [ 32 | "@babel/plugin-proposal-object-rest-spread" 33 | ], 34 | "presets": [ 35 | "@babel/preset-env" 36 | ] 37 | }, 38 | "umd": { 39 | "plugins": [ 40 | "@babel/plugin-proposal-object-rest-spread" 41 | ], 42 | "presets": [ 43 | [ 44 | "@babel/preset-env", 45 | { 46 | "modules": false 47 | } 48 | ] 49 | ] 50 | } 51 | } 52 | }, 53 | "bugs": { 54 | "url": "https://github.com/colinmeinke/points/issues" 55 | }, 56 | "config": { 57 | "commitizen": { 58 | "path": "node_modules/cz-conventional-changelog" 59 | } 60 | }, 61 | "dependencies": { 62 | "svg-arc-to-cubic-bezier": "^3.2.0" 63 | }, 64 | "description": "A specification for storing shape data in Javascript. Includes functions for adding, removing, reordering and converting points", 65 | "devDependencies": { 66 | "@babel/cli": "^7.5.0", 67 | "@babel/core": "^7.5.0", 68 | "@babel/plugin-proposal-object-rest-spread": "^7.5.1", 69 | "@babel/polyfill": "^7.4.4", 70 | "@babel/preset-env": "^7.5.0", 71 | "babel-jest": "^24.8.0", 72 | "commitizen": "^3.1.1", 73 | "cz-conventional-changelog": "^2.1.0", 74 | "jest": "^24.8.0", 75 | "rimraf": "^2.6.3", 76 | "rollup": "^1.16.6", 77 | "rollup-plugin-babel": "^4.3.3", 78 | "rollup-plugin-commonjs": "^10.0.1", 79 | "rollup-plugin-node-resolve": "^5.2.0", 80 | "rollup-plugin-uglify": "^6.0.2", 81 | "semantic-release": "^15.13.18", 82 | "snazzy": "^8.0.0", 83 | "standard": "^12.0.1", 84 | "svg-points": "^6.0.1" 85 | }, 86 | "jest": { 87 | "testRegex": "(/test/.*|\\.test)\\.js$" 88 | }, 89 | "keywords": [ 90 | "add", 91 | "arc", 92 | "bezier", 93 | "convert", 94 | "curves", 95 | "index", 96 | "move", 97 | "path", 98 | "points", 99 | "quadratic", 100 | "remove", 101 | "reverse", 102 | "shapes", 103 | "svg" 104 | ], 105 | "license": "MIT", 106 | "main": "cjs/index.js", 107 | "module": "modules/index.js", 108 | "name": "points", 109 | "repository": { 110 | "type": "git", 111 | "url": "https://github.com/colinmeinke/points.git" 112 | }, 113 | "scripts": { 114 | "build": "npm run build:modules && npm run build:cjs && npm run build:umd", 115 | "build:cjs": "BABEL_ENV=cjs babel src --out-dir cjs", 116 | "build:modules": "BABEL_ENV=modules babel src --out-dir modules", 117 | "build:umd": "npm run build:umd:dev && npm run build:umd:pro", 118 | "build:umd:dev": "BABEL_ENV=umd rollup -c", 119 | "build:umd:pro": "NODE_ENV=production BABEL_ENV=umd rollup -c", 120 | "commit": "git-cz", 121 | "fix": "standard --fix", 122 | "lint": "standard --verbose | snazzy", 123 | "prepublish": "npm run tidy && npm run build", 124 | "test": "jest", 125 | "tidy": "rimraf modules cjs dist", 126 | "semantic-release": "semantic-release" 127 | }, 128 | "version": "0.0.0-development" 129 | } 130 | -------------------------------------------------------------------------------- /test/position.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import position from '../src/position' 4 | 5 | test('`position` should calculate correct position on two point line', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 0, y: 100 } 9 | ] 10 | 11 | expect(position(shape, 0.5)).toMatchObject({ x: 0, y: 50 }) 12 | }) 13 | 14 | test('`position` should calculate correct position on complex two point line', () => { 15 | const shape = [ 16 | { x: 100, y: 10, moveTo: true }, 17 | { x: -100, y: -90 } 18 | ] 19 | 20 | expect(position(shape, 0.25)).toMatchObject({ x: 50, y: -15 }) 21 | }) 22 | 23 | test('`position` should calculate correct position on two point line with undeshoot', () => { 24 | const shape = [ 25 | { x: 0, y: 0, moveTo: true }, 26 | { x: 0, y: 100 } 27 | ] 28 | 29 | expect(position(shape, -0.25)).toMatchObject({ x: 0, y: -25 }) 30 | }) 31 | 32 | test('`position` should calculate correct position on multi point line with undeshoot', () => { 33 | const shape = [ 34 | { x: 50, y: -150, moveTo: true }, 35 | { x: -50, y: -50 }, 36 | { x: 50, y: 50 } 37 | ] 38 | 39 | const interval = -1.5 40 | const sideA = Math.sqrt(Math.pow(100, 2) + Math.pow(100, 2)) 41 | const sideB = Math.sqrt(Math.pow(100, 2) + Math.pow(100, 2)) 42 | const totalLength = sideA + sideB 43 | const undershoot = totalLength * Math.abs(interval) 44 | const ratio = undershoot / sideA + 1 45 | const x = -50 + (100 * ratio) 46 | const y = -50 + (-100 * ratio) 47 | 48 | expect(position(shape, interval)).toMatchObject({ x, y }) 49 | }) 50 | 51 | test('`position` should calculate correct position on two point line with overshoot', () => { 52 | const shape = [ 53 | { x: 0, y: 0, moveTo: true }, 54 | { x: 0, y: 100 } 55 | ] 56 | 57 | expect(position(shape, 1.25)).toMatchObject({ x: 0, y: 125 }) 58 | }) 59 | 60 | test('`position` should calculate correct position on multi point line with overshoot', () => { 61 | const shape = [ 62 | { x: 50, y: -150, moveTo: true }, 63 | { x: -50, y: -50 }, 64 | { x: 50, y: 50 } 65 | ] 66 | 67 | const interval = 1.15 68 | const sideA = Math.sqrt(Math.pow(100, 2) + Math.pow(100, 2)) 69 | const sideB = Math.sqrt(Math.pow(100, 2) + Math.pow(100, 2)) 70 | const totalLength = sideA + sideB 71 | const overshoot = totalLength * interval - totalLength 72 | const ratio = overshoot / sideB + 1 73 | const x = -50 + (100 * ratio) 74 | const y = -50 + (100 * ratio) 75 | 76 | expect(position(shape, interval)).toMatchObject({ x, y }) 77 | }) 78 | 79 | test('`position` should calculate correct position on square', () => { 80 | const shape = [ 81 | { x: 50, y: 50, moveTo: true }, 82 | { x: -50, y: 50 }, 83 | { x: -50, y: -50 }, 84 | { x: 50, y: -50 }, 85 | { x: 50, y: 50 } 86 | ] 87 | 88 | expect(position(shape, 0.75)).toMatchObject({ x: 50, y: -50 }) 89 | }) 90 | 91 | test('`position` should calculate correct position on circle', () => { 92 | const shape = [ 93 | { x: 50, y: 30, moveTo: true }, 94 | { x: 50, y: 70, curve: { type: 'arc', rx: 20, ry: 20 } }, 95 | { x: 50, y: 30, curve: { type: 'arc', rx: 20, ry: 20 } } 96 | ] 97 | 98 | const { x, y } = position(shape, 0.25) 99 | const p = { x: Math.round(x), y: Math.round(y) } 100 | 101 | expect(p).toMatchObject({ x: 30, y: 50 }) 102 | }) 103 | 104 | test('`position` should calculate correct position on line with mid-shape moveTo', () => { 105 | const shape = [ 106 | { x: 0, y: 0, moveTo: true }, 107 | { x: 100, y: 0 }, 108 | { x: 100, y: 20, moveTo: true }, 109 | { x: 0, y: 20 } 110 | ] 111 | 112 | expect(position(shape, 0.75)).toMatchObject({ x: 50, y: 20 }) 113 | }) 114 | 115 | test('`position` should calculate correct angle if b directly below a', () => { 116 | const shape = [ 117 | { x: 0, y: 0, moveTo: true }, 118 | { x: 0, y: 100 } 119 | ] 120 | 121 | const { angle } = position(shape, 0.5) 122 | 123 | expect(angle).toEqual(180) 124 | }) 125 | 126 | test('`position` should calculate correct angle if a directly below b', () => { 127 | const shape = [ 128 | { x: 0, y: 0, moveTo: true }, 129 | { x: 0, y: -100 } 130 | ] 131 | 132 | const { angle } = position(shape, 0.5) 133 | 134 | expect(angle).toEqual(0) 135 | }) 136 | 137 | test('`position` should calculate correct angle if b to right of a', () => { 138 | const shape = [ 139 | { x: 0, y: 0, moveTo: true }, 140 | { x: 100, y: 100 } 141 | ] 142 | 143 | const { angle } = position(shape, 0.5) 144 | 145 | expect(Math.round(angle)).toEqual(135) 146 | }) 147 | 148 | test('`position` should calculate correct angle if a to right of b', () => { 149 | const shape = [ 150 | { x: 0, y: 0, moveTo: true }, 151 | { x: -100, y: 0 } 152 | ] 153 | 154 | const { angle } = position(shape, 0.5) 155 | 156 | expect(angle).toEqual(270) 157 | }) 158 | -------------------------------------------------------------------------------- /test/length.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import length from '../src/length' 4 | 5 | test('`length` should calculate correct length of square', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 100, y: 0 }, 9 | { x: 100, y: 100 }, 10 | { x: 0, y: 100 }, 11 | { x: 0, y: 0 } 12 | ] 13 | 14 | expect(length(shape)).toEqual(400) 15 | }) 16 | 17 | test('`length` should calculate correct length of reversed square', () => { 18 | const shape = [ 19 | { x: 100, y: 100, moveTo: true }, 20 | { x: 100, y: 0 }, 21 | { x: 0, y: 0 }, 22 | { x: 0, y: 100 }, 23 | { x: 100, y: 100 } 24 | ] 25 | 26 | expect(length(shape)).toEqual(400) 27 | }) 28 | 29 | test('`length` should calculate correct length of rectangle', () => { 30 | const shape = [ 31 | { x: 0, y: 0, moveTo: true }, 32 | { x: 100, y: 0 }, 33 | { x: 100, y: 50 }, 34 | { x: 0, y: 50 }, 35 | { x: 0, y: 0 } 36 | ] 37 | 38 | expect(length(shape)).toEqual(300) 39 | }) 40 | 41 | test('`length` should calculate correct length of right angle triangle', () => { 42 | const shape = [ 43 | { x: 0, y: 0, moveTo: true }, 44 | { x: 100, y: 0 }, 45 | { x: 100, y: 50 }, 46 | { x: 0, y: 0 } 47 | ] 48 | 49 | const expectedLength = 50 | Math.sqrt(Math.pow(100, 2) + Math.pow(50, 2)) + 100 + 50 51 | 52 | expect(length(shape)).toEqual(expectedLength) 53 | }) 54 | 55 | test('`length` should calculate correct length of non right angle triangle', () => { 56 | const shape = [ 57 | { x: 100, y: 100, moveTo: true }, 58 | { x: 127, y: 65 }, 59 | { x: 89, y: 72 }, 60 | { x: 100, y: 100 } 61 | ] 62 | 63 | const expectedLength = 64 | Math.sqrt(Math.pow(27, 2) + Math.pow(35, 2)) + 65 | Math.sqrt(Math.pow(38, 2) + Math.pow(7, 2)) + 66 | Math.sqrt(Math.pow(11, 2) + Math.pow(28, 2)) 67 | 68 | expect(length(shape)).toEqual(expectedLength) 69 | }) 70 | 71 | test('`length` should calculate correct length of two point line', () => { 72 | const shape = [ 73 | { x: 20, y: 90, moveTo: true }, 74 | { x: 1, y: 2862 } 75 | ] 76 | 77 | const expectedLength = 78 | Math.sqrt(Math.pow(19, 2) + Math.pow(2772, 2)) 79 | 80 | expect(length(shape)).toEqual(expectedLength) 81 | }) 82 | 83 | test('`length` should calculate correct length of line with mid-shape moveTo', () => { 84 | const shape = [ 85 | { x: 0, y: 0, moveTo: true }, 86 | { x: 100, y: 0 }, 87 | { x: 100, y: 20, moveTo: true }, 88 | { x: 0, y: 20 } 89 | ] 90 | 91 | expect(length(shape)).toEqual(200) 92 | }) 93 | 94 | test('`length` should calculate correct length of circle', () => { 95 | const shape = [ 96 | { x: 50, y: 30, moveTo: true }, 97 | { x: 50, y: 70, curve: { type: 'arc', rx: 20, ry: 20 } }, 98 | { x: 50, y: 30, curve: { type: 'arc', rx: 20, ry: 20 } } 99 | ] 100 | 101 | const expectedLength = Math.round(40 * Math.PI) 102 | const shapeLength = Math.round(length(shape)) 103 | 104 | expect(shapeLength).toEqual(expectedLength) 105 | }) 106 | 107 | test('`length` should calculate correct length of ellipse', () => { 108 | const shape = [ 109 | { x: 50, y: 0, moveTo: true }, 110 | { x: 50, y: 40, curve: { type: 'arc', rx: 3, ry: 20 } }, 111 | { x: 50, y: 0, curve: { type: 'arc', rx: 3, ry: 20 } } 112 | ] 113 | 114 | const expectedLength = Math.round(82.5294189453125) 115 | const shapeLength = Math.round(length(shape)) 116 | 117 | expect(shapeLength).toEqual(expectedLength) 118 | }) 119 | 120 | test('`length` should calculate correct length of abstract curved shape', () => { 121 | const shape = [ 122 | { x: 213, y: 222, moveTo: true }, 123 | { x: 130, y: 183, curve: { type: 'cubic', x1: 219, y1: 150, x2: 165, y2: 139 } }, 124 | { x: 247, y: 51.6, curve: { type: 'cubic', x1: 125, y1: 123, x2: 171, y2: 73.8 } }, 125 | { x: 280, y: 102, curve: { type: 'cubic', x1: 205, y1: 78, x2: 236, y2: 108 } }, 126 | { x: 286, y: 68.2, curve: { type: 'cubic', x1: 281, y1: 90.3, x2: 282, y2: 79 } }, 127 | { x: 289, y: 79.7, curve: { type: 'cubic', x1: 287, y1: 72, x2: 288, y2: 75.8 } }, 128 | { x: 300, y: 79.7, curve: { type: 'cubic', x1: 293, y1: 79.7, x2: 296, y2: 79.7 } }, 129 | { x: 311, y: 79.7, curve: { type: 'cubic', x1: 304, y1: 79.7, x2: 307, y2: 79.7 } }, 130 | { x: 314, y: 68.2, curve: { type: 'cubic', x1: 312, y1: 75.8, x2: 313, y2: 72 } }, 131 | { x: 320, y: 102, curve: { type: 'cubic', x1: 318, y1: 79, x2: 319, y2: 90.3 } }, 132 | { x: 353, y: 51.6, curve: { type: 'cubic', x1: 364, y1: 108, x2: 395, y2: 78 } }, 133 | { x: 470, y: 183, curve: { type: 'cubic', x1: 429, y1: 73.8, x2: 475, y2: 123 } }, 134 | { x: 387, y: 222, curve: { type: 'cubic', x1: 435, y1: 139, x2: 381, y2: 150 } }, 135 | { x: 300, y: 248, curve: { type: 'cubic', x1: 364, y1: 176, x2: 315, y2: 172 } }, 136 | { x: 213, y: 222, curve: { type: 'cubic', x1: 285, y1: 172, x2: 236, y2: 176 } }, 137 | { x: 213, y: 222 } 138 | ] 139 | 140 | const expectedLength = Math.round(1245.733642578125) 141 | const shapeLength = Math.round(length(shape)) 142 | 143 | expect(shapeLength).toEqual(expectedLength) 144 | }) 145 | -------------------------------------------------------------------------------- /test/scale.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import scale from '../src/scale' 4 | 5 | test('`scale` should increase scale correctly from center', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 100, y: 0 }, 9 | { x: 100, y: 100 }, 10 | { x: 0, y: 100 }, 11 | { x: 0, y: 0 } 12 | ] 13 | 14 | const expectedShape = [ 15 | { x: -50, y: -50, moveTo: true }, 16 | { x: 150, y: -50 }, 17 | { x: 150, y: 150 }, 18 | { x: -50, y: 150 }, 19 | { x: -50, y: -50 } 20 | ] 21 | 22 | expect(scale(shape, 2, 'center')).toEqual(expectedShape) 23 | }) 24 | 25 | test('`scale` should decrease scale correctly from center', () => { 26 | const shape = [ 27 | { x: 0, y: 0, moveTo: true }, 28 | { x: 100, y: 0 }, 29 | { x: 100, y: 100 }, 30 | { x: 0, y: 100 }, 31 | { x: 0, y: 0 } 32 | ] 33 | 34 | const expectedShape = [ 35 | { x: 25, y: 25, moveTo: true }, 36 | { x: 75, y: 25 }, 37 | { x: 75, y: 75 }, 38 | { x: 25, y: 75 }, 39 | { x: 25, y: 25 } 40 | ] 41 | 42 | expect(scale(shape, 0.5, 'center')).toEqual(expectedShape) 43 | }) 44 | 45 | test('`scale` should increase scale correctly from top left', () => { 46 | const shape = [ 47 | { x: 0, y: 0, moveTo: true }, 48 | { x: 100, y: 0 }, 49 | { x: 100, y: 100 }, 50 | { x: 0, y: 100 }, 51 | { x: 0, y: 0 } 52 | ] 53 | 54 | const expectedShape = [ 55 | { x: 0, y: 0, moveTo: true }, 56 | { x: 200, y: 0 }, 57 | { x: 200, y: 200 }, 58 | { x: 0, y: 200 }, 59 | { x: 0, y: 0 } 60 | ] 61 | 62 | expect(scale(shape, 2, 'topLeft')).toEqual(expectedShape) 63 | }) 64 | 65 | test('`scale` should decrease scale correctly from top left', () => { 66 | const shape = [ 67 | { x: 0, y: 0, moveTo: true }, 68 | { x: 100, y: 0 }, 69 | { x: 100, y: 100 }, 70 | { x: 0, y: 100 }, 71 | { x: 0, y: 0 } 72 | ] 73 | 74 | const expectedShape = [ 75 | { x: 0, y: 0, moveTo: true }, 76 | { x: 50, y: 0 }, 77 | { x: 50, y: 50 }, 78 | { x: 0, y: 50 }, 79 | { x: 0, y: 0 } 80 | ] 81 | 82 | expect(scale(shape, 0.5, 'topLeft')).toEqual(expectedShape) 83 | }) 84 | 85 | test('`scale` should increase scale correctly from top right', () => { 86 | const shape = [ 87 | { x: 0, y: 0, moveTo: true }, 88 | { x: 100, y: 0 }, 89 | { x: 100, y: 100 }, 90 | { x: 0, y: 100 }, 91 | { x: 0, y: 0 } 92 | ] 93 | 94 | const expectedShape = [ 95 | { x: -100, y: 0, moveTo: true }, 96 | { x: 100, y: 0 }, 97 | { x: 100, y: 200 }, 98 | { x: -100, y: 200 }, 99 | { x: -100, y: 0 } 100 | ] 101 | 102 | expect(scale(shape, 2, 'topRight')).toEqual(expectedShape) 103 | }) 104 | 105 | test('`scale` should decrease scale correctly from top right', () => { 106 | const shape = [ 107 | { x: 0, y: 0, moveTo: true }, 108 | { x: 100, y: 0 }, 109 | { x: 100, y: 100 }, 110 | { x: 0, y: 100 }, 111 | { x: 0, y: 0 } 112 | ] 113 | 114 | const expectedShape = [ 115 | { x: 50, y: 0, moveTo: true }, 116 | { x: 100, y: 0 }, 117 | { x: 100, y: 50 }, 118 | { x: 50, y: 50 }, 119 | { x: 50, y: 0 } 120 | ] 121 | 122 | expect(scale(shape, 0.5, 'topRight')).toEqual(expectedShape) 123 | }) 124 | 125 | test('`scale` should increase scale correctly from bottom right', () => { 126 | const shape = [ 127 | { x: 0, y: 0, moveTo: true }, 128 | { x: 100, y: 0 }, 129 | { x: 100, y: 100 }, 130 | { x: 0, y: 100 }, 131 | { x: 0, y: 0 } 132 | ] 133 | 134 | const expectedShape = [ 135 | { x: -100, y: -100, moveTo: true }, 136 | { x: 100, y: -100 }, 137 | { x: 100, y: 100 }, 138 | { x: -100, y: 100 }, 139 | { x: -100, y: -100 } 140 | ] 141 | 142 | expect(scale(shape, 2, 'bottomRight')).toEqual(expectedShape) 143 | }) 144 | 145 | test('`scale` should decrease scale correctly from bottom right', () => { 146 | const shape = [ 147 | { x: 0, y: 0, moveTo: true }, 148 | { x: 100, y: 0 }, 149 | { x: 100, y: 100 }, 150 | { x: 0, y: 100 }, 151 | { x: 0, y: 0 } 152 | ] 153 | 154 | const expectedShape = [ 155 | { x: 50, y: 50, moveTo: true }, 156 | { x: 100, y: 50 }, 157 | { x: 100, y: 100 }, 158 | { x: 50, y: 100 }, 159 | { x: 50, y: 50 } 160 | ] 161 | 162 | expect(scale(shape, 0.5, 'bottomRight')).toEqual(expectedShape) 163 | }) 164 | 165 | test('`scale` should increase scale correctly from bottom left', () => { 166 | const shape = [ 167 | { x: 0, y: 0, moveTo: true }, 168 | { x: 100, y: 0 }, 169 | { x: 100, y: 100 }, 170 | { x: 0, y: 100 }, 171 | { x: 0, y: 0 } 172 | ] 173 | 174 | const expectedShape = [ 175 | { x: 0, y: -100, moveTo: true }, 176 | { x: 200, y: -100 }, 177 | { x: 200, y: 100 }, 178 | { x: 0, y: 100 }, 179 | { x: 0, y: -100 } 180 | ] 181 | 182 | expect(scale(shape, 2, 'bottomLeft')).toEqual(expectedShape) 183 | }) 184 | 185 | test('`scale` should decrease scale correctly from bottom left', () => { 186 | const shape = [ 187 | { x: 0, y: 0, moveTo: true }, 188 | { x: 100, y: 0 }, 189 | { x: 100, y: 100 }, 190 | { x: 0, y: 100 }, 191 | { x: 0, y: 0 } 192 | ] 193 | 194 | const expectedShape = [ 195 | { x: 0, y: 50, moveTo: true }, 196 | { x: 50, y: 50 }, 197 | { x: 50, y: 100 }, 198 | { x: 0, y: 100 }, 199 | { x: 0, y: 50 } 200 | ] 201 | 202 | expect(scale(shape, 0.5, 'bottomLeft')).toEqual(expectedShape) 203 | }) 204 | 205 | test('`scale` should increase scale correctly when shape array', () => { 206 | const shapes = [ 207 | [ 208 | { x: 0, y: 0, moveTo: true }, 209 | { x: 100, y: 0 }, 210 | { x: 100, y: 100 }, 211 | { x: 0, y: 100 }, 212 | { x: 0, y: 0 } 213 | ], 214 | [ 215 | { x: 50, y: 50, moveTo: true }, 216 | { x: 150, y: 50 }, 217 | { x: 150, y: 150 }, 218 | { x: 50, y: 150 }, 219 | { x: 50, y: 50 } 220 | ] 221 | ] 222 | 223 | const expectedShapes = [ 224 | [ 225 | { x: -75, y: -75, moveTo: true }, 226 | { x: 125, y: -75 }, 227 | { x: 125, y: 125 }, 228 | { x: -75, y: 125 }, 229 | { x: -75, y: -75 } 230 | ], 231 | [ 232 | { x: 25, y: 25, moveTo: true }, 233 | { x: 225, y: 25 }, 234 | { x: 225, y: 225 }, 235 | { x: 25, y: 225 }, 236 | { x: 25, y: 25 } 237 | ] 238 | ] 239 | 240 | expect(scale(shapes, 2, 'center')).toEqual(expectedShapes) 241 | }) 242 | -------------------------------------------------------------------------------- /test/moveIndex.js: -------------------------------------------------------------------------------- 1 | /* globals test expect */ 2 | 3 | import moveIndex from '../src/moveIndex' 4 | 5 | test('`movieIndex` should move index to correct point when positive offset', () => { 6 | const shape = [ 7 | { x: 0, y: 0, moveTo: true }, 8 | { x: 50, y: 25 }, 9 | { x: -10, y: -100 }, 10 | { x: 40, y: 30 }, 11 | { x: 20, y: 50 }, 12 | { x: 0, y: 0 } 13 | ] 14 | 15 | const expectedShape = [ 16 | { x: -10, y: -100, moveTo: true }, 17 | { x: 40, y: 30 }, 18 | { x: 20, y: 50 }, 19 | { x: 0, y: 0 }, 20 | { x: 50, y: 25 }, 21 | { x: -10, y: -100 } 22 | ] 23 | 24 | expect(moveIndex(shape, 2)).toEqual(expectedShape) 25 | }) 26 | 27 | test('`movieIndex` should move index to correct point positive offset more than number of total points', () => { 28 | const shape = [ 29 | { x: 0, y: 0, moveTo: true }, 30 | { x: 50, y: 25 }, 31 | { x: -10, y: -100 }, 32 | { x: 40, y: 30 }, 33 | { x: 20, y: 50 }, 34 | { x: 0, y: 0 } 35 | ] 36 | 37 | const expectedShape = [ 38 | { x: 40, y: 30, moveTo: true }, 39 | { x: 20, y: 50 }, 40 | { x: 0, y: 0 }, 41 | { x: 50, y: 25 }, 42 | { x: -10, y: -100 }, 43 | { x: 40, y: 30 } 44 | ] 45 | 46 | expect(moveIndex(shape, 13)).toEqual(expectedShape) 47 | }) 48 | 49 | test('`movieIndex` should move index to correct point when negative offset', () => { 50 | const shape = [ 51 | { x: 0, y: 0, moveTo: true }, 52 | { x: 50, y: 25 }, 53 | { x: -10, y: -100 }, 54 | { x: 40, y: 30 }, 55 | { x: 20, y: 50 }, 56 | { x: 0, y: 0 } 57 | ] 58 | 59 | const expectedShape = [ 60 | { x: 40, y: 30, moveTo: true }, 61 | { x: 20, y: 50 }, 62 | { x: 0, y: 0 }, 63 | { x: 50, y: 25 }, 64 | { x: -10, y: -100 }, 65 | { x: 40, y: 30 } 66 | ] 67 | 68 | expect(moveIndex(shape, -2)).toEqual(expectedShape) 69 | }) 70 | 71 | test('`movieIndex` should move index to correct point when negative offset more than number of total points', () => { 72 | const shape = [ 73 | { x: 0, y: 0, moveTo: true }, 74 | { x: 50, y: 25 }, 75 | { x: -10, y: -100 }, 76 | { x: 40, y: 30 }, 77 | { x: 20, y: 50 }, 78 | { x: 0, y: 0 } 79 | ] 80 | 81 | const expectedShape = [ 82 | { x: -10, y: -100, moveTo: true }, 83 | { x: 40, y: 30 }, 84 | { x: 20, y: 50 }, 85 | { x: 0, y: 0 }, 86 | { x: 50, y: 25 }, 87 | { x: -10, y: -100 } 88 | ] 89 | 90 | expect(moveIndex(shape, -13)).toEqual(expectedShape) 91 | }) 92 | 93 | test('`movieIndex` should handle moving index when multiple moveTo points', () => { 94 | const shape = [ 95 | { x: 0, y: 0, moveTo: true }, 96 | { x: 0, y: 100 }, 97 | { x: 100, y: 0, moveTo: true }, 98 | { x: 100, y: 100 }, 99 | { x: 200, y: 0, moveTo: true }, 100 | { x: 200, y: 100 }, 101 | { x: 0, y: 0 } 102 | ] 103 | 104 | const expectedShape = [ 105 | { x: 100, y: 0, moveTo: true }, 106 | { x: 100, y: 100 }, 107 | { x: 200, y: 0, moveTo: true }, 108 | { x: 200, y: 100 }, 109 | { x: 0, y: 0 }, 110 | { x: 0, y: 0, moveTo: true }, 111 | { x: 0, y: 100 } 112 | ] 113 | 114 | expect(moveIndex(shape, 2)).toEqual(expectedShape) 115 | }) 116 | 117 | test('`movieIndex` should handle moving index to curve point', () => { 118 | const shape = [ 119 | { x: 0, y: 0, moveTo: true }, 120 | { x: 50, y: 25, curve: { type: 'quadratic', x1: 0, y1: 25 } }, 121 | { x: -10, y: -100 }, 122 | { x: 40, y: 30 }, 123 | { x: 20, y: 50 }, 124 | { x: 0, y: 0 } 125 | ] 126 | 127 | const expectedShape = [ 128 | { x: 50, y: 25, moveTo: true }, 129 | { x: -10, y: -100 }, 130 | { x: 40, y: 30 }, 131 | { x: 20, y: 50 }, 132 | { x: 0, y: 0 }, 133 | { x: 50, y: 25, curve: { type: 'quadratic', x1: 0, y1: 25 } } 134 | ] 135 | 136 | expect(moveIndex(shape, 1)).toEqual(expectedShape) 137 | }) 138 | 139 | test('`moveIndex` should handle move index on non-joining shape', () => { 140 | const shape = [ 141 | { x: 30, y: 40, moveTo: true }, 142 | { x: 40, y: 80 }, 143 | { x: 50, y: 60 } 144 | ] 145 | 146 | const expectedShape = [ 147 | { x: 40, y: 80, moveTo: true }, 148 | { x: 50, y: 60 }, 149 | { x: 30, y: 40, moveTo: true }, 150 | { x: 40, y: 80 } 151 | ] 152 | 153 | expect(moveIndex(shape, 1)).toEqual(expectedShape) 154 | }) 155 | 156 | test('`moveIndex` should handle move index on multi-line joining shape', () => { 157 | const shape = [ 158 | { x: 30, y: 40, moveTo: true }, 159 | { x: 40, y: 80 }, 160 | { x: 50, y: 60 }, 161 | { x: 30, y: 40 }, 162 | { x: 130, y: 40, moveTo: true }, 163 | { x: 140, y: 80 }, 164 | { x: 150, y: 60 }, 165 | { x: 130, y: 40 } 166 | ] 167 | 168 | const expectedShape = [ 169 | { x: 40, y: 80, moveTo: true }, 170 | { x: 50, y: 60 }, 171 | { x: 30, y: 40 }, 172 | { x: 40, y: 80 }, 173 | { x: 130, y: 40, moveTo: true }, 174 | { x: 140, y: 80 }, 175 | { x: 150, y: 60 }, 176 | { x: 130, y: 40 } 177 | ] 178 | 179 | expect(moveIndex(shape, 1)).toEqual(expectedShape) 180 | }) 181 | 182 | test('`moveIndex` should handle a more complex move index on multi-line joining shape', () => { 183 | const shape = [ 184 | { x: 30, y: 40, moveTo: true }, 185 | { x: 40, y: 80 }, 186 | { x: 50, y: 60 }, 187 | { x: 30, y: 40 }, 188 | { x: 130, y: 40, moveTo: true }, 189 | { x: 140, y: 80 }, 190 | { x: 150, y: 60 }, 191 | { x: 130, y: 40 } 192 | ] 193 | 194 | const expectedShape = [ 195 | { x: 150, y: 60, moveTo: true }, 196 | { x: 130, y: 40 }, 197 | { x: 140, y: 80 }, 198 | { x: 150, y: 60 }, 199 | { x: 30, y: 40, moveTo: true }, 200 | { x: 40, y: 80 }, 201 | { x: 50, y: 60 }, 202 | { x: 30, y: 40 } 203 | ] 204 | 205 | expect(moveIndex(shape, 11)).toEqual(expectedShape) 206 | }) 207 | 208 | test('`moveIndex` should handle a move index on multi-line mixed joining and non-joining shape', () => { 209 | const shape = [ 210 | { x: 10, y: 10, moveTo: true }, 211 | { x: 10, y: 100 }, 212 | { x: 100, y: 10, moveTo: true }, 213 | { x: 200, y: 100 }, 214 | { x: 100, y: 100 }, 215 | { x: 100, y: 10 }, 216 | { x: 200, y: 10, moveTo: true }, 217 | { x: 10, y: 10 } 218 | ] 219 | 220 | const expectedShape = [ 221 | { x: 200, y: 100, moveTo: true }, 222 | { x: 100, y: 100 }, 223 | { x: 100, y: 10 }, 224 | { x: 200, y: 100 }, 225 | { x: 200, y: 10, moveTo: true }, 226 | { x: 10, y: 10 }, 227 | { x: 10, y: 10, moveTo: true }, 228 | { x: 10, y: 100 } 229 | ] 230 | 231 | expect(moveIndex(shape, 3)).toEqual(expectedShape) 232 | }) 233 | 234 | test('`moveIndex` should handle move index on non-joining shape with curves', () => { 235 | const shape = [ 236 | { x: 0, y: 0, moveTo: true }, 237 | { x: 100, y: 100, curve: { type: 'cubic', x1: 100, y1: 0, x2: 0, y2: 100 } }, 238 | { x: 200, y: 0, curve: { type: 'cubic', x1: 200, y1: 100, x2: 100, y2: 0 } }, 239 | { x: 300, y: 100, curve: { type: 'cubic', x1: 300, y1: 0, x2: 200, y2: 100 } } 240 | ] 241 | 242 | const expectedShape = [ 243 | { x: 100, y: 100, moveTo: true }, 244 | { x: 200, y: 0, curve: { type: 'cubic', x1: 200, y1: 100, x2: 100, y2: 0 } }, 245 | { x: 300, y: 100, curve: { type: 'cubic', x1: 300, y1: 0, x2: 200, y2: 100 } }, 246 | { x: 0, y: 0, moveTo: true }, 247 | { x: 100, y: 100, curve: { type: 'cubic', x1: 100, y1: 0, x2: 0, y2: 100 } } 248 | ] 249 | 250 | expect(moveIndex(shape, 1)).toEqual(expectedShape) 251 | }) 252 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Points 2 | 3 | A specification for storing shape data in Javascript. Includes 4 | [functions](#functions) for adding, removing, reordering, 5 | converting and manipulating points. 6 | 7 | If working with SVG you might find it well paired with [svg-points](https://github.com/colinmeinke/svg-points). 8 | 9 | If you are looking to convert a SVG DOM node directly to points, 10 | then check out the `frameShape` function of 11 | [Wilderness DOM node](https://github.com/colinmeinke/wilderness-dom-node#readme). 12 | 13 | **4.0kb gzipped.** 14 | 15 | ## Example shape 16 | 17 | ```js 18 | const shape = [ 19 | { x: 50, y: 30, moveTo: true }, 20 | { x: 50, y: 70, curve: { type: 'arc', rx: 20, ry: 20, sweepFlag: 1 } }, 21 | { x: 50, y: 30, curve: { type: 'arc', rx: 20, ry: 20, sweepFlag: 1 } } 22 | ] 23 | ``` 24 | 25 | ## Functions 26 | 27 | - [`add()`](#add) – add additional points to a shape 28 | - [`boundingBox()`](#boundingBox) – get a shape's bounding box and 29 | center coordinates 30 | - [`cubify()`](#cubify) – convert shape's curves to cubic beziers 31 | - [`length()`](#length) – get a shape's length 32 | - [`moveIndex()`](#moveIndex) – change the starting point of a shape 33 | - [`offset()`](#offset) – offset a shape 34 | - [`position()`](#position) – find the coordinates and angle at a 35 | specific point of a shape 36 | - [`remove()`](#remove) – remove unrequired points of a shape 37 | - [`reverse()`](#reverse) – reverse the order of points of a shape 38 | - [`rotate()`](#rotate) – rotate a shape 39 | - [`scale()`](#scale) – scale a shape 40 | 41 | ## Specification 42 | 43 | A shape is an array of 2 or more point objects. 44 | 45 | A line should be drawn between each point in a shape. 46 | 47 | Adding a `moveTo` property to a point indicates that 48 | a line should *not* be drawn to that point from the 49 | previous. 50 | 51 | The first point in a shape must include the `moveTo` 52 | property. 53 | 54 | ### Point types 55 | 56 | Each point is somewhere on an `x`, `y` plane. Therefore, at 57 | the very least each point object requires `x` and `y` 58 | properties. Values should be numeric. 59 | 60 | #### Basic 61 | 62 | ```js 63 | { x: 10, y: 25 } 64 | ``` 65 | 66 | #### Arc 67 | 68 | ```js 69 | { 70 | x: 80, 71 | y: 35, 72 | curve: { 73 | type: 'arc', 74 | rx: 2, 75 | ry: 2, 76 | xAxisRotation: 45, 77 | sweepFlag: 1, 78 | largeArcFlag: 1 79 | } 80 | } 81 | ``` 82 | 83 | The curve properties `xAxisRotation`, `sweepFlag` and 84 | `largeArcFlag` are all optional and if missing are assumed to 85 | be `0`. 86 | 87 | #### Quadratic bezier 88 | 89 | ```js 90 | { 91 | x: 100, 92 | y: 200, 93 | curve: { 94 | type: 'quadratic', 95 | x1: 50, 96 | y1: 200 97 | } 98 | } 99 | ``` 100 | 101 | #### Cubic bezier 102 | 103 | ```js 104 | { 105 | x: 5, 106 | y: 10, 107 | curve: { 108 | type: 'cubic', 109 | x1: 2, 110 | y1: 0, 111 | x2: 3, 112 | y2: 10 113 | } 114 | } 115 | ``` 116 | 117 | ## Installation 118 | 119 | ``` 120 | npm install points 121 | ``` 122 | 123 | ## Function usage 124 | 125 | ### add 126 | 127 | ```js 128 | import { add } from 'points' 129 | const newShape = add(shape, 25) 130 | ``` 131 | 132 | Takes an existing shape array as the first argument, and the 133 | total number of desired points as the second argument. Adds 134 | points without changing the shape and returns a new shape 135 | array. 136 | 137 | ### boundingBox 138 | 139 | ```js 140 | import { boundingBox } from 'points' 141 | const { top, right, bottom, left, center } = boundingBox(shape) 142 | ``` 143 | 144 | Takes an existing shape array, or an array of shape arrays, 145 | as the only argument and returns an object of bounding 146 | coordinates including a `center` property containing the 147 | `x`, `y` values. 148 | 149 | ### cubify 150 | 151 | ```js 152 | import { cubify } from 'points' 153 | const newShape = cubify(shape) 154 | ``` 155 | 156 | Takes an existing shape array as the only argument, or an 157 | array of shape arrays, and converts any arc or quadratic 158 | bezier points to cubic bezier points. 159 | 160 | Returns a new shape array or an array of shape arrays, 161 | depending on input. 162 | 163 | ### length 164 | 165 | ```js 166 | import { length } from 'points' 167 | const value = length(shape, 1) 168 | ``` 169 | 170 | Takes an existing shape array as the first argument. The 171 | optional second argument takes a number above 0 but below 172 | 180. This second argument is the accuracy (in degrees) used 173 | to calculate when a curve is *straight enough* to be 174 | considered a straight line. Returns the length of the shape. 175 | 176 | ### moveIndex 177 | 178 | ```js 179 | import { moveIndex } from 'points'; 180 | const newShape = moveIndex(shape, 3); 181 | ``` 182 | 183 | Takes an existing shape array as the first argument, and the 184 | desired number of points to shift the index as the second 185 | argument (this can be a negative integer too). Returns a new 186 | shape array. 187 | 188 | ### offset 189 | 190 | ```js 191 | import { offset } from 'points' 192 | const newShape = offset(shape, 10, 20) 193 | ``` 194 | 195 | Takes an existing shape array, or an array of shape arrays, 196 | as the first argument, the horizontal offset as the second 197 | argument, and the vertical offset as the third argument. 198 | 199 | Returns a new shape array or an array of shape arrays, 200 | depending on input. 201 | 202 | ### position 203 | 204 | ```js 205 | import { position } from 'points' 206 | const { angle, x, y } = position(shape, 0.5, 1) 207 | ``` 208 | 209 | Takes an existing shape array as the first argument, and 210 | an interval (a number from 0 to 1) as the second argument. 211 | The optional third argument takes a number above 0 but below 212 | 180. This third argument is the accuracy (in degrees) used 213 | to calculate when a curve is *straight enough* to be 214 | considered a straight line. Returns an object that includes 215 | the `x` and `y` coordinates at the interval of the shape, 216 | and the `angle` of that point with the vertical. 217 | 218 | ### remove 219 | 220 | ```js 221 | import { remove } from 'points' 222 | const newShape = remove(shape) 223 | ``` 224 | 225 | Takes an existing shape array, or an array of shape 226 | arrays, as the only argument, and removes any points that 227 | do not affect the shape. 228 | 229 | Returns a new shape array or an array of shape arrays, 230 | depending on input. 231 | 232 | ### reverse 233 | 234 | ```js 235 | import { reverse } from 'points' 236 | const newShape = reverse(shape) 237 | ``` 238 | 239 | Takes an existing shape array, or an array of shape 240 | arrays, as the only argument, and reverses the order of 241 | the points. 242 | 243 | Returns a new shape array or an array of shape arrays, 244 | depending on input. 245 | 246 | ### rotate 247 | 248 | ```js 249 | import { rotate } from 'points' 250 | const newShape = rotate(shape, 45) 251 | ``` 252 | 253 | Takes an existing shape array, or an array of shape arrays, 254 | as the first argument. Takes the clockwise angle of rotation 255 | as the second argument. 256 | 257 | Returns a new shape array or an array of shape arrays, 258 | depending on input. 259 | 260 | ### scale 261 | 262 | ```js 263 | import { scale } from 'points' 264 | const newShape = scale(shape, 0.5, 'topLeft') 265 | ``` 266 | 267 | Takes an existing shape array, or an array of shape arrays, 268 | as the first argument. Takes the scale factor as the second 269 | argument and an anchor point as the third argument. 270 | 271 | The anchor point can take any of the following strings: 272 | 273 | - center (default) 274 | - topLeft 275 | - topRight 276 | - bottomRight 277 | - bottomLeft 278 | 279 | Returns a new shape array or an array of shape arrays, 280 | depending on input. 281 | 282 | ## CommonJS 283 | 284 | This is how you get to the good stuff if you're using 285 | `require`. 286 | 287 | ```js 288 | const Points = require('points') 289 | const add = Points.add 290 | const boundingBox = Points.boundingBox 291 | const cubify = Points.cubify 292 | const moveIndex = Points.moveIndex 293 | const offset = Points.offset 294 | const position = Points.position 295 | const remove = Points.remove 296 | const reverse = Points.reverse 297 | const scale = Points.scale 298 | ``` 299 | 300 | ## UMD 301 | 302 | And if you just want to smash in a Javascript file you're 303 | also covered. Drop this in place ... 304 | 305 | [https://unpkg.com/points/dist/points.min.js](https://unpkg.com/points/dist/points.min.js) 306 | 307 | Then access it on the `Points` global variable. 308 | 309 | ```js 310 | const add = Points.add 311 | const boundingBox = Points.boundingBox 312 | const cubify = Points.cubify 313 | const moveIndex = Points.moveIndex 314 | const offset = Points.offset 315 | const position = Points.position 316 | const remove = Points.remove 317 | const reverse = Points.reverse 318 | const scale = Points.scale 319 | ``` 320 | 321 | ## Help make this better 322 | 323 | [Issues](https://github.com/colinmeinke/points/issues/new) 324 | and pull requests gratefully received! 325 | 326 | I'm also on twitter [@colinmeinke](https://twitter.com/colinmeinke). 327 | 328 | Thanks :star2: 329 | 330 | ## License 331 | 332 | [ISC](./LICENSE.md). 333 | --------------------------------------------------------------------------------