Skip to content

Instantly share code, notes, and snippets.

@sttt
Created March 24, 2018 10:57
Show Gist options
  • Save sttt/f9db4906eb3f69ec9a9acd69c8d6d815 to your computer and use it in GitHub Desktop.
Save sttt/f9db4906eb3f69ec9a9acd69c8d6d815 to your computer and use it in GitHub Desktop.
Meatballs
<main class="overflow-hidden vh-100 flex items-stretch overflow-hidden">
<svg class="w-100 h-100" viewBox="0 0 1200 1200" preserveAspectRatio="xMidYMid slice">
<defs>
<pattern id="meatball" patternUnits="userSpaceOnUse" width="400" height="400">
<image xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/149125/meatball.jpg" x="0" y="0" width="766" height="748" />
</pattern>
</defs>
<g stroke-width="6" fill="url(#meatball)">
<circle id="js-circle1" cx="400" cy="600" r="96" />
<circle id="js-circle2" cx="400" cy="600" r="64" stroke="none" />
<path id="js-connector" d="" />
</g>
</svg>
</main>
// Dom Nodes
const circle1 = document.querySelector('#js-circle1');
const circle2 = document.querySelector('#js-circle2');
const connector = document.querySelector('#js-connector');
const VIEWBOX_SIZE = { W: 1200, H: 1200 };
const SIZES = {
CIRCLE1: 96,
CIRCLE2: 64,
};
const circle1$ = Rx.Observable.of([600, 600])
.do(loc => { moveTo(loc, circle1); });
const circle2$ = Rx.Observable.interval(0, Rx.Scheduler.animationFrame)
.map(frame => 250 * Math.sin(frame / 200))
.map(x => [600 + x, 600])
.do(loc => { moveTo(loc, circle2); });
Rx.Observable
.combineLatest(circle1$, circle2$, (circle1Loc, circle2Loc) =>
metaball(SIZES.CIRCLE1, SIZES.CIRCLE2, circle1Loc, circle2Loc),
)
.subscribe(path => {
connector.setAttribute('d', path);
});
/**
* Based on Metaball script by SATO Hiroyuki
* http://shspage.com/aijs/en/#metaball
*/
function metaball(radius1, radius2, center1, center2, handleSize = 2.4, v = 0.5) {
const HALF_PI = Math.PI / 2;
const d = dist(center1, center2);
const maxDist = radius1 + radius2 * 2.5;
let u1, u2;
if (radius1 === 0 || radius2 === 0 || d > maxDist || d <= Math.abs(radius1 - radius2)) {
return '';
}
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),
);
} else {
u1 = 0;
u2 = 0;
}
// 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 metaballToPath(
p1, p2, p3, p4,
h1, h2, h3, h4,
d > radius1,
radius2,
);
}
function metaballToPath(p1, p2, p3, p4, h1, h2, h3, h4, escaped, r) {
return [
'M', p1,
'C', h1, h3, p3,
'A', r, r, 0, escaped ? 1 : 0, 0, p4,
'C', h4, h2, p2,
].join(' ');
}
/**
* 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 src="https://unpkg.com/@reactivex/rxjs@5.4.3/dist/global/Rx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/tachyons/4.6.2/tachyons.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment