12 min read
Purple snowflake fractal Try it out!
[Source code]

Why I made this

The Web Canvas APIs have been an area I haven’t explored much, so I wanted to fill in some gaps and step away from specialization. It’s fun to learn new things!

I also had the end goal of showing this to my family, especially my nieces and nephews who are at the age of tinkering and marvelling. I was not disappointed when I got to showcase it, though I did get the “so do you know how to make a video game” question. 😅

How it works

This was created using vanilla CSS and JavaScript. Back to the basics!

All of the logic fits into one ~275 line file, with straightforward functions for rendering, sharing, and downloading.

Rendering the fractal has some math and logic that is not terribly complicated, but I’ll leave out of this post. See the section below on Inspiration if you’d like to learn more about that. The gist of this is that recursion is used when creating lines (branches), with several variables controlled as config values by the user. The Canvas API can be strange at first, but building a little project like this can really make it seem less daunting.

function paint(): void {
	const ctx = canvas.getContext('2d')!;
	canvas.width = window.innerWidth;
	canvas.height = window.innerHeight;

	// config
	let size = Number(sizeInput.value) ?? 0;
	let branches = Number(branchesInput.value) ?? 0;
	let offshoots = Number(offshootsInput.value) ?? 0;
	let levels = Number(levelsInput.value) ?? 0;
	let scale = Number(scaleInput.value) ?? 0;
	let angle = Number(angleInput.value) ?? 0;

	ctx.lineCap = 'round';

	if (shadowsInput.checked) {
		ctx.shadowColor = 'rgba(0, 0, 0, 0.7)';
		ctx.shadowOffsetX = 10;
		ctx.shadowOffsetY = 5;
		ctx.shadowBlur = 10;

	function drawBranch(level: number): void {
		if (level > Math.min(levels, MAX_LEVELS)) return;

		ctx.moveTo(0, 0);
		ctx.lineTo(size, 0);

		for (let i = 0; i < offshoots; i += 1) {;

			const translate = size - (size / offshoots) * i;
			ctx.translate(translate, 0);
			ctx.scale(scale, scale);;
			drawBranch(level + 1);

			if (reflectInput.checked) {;
				drawBranch(level + 1);


		if (dotsInput.checked) {
			ctx.arc(0, size, size * 0.1, 0, Math.PI * 2);

	function drawFractal(): void {
		ctx.clearRect(0, 0, canvas.width, canvas.height);;

		ctx.strokeStyle = colorInput.value ?? 'white';
		ctx.fillStyle = colorInput.value ?? 'white';
		ctx.lineWidth = Number(branchWidthInput.value) ?? 0;

		ctx.translate(canvas.width / 2, canvas.height / 2);
		ctx.scale(1, 1);

		for (let i = 0; i < branches; i += 1) {
			ctx.rotate((Math.PI * 2) / branches);



The controls are simple HTML inputs for toggles, sliders, and a color picker. There are JS listeners that keep track of the values and trigger a rerender on each change.

The sharing feature has 2 parts: serialization and deserialization. When the “Share” button is clicked all of the config fields are abbreviated, combined with their values, and encoded into a query string, like This URL is then copied to the clipboard.

function copyConfigToClipboard() {
	const config = {
		sz: sizeInput.value,
		b: branchesInput.value,
		o: offshootsInput.value,
		l: levelsInput.value,
		sc: scaleInput.value,
		a: angleInput.value,
		bw: branchWidthInput.value,
		c: colorInput.value,
		sh: String(shadowsInput.checked),
		r: String(reflectInput.checked),
		d: String(dotsInput.checked),
	const params = new URLSearchParams(config);
	const encoded = encodeURIComponent(params.toString());

	const fullUrl = `${window.location.origin}?${encoded.toString()}`;

		.then(() => {
			window.alert('Copied to clipboard!');
		.catch(() => {
			window.alert('Error: Could not copy to clipboard!');

On the flip side, the visitor opening a share link will have all of these config values parsed and used to update the renderer.

function loadURLParams() {
	const params = new URLSearchParams(decodeURIComponent(;

	const size = params.get('sz');
	const branches = params.get('b');
	const offshoots = params.get('o');
	const levels = params.get('l');
	const scale = params.get('sc');
	const angle = params.get('a');
	const branchWidth = params.get('bw');
	const color = params.get('c');
	const shadows = params.get('sh');
	const reflect = params.get('r');
	const dots = params.get('d');

	const hasNoControlParams = [
	].every((s) => !s);

	if (size) sizeInput.value = size;
	if (branches) branchesInput.value = branches;
	if (offshoots) offshootsInput.value = offshoots;
	if (levels) levelsInput.value = levels;
	if (scale) scaleInput.value = scale;
	if (angle) angleInput.value = angle;
	if (branchWidth) branchWidthInput.value = branchWidth;
	if (color) colorInput.value = color;
	if (shadows) shadowsInput.checked = shadows === 'true';
	if (reflect) reflectInput.checked = reflect === 'true';
	if (dots) dotsInput.checked = dots === 'true';

	if (hasNoControlParams) openControls();

The download capability uses built-in JavaScript functions to convert the canvas into a URL that the browser will stream into a new PNG file download:

function downloadAsPng() {
	const fileData = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream');
	const fileName = `fractals-${}.png`;

	const link = document.createElement('a');
	link.setAttribute('href', fileData);
	link.setAttribute('download', fileName);;


Complex blue fractal Green hex fractal Red barbs fractal Gold feathers fractal Simple pink fractal with dots Try it out yourself, make your coolest-looking fractals, and share with friends!


I learned how to model the core canvas logic watching Learn Creative Coding: Fractals from Frank’s Laboratory.

Previous Post

Concentric Time

7 min read
A simple and fun way to view the time!
Next Post

Working Away From Home

6 min read
Discussing some pros and cons of working from home, and what an alternative could achieve.