Skip to content

Commit

Permalink
πŸ‘©β€πŸŽ¨ Draw and animate a harmonograph in SVG
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Page committed May 2, 2020
0 parents commit 840fa30
Show file tree
Hide file tree
Showing 8 changed files with 7,298 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/publish-master.yml
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 }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.DS_STORE
21 changes: 21 additions & 0 deletions LICENSE
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.
123 changes: 123 additions & 0 deletions README.md
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
166 changes: 166 additions & 0 deletions index.js
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;
Loading

0 comments on commit 840fa30

Please sign in to comment.