Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mattdesl committed Oct 25, 2019
0 parents commit bf2dbb4
Show file tree
Hide file tree
Showing 13 changed files with 6,663 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
bower_components
node_modules
*.log
.DS_Store
bundle.js
6 changes: 6 additions & 0 deletions .npmignore
@@ -0,0 +1,6 @@
bower_components
node_modules
*.log
.DS_Store
.npmignore
LICENSE.md
21 changes: 21 additions & 0 deletions LICENSE.md
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Matt DesLauriers

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.

107 changes: 107 additions & 0 deletions README.md
@@ -0,0 +1,107 @@
# pack-spheres

Brute force circle/sphere packing in 2D or 3D.

<img src="demos/2d.png" width="25%" />
<img src="demos/2d-circle.png" width="25%" />
<img src="demos/3d.png" width="25%" />

See [./demos](./demos) for examples.

```js
const pack = require('pack-spheres');

const circles = pack({
dimensions: 2,
packAttempts: 500,
maxCount: 1000,
minRadius: 0.05,
maxRadius: 0.5,
padding: 0.0025
});

console.log('Got %d circles', circles.length);
console.log(circles[0].position, circles[0].radius);
```

Returns an array of objects with normalized values between `-1.0` and `1.0`, but the algorithm works on arbitrary units (for example, you could use pixels instead).

```js
[
{
position: [ x, y, z ],
radius: Number
},
...
]
```

If you specify `{ dimensions: 2 }`, the `position` will only contain `[ x, y ]`.

## Install

