Skip to content

Instantly share code, notes, and snippets.

@chrisgannon
Created March 24, 2016 11:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chrisgannon/7f70c0d602899973acd3 to your computer and use it in GitHub Desktop.
Save chrisgannon/7f70c0d602899973acd3 to your computer and use it in GitHub Desktop.
SVG Bubble Slider
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800px" height="600px" viewBox="0 0 800 600" text-rendering="auto">
<defs>
<circle class="dot" fill="#FF69B4" stroke="none" stroke-width="0" stroke-miterlimit="10" cx="0" cy="300"/>
<g class="textGroup">
<text class='label'>HI</text>
</g>
<filter id="goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 31 -12" result="cm" />
</filter>
<g class="speechBubbleGroup">
<path class="speechBubbleStroke" fill="none" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
M69.361,112.063l16.377,28.99l16.562-28.94c37.277-5.125,65.195-27.13,65.195-53.494C167.496,28.456,130.766,4,85.515,4
C40.275,4,4,28.456,4,58.619c0,26.291,27.361,48.249,65.361,53.448V112.063z"/>
<path class="speechBubbleFill" d="M69.361,109.063l16.377,28.99l16.562-28.94
c37.277-5.125,65.195-27.13,65.195-53.494C167.496,25.456,130.766,1,85.515,1C40.275,1,4,25.456,4,55.619
c0,26.291,27.361,48.249,65.361,53.448V109.063z"/>
<text class="iconLabel" x="85" y="67">WONDERFUL!</text>
</g>
<g class="popLines" fill="none" stroke="#FF69B4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10">
<line x1="107.923" y1="21.37" x2="116.935" y2="4"/>
<line x1="59.335" y1="24.909" x2="50.5" y2="8.057"/>
<line x1="21.789" y1="56.11" x2="4" y2="53.67"/>
<line x1="26.782" y1="98.86" x2="8.057" y2="108.34"/>
<line x1="65.885" y1="125.86" x2="56.43" y2="145.37"/>
<line x1="112.429" y1="121.342" x2="121.03" y2="142.564"/>
<line x1="147.535" y1="93.82" x2="168.155" y2="101.45"/>
<line x1="149.742" y1="49.01" x2="168.155" y2="42.56"/>
</g>
<path id="happy" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M22,8c1.1,0,2,1.3,2,3s-0.9,3-2,3s-2-1.3-2-3
S20.9,8,22,8z M10,8c1.1,0,2,1.3,2,3s-0.9,3-2,3s-2-1.3-2-3S8.9,8,10,8z M16,28c-5.2,0-9.5-4.4-10-9.9c2.9,1.7,6.4,2.7,10,2.7
s7.1-1,10-2.7C25.5,23.6,21.2,28,16,28L16,28z"/>
<path id="evil" d="M32,2c0-1.4-0.3-2.8-0.8-4c-1,2.4-3,4.3-5.5,5.3C23,1.2,19.7,0,16,0S9,1.2,6.3,3.3C3.8,2.3,1.9,0.4,0.8-2
C0.3-0.8,0,0.6,0,2c0,2.3,0.8,4.4,2.1,6.1C0.8,10.4,0,13.1,0,16c0,8.8,7.2,16,16,16s16-7.2,16-16c0-2.9-0.8-5.6-2.1-7.9
C31.2,6.4,32,4.3,32,2z M18,11.9c0.1-1.5,1.4-2.5,2.5-3C21.6,8.3,22.7,8,22.8,8c0.5-0.1,1.1,0.2,1.2,0.7s-0.2,1.1-0.7,1.2
c-0.6,0.1-1.2,0.4-1.8,0.7C21.8,11,22,11.5,22,12c0,1.1-0.9,2-2,2s-2-0.9-2-2C18,12,18,11.9,18,11.9L18,11.9z M8,8.8
C8.2,8.2,8.7,7.9,9.2,8c0,0,1.1,0.3,2.2,0.8c1.1,0.6,2.5,1.6,2.6,3c0,0,0,0.1,0,0.1c0,1.1-0.9,2-2,2s-2-0.9-2-2c0-0.5,0.2-1,0.5-1.4
C10,10.4,9.3,10.1,8.8,10C8.2,9.8,7.9,9.3,8,8.8L8,8.8z M16,26c-3.6,0-6.8-1.9-8.6-4.9l2.6-1.5c1.2,2,3.5,3.4,6,3.4s4.8-1.4,6-3.4
l2.6,1.5C22.8,24.1,19.6,26,16,26z"/>
<path id="cool" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M16,26c-1.5,0-2.9-0.3-4.2-0.9l1-1.7
c1,0.4,2.1,0.7,3.2,0.7c2.9,0,5.5-1.6,6.9-3.9l1.7,1C22.8,24.1,19.6,26,16,26L16,26z M26,12c0,1.1-0.9,2-2,2h-4c-1.1,0-2-0.9-2-2h-4
c0,1.1-0.9,2-2,2H8c-1.1,0-2-0.9-2-2V9c0-0.6,0.4-1,1-1h6c0.6,0,1,0.4,1,1v1h4V9c0-0.6,0.5-1,1-1h6c0.5,0,1,0.4,1,1V12z"/>
<path id="confused" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M22,8c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2
S20.9,8,22,8z M10,8c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S8.9,8,10,8z M21.5,25.3c-2.6,0.9-5.5-0.4-6.4-3
c-0.6-1.6-2.3-2.4-3.8-1.8c-1.4,0.5-2.2,2-1.9,3.5h-2c-0.3-2.3,1-4.5,3.2-5.3c2.6-0.9,5.5,0.4,6.4,3c0.6,1.6,2.3,2.4,3.8,1.8
c1.4-0.5,2.2-2,1.9-3.5h2C25,22.3,23.7,24.5,21.5,25.3z"/>
<path id="sad" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M22,8c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2
S20.9,8,22,8z M10,8c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S8.9,8,10,8z M22,24.4c-1.2-2-3.5-3.4-6-3.4s-4.8,1.4-6,3.4l-2.6-1.5
C9.2,19.9,12.4,18,16,18s6.8,1.9,8.6,4.9L22,24.4z"/>
<path id="shocked" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M10,14c-1.1,0-2-1.3-2-3s0.9-3,2-3s2,1.3,2,3
S11.1,14,10,14z M16,26c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S18.2,26,16,26z M22,14c-1.1,0-2-1.3-2-3s0.9-3,2-3s2,1.3,2,3
S23.1,14,22,14z"/>
<path id="wondering" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M22,8c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2
S20.9,8,22,8z M8,10c0-1.1,0.9-2,2-2s2,0.9,2,2s-0.9,2-2,2S8,11.1,8,10z M10.4,25.2l-0.7-2.4l13.7-4l0.7,2.4L10.4,25.2z"/>
<path id="angry" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M18,11.9c0.1-1.5,1.4-2.5,2.5-3
C21.6,8.3,22.7,8,22.8,8c0.5-0.1,1.1,0.2,1.2,0.7s-0.2,1.1-0.7,1.2c-0.6,0.1-1.2,0.4-1.8,0.7C21.8,11,22,11.5,22,12c0,1.1-0.9,2-2,2
s-2-0.9-2-2C18,12,18,11.9,18,11.9L18,11.9z M8,8.8C8.2,8.2,8.7,7.9,9.2,8c0,0,1.1,0.3,2.2,0.8c1.1,0.6,2.5,1.6,2.6,3
c0,0,0,0.1,0,0.1c0,1.1-0.9,2-2,2s-2-0.9-2-2c0-0.5,0.2-1,0.5-1.4C10,10.4,9.3,10.1,8.8,10C8.2,9.8,7.9,9.3,8,8.8L8,8.8z M22,24.4
c-1.2-2-3.5-3.4-6-3.4s-4.8,1.4-6,3.4l-2.6-1.5C9.2,19.9,12.4,18,16,18s6.8,1.9,8.6,4.9L22,24.4z"/>
<path id="baffled" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M8,13c0-1.7,1.3-3,3-3s3,1.3,3,3s-1.3,3-3,3
S8,14.7,8,13z M20,24h-8v-2h8V24z M21,16c-1.7,0-3-1.3-3-3s1.3-3,3-3s3,1.3,3,3S22.7,16,21,16z M12,13c0,0.6-0.4,1-1,1s-1-0.4-1-1
s0.4-1,1-1S12,12.4,12,13z M22,13c0,0.6-0.4,1-1,1s-1-0.4-1-1s0.4-1,1-1S22,12.4,22,13z"/>
<path id="smile" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M22,8c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2
S20.9,8,22,8z M10,8c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S8.9,8,10,8z M16,26c-3.6,0-6.8-1.9-8.6-4.9l2.6-1.5c1.2,2,3.5,3.4,6,3.4
s4.8-1.4,6-3.4l2.6,1.5C22.8,24.1,19.6,26,16,26z"/>
<path id="sleepy" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M9.7,10.3c-0.4,0.4-1,0.4-1.4,0s-0.4-1,0-1.4
c1.4-1.4,4-1.4,5.4,0c0.4,0.4,0.4,1,0,1.4c-0.2,0.2-0.5,0.3-0.7,0.3s-0.5-0.1-0.7-0.3C11.7,9.7,10.3,9.7,9.7,10.3z M16,26
c-2.2,0-4-2.2-4-5s1.8-5,4-5s4,2.2,4,5S18.2,26,16,26z M23.7,10.3c-0.2,0.2-0.5,0.3-0.7,0.3s-0.5-0.1-0.7-0.3c-0.6-0.6-2-0.6-2.6,0
c-0.4,0.4-1,0.4-1.4,0s-0.4-1,0-1.4c1.4-1.4,4-1.4,5.4,0C24.1,9.3,24.1,9.9,23.7,10.3z"/>
<path id="tongue" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M10,8c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2
S8.9,8,10,8z M24,20h-2v3c0,1.7-1.3,3-3,3s-3-1.3-3-3v-3H8v-2h16V20z M22,12c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S23.1,12,22,12z"/>
<path id="grin" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M22,7.4c2,0,3.6,1.6,3.6,3.6
c0,0.2,0,0.4-0.1,0.6c-0.1,0.3-0.3,0.5-0.6,0.5s-0.6-0.2-0.6-0.5c-0.2-1.1-1.2-1.7-2.3-1.7s-2.1,0.5-2.3,1.7c0,0.3-0.3,0.5-0.6,0.5
l0,0c-0.3,0-0.6-0.2-0.6-0.5c0-0.2-0.1-0.4-0.1-0.6C18.4,9,20,7.4,22,7.4L22,7.4z M10,7.4c2,0,3.6,1.6,3.6,3.6c0,0.2,0,0.4-0.1,0.6
c-0.1,0.3-0.3,0.5-0.6,0.5s-0.6-0.2-0.6-0.5c-0.2-1.1-1.2-1.7-2.3-1.7s-2.1,0.5-2.3,1.7c-0.1,0.3-0.3,0.5-0.6,0.5l0,0
c-0.3,0-0.6-0.2-0.6-0.5c0-0.2-0.1-0.4-0.1-0.6C6.4,9,8,7.4,10,7.4L10,7.4z M6,18h6v7.7C8.6,24.9,6,21.7,6,18z M14,26v-8h4v8H14z
M20,25.7V18h6C26,21.7,23.4,24.9,20,25.7z"/>
<path id="neutral" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M20,24h-8v-2h8V24z M22,8c1.1,0,2,0.9,2,2
s-0.9,2-2,2s-2-0.9-2-2S20.9,8,22,8z M10,8c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S8.9,8,10,8z"/>
</defs>
<g class="dotGroup">
<g class="dotContainer" filter="url(#goo)" >
<rect class="hitArea"/>
</g>
<g class="iconContainer"/>
<g class="bubbleContainer"/>
</g>
<g class="shine" opacity="0">
<!-- <circle fill="rgba(72, 139, 218, 0)" stroke="#C6FF69" stroke-width="0" stroke-miterlimit="10" cx="400" cy="300" r="50"/> -->
<ellipse id="shineM" fill="#FFF" opacity="0.12" stroke="none" stroke-width="0" stroke-miterlimit="10" cx="399" cy="277" rx="20" ry="12"/>
<ellipse id="shineL" fill="#FFF" opacity="0.12" stroke="none" stroke-width="0" stroke-miterlimit="10" cx="420" cy="277" rx="10" ry="5"/>
<ellipse id="shineR" fill="#FFF" opacity="0.12" stroke="none" stroke-width="0" stroke-miterlimit="10" cx="420" cy="277" rx="10" ry="5"/>
</g>
</svg>
</div>
console.clear();
var xmlns = "http://www.w3.org/2000/svg",
xlinkns = "http://www.w3.org/1999/xlink",
select = function(s) {
return document.querySelector(s);
},
selectAll = function(s) {
return document.querySelectorAll(s);
},
container = select('.container'),
dotContainer = select('.dotContainer'),
iconContainer = select('.iconContainer'),
bubbleContainer = select('.bubbleContainer'),
hitArea = select('.hitArea'),
dotGroup = select('.dotGroup'),
spacer = 60,
minDragX,
numItems = 20,
dotSize = 10,
//step, = spacer + (dotSize / 2),
snapArray = [],
multiplier = 4.8,
iconArray = [ 'evil', 'cool', 'confused', 'sad', 'shocked', 'smile', 'wondering', 'happy','angry', 'baffled', 'sleepy', 'tongue', 'grin', 'neutral'],
numItems = iconArray.length,
currentIconId,
oldIconId = -1,
currentSpeechBubble,
dragger,
uiColor = '#FF5EAE',
textColor = '#FF5EAE',
iconColor = '#FFF',
bubbleFillColor = '#FFF'
//center the container cos it's pretty an' that
TweenMax.set(container, {
position: 'absolute',
top: '50%',
left: '50%',
xPercent: -50,
yPercent: -50
})
TweenMax.set('svg', {
visibility: 'visible'
})
//set colour
TweenMax.set(['line'], {
stroke:uiColor
})
TweenMax.set(['.speechBubbleFill'], {
fill:bubbleFillColor
})
TweenMax.set(['.speechBubbleStroke'], {
stroke:uiColor
})
TweenMax.set(['.dot'], {
fill:uiColor
})
TweenMax.set([ '.iconLabel'], {
fill:textColor
})
var mainTl = new TimelineMax({
paused: true
});
function makeMenu() {
var tl;
for (var i = 0; i < numItems; i++) {
var c = select('.dot').cloneNode(true);
//c.data.btnId = i;
//var tg = select('.textGroup').cloneNode(true);
//var t = tg.querySelector('.label'); //.cloneNode(true);
var ic = select('#' +iconArray[i]).cloneNode(true);
ic.setAttribute('class', 'icon');
dotContainer.appendChild(c);
iconContainer.appendChild(ic);
c.setAttributeNS(null, 'btnId', i);
TweenMax.set(c, {
attr: {
cx: (i * spacer),
r: dotSize
}
})
TweenMax.set(ic, {
x: (i * spacer) - 16,
y:300 - 16,
width:0,
height:0,
transformOrigin:'50% 50%',
scale:0,
alpha:0,
fill:iconColor
})
//t.textContent = (i + 1);
snapArray.push((-i * (spacer)));
tl = new TimelineMax({});
tl.to(c, 1, {
attr: {
r: dotSize * multiplier
},
ease: Linear.easeNone
})
.to(ic, 1, {
alpha: 1,
scale: 2,
ease: Linear.easeNone
}, '-=1')
.to(c, 1, {
attr: {
r: dotSize
},
ease: Linear.easeNone
})
.to(ic, 1, {
alpha: 0,
scale: 0,
ease: Linear.easeNone
}, '-=1')
mainTl.add(tl, (i / 2));
}
minDragX = (-(numItems - 1) * spacer);
dragger = Draggable.create(dotContainer, {
type: 'x',
bounds: {
minX: minDragX,
maxX: 0
},
onDrag: dragSlider,
onDragStart: dragStart,
onThrowUpdate: dragSlider,
throwProps: true,
onThrowComplete: throwComplete,
minDuration: 1,
snap: snapArray
})
dragger[0].disable();
TweenMax.set(dotGroup, {
x: 400
})
TweenMax.set(hitArea, {
width: dotContainer.getBBox().width,
height: dotSize * multiplier * 2,
x: dotContainer._gsTransform.x,
y: select('.dot').getAttribute('cy') - ((dotSize * multiplier)),
fill: 'transparent'
})
TweenMax.to([dotContainer, iconContainer], 2, {
x: (snapArray[Math.floor(numItems / 2)]),
onUpdate: dragSlider,
onComplete: function(){
throwComplete();
dragger[0].enable();
},
ease:Elastic.easeOut.config(1, 0.85)
})
createSpeechBubble();
} //end function
function dragSlider() {
var posX = dotContainer._gsTransform.x;
//console.log(posX)
TweenMax.to(mainTl, 0.5, {
//time:((posX/minDragX) * (mainTl.duration()-2))+1,
time: ((posX / minDragX) * (mainTl.duration() - 2)) + 1,
ease: Elastic.easeOut.config(2, 0.75)
})
//mainTl.time(((posX/minDragX) * (mainTl.duration()-2))+1)
TweenMax.set(iconContainer, {
x: posX
})
TweenMax.to( bubbleContainer,1, {
x: posX,
ease:Elastic.easeOut.config(1, 0.5)
})
}
function throwComplete() {
//var segId = Math.round(normalizedRotation / rotationStep)
var landed = Math.ceil(dotContainer._gsTransform.x / spacer);
currentIconId = Math.abs(landed);
//currentIconId = bubbleId;
//console.log(iconArray[currentIconId])
showSpeechBubble();
/*console.log(bubbleId)
TweenMax.to('.shine', 0.2, {
alpha: 0.8
})
TweenMax.set('#shineL', {
alpha: (bubbleId == 0) ? 0 : 0.12
})
TweenMax.set('#shineR', {
alpha: (bubbleId == (numItems - 1)) ? 0 : 0.12
})
*/
}
function createSpeechBubble(){
currentSpeechBubble = select('.speechBubbleGroup').cloneNode(true);
bubbleContainer.appendChild(currentSpeechBubble);
TweenMax.set(currentSpeechBubble, {
y:80,
scale:0,
transformOrigin:'50% 100%'
})
}
function showSpeechBubble(){
currentSpeechBubble.querySelector('text').textContent = iconArray[currentIconId].toUpperCase();
var tl = new TimelineMax();
tl.set(currentSpeechBubble, {
x:currentIconId * spacer - (currentSpeechBubble.getBBox().width/2) - 5,
rotation:(oldIconId < currentIconId) ? 45 : -45
})
.to(currentSpeechBubble, 1, {
rotation:0,
ease:Elastic.easeOut.config(1, 0.6),
scaleX:1
})
.to(currentSpeechBubble, 0.6, {
ease:Elastic.easeOut.config(1, 0.6),
//alpha:1,
scaleY:1
},'-=1')
}
function clearSpeechBubble(){
var pl = select('.popLines').cloneNode(true);
bubbleContainer.appendChild(pl);
var tl = new TimelineMax({onComplete:function(){
if(this.data.lines){
bubbleContainer.removeChild(this.data.lines);
}
}});
tl.data = {lines:pl};
tl.set(pl, {
x:currentSpeechBubble._gsTransform.x,
y:currentSpeechBubble._gsTransform.y,
transformOrigin:'50% 50%',
scale:0.8
})
.to(pl.querySelectorAll('line'), 0.3, {
drawSVG:'100% 100%',
ease:Linear.easeNone
})
.to(pl, 0.3, {
scale:1.4,
ease:Expo.easeOut
},'-=0.3')
TweenMax.set(currentSpeechBubble, {
scale:0
})
}
function dragStart(){
clearSpeechBubble()
}
document.body.onclick = function(e){
//console.log(e.target.className.baseVal)
if(e.target.className.baseVal !== 'dot'){
return;
}
//console.log(e.target.getAttribute('btnId'));
oldIconId = currentIconId;
currentIconId = parseInt(e.target.getAttribute('btnId'));
if(oldIconId == currentIconId){
return
}
clearSpeechBubble();
TweenMax.to([dotContainer, iconContainer], 0.8, {
x: (snapArray[currentIconId]),
onUpdate: dragSlider,
onComplete: throwComplete,
ease:Power1.easeOut
//ease:Back.easeOut
})
}
makeMenu();
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.2/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.2/utils/Draggable.min.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/ThrowPropsPlugin.min.js"></script>
<script src="//s3-us-west-2.amazonaws.com/s.cdpn.io/16327/DrawSVGPlugin.js?r=12"></script>
body {
background-color:#2d2d2d;
overflow: hidden;
/* font-family: 'Fjalla One', sans-serif; */
font-family: 'Passion One', sans-serif;
}
body,
html {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
.container{
position:absolute;
max-width:100%;
}
svg{
max-width:100%;
visibility:hidden;
/* overflow:visible; */
}
.shine, .iconContainer{
pointer-events:none;
}
.dot{
cursor:pointer;
}
.shine circle{
stroke:#FFF;
}
.speechBubbleGroup text{
text-anchor:middle;
font-size:29px;
}
line{
vector-effect:"non-scaling-stroke";
}

SVG Bubble Slider

A dynamic menu I've built that can be used with either icons or text for the slider bubbles.

This one is a fun emoticon rating slider - you can drag and throw or just click to show how you feel.

A Pen by Chris Gannon on CodePen.

License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment