adding recBubbles

adding common module
This commit is contained in:
Sheldan
2024-04-01 11:58:59 +02:00
parent 5e995525c2
commit 24d1ba2b3c
9 changed files with 1604 additions and 0 deletions

View File

@@ -34,6 +34,12 @@ jobs:
- name: Orbits Build
run: npx vite build
working-directory: orbits
- name: recBubbles Install dependencies
run: npm install
working-directory: recBubbles
- name: recBubbles Build
run: npx vite build
working-directory: recBubbles
- name: Move index
run: cp index.html dist/
- name: Setup Pages

75
canvas-common/common.js Normal file
View File

@@ -0,0 +1,75 @@
export function pointDistance(pointA, pointB) {
return Math.sqrt(Math.pow(pointA.x - pointB.x, 2) +
Math.pow(pointA.y - pointB.y, 2));
}
// https://stackoverflow.com/questions/9899372/vanilla-javascript-equivalent-of-jquerys-ready-how-to-call-a-function-whe
export function docReady(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(fn, 1);
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
export function createRainbowColors(frequency, alpha=255){
var colors = [];
var most = 2 * Math.PI / frequency;
for (var i = 0; i < most; ++i) {
var red = Math.sin(frequency * i + 0) * 127 + 128;
var green = Math.sin(frequency * i + 2) * 127 + 128;
var blue = Math.sin(frequency * i + 4) * 127 + 128;
var color = {r: red << 0, g: green << 0, b: blue << 0, a: alpha};
addRGBStyle(color);
addRGBAStyle(color);
colors.push(color)
}
return colors;
}
export function addRGBStyle(color) {
color.styleRGB = '#' + d2h(color.r) + d2h(color.g) + d2h(color.b);
}
export function addRGBAStyle(color){
color.styleRGBA = 'rgba(%red, %green, %blue, %alpha)'
.replace('%red', color.r)
.replace('%blue', color.b)
.replace('%green', color.g)
.replace('%alpha', color.a / 277);
}
export function d2h(d) {
return (d / 256 + 1 / 512).toString(16).substring(2, 4);
}
export function toRad(angle) {
return angle / 180 * Math.PI;
}
export function toDeg(angle) {
return angle * 180 / Math.PI;
}
//http://stackoverflow.com/questions/23150333/html5-javascript-dataurl-to-blob-blob-to-dataurl
export function dataURLtoBlob(dataurl) {
let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {type: mime});
}
export function downloadCanvas(name, canvas_obj, downloadBtn) {
downloadBtn.download = name + '_' + new Date().toISOString() + '.png';
let imageData = canvas_obj.toDataURL({
format: 'png',
multiplier: 4
});
let blob = dataURLtoBlob(imageData);
downloadBtn.href = URL.createObjectURL(blob);
}

View File

@@ -0,0 +1,13 @@
{
"name": "canvas-common",
"version": "1.0.0",
"description": "",
"main": "common.js",
"private": true,
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT"
}

View File

@@ -4,5 +4,6 @@
</head>
<body>
<a href="/orbits">Simulation of the solar system</a>
<a href="/recBubbles">Recursive bubbles</a>
</body>
</html>

1179
recBubbles/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
recBubbles/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "recbubbles",
"version": "1.0.0",
"description": "",
"private": true,
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"canvas-common": "file:../canvas-common"
},
"devDependencies": {
"vite": "^5.1.5"
}
}

47
recBubbles/src/index.html Normal file
View File

@@ -0,0 +1,47 @@
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>recBubbles</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html, body, div, canvas {
margin: 0;
padding: 0;
}
.controls {
position: absolute;
top: 0px;
right: 10px
}
</style>
</head>
<body>
<canvas id="canvas" style="display: block"></canvas>
<div class="controls">
<button onclick="toggleControls()" id="hideControlsBtn">Show controls</button>
<div id="controls" style="display: none">
<label for="maxTries">Maximum amount of tries</label>
<input type="number" id="maxTries"> <br>
<label for="splitMinSize"><span title="Needs to be more than the minimum circle size"></span>Minimum size for circles to get sub circles</label>
<input type="number" id="splitMinSize"> <br>
<label for="minSize"><span title="Needs to be less than the minimum size for sub circles"></span>Minimum size for circles</label>
<input type="number" id="minSize"> <br>
<label for="funkyMode">Dont clear on restart (behaves very weird)</label>
<input type="checkbox" id="funkyMode"> <br>
<label for="widthInput">Width</label>
<input type="number" id="widthInput">
<label for="heightInput">Height</label>
<input type="number" id="heightInput">
<button onclick="restartBubbles()">Restart</button>
<a id="download" onclick="exportCanvas()">Download</a>
</div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>

