This was created to give me an excuse at attempting to create a video tutorial, just to see what it's like.
A Pen by Mariusz Dabrowski on CodePen.
<button> | |
<svg viewBox="0 0 242 109" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |
<g class="ears"> | |
<g class="ear-left"> | |
<ellipse class="ear-left-outer" transform="matrix(0.9391 -0.3436 0.3436 0.9391 -3.6062 17.8444)" cx="48.5" cy="19.1" rx="11.4" ry="13.8"/> | |
<ellipse class="ear-left-inner" transform="matrix(0.9391 -0.3436 0.3436 0.9391 -3.8876 17.4659)" cx="47.3" cy="19.7" rx="7.3" ry="11.2"/> | |
</g> | |
<g class="ear-right"> | |
<ellipse class="ear-right-outer" transform="matrix(0.3436 -0.9391 0.9391 0.3436 106.5379 189.869)" cx="189.1" cy="18.7" rx="14.4" ry="11.9"/> | |
<ellipse class="ear-right-inner" transform="matrix(0.3436 -0.9391 0.9391 0.3436 106.8522 191.5127)" cx="190.4" cy="19.3" rx="11.7" ry="7.7"/> | |
</g> | |
</g> | |
<g class="eyes"> | |
<defs> | |
<clipPath id="eyeRightClip"> | |
<path d="M175,25c0-11-7.8-20-17.5-20S140,14,140,25c0,0.7,0,1.3,0.1,2h34.8 C175,26.3,175,25.7,175,25z"/> | |
</clipPath> | |
</defs> | |
<g class="eye-right"> | |
<path class="eye-right-outer" d="M174.9,27H186c0-0.3,0-0.7,0-1c0-14.4-11.6-26-26-26c-14.4,0-26,11.6-26,26 c0,0.3,0,0.7,0,1h6.1H174.9z"/> | |
<path class="eye-right-inner" d="M175,25c0-11-7.8-20-17.5-20S140,14,140,25c0,0.7,0,1.3,0.1,2h34.8 C175,26.3,175,25.7,175,25z"/> | |
<g clip-path="url(#eyeRightClip)"> | |
<circle class="eye-right-pupil" cx="158" cy="18" r="5"/> | |
</g> | |
</g> | |
<defs> | |
<clipPath id="eyeLeftClip"> | |
<path d="M97,25c0-11-7.8-20-17.5-20S62,14,62,25c0,0.7,0,1.3,0.1,2h34.8C97,26.3,97,25.7,97,25z" /> | |
</clipPath> | |
</defs> | |
<g class="eye-left"> | |
<path class="eye-left-outer" d="M96.9,27h6.1c0-0.3,0-0.7,0-1c0-14.4-11.6-26-26-26C62.6,0,51,11.6,51,26 c0,0.3,0,0.7,0,1h11.1H96.9z"/> | |
<path class="eye-left-inner" d="M97,25c0-11-7.8-20-17.5-20S62,14,62,25c0,0.7,0,1.3,0.1,2h34.8C97,26.3,97,25.7,97,25z" /> | |
<g clip-path="url(#eyeLeftClip)"> | |
<circle class="eye-left-pupil" cx="80" cy="17.7" r="5"/> | |
</g> | |
</g> | |
</g> | |
<g class="nostrils"> | |
<g class="nostril-right"> | |
<ellipse class="nostril-right-outer" cx="130.5" cy="27.5" rx="6.5" ry="5.5"/> | |
<circle class="nostril-right-inner" cx="130" cy="28" r="4"/> | |
</g> | |
<g class="nostril-left"> | |
<ellipse class="nostril-left-outer" cx="106.5" cy="27.5" rx="6.5" ry="5.5"/> | |
<circle class="nostril-left-inner" cx="107" cy="28" r="4"/> | |
</g> | |
</g> | |
<path class="body" d="M218,98H24C10.8,98,0,87.2,0,74V51c0-13.2,10.8-24,24-24h194c13.2,0,24,10.8,24,24v23 C242,87.2,231.2,98,218,98z"/> | |
<g class="freckles"> | |
<circle class="freckle" cx="13.7" cy="41.4" r="1.6"/> | |
<circle class="freckle" cx="20.1" cy="44.7" r="1.6"/> | |
<circle class="freckle" cx="19.6" cy="37.8" r="1.6"/> | |
</g> | |
<defs> | |
<clipPath id="mouthClip"> | |
<path d="M218,98H24C10.8,98,0,87.2,0,74V51c0-13.2,10.8-24,24-24h194c13.2,0,24,10.8,24,24v23 C242,87.2,231.2,98,218,98z"/> | |
</clipPath> | |
</defs> | |
<g class="mouth" clip-path="url(#mouthClip)"> | |
<g class="mouth-pieces"> | |
<path class="mouth-back" d="M23.6,168.2l-3-56.1c0-7.8,6.4-14.1,14.1-14.1h172.4c7.8,0,14.1,6.4,14.1,14.1l-3,56.1"/> | |
<path class="tongue" d="M174.9,168.2c-7.3-5-24.5-9.9-54.8-9.9s-48,5.1-54.8,9.9"/> | |
</g> | |
</g> | |
<g class="teeth"> | |
<path class="tooth-left" d="M115,97.9v7.5c0,2-1.7,3.6-3.6,3.6H89.7c-2,0-3.6-1.7-3.6-3.6v-7.5H115z"/> | |
<path class="tooth-right" d="M154,97.9v7.5c0,2-1.7,3.6-3.6,3.6h-21.7c-2,0-3.6-1.7-3.6-3.6v-7.5H154z"/> | |
</g> | |
</svg> | |
</button> | |
<a target="_blank" href="https://codepen.io/MarioD/post/c76a53d9652de6ec9bf1217c1bea47e4/interactive-hippo-button-tutorial" class="link">Read / watch the tutorial</a> |
// -------------- | |
// Hover animaton | |
// -------------- | |
const mouthSpeed = 0.3; | |
const easeType = Power2.easeOut; | |
const mouthOpen = gsap.timeline({ paused: true }); | |
mouthOpen.to('.mouth-back', {duration: mouthSpeed, ease: easeType, y: -70}, 0); | |
mouthOpen.to('.tongue', {duration: mouthSpeed * 1.5, ease: easeType, y: -70}, 0); | |
mouthOpen.to('.teeth', {duration: mouthSpeed, ease: easeType, y: -70, scaleY: 1.2}, 0); | |
mouthOpen.to('.body', {duration: mouthSpeed, ease: easeType, scaleY: 1.06, transformOrigin: 'center bottom'}, 0); | |
mouthOpen.to('.freckles', {duration: mouthSpeed, ease: easeType, y: -10}, 0); | |
mouthOpen.to('.ears', {duration: mouthSpeed, ease: easeType, y: 6}, 0); | |
mouthOpen.to('.eye-right', {duration: mouthSpeed, ease: easeType, x: -2}, 0); | |
mouthOpen.to('.eye-left', {duration: mouthSpeed, ease: easeType, x: 2}, 0); | |
mouthOpen.to('.eyes', {duration: mouthSpeed, ease: easeType, y: 2}, 0); | |
mouthOpen.to('.nostrils', {duration: mouthSpeed, ease: easeType, y: -6}, 0); | |
// ------------ | |
// Mouse events | |
// ------------ | |
const button = document.querySelector('button'); | |
button.addEventListener('mouseenter', enterButton); | |
button.addEventListener('mouseleave', leaveButton); | |
function enterButton() { mouthOpen.play(); } | |
function leaveButton() { mouthOpen.reverse(); } | |
// ---------- | |
// Ear wiggle | |
// ---------- | |
const earWiggle = gsap.timeline({ paused: true, repeat: 2 }); | |
earWiggle.set('.ear-right', { transformOrigin: "center center" }); | |
earWiggle.to('.ear-right', { duration: 0.1, rotation: 45 }); | |
earWiggle.to('.ear-right', { duration: 0.1, rotation: 0 }); | |
window.setInterval(earWigglePlay, 2500); | |
function earWigglePlay() { earWiggle.play(0); } | |
// ------------ | |
// Eye tracking | |
// ------------ | |
const eyeRightPupil = document.querySelector('.eye-right-pupil'); | |
const eyeLeftPupil = document.querySelector('.eye-left-pupil'); | |
const eyeLeftInner = document.querySelector('.eye-left-inner'); | |
const innerEyeWidth = eyeLeftInner.getBoundingClientRect().width; | |
const innerEyeHeight = eyeLeftInner.getBoundingClientRect().height; | |
const pupilWidth = eyeLeftPupil.getBoundingClientRect().width; | |
const pupilHeight = eyeLeftPupil.getBoundingClientRect().height; | |
const xMovement = (innerEyeWidth - pupilWidth)/2; | |
const yMovement = (innerEyeHeight - pupilHeight)/2; | |
window.addEventListener('mousemove', updateEyePosition); | |
function updateEyePosition(event) { | |
const posX = ((event.clientX / document.body.clientWidth) * 2 - 1) * xMovement; | |
const posY = ((event.clientY / document.body.clientHeight) * 2 - 1) * yMovement; | |
eyeLeftPupil.style.transform = `translate(${posX}px, ${posY}px)`; | |
eyeRightPupil.style.transform = `translate(${posX}px, ${posY}px)`; | |
} | |
// Last minute link to the tutorial | |
button.addEventListener('click', () => window.open('https://codepen.io/MarioD/post/interactive-hippo-button-tutorial')); |
<script src="https://cdn.jsdelivr.net/npm/gsap@3.0.1/dist/gsap.min.js"></script> |
html, | |
body { | |
height: 100%; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
} | |
body { | |
margin: 0; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
background: #1e313f; | |
} | |
button { | |
width: 242px; | |
border: 0; | |
padding: 0; | |
background: transparent; | |
cursor: pointer; | |
} | |
.ear-left-outer, .ear-right-outer {fill:#919191;} | |
.ear-left-inner, .ear-right-inner {fill:#6D6D6D;} | |
.eye-right-outer, .eye-left-outer, .nostril-right-outer, .nostril-left-outer, .body {fill:#AAAAAA;} | |
.eye-right-inner, .eye-left-inner {fill:#FFFFFF;} | |
.nostril-right-inner, .nostril-left-inner{fill:#8C8C8C;} | |
.freckle {fill:#7C7C7C;} | |
.tongue {fill:#FF4848;} | |
.tooth-left, .tooth-right {fill:#FFFFE1;} | |
.link { | |
font-family: 'Roboto', sans-serif; | |
font-weight: 400; | |
font-size: 13px; | |
color: #ffffe3; | |
text-decoration: none; | |
position: absolute; | |
bottom: 20px; | |
right: 20px; | |
border: 2px solid #335067; | |
padding: 10px 14px; | |
border-radius: 4px; | |
transition: background 0.2s; | |
} | |
.link:hover { | |
background: #243a4a; | |
} |
<link href="https://fonts.googleapis.com/css?family=Roboto:400" rel="stylesheet" /> |