Based on https://dribbble.com/shots/3410102-A-bit-of-healthiness
Interested in learning how to animate with RxJS? Here are a couple of resources:
A Pen by Varun Vachhar on CodePen.
<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> |
Based on https://dribbble.com/shots/3410102-A-bit-of-healthiness
Interested in learning how to animate with RxJS? Here are a couple of resources:
A Pen by Varun Vachhar on CodePen.
// 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" /> |