-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
π©βπ¨ Draw and animate a harmonograph in SVG
- Loading branch information
Alex Page
committed
May 2, 2020
0 parents
commit 840fa30
Showing
8 changed files
with
7,298 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
name: Publish package on master | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
|
||
jobs: | ||
deploy: | ||
|
||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v1 | ||
- uses: actions/setup-node@v1 | ||
with: | ||
node-version: '10.x' | ||
registry-url: 'https://registry.npmjs.org' | ||
- run: | | ||
npm install | ||
npm run test | ||
- run: npm publish --access public | ||
env: | ||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} |
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,2 @@ | ||
node_modules | ||
.DS_STORE |
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,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2020 Alex Page | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
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,123 @@ | ||
# @harmonograph/svg | ||
|
||
> π©βπ¨ Draw and animate a harmonograph in SVG | ||
|
||
## Install | ||
|
||
```shell | ||
npm install @harmonograph/svg | ||
``` | ||
|
||
|
||
## Get started | ||
|
||
The harmonograph is a mechanical apparatus that uses pendulums to create a geometric image. This creates an SVG of a harmonograph. | ||
|
||
|
||
### Create harmonograph SVG | ||
|
||
```js | ||
const generateHarmonographSVG = require('@harmonograph/svg'); | ||
|
||
const harmonograph = generateHarmonographSVG({ | ||
size: 700, | ||
strokeWidth: 1, | ||
strokeColor: '#000', | ||
pendulumTime: 150, | ||
pendulums: [{ | ||
amplitude: 200, frequency: 2.985, phase: 2.054, damping: 0.001 | ||
}, | ||
{ | ||
amplitude: 200, frequency: 3.006, phase: 1.820, damping: 0.008 | ||
}, | ||
{ | ||
amplitude: 200, frequency: 3.003, phase: 2.283, damping: 0.001 | ||
}, | ||
{ | ||
amplitude: 200, frequency: 1.994, phase: 1.155, damping: 0.001 | ||
}] | ||
}); | ||
``` | ||
|
||
This returns an SVG of a drawn harmonograph | ||
|
||
```html | ||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 700"><rect fill="transparent" width="100%" height="100%"></rect><path stroke="#000" stroke-width="1" fill="none" d="M 679.068 646.723 C 646.36 628.881, 417.218 495.899, 249.676 392.849 S -28.969 212.083, 9.448 201.383 229.928 241.52, 402.486 287.403 712.231 363.501, 699.031 350.759 524.344 287.226, 350.469 262.826 16.412 251.089, 4.27 315.364 129.74 490.425, 301.232 560.133 652.343 645.958, 689.393 576.041 615.481 354.027, 450.006 221.669 89.434 -8.64, 28.434 0.66 49.576 125.994, 205.543 266.677 567.831 557.749, 651.306 617.899 683.011 644.863, 539.811 559.696 183.558 350.442, 79.541 279.709 -3.948 196.458, 123.519 225.983 466.178 316.111, 588.361 340.053 721.459 346.467, 612.334 310.892 290.483 232.23, 152.891 243.93 -26.608 327.129, 62 417.687 356.315 606.371, 506.257 624.646 727.936 574.222, 661.594 451.855 400.89 165.032, 241.915 75.065 -16.861 -19.959, 25.981 64.799 247.753 314.759, 412.286 443.909 702.305 653.591, 683.688 641.708 505.273 531.422, 338.765 432.805 23.99 247.771, 18.19 225.929 149.785 243.301"></path></svg> | ||
``` | ||
|
||
|
||
### Create randomised harmonograph SVG | ||
|
||
To create a randomised harmonograph, do not add the pendulums. | ||
|
||
```js | ||
const generateHarmonographSVG = require('@harmonograph/svg'); | ||
|
||
const harmonograph = generateHarmonographSVG({ | ||
size: 700, | ||
strokeWidth: 1, | ||
strokeColor: '#000', | ||
pendulumTime: 150, | ||
}); | ||
``` | ||
|
||
### Animate the path of the harmonograph SVG | ||
|
||
```js | ||
const generateHarmonographSVG = require('@harmonograph/svg'); | ||
|
||
const harmonograph = generateHarmonographSVG({ | ||
size: 700, | ||
strokeWidth: 1, | ||
strokeColor: '#000', | ||
pendulumTime: 150, | ||
animatePath: true | ||
}); | ||
``` | ||
|
||
### Animate the path of the harmonograph SVG with set duration and bezier curve | ||
|
||
```js | ||
const generateHarmonographSVG = require('@harmonograph/svg'); | ||
|
||
const harmonograph = generateHarmonographSVG({ | ||
size: 700, | ||
strokeWidth: 1, | ||
strokeColor: '#000', | ||
pendulumTime: 150, | ||
animatePath: { | ||
duration: '10s', | ||
easing: 'ease-in-out' | ||
} | ||
}); | ||
``` | ||
|
||
|
||
## Options | ||
|
||
| Option | Description | Default value | Type | | ||
| --- | --- | --- | --- | | ||
| size | The size of the svg | `700` | _number_ | | ||
| strokeWidth | The width of the line | `1` | _number_ | | ||
| strokeColor | The color of the line | `#000` | _string_ | | ||
| pendulumTime | How long the pendulum swings in seconds | `150` | _number_ | | ||
| animatePath | How the path animates | `false` | _object_ or _boolean_ | | ||
| animatePath.duration | The time for the animation to end | `15000ms` | _string_ | | ||
| animatePath.easing | The speed curve of the animation | `linear` | _string_ | | ||
| pendulum | Two pendulums require four items ( x, y and x, y ). Each X and Y value is an object that contains _amplitude_, _frequency_, _phase_, and _damping_ ( see pendulum options below ) | `random values` | _array_ | | ||
|
||
|
||
## Pendulums object | ||
|
||
| Parameter | Description | Default value | Type | | ||
| --- | --- | --- | --- | | ||
| pendulum.amplitude | How far a pendulum swings back and forth, must be from `0` - `360` degrees | `random number` | _number_ | | ||
| pendulum.frequency | How fast a pendulum swings back and forth, for best results use decimal values around `2` and `3` | `random number` | _number_ | | ||
| pendulum.phase | The rate that a pendulum loses its energy, or slows down, must be from `0` to `Ο` | `random number` | _number_ | | ||
| pendulum.damping | The offset from the normal starting position of a pendulum, must be from `0` to `0.01` | `random number` | _number_ | | ||
|
||
|
||
## Release History | ||
|
||
* v0.0.0 - π₯ Initial version |
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,166 @@ | ||
const {h} = require('preact'); | ||
const render = require('preact-render-to-string'); | ||
const {svgPathProperties: SvgPathProperties} = require('svg-path-properties'); | ||
|
||
const generateHarmonograph = require('@harmonograph/xy'); | ||
const {randomPendulums} = require('@harmonograph/xy'); | ||
|
||
/** | ||
* Rounds a number to three decimal places | ||
* | ||
* @param {number} numberToRound - The number to round | ||
* @param {number} maxDecimals - The maximum number of decimals | ||
* | ||
* @returns {number} - The rounded number | ||
*/ | ||
const round = (numberToRound, maxDecimals = 3) => Number( | ||
Math.round(numberToRound + 'e' + maxDecimals) + 'e-' + maxDecimals | ||
); | ||
|
||
/** | ||
* Reduce number of XY points by creating bezier curves | ||
* | ||
* @param {number} drawingTime - Total time the pendulums swing | ||
* @param {number} size - The size of the pendulum | ||
* @param {object} pendulums - The pendulum settings, see randomPendulum | ||
* | ||
* @returns {string} - The SVG path data as a string | ||
*/ | ||
const harmonographBezierPath = (pendulumTime, size, pendulums) => { | ||
const {x, y} = generateHarmonograph(pendulumTime, size, pendulums); | ||
|
||
const harmonograph = { | ||
x: [], | ||
y: [], | ||
cpX: [], // Control point X | ||
cpY: [] // Control point Y | ||
}; | ||
|
||
const step = 50; | ||
const factor = 0.5 * step / 3; | ||
const totalPoints = x.length; | ||
|
||
// Reduce the points by steps of 50 and create controlPoints | ||
for (let i = 0; i < totalPoints; i += step) { | ||
harmonograph.x.push(x[i]); | ||
harmonograph.y.push(y[i]); | ||
|
||
// Get the control points for the stepped values | ||
const previous = i <= 0 ? 0 : i - 1; | ||
const next = i >= totalPoints ? totalPoints - 1 : i + 1; | ||
|
||
const controlPointX = factor * (x[next] - x[previous]); | ||
const controlPointY = factor * (y[next] - y[previous]); | ||
|
||
harmonograph.cpX.push(controlPointX); | ||
harmonograph.cpY.push(controlPointY); | ||
} | ||
|
||
// Create the SVG data path | ||
const svg = [ | ||
'M', | ||
harmonograph.x[0], | ||
harmonograph.y[0], | ||
'C', | ||
round(harmonograph.x[0] + harmonograph.cpX[0]), | ||
round(harmonograph.y[0] + harmonograph.cpY[0]) + ',', | ||
round(harmonograph.x[1] - harmonograph.cpX[1]), | ||
round(harmonograph.y[1] - harmonograph.cpY[1]) + ',', | ||
harmonograph.x[1], | ||
harmonograph.y[1] | ||
]; | ||
|
||
// Create the curves | ||
const totalCurvedPoints = harmonograph.x.length; | ||
if (totalCurvedPoints > 2) { | ||
svg.push('S'); | ||
|
||
for (let i = 2; i < totalCurvedPoints; i++) { | ||
svg.push(round(harmonograph.x[i] - harmonograph.cpX[i])); | ||
svg.push(round(harmonograph.y[i] - harmonograph.cpY[i]) + ','); | ||
svg.push(harmonograph.x[i]); | ||
svg.push(harmonograph.y[i]); | ||
} | ||
} | ||
|
||
// Send back the svg data | ||
return svg.join(' '); | ||
}; | ||
|
||
/** | ||
* Create a randomised harmonograph SVG | ||
* | ||
* Resources: | ||
* - https://en.wikipedia.org/wiki/Harmonograph | ||
* - https://aschinchon.wordpress.com/2014/10/13/beautiful-curves-the-harmonograph/ | ||
* | ||
* @param {object} userSettings - The users settings | ||
* @param {number} userSettings.size - The size of the svg | ||
* @param {number} userSettings.strokeWidth - The width of the line | ||
* @param {string} userSettings.strokeColor - The color of the harmonograph | ||
* @param {number} userSettings.pendulumTime - How long the pendulum swings | ||
* @param {number} userSettings.animated - If the SVG path is animated | ||
* @param {array} userSettings.pendulum - The pendulum settings, see randomPendulum | ||
* | ||
* @returns {string} - The SVG element | ||
*/ | ||
const generateHarmonographSVG = userSettings => { | ||
const { | ||
size, | ||
strokeWidth, | ||
strokeColor, | ||
backgroundColor, | ||
pendulumTime, | ||
animatePath, | ||
pendulums | ||
} = { | ||
size: 700, | ||
strokeWidth: 1, | ||
strokeColor: '#000', | ||
backgroundColor: 'transparent', | ||
pendulumTime: 150, | ||
animatePath: false, | ||
pendulums: randomPendulums(), | ||
...userSettings | ||
}; | ||
|
||
// Reduce the number of XY points by using bezier curves | ||
const harmonographPath = harmonographBezierPath(pendulumTime, size, pendulums); | ||
|
||
const pathProperties = new SvgPathProperties(harmonographPath); | ||
const pathLength = pathProperties.getTotalLength(); | ||
|
||
let styleElement = null; | ||
if (animatePath) { | ||
const animationSettings = { | ||
duration: '15000ms', | ||
easing: 'linear', | ||
...animatePath | ||
}; | ||
|
||
styleElement = h('style', null, `path{stroke-dasharray:${pathLength};stroke-dashoffset:${pathLength};animation:go ${animationSettings.duration} ${animationSettings.easing};}@keyframes go{from{stroke-dashoffset:${pathLength}}to{stroke-dashoffset:0;}}`); | ||
} | ||
|
||
// // Create the svg element | ||
const svg = h('svg', { | ||
xmlns: 'http://www.w3.org/2000/svg', | ||
viewBox: `0 0 ${size} ${size}`, | ||
backgroundColor | ||
}, | ||
styleElement, | ||
h('path', { | ||
stroke: strokeColor, | ||
'stroke-width': strokeWidth, | ||
fill: 'none', | ||
d: harmonographPath | ||
})); | ||
|
||
const svgHTML = render(svg); | ||
|
||
// Send the svg element | ||
return svgHTML; | ||
}; | ||
|
||
module.exports = generateHarmonographSVG; | ||
module.exports.randomPendulums = randomPendulums; | ||
module.exports.harmonographBezierPath = harmonographBezierPath; |
Oops, something went wrong.