Use [npm](https://npmjs.com/) to install.

```sh
npm install pack-spheres --save
```

## Usage

#### `spheres = pack([opt])`

Packs 3D spheres (default) or 2D circles with the given options:

- `dimensions` — Can either be 3 (default) for spheres, or 2 for circles
- `bounds` — The normalized bounding box from `-1.0` to `1.0` that spheres are randomly generated within and clip to, default 1.0
- `packAttempts` — Number of attempts per sphere to pack within the space, default 500
- `maxCount` — The max number of total spheres that will be packed, default 1000 (note: you may not always reach the maxCount if all spheres could not be packed)
- `minRadius` — A number or [generator function](#generator-functions) that specifies the min (starting) radius for placed spheres (default 0.01)
- `maxRadius` — A number or [generator function](#generator-functions) that specifies the max radius a sphere will grow to (default 0.5)
- `maxGrowthSteps` — A number or[ generator function](#generator-functions) that specifies the max number of steps a sphere will grow before stopping (default Infinity)
- `padding` — A number or [generator function](#generator-functions) that specifies the padding around this sphere (default 0)

## Override Functions

You can pass in override functions to change the behaviour:

- `random` — A function that returns a 0..1 random value used by the default `sample` function, defaults to `Math.random()`
- `sample` — A function that returns a 2D or 3D vector for where a new sphere should be placed
- `outside` — A function that takes in a sphere's `(position, radius, padding)` and returns `true` if the sphere is considered to be "outside" of your virtual bounding region. Defaults to a bounding cube/box

```js
// Some utility for randomness
const Random = require('canvas-sketch-util/random');

// Generate circles in a 2D unit circle
const bounds = 1;
const shapes = pack({
bounds,
// Generate a random point inside a 2D circle
sample: () => Random.insideCircle(bounds),
// See if mag(pos - center) >= bounds
outside: (position, radius) => {
const length = Math.sqrt(
position[0] * position[0] + position[1] * position[1]
);
return length + radius >= bounds;
}
});
```

## Generator Functions

Instead of having all spheres start with, say, a fixed `minRadius`, you can pass a function that will get used for each new sphere being placed:

```js
// Some utility for randomness
const Random = require('canvas-sketch-util/random');

const spheres = pack({
minRadius: () => Random.range(0, 0.5)
});
```

## License

MIT, see [LICENSE.md](http://github.com/mattdesl/pack-spheres/blob/master/LICENSE.md) for details.
Binary file added demos/2d-circle.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/2d.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/3d.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions demos/circles-in-circle.js
@@ -0,0 +1,58 @@
const canvasSketch = require("canvas-sketch");
const Random = require("canvas-sketch-util/random");
const pack = require("..");

const settings = {
dimensions: [1024, 1024]
};

const sketch = ({ width, height }) => {
const size = Math.min(width, height);
const margin = width * 0.1;
const scale = 0.5 * size - margin;

const bounds = 1;
const shapes = pack({
bounds,
sample: () => Random.insideCircle(bounds),
outside: (position, radius) => {
// See if length of circle + radius
// exceeds the bounds
const length = Math.sqrt(
position[0] * position[0] + position[1] * position[1]
);
return length + radius >= bounds;
},
maxCount: 2500,
dimensions: 2,
minRadius: 0.015,
maxRadius: 0.25,
padding: 0.0025
});

return ({ context, width, height }) => {
// Clear background
context.fillStyle = "white";
context.fillRect(0, 0, width, height);

// Centered origin point
context.translate(width / 2, height / 2);
// Scale from -1..1 to -scale..scale
context.scale(scale, scale);

shapes.forEach(shape => {
context.beginPath();
context.arc(
shape.position[0],
shape.position[1],
shape.radius,
0,
Math.PI * 2
);
context.fillStyle = "black";
context.fill();
});
};
};

canvasSketch(sketch, settings);
43 changes: 43 additions & 0 deletions demos/circles-in-square.js
@@ -0,0 +1,43 @@
const canvasSketch = require("canvas-sketch");
const pack = require("..");

const settings = {
dimensions: [1024, 1024]
};

const sketch = ({ width, height }) => {
const size = Math.min(width, height);
const margin = width * 0.1;
const scale = 0.5 * size - margin;

const shapes = pack({
dimensions: 2,
padding: 0.0025
});

return ({ context, width, height }) => {
// Clear background
context.fillStyle = "white";
context.fillRect(0, 0, width, height);

// Centered origin point
context.translate(width / 2, height / 2);
// Scale from -1..1 to -scale..scale
context.scale(scale, scale);

shapes.forEach(shape => {
context.beginPath();
context.arc(
shape.position[0],
shape.position[1],
shape.radius,
0,
Math.PI * 2
);
context.fillStyle = "black";
context.fill();
});
};
};

canvasSketch(sketch, settings);
87 changes: 87 additions & 0 deletions demos/spheres-in-cube.js
@@ -0,0 +1,87 @@
// Ensure ThreeJS is in global scope for the 'examples/'
global.THREE = require("three");

// Include any additional ThreeJS examples below
require("three/examples/js/controls/OrbitControls");

const pack = require("..");
const canvasSketch = require("canvas-sketch");

const settings = {
dimensions: [512, 512],
// Make the loop animated
animate: true,
// Get a WebGL canvas rather than 2D
context: "webgl"
};

const sketch = ({ context }) => {
// Create a renderer
const renderer = new THREE.WebGLRenderer({
canvas: context.canvas
});

// WebGL background color
renderer.setClearColor("#000", 1);

// Setup a camera
const camera = new THREE.PerspectiveCamera(50, 1, 0.01, 100);
camera.position.set(2, 2, -4);
camera.lookAt(new THREE.Vector3());

// Setup camera controller
const controls = new THREE.OrbitControls(camera, context.canvas);

// Setup your scene
const scene = new THREE.Scene();

// Setup a geometry
const geometry = new THREE.SphereGeometry(1, 32, 16);

// Setup a material
const material = new THREE.MeshNormalMaterial();

// Generate spheres with default params (3D)
const spheres = pack();

// Setup meshes with geometry + material
const meshes = spheres.map(sphere => {
const mesh = new THREE.Mesh(geometry, material);
mesh.position.fromArray(sphere.position);
mesh.scale.setScalar(sphere.radius);
return mesh;
});

// Add to scene
meshes.forEach(m => scene.add(m));

// visualize bounding cube
scene.add(
new THREE.Box3Helper(
new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1))
)
);

// draw each frame
return {
// Handle resize events here
resize({ pixelRatio, viewportWidth, viewportHeight }) {
renderer.setPixelRatio(pixelRatio);
renderer.setSize(viewportWidth, viewportHeight, false);
camera.aspect = viewportWidth / viewportHeight;
camera.updateProjectionMatrix();
},
// Update & render your scene here
render({ time }) {
controls.update();
renderer.render(scene, camera);
},
// Dispose of events & renderer for cleaner hot-reloading
unload() {
controls.dispose();
renderer.dispose();
}
};
};

canvasSketch(sketch, settings);

0 comments on commit bf2dbb4

Please sign in to comment.