A way to fill non-overlapping circles.
Created
May 12, 2020 11:01
-
-
Save artemuzz/1a0deede5a39cca11ee8e07fed7b745f to your computer and use it in GitHub Desktop.
Scallop shells
This file contains hidden or 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
"use strict"; | |
/**** parameters you should try to modify */ | |
const minRadius = 25; | |
const maxRadius = 150; | |
const margin = 2; // minimum distance between 2 circles | |
/**** modifications beyond this line at your own risk */ | |
let canv, ctx; | |
let maxx, maxy; // canvas sizes (in pixels) | |
let circles; | |
let events = []; | |
// shortcuts for Math.… | |
const mrandom = Math.random; | |
const mfloor = Math.floor; | |
const mround = Math.round; | |
const mceil = Math.ceil; | |
const mabs = Math.abs; | |
const mmin = Math.min; | |
const mmax = Math.max; | |
const mPI = Math.PI; | |
const mPIS2 = Math.PI / 2; | |
const m2PI = Math.PI * 2; | |
const msin = Math.sin; | |
const mcos = Math.cos; | |
const matan2 = Math.atan2; | |
const mhypot = Math.hypot; | |
const msqrt = Math.sqrt; | |
const rac3 = msqrt(3); | |
const rac3s2 = rac3 / 2; | |
const mPIS3 = Math.PI / 3; | |
//----------------------------------------------------------------------------- | |
// miscellaneous functions | |
//----------------------------------------------------------------------------- | |
function alea (min, max) { | |
// random number [min..max[ . If no max is provided, [0..min[ | |
if (typeof max == 'undefined') return min * mrandom(); | |
return min + (max - min) * mrandom(); | |
} | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function intAlea (min, max) { | |
// random integer number [min..max[ . If no max is provided, [0..min[ | |
if (typeof max == 'undefined') { | |
max = min; min = 0; | |
} | |
return mfloor(min + (max - min) * mrandom()); | |
} // intAlea | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function arrayShuffle (array) { | |
/* randomly changes the order of items in an array | |
only the order is modified, not the elements | |
*/ | |
let k1, temp; | |
for (let k = array.length - 1; k >= 1; --k) { | |
k1 = intAlea(0, k + 1); | |
temp = array[k]; | |
array[k] = array[k1]; | |
array[k1] = temp; | |
} // for k | |
return array | |
} // arrayShuffle | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function randomOrder (range) { | |
/* returns an array with numbers 0..range-1 in random order */ | |
let array = []; | |
for ( let k = 0; k < range; ++k) array[k] = k; | |
return arrayShuffle(array); | |
} // randomOrder | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function randomElement (array) { | |
return array[intAlea(array.length)]; | |
} // randomElement | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
/* returns intermediate value between v0 and v1, | |
alpha = 0 will return v0, alpha = 1 will return v1 | |
values of alpha outside [0,1] may be used to compute points outside the v0-v1 range | |
*/ | |
function lerp (v0, v1, alpha) { | |
return (1 - alpha) * v0 + alpha * v1; | |
} // function lerp; | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
/* returns point based on : | |
- an origin point | |
- a distance | |
- a direction given by two coordinates | |
*/ | |
function dirPoint (porg, dist, dir) { | |
let distDir = mhypot (dir[0], dir[1]); | |
return [porg[0] + dist * dir[0] / distDir, | |
porg[1] + dist * dir[1] / distDir]; | |
} // function dirPoint; | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
/* returns two coordinates representing move from porg to pend | |
*/ | |
function diffPoints (porg, pend) { | |
return [pend[0] - porg[0], | |
pend[1] - porg[1]]; | |
} // function diffPoints; | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function sumPoints (pa, pb) { | |
return [pa[0] + pb[0], | |
pa[1] + pb[1]]; | |
} // function sumPoints; | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
/* returns intermediate point between p0 and p1, | |
alpha = 0 will return p0, alpha = 1 will return p1 | |
values of alpha outside [0,1] may be used to compute points outside the p0-p1 segment | |
*/ | |
function intermediate (p0, p1, alpha) { | |
return [(1 - alpha) * p0[0] + alpha * p1[0], | |
(1 - alpha) * p0[1] + alpha * p1[1]]; | |
} // function intermediate | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function length (p0, p1) { | |
/* distance between points */ | |
return mhypot (p0[0] - p1[0], p0[1] - p1[1]); | |
} // function length | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
/* find first occurence of element in array | |
returns index if found | |
returns -1 if not found but safe == true | |
throws error if not found and safe == false | |
*/ | |
function findIndex (array, element, safe = false) { | |
let idx = array.indexOf(element); | |
if (idx != -1 || safe) return idx; | |
throw ('not found element in array'); | |
} // removeFromArray | |
//----------------------------------------------------------------------------- | |
function Circle(x, y, radius) { | |
this.c = [x, y]; | |
this.radius = radius; | |
} // Circle | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
Circle.prototype.draw = function(stroke) { | |
let th1, th2, k; | |
th1 = alea(0, m2PI); | |
let xa = this.c[0] + this.radius * mcos(th1); | |
let ya = this.c[1] + this.radius * msin(th1); | |
let dk = mmax( 8, mround( this.radius / 5)) | |
for (k = 0 ; k < dk ; ++k) { | |
th2 = th1 + k * m2PI / dk; | |
ctx.beginPath(); | |
ctx.moveTo (xa,ya); | |
ctx.lineTo (this.c[0] + this.radius * mcos(th2), this.c[1] + this.radius * msin(th2)); | |
ctx.strokeStyle = stroke; | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
} | |
} // | |
//------------------------------------------------------------------------ | |
function createCircles() { | |
let x, y, maxRad, d, closest; | |
let limit = minRadius + margin; | |
circles = []; // initially empty | |
let failcnt = 0; | |
retry: | |
while (circles.length < 5000) { | |
failcnt ++; | |
if (failcnt > 500) { | |
// console.log ( failcnt ); | |
break; // 100 unsuccessful tries, give up | |
} | |
x = alea(maxx); | |
y = alea(maxy); | |
maxRad = x; | |
maxRad = mmin(maxRad, maxx - x); | |
maxRad = mmin(maxRad, y); | |
maxRad = mmin(maxRad, maxy - y); | |
if (maxRad < limit) continue retry; | |
let closest = -1; | |
for (let k = 0; k < circles.length; ++k) { | |
d = length([x, y], circles[k].c) - circles[k].radius; | |
if (d < limit) continue retry; // too close | |
if (d < maxRad) { | |
maxRad = d; | |
closest = k; | |
} | |
} // for k | |
maxRad -= margin; | |
// if we have much place, do not allways use maxRadius | |
if (maxRad > maxRadius) { | |
maxRad = alea(mmax(minRadius, 0.8 * maxRadius), maxRadius); | |
// move chosen point towards closest neighbour | |
if (closest >= 0) { | |
let cl = circles[closest]; | |
d = length([x, y], cl.c); // actual distance | |
let d1 = maxRad + margin + cl.radius; // desired distance | |
x = cl.c[0] + (x - cl.c[0]) * d1 / d; | |
y = cl.c[1] + (y - cl.c[1]) * d1 / d; | |
} | |
} | |
let nc = new Circle(x, y, mmin(maxRadius, maxRad )) | |
circles.push (nc); | |
failcnt = 0; | |
} // while | |
ctx.fillStyle = '#000'; | |
ctx.fillRect(0, 0, maxx, maxy); | |
let hue = intAlea(360); | |
// circles.sort ( (c1, c2) => c2.radius - c1.radius); | |
circles.forEach( (circle,k) => circle.draw(`hsl(${hue}, 100%, ${intAlea(20,80)}%)`) ); | |
} // createCircles | |
//------------------------------------------------------------------------ | |
function startOver() { | |
// canvas dimensions | |
maxx = window.innerWidth; | |
maxy = window.innerHeight; | |
canv.style.left = ((window.innerWidth ) - maxx) / 2 + 'px'; | |
canv.style.top = ((window.innerHeight ) - maxy) / 2 + 'px'; | |
canv.width = maxx; | |
canv.height = maxy; | |
if (maxx < 10) return false; // not yet ready | |
createCircles(); | |
return true; | |
} // startOver | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function clickCanvas() { | |
events.push({event: 'click'}); | |
} | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function resize() { | |
events.push({event: 'resize'}); | |
} | |
//------------------------------------------------------------------------ | |
//------------------------------------------------------------------------ | |
let animate = (()=>{ // scope for animate | |
let state; | |
return function (tstamp) { | |
let event; | |
while (event = events.shift()) { | |
switch (event.event) { | |
case 'init' : | |
case 'click' : | |
case 'resize' : | |
state = 1; | |
break; | |
} //switch (event.event) | |
} // while events | |
switch (state) { | |
case 1: | |
if (startOver()) ++state; | |
break; | |
} // switch (state) | |
window.requestAnimationFrame(animate); | |
} // animate | |
})(); // scope for animate | |
//------------------------------------------------------------------------ | |
//------------------------------------------------------------------------ | |
// beginning of execution | |
window.addEventListener("load",function() { | |
{ | |
canv = document.createElement('canvas'); | |
canv.style.position="absolute"; | |
document.body.appendChild(canv); | |
ctx = canv.getContext('2d'); | |
canv.setAttribute('title','Click for new pattern'); | |
} // canvas creation | |
window.addEventListener('click',clickCanvas); | |
window.addEventListener('resize',resize); | |
/* launch animation */ | |
events.push ({event: 'init'}); | |
window.requestAnimationFrame(animate); // animate | |
}); // window load listener |
This file contains hidden or 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
body { | |
font-family: Arial, Helvetica, "Liberation Sans", FreeSans, sans-serif; | |
background-color: #000; | |
margin:0; | |
padding:0; | |
border-width:0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment