A Pen by Andreas Storm on CodePen.
Created
September 24, 2018 13:04
-
-
Save gitCommitLit/ecbcacee16393f010d5e59166d2701f7 to your computer and use it in GitHub Desktop.
Liquid Button
This file contains 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
<!-- For more examples: https://codepen.io/Zaku/pen/eRmRxz --> | |
<svg class="liquid-button" | |
data-text="BUTTON" | |
data-force-factor="0.1" | |
data-layer-1-viscosity="0.5" | |
data-layer-2-viscosity="0.4" | |
data-layer-1-mouse-force="400" | |
data-layer-2-mouse-force="500" | |
data-layer-1-force-limit="1" | |
data-layer-2-force-limit="2" | |
data-color1="#4DE7BF" | |
data-color2="#0A3FC7" | |
data-color3="#2A62F4"></svg> |
This file contains 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
const LiquidButton = class LiquidButton { | |
constructor(svg) { | |
const options = svg.dataset; | |
this.id = this.constructor.id || (this.constructor.id = 1); | |
this.constructor.id++; | |
this.xmlns = 'http://www.w3.org/2000/svg'; | |
this.tension = options.tension * 1 || 0.4; | |
this.width = options.width * 1 || 200; | |
this.height = options.height * 1 || 50; | |
this.margin = options.margin || 40; | |
this.hoverFactor = options.hoverFactor || -0.1; | |
this.gap = options.gap || 5; | |
this.debug = options.debug || false; | |
this.forceFactor = options.forceFactor || 0.2; | |
this.color1 = options.color1 || '#36DFE7'; | |
this.color2 = options.color2 || '#8F17E1'; | |
this.color3 = options.color3 || '#BF09E6'; | |
this.textColor = options.textColor || '#FFFFFF'; | |
this.text = options.text || 'Button'; | |
this.svg = svg; | |
this.layers = [{ | |
points: [], | |
viscosity: 0.5, | |
mouseForce: 100, | |
forceLimit: 2, | |
},{ | |
points: [], | |
viscosity: 0.8, | |
mouseForce: 150, | |
forceLimit: 3, | |
}]; | |
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex++) { | |
const layer = this.layers[layerIndex]; | |
layer.viscosity = options['layer-' + (layerIndex + 1) + 'Viscosity'] * 1 || layer.viscosity; | |
layer.mouseForce = options['layer-' + (layerIndex + 1) + 'MouseForce'] * 1 || layer.mouseForce; | |
layer.forceLimit = options['layer-' + (layerIndex + 1) + 'ForceLimit'] * 1 || layer.forceLimit; | |
layer.path = document.createElementNS(this.xmlns, 'path'); | |
this.svg.appendChild(layer.path); | |
} | |
this.wrapperElement = options.wrapperElement || document.body; | |
if (!this.svg.parentElement) { | |
this.wrapperElement.append(this.svg); | |
} | |
this.svgText = document.createElementNS(this.xmlns, 'text'); | |
this.svgText.setAttribute('x', '50%'); | |
this.svgText.setAttribute('y', '50%'); | |
this.svgText.setAttribute('dy', ~~(this.height / 8) + 'px'); | |
this.svgText.setAttribute('font-size', ~~(this.height / 3)); | |
this.svgText.style.fontFamily = 'sans-serif'; | |
this.svgText.setAttribute('text-anchor', 'middle'); | |
this.svgText.setAttribute('pointer-events', 'none'); | |
this.svg.appendChild(this.svgText); | |
this.svgDefs = document.createElementNS(this.xmlns, 'defs') | |
this.svg.appendChild(this.svgDefs); | |
this.touches = []; | |
this.noise = options.noise || 0; | |
document.body.addEventListener('touchstart', this.touchHandler); | |
document.body.addEventListener('touchmove', this.touchHandler); | |
document.body.addEventListener('touchend', this.clearHandler); | |
document.body.addEventListener('touchcancel', this.clearHandler); | |
this.svg.addEventListener('mousemove', this.mouseHandler); | |
this.svg.addEventListener('mouseout', this.clearHandler); | |
this.initOrigins(); | |
this.animate(); | |
} | |
get mouseHandler() { | |
return (e) => { | |
this.touches = [{ | |
x: e.offsetX, | |
y: e.offsetY, | |
force: 1, | |
}]; | |
}; | |
} | |
get touchHandler() { | |
return (e) => { | |
this.touches = []; | |
const rect = this.svg.getBoundingClientRect(); | |
for (let touchIndex = 0; touchIndex < e.changedTouches.length; touchIndex++) { | |
const touch = e.changedTouches[touchIndex]; | |
const x = touch.pageX - rect.left; | |
const y = touch.pageY - rect.top; | |
if (x > 0 && y > 0 && x < this.svgWidth && y < this.svgHeight) { | |
this.touches.push({x, y, force: touch.force || 1}); | |
} | |
} | |
e.preventDefault(); | |
}; | |
} | |
get clearHandler() { | |
return (e) => { | |
this.touches = []; | |
}; | |
} | |
get raf() { | |
return this.__raf || (this.__raf = ( | |
window.requestAnimationFrame || | |
window.webkitRequestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
function(callback){ setTimeout(callback, 10)} | |
).bind(window)); | |
} | |
distance(p1, p2) { | |
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); | |
} | |
update() { | |
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex++) { | |
const layer = this.layers[layerIndex]; | |
const points = layer.points; | |
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) { | |
const point = points[pointIndex]; | |
const dx = point.ox - point.x + (Math.random() - 0.5) * this.noise; | |
const dy = point.oy - point.y + (Math.random() - 0.5) * this.noise; | |
const d = Math.sqrt(dx * dx + dy * dy); | |
const f = d * this.forceFactor; | |
point.vx += f * ((dx / d) || 0); | |
point.vy += f * ((dy / d) || 0); | |
for (let touchIndex = 0; touchIndex < this.touches.length; touchIndex++) { | |
const touch = this.touches[touchIndex]; | |
let mouseForce = layer.mouseForce; | |
if ( | |
touch.x > this.margin && | |
touch.x < this.margin + this.width && | |
touch.y > this.margin && | |
touch.y < this.margin + this.height | |
) { | |
mouseForce *= -this.hoverFactor; | |
} | |
const mx = point.x - touch.x; | |
const my = point.y - touch.y; | |
const md = Math.sqrt(mx * mx + my * my); | |
const mf = Math.max(-layer.forceLimit, Math.min(layer.forceLimit, (mouseForce * touch.force) / md)); | |
point.vx += mf * ((mx / md) || 0); | |
point.vy += mf * ((my / md) || 0); | |
} | |
point.vx *= layer.viscosity; | |
point.vy *= layer.viscosity; | |
point.x += point.vx; | |
point.y += point.vy; | |
} | |
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) { | |
const prev = points[(pointIndex + points.length - 1) % points.length]; | |
const point = points[pointIndex]; | |
const next = points[(pointIndex + points.length + 1) % points.length]; | |
const dPrev = this.distance(point, prev); | |
const dNext = this.distance(point, next); | |
const line = { | |
x: next.x - prev.x, | |
y: next.y - prev.y, | |
}; | |
const dLine = Math.sqrt(line.x * line.x + line.y * line.y); | |
point.cPrev = { | |
x: point.x - (line.x / dLine) * dPrev * this.tension, | |
y: point.y - (line.y / dLine) * dPrev * this.tension, | |
}; | |
point.cNext = { | |
x: point.x + (line.x / dLine) * dNext * this.tension, | |
y: point.y + (line.y / dLine) * dNext * this.tension, | |
}; | |
} | |
} | |
} | |
animate() { | |
this.raf(() => { | |
this.update(); | |
this.draw(); | |
this.animate(); | |
}); | |
} | |
get svgWidth() { | |
return this.width + this.margin * 2; | |
} | |
get svgHeight() { | |
return this.height + this.margin * 2; | |
} | |
draw() { | |
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex++) { | |
const layer = this.layers[layerIndex]; | |
if (layerIndex === 1) { | |
if (this.touches.length > 0) { | |
while (this.svgDefs.firstChild) { | |
this.svgDefs.removeChild(this.svgDefs.firstChild); | |
} | |
for (let touchIndex = 0; touchIndex < this.touches.length; touchIndex++) { | |
const touch = this.touches[touchIndex]; | |
const gradient = document.createElementNS(this.xmlns, 'radialGradient'); | |
gradient.id = 'liquid-gradient-' + this.id + '-' + touchIndex; | |
const start = document.createElementNS(this.xmlns, 'stop'); | |
start.setAttribute('stop-color', this.color3); | |
start.setAttribute('offset', '0%'); | |
const stop = document.createElementNS(this.xmlns, 'stop'); | |
stop.setAttribute('stop-color', this.color2); | |
stop.setAttribute('offset', '100%'); | |
gradient.appendChild(start); | |
gradient.appendChild(stop); | |
this.svgDefs.appendChild(gradient); | |
gradient.setAttribute('cx', touch.x / this.svgWidth); | |
gradient.setAttribute('cy', touch.y / this.svgHeight); | |
gradient.setAttribute('r', touch.force); | |
layer.path.style.fill = 'url(#' + gradient.id + ')'; | |
} | |
} else { | |
layer.path.style.fill = this.color2; | |
} | |
} else { | |
layer.path.style.fill = this.color1; | |
} | |
const points = layer.points; | |
const commands = []; | |
commands.push('M', points[0].x, points[0].y); | |
for (let pointIndex = 1; pointIndex < points.length; pointIndex += 1) { | |
commands.push('C', | |
points[(pointIndex + 0) % points.length].cNext.x, | |
points[(pointIndex + 0) % points.length].cNext.y, | |
points[(pointIndex + 1) % points.length].cPrev.x, | |
points[(pointIndex + 1) % points.length].cPrev.y, | |
points[(pointIndex + 1) % points.length].x, | |
points[(pointIndex + 1) % points.length].y | |
); | |
} | |
commands.push('Z'); | |
layer.path.setAttribute('d', commands.join(' ')); | |
} | |
this.svgText.textContent = this.text; | |
this.svgText.style.fill = this.textColor; | |
} | |
createPoint(x, y) { | |
return { | |
x: x, | |
y: y, | |
ox: x, | |
oy: y, | |
vx: 0, | |
vy: 0, | |
}; | |
} | |
initOrigins() { | |
this.svg.setAttribute('width', this.svgWidth); | |
this.svg.setAttribute('height', this.svgHeight); | |
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex++) { | |
const layer = this.layers[layerIndex]; | |
const points = []; | |
for (let x = ~~(this.height / 2); x < this.width - ~~(this.height / 2); x += this.gap) { | |
points.push(this.createPoint( | |
x + this.margin, | |
this.margin | |
)); | |
} | |
for (let alpha = ~~(this.height * 1.25); alpha >= 0; alpha -= this.gap) { | |
const angle = (Math.PI / ~~(this.height * 1.25)) * alpha; | |
points.push(this.createPoint( | |
Math.sin(angle) * this.height / 2 + this.margin + this.width - this.height / 2, | |
Math.cos(angle) * this.height / 2 + this.margin + this.height / 2 | |
)); | |
} | |
for (let x = this.width - ~~(this.height / 2) - 1; x >= ~~(this.height / 2); x -= this.gap) { | |
points.push(this.createPoint( | |
x + this.margin, | |
this.margin + this.height | |
)); | |
} | |
for (let alpha = 0; alpha <= ~~(this.height * 1.25); alpha += this.gap) { | |
const angle = (Math.PI / ~~(this.height * 1.25)) * alpha; | |
points.push(this.createPoint( | |
(this.height - Math.sin(angle) * this.height / 2) + this.margin - this.height / 2, | |
Math.cos(angle) * this.height / 2 + this.margin + this.height / 2 | |
)); | |
} | |
layer.points = points; | |
} | |
} | |
} | |
const redraw = () => { | |
button.initOrigins(); | |
}; | |
const buttons = document.getElementsByClassName('liquid-button'); | |
for (let buttonIndex = 0; buttonIndex < buttons.length; buttonIndex++) { | |
const button = buttons[buttonIndex]; | |
button.liquidButton = new LiquidButton(button); | |
} |
This file contains 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
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.4/dat.gui.min.js"></script> |
This file contains 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, html | |
padding: 0 | |
margin: 0 | |
width: 100% | |
height: 100% | |
user-select: none | |
display: grid | |
.liquid-button | |
margin: auto | |
path | |
cursor: pointer |
This file contains 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
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment