mirror of
https://github.com/Sheldan/canvas.git
synced 2026-01-02 07:14:21 +00:00
adding recBubbles
adding common module
This commit is contained in:
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -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
75
canvas-common/common.js
Normal 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);
|
||||
}
|
||||
13
canvas-common/package.json
Normal file
13
canvas-common/package.json
Normal 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"
|
||||
}
|
||||
@@ -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
1179
recBubbles/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
recBubbles/package.json
Normal file
19
recBubbles/package.json
Normal 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
47
recBubbles/src/index.html
Normal 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
255
recBubbles/src/js/main.js
Normal 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();
|
||||
}
|
||||
|
||||
|
||||
9
recBubbles/vite.config.js
Normal file
9
recBubbles/vite.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
root: 'src',
|
||||
build: {
|
||||
outDir: '../../dist/recBubbles'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user