255
recBubbles/src/js/main.js Normal file
View File

@@ -0,0 +1,255 @@
import {createRainbowColors, docReady, pointDistance, downloadCanvas} from "canvas-common";
let ctx = {};
let canvas = {};
let maxTriesField = {};
let funkyModeBox = {};
let splitMinField = {};
let minSizeField = {};
let widthField = {};
let heightField = {};
let downloadButton = {};
let config = {
size: {
width: window.innerWidth,
height: window.innerHeight
},
recBubbles: {
maxTries: 5,
splitMinSize: 2,
fps: 10000,
showControls: false,
funkyMode: false,
stopping: false
}
};
config.recBubbles.relevantSize = Math.min(config.size.width, config.size.height);
let rootCircle = {
x: config.size.width / 2,
y: config.size.height / 2,
radius: config.recBubbles.relevantSize / 2,
circles: []
};
config.recBubbles.maxRadius = rootCircle.radius;
config.recBubbles.minRadius = -1;
// results in 101 different colors
let rainbow = createRainbowColors(1/16);
// max distance from top left corner
let max = Math.sqrt(rootCircle.x * rootCircle.x + rootCircle.y * rootCircle.y) / 2;
function toggleControls() {
config.recBubbles.showControls = !config.recBubbles.showControls;
let hideControlsBtn = document.getElementById('hideControlsBtn')
let controlsElement = document.getElementById('controls');
if(!config.recBubbles.showControls) {
hideControlsBtn.innerText = 'Show controls'
controlsElement.style.display = 'none'
} else {
hideControlsBtn.innerText = 'Hide controls'
controlsElement.style.display = 'block'
}
}
function restart() {
config.recBubbles.maxTries = parseInt(maxTriesField.value);
config.recBubbles.funkyMode = funkyModeBox.checked;
config.recBubbles.splitMinSize = parseInt(splitMinField.value);
config.recBubbles.minRadius = parseInt(minSizeField.value);
if(config.recBubbles.splitMinSize <= config.recBubbles.minRadius) {
config.recBubbles.splitMinSize = config.recBubbles.minRadius + 1;
}
config.size.width = parseInt(widthField.value)
config.size.height = parseInt(heightField.value)
config.recBubbles.relevantSize = Math.min(config.size.width, config.size.height);
rootCircle = {
x: config.size.width / 2,
y: config.size.height / 2,
radius: config.recBubbles.relevantSize / 2,
circles: []
};
max = Math.sqrt(rootCircle.x * rootCircle.x + rootCircle.y * rootCircle.y) / 2;
config.recBubbles.maxRadius = rootCircle.radius;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if(!config.recBubbles.funkyMode) {
config.recBubbles.stopping = true;
}
setTimeout(() => {
config.recBubbles.stopping = false;
startDrawing()
}, 500)
}
window.toggleControls = toggleControls;
window.restartBubbles = restart;
window.exportCanvas = exportCanvas;
function initControls() {
maxTriesField.value = config.recBubbles.maxTries;
funkyModeBox.value = config.recBubbles.funkyMode;
splitMinField.value = config.recBubbles.splitMinSize;
minSizeField.value = config.recBubbles.minRadius;
widthField.value = config.size.width;
heightField.value = config.size.height;
}
function exportCanvas() {
downloadCanvas('recBubbles', canvas, downloadButton)
}
function startDrawing() {
canvas.width = config.size.width;
canvas.height = config.size.height;
ctx.strokeStyle = rainbow[rainbow.length - 1].styleRGB;
paintCircle(rootCircle);
addCirclesInCircle(rootCircle);
initControls();
rootCircle.circles.forEach(function (circleToPaint) {
paintCircle(circleToPaint);
});
}
function loadControls() {
maxTriesField = document.getElementById('maxTries');
funkyModeBox = document.getElementById('funkyMode');
splitMinField = document.getElementById('splitMinSize');
minSizeField = document.getElementById('minSize');
widthField = document.getElementById('widthInput');
heightField = document.getElementById('heightInput');
downloadButton = document.getElementById('download');
}
docReady(function() {
loadControls();
canvas = document.getElementById('canvas')
ctx = canvas.getContext("2d");
ctx.translate(0.5, 0.5); // to make better anti-aliasing
startDrawing();
});
function addCirclesInCircle(circle){
if(config.recBubbles.stopping) {
return;
}
let posFound = false;
let newRandomPoint;
let tries = 0;
do {
// this tries any random point within the circle
newRandomPoint = randomPointInCircle(circle);
tries++;
// if it collides with any other point already present, this checks only the top level circles, as the rest would be contained anyway
if (!collidesWithOtherCircle(newRandomPoint, circle)) {
posFound = true;
}
} while(tries < config.recBubbles.maxTries && !posFound);
if(posFound) {
newRandomPoint.radius = getAvailableRadius(newRandomPoint, circle, config.size.width, true);
if(newRandomPoint.radius > config.recBubbles.minRadius){
// create a new circle object
newRandomPoint.circles = [];
newRandomPoint.parent = circle;
circle.circles.push(newRandomPoint);
let distance = pointDistance(newRandomPoint, rootCircle)
// select a color which is relatively appropriate according to how far out we are
let colorIndex = ((distance / max * rainbow.length) << 0) % rainbow.length;
let color = rainbow[colorIndex];
color.a = 255;
ctx.strokeStyle = color.styleRGB;
paintCircle(newRandomPoint);
} else {
posFound = false
}
}
if(posFound){
// continue on with our journey
setTimeout(function () {
addCirclesInCircle(circle)
}, 1000 / config.recBubbles.fps)
} else {
// if we ended up not finding a position, lets see if we filled the circle
setTimeout(function () {
let filledCircle = false;
for(let i = 0; i < circle.circles.length; i++){
// if a child circle did not receive any child circles yet, but the size of the circle is above the minimum radius
// we are going add more circles to that circle
// the children check is if we already filled that circle
if(circle.circles[i].circles.length === 0 && circle.circles[i].radius > config.recBubbles.splitMinSize){
addCirclesInCircle(circle.circles[i]);
filledCircle = true;
break;
}
}
// if we didnt find a child circle to fill (because too small, or already "all" filled, we continue own with the parent
// if we continue with the parent, that could create new children circles for the parent
// here we are basically going up in the recursion one level, and then creating new sub levels of recursion
if(!filledCircle && circle.parent){
addCirclesInCircle(circle.parent);
}
}, 1000 / config.recBubbles.fps)
}
}
/**
* this method gets the maximum radius possible within the given circle. it also checks any potential circles which are already
* present within the circle
* @param point the point we are trying to find the radius for
* @param circle the circle to check against for
* @param currentMax the currently maximum possible radius
* @param checkChildren whether or not to check the children of the circle, this is important, because, in the first iteration we are interested in checking against the circle
* the point will be contained in, and only _those_ children. The children of the children are not interesting, because the parent defines the bounding box
* @returns {number} the found maximum circle
*/
function getAvailableRadius(point, circle, currentMax, checkChildren){
let distanceToCenter = pointDistance(point, circle);
let distance = Math.abs(circle.radius - distanceToCenter)
if(distance < currentMax && distance > 0){
currentMax = Math.min(distance, config.recBubbles.maxRadius);
}
if(checkChildren) { // only check for the children, if we are currently checking within the parent circle
for(let i = 0; i < circle.circles.length; i++){
currentMax = getAvailableRadius(point, circle.circles[i], currentMax, false);
}
}
return currentMax;
}
function collidesWithOtherCircle(point, superCircle){
for(let superCircleIndex = 0; superCircleIndex < superCircle.circles.length; superCircleIndex++){
if(isInCircle(point, superCircle.circles[superCircleIndex])){
return true;
}
}
return false;
}
function isInCircle(point, circle){
return pointDistance(point, circle) < circle.radius;
}
function randomPointInCircle(circle){
let randomDistance = Math.random() * circle.radius;
let randomArc = 2 * Math.PI * Math.random();
return {
x: circle.x + Math.cos(randomArc) * randomDistance,
y: circle.y + Math.sin(randomArc) * randomDistance
};
}
function paintCircle(circle){
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI);
ctx.stroke();
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
export default defineConfig({
base: './',
root: 'src',
build: {
outDir: '../../dist/recBubbles'
}
})