Skip to content

Instantly share code, notes, and snippets.

@polyakovin
Created June 18, 2020 23:16
Show Gist options
  • Save polyakovin/740f6c9d51e7d0e24cef947ffd59d716 to your computer and use it in GitHub Desktop.
Save polyakovin/740f6c9d51e7d0e24cef947ffd59d716 to your computer and use it in GitHub Desktop.
Code for a fractal generator
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fractals</title>
<style>
canvas {
vertical-align: top;
}
form {
display: inline-block;
}
</style>
</head>
<body>
<canvas id="picture"></canvas>
<form>
<label>
Presets:
<select id="presetsList" value="koch">
<option value="koch">Koch Curve</option>
</select>
</label><br>
<label>
Rotation Angle:
<input type="number" id="baseAngleInput" value="60" min="10" max="180" step="10">&deg;
</label>
<label>
Stage:
<input type="number" id="stageInput" value="7" min="1" max="8">
</label>
<fieldset>
<legend>Rules</legend>
<label>
start:
<input type="text" id="startInput">
</label><br>
<label>
F &rarr;
<input type="text" id="patternF">
</label><br>
<label>
f &rarr;
<input type="text" id="patternf">
</label><br>
<label>
G &rarr;
<input type="text" id="patternG">
</label><br>
<label>
X &rarr;
<input type="text" id="patternX">
</label><br>
<label>
Y &rarr;
<input type="text" id="patternY">
</label><br>
</fieldset>
</form>
<script>
const ctx = picture.getContext('2d');
const WIDTH = 400;
const HEIGHT = 400;
const initialPoint = [WIDTH / 2, HEIGHT / 2];
const positionStore = [];
const actions = {
F: () => drawLine(lastPoint, getNextPoint()),
G: () => drawLine(lastPoint, getNextPoint()),
f: () => moveLine(getNextPoint()),
'+': () => currentAngle -= baseAngle,
'-': () => currentAngle += baseAngle,
'[': () => positionStore.push({ lastPoint, currentAngle }),
']': () => {
const saving = positionStore.pop();
lastPoint = saving.lastPoint;
currentAngle = saving.currentAngle;
},
};
const presets = {
koch: {
title: 'Koch Curve',
start: 'F',
rules: {
F: 'F-F++F-F',
},
angle: 60,
maxStage: 8,
color: 'black',
},
snowflake: {
title: 'Koch Snowflake',
start: '-F++F++F',
rules: {
F: 'F-F++F-F',
},
angle: 72,
maxStage: 8,
color: 'blue',
},
cantor: {
title: 'Cantor Set',
start: 'F',
rules: {
F: 'FfF',
f: 'fff',
},
angle: 60,
maxStage: 8,
color: 'black',
},
triangle: {
title: 'Sierpinski Triangle',
start: 'F-G-G',
rules: {
F: 'F-G+F+G-F',
G: 'GG',
},
angle: 120,
maxStage: 9,
color: 'blue',
},
gilbert: {
title: 'Gilbert Curve',
start: '-YF+XFX+FY-',
rules: {
X: '-YF+XFX+FY-',
Y: '+XF-YFY-FX+',
},
angle: 90,
maxStage: 8,
color: 'red',
},
dragon: {
title: 'Dragon Curve',
start: 'FX',
rules: {
X: 'X+YF+',
Y: '-FX-Y',
},
angle: 90,
maxStage: 18,
color: 'random',
},
tree: {
title: 'Fractal Plant',
start: '---X',
rules: {
X: 'F-[[X]+X]+F[+FX]-X',
F: 'FF',
},
angle: 25,
maxStage: 9,
color: 'randomGreen',
},
};
let baseColor = 'green';
let start, theorem, baseAngle;
let baseLength = 5; // any
let currentAngle = 0;
let lastPoint = initialPoint;
let minX = lastPoint[0];
let minY = lastPoint[1];
let maxX = lastPoint[0];
let maxY = lastPoint[1];
const defaultPreset = 'tree';
presetsList.innerHTML = Object.keys(presets).map(preset => `<option value="${preset}"${preset === defaultPreset ? ' selected' : ''}>${presets[preset].title}</option>`);
setPictureSize(WIDTH, HEIGHT);
setPreset(defaultPreset);
patternF.addEventListener('change', handleFractalParamsChanges);
patternf.addEventListener('change', handleFractalParamsChanges);
patternG.addEventListener('change', handleFractalParamsChanges);
patternX.addEventListener('change', handleFractalParamsChanges);
patternY.addEventListener('change', handleFractalParamsChanges);
startInput.addEventListener('change', handleFractalParamsChanges);
stageInput.addEventListener('change', handleFractalParamsChanges);
baseAngleInput.addEventListener('change', handleFractalParamsChanges);
presetsList.addEventListener('change', handlePresetChange);
function handlePresetChange() {
setPreset(presetsList.value);
}
function handleFractalParamsChanges() {
setDefaults();
lastPoint = initialPoint;
minX = lastPoint[0];
minY = lastPoint[1];
maxX = lastPoint[0];
maxY = lastPoint[1];
baseAngle = convertGradusesToRadians(baseAngleInput.value);
start = startInput.value;
theorem = {
F: patternF.value,
f: patternf.value,
G: patternG.value,
X: patternX.value,
Y: patternY.value,
};
buildFractal(start, theorem, stageInput.value);
}
function buildFractal(start, theorems, stage = 1) {
let resultString = start;
for (let i = 1; i < stage; i++) {
resultString = resultString.split('').map(char => theorems[char] ? theorems[char] : char).join('');
}
const firstPoint = lastPoint;
setDefaults();
resultString.split('').forEach(char => actions[char] && actions[char]());
const imageWidth = maxX - minX;
const imageHeight = maxY - minY;
let k = imageWidth > imageHeight ? WIDTH / imageWidth : HEIGHT / imageHeight;
baseLength *= k;
const diffRatio = imageHeight < imageWidth ? imageHeight / imageWidth : imageWidth / imageHeight;
lastPoint[0] = (firstPoint[0] - minX) * k + (WIDTH - imageWidth * k) / 2;
lastPoint[1] = (firstPoint[1] - minY) * k + (WIDTH - imageHeight * k) / 2;
setDefaults();
resultString.split('').forEach(char => actions[char] && actions[char]());
}
function setDefaults() {
currentAngle = 0;
ctx.clearRect(0, 0, WIDTH, HEIGHT);
}
function moveLine(nextPoint) {
lastPoint = nextPoint;
}
function getNextPoint() {
const nextPoint = [
lastPoint[0] + baseLength * Math.cos(currentAngle),
lastPoint[1] + baseLength * Math.sin(currentAngle),
];
if (minX > nextPoint[0]) {
minX = nextPoint[0];
}
if (maxX < nextPoint[0]) {
maxX = nextPoint[0];
}
if (minY > nextPoint[1]) {
minY = nextPoint[1];
}
if (maxY < nextPoint[1]) {
maxY = nextPoint[1];
}
lastPoint = nextPoint;
return nextPoint;
}
function convertGradusesToRadians(angle) {
return angle * Math.PI / 180;
}
function setPreset(presetName) {
const maxStage = presets[presetName].maxStage;
stageInput.max = maxStage;
if (stageInput.value > maxStage) {
stageInput.value = maxStage;
};
baseAngle = convertGradusesToRadians(presets[presetName].angle);
start = presets[presetName].start;
theorem = presets[presetName].rules;
baseColor = presets[presetName].color;
baseAngleInput.value = presets[presetName].angle;
startInput.value = start;
patternF.value = '';
patternf.value = '';
patternG.value = '';
patternX.value = '';
patternY.value = '';
for (const rule in theorem) {
document.getElementById(`pattern${rule}`).value = theorem[rule];
}
handleFractalParamsChanges();
}
function setPictureSize(WIDTH, HEIGHT) {
picture.width = WIDTH;
picture.height = HEIGHT;
}
function drawLine(start, end) {
let strokeColor;
if (baseColor === 'random') {
const red = Math.floor(Math.random() * 255);
const green = Math.floor(Math.random() * 255);
const blue = Math.floor(Math.random() * 255);
strokeColor = `rgb(${red}, ${green}, ${blue})`;
} else if (baseColor === 'randomGreen') {
const red = Math.floor(Math.random() * 100 + 55);
const green = Math.floor(Math.random() * 100 + 100);
const opacity = Math.floor(Math.random() * 50 + 50) / 100;
strokeColor = `rgba(${red}, ${green}, 0, ${opacity})`;
} else {
strokeColor = baseColor;
}
ctx.strokeStyle = strokeColor;
ctx.beginPath();
ctx.moveTo(start[0], HEIGHT - start[1]);
ctx.lineTo(end[0], HEIGHT - end[1]);
ctx.stroke();
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment