css-houdini-fractals
Version:
Fractals Drawing with CSS Houdini
175 lines (160 loc) • 6.68 kB
JavaScript
/* 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.
// Default (x, y) coordinates based on type of shape.
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);