-
Notifications
You must be signed in to change notification settings - Fork 2
/
fractals.js
executable file
·173 lines (159 loc) · 6.29 KB
/
fractals.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/* global registerPaint */
/* jshint esversion:6 */
/**
* Fractal Drawing with CSS Houdini
*
* @link https://houdini.how/
* @link https://github.com/GoogleChromeLabs/houdini.how
* @link https://developer.mozilla.org/en-US/docs/Web/Houdini
* @link https://fractalfoundation.org/resources/what-are-fractals/
* @link https://en.wikipedia.org/wiki/
* @link https://mathworld.wolfram.com/Fractal.html
* @link https://www.wired.com/2010/09/fractal-patterns-in-nature/
* @author Conrad Sollitt (https://conradsollitt.com)
* @license CC0 "Public Domain" license
*/
class Fractals {
/**
* Required Houdini API for Custom Properties.
* If a variable is not included then `props.get(name)` will
* return `null` from the `paint()` function.
*/
static get inputProperties() {
return [
'--colors',
'--angle',
'--starting-length-percent',
'--next-line-size',
'--shape',
'--max-draw-count',
'--debug-to-console',
'--show-origin',
];
}
/**
* Houdini API
*
* @param {PaintRenderingContext2D} ctx
* @param {object} size
* @param {object} props
*/
paint(ctx, size, props) {
// Optional debug options
const debugToConsole = (props.get('--debug-to-console').toString().trim() === '1');
let startDate;
if (debugToConsole) {
startDate = new Date();
console.log('-'.repeat(80));
console.log(`Starting CSS [paint(fractals)] at: ${startDate.toLocaleTimeString()}`);
console.log(size);
console.log(props);
}
this.showOrigin = (props.get('--show-origin').toString().trim() === '1');
// Default angle to 30%
this.angle = parseInt(props.get('--angle'), 10) || 30;
// Starting Line Length - Default to 22 %
let startingLengthPct = parseInt(props.get('--starting-length-percent'), 10);
if (isNaN(startingLengthPct) || startingLengthPct < 5 || startingLengthPct > 95) {
startingLengthPct = 22;
}
const length = (size.height / (100 / startingLengthPct));
// Next line size - defaults to 0.8 for 80%
this.nextLineSize = parseFloat(props.get('--next-line-size'));
if (isNaN(this.nextLineSize) || this.nextLineSize < 0.1 || this.nextLineSize > 0.9) {
this.nextLineSize = 0.8;
}
// For safety due to recursive function or by design if the number
// is small only half (give or take) the the drawing will render.
// If [--max-draw-count] is reached the number of shapes will be
// slightly lager than the max draw count due to recursive functions
// that need to finish running.
this.maxDrawCount = parseInt(props.get('--max-draw-count'), 10) || 10000;
this.drawCount = 0;
// Colors are space delimited
this.colors = props.get('--colors').toString().trim().split(' ').map(s => s.trim());
this.colorCount = this.colors.length;
// Function for type Shape to Draw - Defaults to line.
let shape;
const x = (size.width / 2);
const y = size.height - (this.showOrigin ? 4 : 0);
switch (props.get('--shape').toString().trim()) {
case 'circle':
shape = (ctx, diameter) => {
const radius = (diameter / 2);
ctx.arc(0, -radius, radius, 0, Math.PI * 2, true);
};
break;
case 'square':
shape = (ctx, length) => {
// Typically when using canvas a square would be drawn using
// `rect()`, however due to the rotated origin each line of
// the square must be drawn to match the layout of other shapes.
// ctx.rect(x, y, length, length);
const half_len = length / 2;
ctx.moveTo(0, 0);
ctx.lineTo(half_len, 0);
ctx.lineTo(half_len, -length);
ctx.lineTo(0, -length);
ctx.lineTo(-half_len, -length);
ctx.lineTo(-half_len, 0);
ctx.lineTo(0, 0);
};
break;
default: // 'line'
shape = (ctx, length) => {
ctx.moveTo(0, 0);
ctx.lineTo(0, -length);
};
}
// Start call to recursive `draw()`
this.draw(ctx, shape, x, y, length, 0, 0);
if (debugToConsole) {
const endDate = new Date();
const duration = endDate.getTime() - startDate.getTime();
console.log(`Finished CSS [paint(fractals)] at: ${startDate.toLocaleTimeString()}`);
console.log(`Duration in milliseconds: ${duration}`);
console.log(`Function calls: ${this.drawCount}`);
}
}
/**
* Recursive function that draws the fractals
*
* @param {PaintRenderingContext2D} ctx
* @param {function} shape
* @param {number} x
* @param {number} y
* @param {number} length
* @param {number} angle
*/
draw(ctx, shape, x, y, length, angle) {
// Draw Shape
ctx.beginPath();
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle * Math.PI / 180);
if (this.colorCount > 0) {
const colorIndex = this.drawCount % this.colorCount;
ctx.strokeStyle = this.colors[colorIndex];
}
shape(ctx, length);
ctx.stroke();
// This helps with debugging when adding new shapes
if (this.showOrigin) {
ctx.fillStyle = ctx.strokeStyle;
ctx.fillRect(0, 0, 4, 4);
}
// Exit once the line length becomes too small or
// if the number of max function calls is exceeded.
this.drawCount++;
if (this.drawCount > this.maxDrawCount || length < 10) {
ctx.restore();
return;
}
// Recursively call this function twice, once for the left side and once for the right side
this.draw(ctx, shape, 0, -length, length * this.nextLineSize, -this.angle);
this.draw(ctx, shape, 0, -length, length * this.nextLineSize, this.angle);
ctx.restore();
}
}
registerPaint('fractals', Fractals);