Created
June 18, 2020 23:16
-
-
Save polyakovin/740f6c9d51e7d0e24cef947ffd59d716 to your computer and use it in GitHub Desktop.
Code for a fractal generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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">° | |
</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 → | |
<input type="text" id="patternF"> | |
</label><br> | |
<label> | |
f → | |
<input type="text" id="patternf"> | |
</label><br> | |
<label> | |
G → | |
<input type="text" id="patternG"> | |
</label><br> | |
<label> | |
X → | |
<input type="text" id="patternX"> | |
</label><br> | |
<label> | |
Y → | |
<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