├── .gitignore
├── README.md
├── source
├── line.js
├── index.html
├── vertex.js
├── index.js
└── polygon.js
├── webpack.config.js
├── package.json
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log*
2 | node_modules
3 | *.DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Crystallization
2 |
3 | A JavaScript Canvas implementation of an old Flash experiment, exploring the recursive subdivision of polygons to form crystallisation like patterns.
4 |
--------------------------------------------------------------------------------
/source/line.js:
--------------------------------------------------------------------------------
1 | // ——————————————————————————————————————————————————
2 | // Line
3 | // ——————————————————————————————————————————————————
4 |
5 | class Line {
6 | constructor(start, end) {
7 | this.start = start;
8 | this.end = end;
9 | }
10 | }
11 |
12 | // ——————————————————————————————————————————————————
13 | // Exports
14 | // ——————————————————————————————————————————————————
15 |
16 | export default Line;
17 |
--------------------------------------------------------------------------------
/source/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var HtmlWebpackPlugin = require('html-webpack-plugin');
2 | var webpack = require('webpack');
3 | var isDev = process.argv.indexOf('-p') === -1;
4 | var config = {
5 | context: __dirname + '/source',
6 | entry: {
7 | main: './index.js'
8 | },
9 | output: {
10 | path: __dirname + '/deploy',
11 | filename: '[name]-[hash:6].js'
12 | },
13 | module: {
14 | loaders: [{
15 | test: /\.js$/,
16 | loader: 'babel',
17 | exclude: /(node_modules)/,
18 | query: {
19 | presets: ['es2015', 'stage-0']
20 | }
21 | }]
22 | },
23 | resolve: {
24 | extensions: ['', '.js']
25 | },
26 | plugins: [
27 | new HtmlWebpackPlugin({
28 | template: 'index.html'
29 | })
30 | ]
31 | };
32 | if (isDev) {
33 | config.devtool = 'eval-source-map';
34 | } else {
35 | config.plugins.push(
36 | new webpack.optimize.AggressiveMergingPlugin(),
37 | new webpack.optimize.UglifyJsPlugin({
38 | compress: { warnings: false }
39 | }),
40 | new webpack.optimize.DedupePlugin()
41 | );
42 | }
43 | module.exports = config;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crystallization",
3 | "version": "1.0.0",
4 | "description": "Crystallization",
5 | "main": "index.js",
6 | "scripts": {
7 | "clean": "rm -rf deploy",
8 | "build": "npm run clean && webpack -p --env production",
9 | "start": "webpack-dev-server --hot --inline --port 3000",
10 | "commit_deploy": "git add deploy -A && git commit -m \"deploy files at `date`\" -n > /dev/null 2>&1; exit 0",
11 | "ghpages": "git subtree split --prefix deploy/ -b gh-pages && git push -f origin gh-pages:gh-pages && git branch -D gh-pages",
12 | "deploy": "npm run build && npm run commit_deploy && npm run ghpages"
13 | },
14 | "author": "Justin Windle ",
15 | "license": "ISC",
16 | "devDependencies": {
17 | "babel-core": "^6.18.2",
18 | "babel-loader": "^6.2.8",
19 | "babel-preset-es2015": "^6.18.0",
20 | "babel-preset-stage-0": "^6.16.0",
21 | "html-webpack-plugin": "^2.24.1",
22 | "webpack": "^1.13.3",
23 | "webpack-dev-server": "^1.16.2"
24 | },
25 | "dependencies": {
26 | "sketch-js": "^1.1.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/source/vertex.js:
--------------------------------------------------------------------------------
1 | // ——————————————————————————————————————————————————
2 | // Vertex
3 | // ——————————————————————————————————————————————————
4 |
5 | class Vertex {
6 | constructor(x = 0, y = 0) {
7 | this.x = x;
8 | this.y = y;
9 | }
10 | distanceSq(other) {
11 | const dx = other.x - this.x;
12 | const dy = other.y - this.y;
13 | return dx * dx + dy * dy;
14 | }
15 | distance(other) {
16 | const dx = other.x - this.x;
17 | const dy = other.y - this.y;
18 | return Math.sqrt(dx * dx + dy * dy);
19 | }
20 | angle(other) {
21 | const dx = other.x - this.x;
22 | const dy = other.y - this.y;
23 | return Math.atan2(dy, dx);
24 | }
25 | lerp(other, amount) {
26 | const dx = other.x - this.x;
27 | const dy = other.y - this.y;
28 | return new Vertex(this.x + dx * amount, this.y + dy * amount);
29 | }
30 | clone() {
31 | return new Vertex(this.x, this.y);
32 | }
33 | }
34 |
35 | // ——————————————————————————————————————————————————
36 | // Exports
37 | // ——————————————————————————————————————————————————
38 |
39 | export default Vertex;
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Justin Windle
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/source/index.js:
--------------------------------------------------------------------------------
1 | // ——————————————————————————————————————————————————
2 | // Dependencies
3 | // ——————————————————————————————————————————————————
4 |
5 | import Polygon from './polygon';
6 | import Vertex from './vertex';
7 | import Sketch from 'sketch-js';
8 |
9 | // ——————————————————————————————————————————————————
10 | // Main
11 | // ——————————————————————————————————————————————————
12 |
13 | const Crystallisation = Sketch.create({
14 | // Sketch settings
15 | container: document.getElementById('container'),
16 | autoclear: false,
17 | retina: 'auto',
18 | // Configurable
19 | settings: {
20 | iterations: 50,
21 | randomness: 0.25,
22 | opposite: 0.1,
23 | minAngle: 0.4,
24 | minSide: 2
25 | },
26 | // Custom properties
27 | polygons: [],
28 | lines: [],
29 | // Setup
30 | setup() {
31 | this.reset();
32 | },
33 | // Subdivide next polygon
34 | step() {
35 | // Choose a polygon to subdivide
36 | const index = ~~random(this.polygons.length - 1);
37 | // Subdivide the polygon
38 | const slices = this.polygons[index].subdivide(
39 | this.settings.randomness,
40 | this.settings.opposite
41 | );
42 | // Check whether all slices are usable
43 | let drop = false;
44 | let i, slice, n = slices.length;
45 | for (i = 0; i < n; i++) {
46 | slice = slices[i];
47 | if (slice.minAngle() < this.settings.minAngle) {
48 | drop = true;
49 | break;
50 | }
51 | }
52 | for (i = 0; i < n; i++) {
53 | slice = slices[i];
54 | if (slice.minSide() < this.settings.minSide) {
55 | drop = true;
56 | break;
57 | }
58 | }
59 | // If all slices are usable
60 | if (!drop) {
61 | // Remember unique lines from the chosen polygons
62 | this.polygons[index].unique.forEach(line => this.lines.push(line));
63 | // Draw and store the slices
64 | // TODO move to top level
65 |
66 | slices.forEach(slice => {
67 | this.polygons.push(slice);
68 | slice.draw(this);
69 | });
70 | this.fill();
71 | this.stroke();
72 | // Remove the original polygon
73 | this.polygons.splice(index, 1);
74 | }
75 | },
76 | // Clears the canvas
77 | clear() {
78 | this.clearRect(0, 0, this.canvas.width, this.canvas.height);
79 | },
80 | // Clears polygons to a single rectangle
81 | reset() {
82 | // Initial bounding box points
83 | const a = new Vertex(0, 0);
84 | const b = new Vertex(this.width, 0);
85 | const c = new Vertex(this.width, this.height);
86 | const d = new Vertex(0, this.height);
87 | // Initial bounding polygon
88 | this.polygons = [new Polygon(a, b, c, d)];
89 | // Clear stored lines
90 | this.lines = [];
91 | this.clear();
92 | },
93 | resize() {
94 | this.strokeStyle = '#333';
95 | this.fillStyle = '#fcfcfc';
96 | this.lineWidth = 0.25;
97 | },
98 | // Toggle update loop
99 | toggle() {
100 | if (this.running) this.stop();
101 | else this.start();
102 | },
103 | // Sketch.js update loop
104 | draw() {
105 | for (let i = 0; i < this.settings.iterations; i++) {
106 | this.step();
107 | }
108 | },
109 | // Save output as an image
110 | export() {
111 | window.open(this.canvas.toDataURL(), 'Crystallisation');
112 | }
113 | });
114 |
115 | const gui = new dat.GUI();
116 | gui.add(Crystallisation.settings, 'minSide').min(0).max(100).name('Min Side Length');
117 | gui.add(Crystallisation.settings, 'minAngle').min(0.0).max(1.2).step(0.01).name('Min Angle (rad)');
118 | gui.add(Crystallisation.settings, 'iterations').min(1).max(100).name('Iterations');
119 | gui.add(Crystallisation.settings, 'randomness').min(0.0).max(1.0).step(0.01).name('Randomness');
120 | gui.add(Crystallisation.settings, 'opposite').min(0.0).max(1.0).step(0.01).name('Opposite Sides');
121 | gui.add(Crystallisation, 'toggle').name('Start / Stop');
122 | gui.add(Crystallisation, 'reset').name('Reset Polygons');
123 | gui.add(Crystallisation, 'clear').name('Clear Canvas');
124 | gui.add(Crystallisation, 'export').name('Save');
125 | gui.close();
126 |
--------------------------------------------------------------------------------
/source/polygon.js:
--------------------------------------------------------------------------------
1 | // ——————————————————————————————————————————————————
2 | // Dependencies
3 | // ——————————————————————————————————————————————————
4 |
5 | import Vertex from './vertex';
6 | import Line from './line';
7 |
8 | // ——————————————————————————————————————————————————
9 | // Helpers
10 | // ——————————————————————————————————————————————————
11 |
12 | // Cosine rule (SSS) a^2 = b^2 + c^2 - 2bc cos A
13 | const SSS = (a, b, c) => Math.acos(a * a + b * b - c * c) / (2 * a * b);
14 |
15 | // ——————————————————————————————————————————————————
16 | // Polygon
17 | // ——————————————————————————————————————————————————
18 |
19 | class Polygon {
20 | constructor(...vertices) {
21 | this.vertices = vertices;
22 | // Assign principal generation
23 | this.generation = 0;
24 | // unique lines
25 | this.unique = [];
26 | }
27 | // Subdivides this polygon into 2 and returns both
28 | subdivide(randomness = 0.0, opposite = 0.5) {
29 | let i1, i2, j1, j2, l1, l2, nv, p1, p2, v1, v2;
30 | // Current number of sides
31 | nv = this.vertices.length;
32 | // Choose two unique indices
33 | i1 = ~~random(nv);
34 | i2 = random() < opposite ? (~~(i1 + nv / 2) % nv) : ~~random(nv);
35 | while (i2 === i1) { i2 = ~~random(nv); }
36 | // Choose lerp points
37 | l1 = 0.5 + random(randomness * -0.5, randomness * 0.5);
38 | l2 = 0.5 + random(randomness * -0.5, randomness * 0.5);
39 | // Create new vertices as linear interpolations between adjacent
40 | v1 = this.vertices[i1].lerp(this.vertices[(i1 + 1) % nv], l1);
41 | v2 = this.vertices[i2].lerp(this.vertices[(i2 + 1) % nv], l2);
42 | // Winding iterators
43 | [j1, j2] = [i1, i2];
44 | // First polygon winds clockwise from v1 to v2
45 | p1 = new Polygon(v1);
46 | while (j1 !== i2) {
47 | p1.vertices.push(this.vertices[j1 = (j1 + 1) % nv]);
48 | }
49 | p1.vertices.push(v2);
50 | // Second polygon winds clockwise from v2 to v1
51 | p2 = new Polygon(v2);
52 | while (j2 !== i1) {
53 | p2.vertices.push(this.vertices[j2 = (j2 + 1) % nv]);
54 | }
55 | p2.vertices.push(v1);
56 | // Increment generations
57 | p1.generation = this.generation + 1;
58 | p2.generation = this.generation + 1;
59 | // Store this unique line only
60 | p1.unique.push(new Line(v1, v2));
61 | // Return new polygons
62 | return [p1, p2];
63 | }
64 | // Computes the centroid point
65 | centroid() {
66 | // Centroid points
67 | let cx = 0.0;
68 | let cy = 0.0;
69 | // Sum all vertices
70 | this.vertices.forEach(vertex => {
71 | cx += vertex.x;
72 | cy += vertex.y;
73 | });
74 | // Take an average as the centroid
75 | return new Vertex(
76 | cx / this.vertices.length,
77 | cy / this.vertices.length
78 | );
79 | }
80 | // Computes the minimum angle between vertices
81 | minAngle() {
82 | let val = Number.MAX_VALUE;
83 | let len = this.vertices.length;
84 | // Build triangles from vertices
85 | let prev, next, a, b, c, A, B, C;
86 | this.vertices.forEach((vertex, index) => {
87 | // Next / previous vertices in loop
88 | prev = this.vertices[(index - 1 + len) % len];
89 | next = this.vertices[(index + 1 + len) % len];
90 | // Compute triangle sides
91 | a = prev.distance(vertex);
92 | b = next.distance(vertex);
93 | c = next.distance(prev);
94 | // Compute angles
95 | A = SSS(b, c, a);
96 | B = SSS(c, a, b);
97 | C = Math.PI - A - B;
98 | // store the lowest
99 | val = min(val, C);
100 | });
101 | return val;
102 | }
103 | // Computes the minimum side length
104 | minSide() {
105 | let side = Number.MAX_VALUE;
106 | let prev = this.vertices[this.vertices.length - 1];
107 | this.vertices.forEach(vertex => {
108 | side = Math.min(side, vertex.distanceSq(prev));
109 | prev = vertex;
110 | });
111 | return Math.sqrt(side);
112 | }
113 | // Computes the perimeter of this polygon
114 | perimeter() {
115 | let first = this.vertices[0];
116 | let last = this.vertices[this.vertices.length - 1];
117 | let result = last.distance(first);
118 | this.vertices.reduce((a, b) => result += a.distance(b), b);
119 | return result;
120 | }
121 | // Draws this polygon to a given context
122 | draw(context) {
123 | context.beginPath();
124 | this.vertices.forEach(({ x, y }, index) => {
125 | if (index === 0) {
126 | context.moveTo(x, y);
127 | } else {
128 | context.lineTo(x, y);
129 | }
130 | });
131 | context.closePath();
132 | }
133 | }
134 |
135 | // ——————————————————————————————————————————————————
136 | // Exports
137 | // ——————————————————————————————————————————————————
138 |
139 | export default Polygon;
140 |
--------------------------------------------------------------------------------