Skip to content

Instantly share code, notes, and snippets.

@iosonosempreio
Created June 9, 2020 18:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iosonosempreio/86b406409beca9a696efd778b25b234b to your computer and use it in GitHub Desktop.
Save iosonosempreio/86b406409beca9a696efd778b25b234b to your computer and use it in GitHub Desktop.
Metaball interpolation

Interpolate path

<html>
<header>
<title>Metaballs PIXIjs</title>
<style>
html, body {
margin: 0;
font-family: sans-serif;
}
</style>
</header>
<body>
<svg width="960" height="500">
<g transform="translate(480,250)">
<!-- elements will be inserted here -->
</g>
</svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
// some sample data
let data=[
{x:40, y:0, r:10},
{x:20, y:40, r:15},
{x:-20, y:40, r:20},
{x:-40, y:0, r:25},
{x:-20, y:-40, r:6},
{x:20, y:-40, r:12}
]
data.forEach(d=>d.c=[d.x,d.y]);
let d='';
for (let i=0; i<data.length; i++) {
const circles = data.slice(0,3);
data.push(data.shift());
// calculate points and handles for metaball path
const pointsAndHandles=curvesBetweenCircles(circles[0].r, circles[1].r, circles[0].c, circles[1].c);
// calculate for the next one, to draw arch (this could be optimized)
const next=curvesBetweenCircles(circles[1].r, circles[2].r, circles[1].c, circles[2].c);
// set the starting point of the path ONLY in case it is the first circle
if (i===0) d+=`M ${pointsAndHandles.p[1][0]},${pointsAndHandles.p[1][1]} `;
// draw bezier curve and arc
d+=`C ${pointsAndHandles.h[1][0]},${pointsAndHandles.h[1][1]} ${pointsAndHandles.h[3][0]},${pointsAndHandles.h[3][1]} ${pointsAndHandles.p[3][0]},${pointsAndHandles.p[3][1]}`
d+=`A ${next.r1}, ${next.r1}, 1, 0, 1, ${next.p[1][0]}, ${next.p[1][1]}`
}
// close the path
d+='Z'
// draw the path
d3.select('svg > g')
.append('path')
.attr('d',d)
.attr('fill','#CDDC39')
.attr('stroke','#7CB342');
// draw the circles and numbers for visual feedback
data.forEach((c,i)=>{
d3.select('svg > g')
.append('circle')
.attr('r',c.r)
.attr('cx',c.x)
.attr('cy',c.y)
.attr('fill','rgba(255,255,255,0.4)');
d3.select('svg > g')
.append('text')
.attr('x',c.x)
.attr('y',c.y+3)
.attr('text-anchor','middle')
.attr('font-size','9')
.text(i)
})
/**
* Based on Metaball script by SATO Hiroyuki
* http://shspage.com/aijs/en/#metaball
*/
function curvesBetweenCircles(radius1, radius2, center1, center2, handleSize = 2.4, v = 0.5) {
const HALF_PI = Math.PI / 2;
const d = dist(center1, center2);
let u1 = 0, u2 = 0;
if (radius1 === 0 || radius2 === 0 || d <= Math.abs(radius1 - radius2)) {
return null;
}
if (d < radius1 + radius2) {
u1 = Math.acos(
(radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d),
);
u2 = Math.acos(
(radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d),
);
}
// All the angles
const angleBetweenCenters = angle(center2, center1);
const maxSpread = Math.acos((radius1 - radius2) / d);
const angle1 = angleBetweenCenters + u1 + (maxSpread - u1) * v;
const angle2 = angleBetweenCenters - u1 - (maxSpread - u1) * v;
const angle3 = angleBetweenCenters + Math.PI - u2 - (Math.PI - u2 - maxSpread) * v;
const angle4 = angleBetweenCenters - Math.PI + u2 + (Math.PI - u2 - maxSpread) * v;
// Points
const p1 = getVector(center1, angle1, radius1);
const p2 = getVector(center1, angle2, radius1);
const p3 = getVector(center2, angle3, radius2);
const p4 = getVector(center2, angle4, radius2);
// Define handle length by the
// distance between both ends of the curve
const totalRadius = radius1 + radius2;
const d2Base = Math.min(v * handleSize, dist(p1, p3) / totalRadius);
// Take into account when circles are overlapping
const d2 = d2Base * Math.min(1, d * 2 / (radius1 + radius2));
const r1 = radius1 * d2;
const r2 = radius2 * d2;
const h1 = getVector(p1, angle1 - HALF_PI, r1);
const h2 = getVector(p2, angle2 + HALF_PI, r1);
const h3 = getVector(p3, angle3 + HALF_PI, r2);
const h4 = getVector(p4, angle4 - HALF_PI, r2);
return {
p : [ p1, p2, p3, p4 ],
h : [ h1, h2, h3, h4 ],
escaped : d > radius1,
r : radius2,
r1 : radius1
}
}
/**
* Utils
*/
function moveTo([x, y] = [0, 0], element) {
element.setAttribute('cx', x);
element.setAttribute('cy', y);
}
function line([x1, y1] = [0, 0], [x2, y2] = [0, 0], element) {
element.setAttribute('x1', x1);
element.setAttribute('y1', y1);
element.setAttribute('x2', x2);
element.setAttribute('y2', y2);
}
function dist([x1, y1], [x2, y2]) {
return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5;
}
function angle([x1, y1], [x2, y2]) {
return Math.atan2(y1 - y2, x1 - x2);
}
function getVector([cx, cy], a, r) {
return [cx + r * Math.cos(a), cy + r * Math.sin(a)];
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment