T-Shirt cannon button with GreenSock.
Not sure why, but the tip of the cannon gets cut off in Chrome sometimes which isn't ideal.
Enjoy!
- const POSITIONS = ['middle', 'left', 'right', 'bottom'] | |
mixin shirt(posIndex) | |
svg.t-shirt(class=`t-shirt--${POSITIONS[posIndex]}` xmlns='http://www.w3.org/2000/svg' width='245' height='230' viewbox='0 0 64.8 60.9') | |
defs | |
if (posIndex === 0) | |
clipPath#clipMain | |
rect(width="65" height="61") | |
if (posIndex === 1) | |
clipPath#clipLeft | |
rect(width="22.5" height="61") | |
if (posIndex === 2) | |
clipPath#clipRight | |
rect(x="42.3" width="22.5" height="61") | |
g.t-shirt__shirt(stroke='#000') | |
if (posIndex === 1 || posIndex === 2) | |
g.t-shirt__arm(class=`t-shirt__arm--${posIndex === 1 ? 'left' : 'right'}` clip-path=`url(#clip${posIndex === 1 ? 'Left' : 'Right'})`) | |
path(d='M251.8 109.2a36 17.5 0 01-34 11.6 36 17.5 0 01-33.9-11.6l-31.5 4.8-50 50 37 36.8 13-13v142.7h130.9V187.7l13.1 13.1 36.9-36.8-50-50z' transform='matrix(.26468 0 0 .2626 -25.2 -27.2)' stroke-width='5' stroke-linecap='square') | |
if (posIndex === 0) | |
g.t-shirt__middle(clip-path="url(#clipMain)") | |
path(d='M90.5 151.3a9.5 4.6 0 01-9 3 9.5 4.6 0 01-9-3l-2.3.4v58.2h22.7v-58.2z' stroke-width='1.3' stroke-linecap='square' transform='matrix(1.00036 0 0 .99247 -49.2 -148.7)') | |
if (posIndex === 3) | |
g.t-shirt__fold | |
path(stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round' d='M70.2 197.8h22.7v12H70.2z' transform='matrix(1.00036 0 0 .99247 -49.2 -148.7)') | |
mixin cannon(posIndex) | |
svg(class=`${posIndex === 0 ? 'cannon__shirt' : 'cannon'}` xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewbox='0 0 16.7 87.1') | |
if (posIndex === 0) | |
g | |
path(stroke='#000' stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round' d='M55.1 223.9h22.7v12H55.1z' transform='matrix(0 -1.00036 .99247 0 -219.8 98)') | |
if (posIndex === 1) | |
g(transform='matrix(0 -1.00036 .99247 0 -219.8 98)') | |
path.cannon__plastic(stroke='#000' stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round' d='M11.6 222.1h85.7v15.5H11.6z') | |
rect.cannon__shine(width='20.4' height='1.9' x='63.2' y='223.7' ry='1') | |
g(stroke='#000' stroke-linecap='round' stroke-linejoin='round') | |
path.cannon__band(transform='matrix(-.26547 0 0 -.24756 81.3 272.7)' d='M-59.7 143v60.6h25.3v-60.7z' stroke-width='6.3') | |
button | |
.button | |
.t-shirt__cannon.button__cannon | |
.t-shirt__cannon-content | |
+cannon(0) | |
+cannon(1) | |
.t-shirt__container | |
.t-shirt__wrapper.button__shirt | |
//- Middle | |
+shirt(0) | |
//- Left arm | |
+shirt(1) | |
//- Right arm | |
+shirt(2) | |
//- Bottom | |
+shirt(3) | |
.button__text | |
.dummy Ordered | |
.text.text--order(data-splitting='') Order | |
.text.text--ordered(data-splitting='') Ordered | |
const { | |
Splitting, | |
gsap: { timeline, set }, | |
} = window | |
const CLIP = new Audio( | |
'https://s3-us-west-2.amazonaws.com/s.cdpn.io/605876/t-shirt-cannon-pop.mp3' | |
) | |
// Split the order letter | |
Splitting() | |
const SHIRT_SEGMENTS = [...document.querySelectorAll('.t-shirt')] | |
const SHIRT = document.querySelector('.t-shirt__wrapper') | |
const LEFT_ARM = SHIRT_SEGMENTS[1] | |
const RIGHT_ARM = SHIRT_SEGMENTS[2] | |
const FOLD = SHIRT_SEGMENTS[3].querySelector('.t-shirt__fold') | |
const CLIPS = [...document.querySelectorAll('clipPath rect')] | |
const BUTTON = document.querySelector('button') | |
document.documentElement.style.setProperty('--hue', Math.random() * 360) | |
set(FOLD, { transformOrigin: '50% 100%', scaleY: 0 }) | |
set(CLIPS, { transformOrigin: '50% 0' }) | |
set('.cannon__shirt', { opacity: 0 }) | |
set('.cannon', { y: 28 }) | |
set('.text--ordered .char', { y: '100%' }) | |
const SPEED = 0.15 | |
const FOLD_TL = () => | |
new timeline() | |
.to( | |
LEFT_ARM, | |
{ | |
duration: SPEED, | |
rotateY: -180, | |
transformOrigin: `${(22 / 65.3) * 100}% 50%`, | |
}, | |
0 | |
) | |
.to( | |
RIGHT_ARM, | |
{ | |
duration: SPEED, | |
rotateY: -180, | |
transformOrigin: `${((65.3 - 22) / 65.3) * 100}% 50%`, | |
}, | |
SPEED | |
) | |
.to(FOLD, { duration: SPEED / 4, scaleY: 1 }, SPEED * 2) | |
.to(FOLD, { duration: SPEED, y: -47 }, SPEED * 2 + 0.01) | |
.to(CLIPS, { duration: SPEED, scaleY: 0.2 }, SPEED * 2) | |
.to('.cannon', { duration: SPEED, y: 0 }, SPEED * 2) | |
// FOLD_TL() | |
const LOAD_TL = () => | |
new timeline() | |
.to('.button__shirt', { | |
transformOrigin: '50% 13%', | |
rotate: 90, | |
duration: 0.15, | |
}) | |
.to('.button__shirt', { | |
duration: 0.15, | |
y: 60, | |
}) | |
.to('.t-shirt__cannon', { | |
y: 5, | |
repeat: 1, | |
yoyo: true, | |
duration: 0.1, | |
}) | |
.to('.t-shirt__cannon', { | |
y: 50, | |
duration: 0.5, | |
delay: 0.1, | |
}) | |
const FIRE_TL = () => | |
new timeline() | |
.set('.t-shirt__cannon', { | |
rotate: 48, | |
x: -85, | |
scale: 2.5, | |
}) | |
.set('.cannon__shirt', { opacity: 1 }) | |
.to('.t-shirt__cannon-content', { duration: 1, y: -35 }) | |
.to('.t-shirt__cannon-content', { duration: 0.25, y: -37.5 }) | |
.to('.t-shirt__cannon-content', { duration: 0.015, y: -30.5 }) | |
.to( | |
'.cannon__shirt', | |
{ onStart: () => CLIP.play(), duration: 0.5, y: '-25vmax' }, | |
'<' | |
) | |
.to('.text--ordered .char', { duration: 0.15, stagger: 0.1, y: '0%' }) | |
.to('button', { duration: 7 * 0.15, '--hue': 116, '--lightness': 55 }, '<') | |
const ORDER_TL = new timeline({ paused: true }) | |
ORDER_TL.set('.cannon__shirt', { opacity: 0 }) | |
ORDER_TL.set('button', { '--hue': 260, '--lightness': 20 }) | |
ORDER_TL.to('button', { scale: 300 / BUTTON.offsetWidth, duration: SPEED }) | |
ORDER_TL.to('.text--order .char', { stagger: 0.1, y: '100%', duration: 0.1 }) | |
ORDER_TL.to(SHIRT, { | |
// Based on styling. 25px + 0.5rem | |
x: BUTTON.offsetWidth / 2 - 33, | |
duration: 0.2, | |
}) | |
// ORDER_TL.to(BUTTON, { scale: 3 }) | |
ORDER_TL.add(FOLD_TL()) | |
ORDER_TL.add(LOAD_TL()) | |
ORDER_TL.add(FIRE_TL()) | |
BUTTON.addEventListener('click', () => { | |
if (ORDER_TL.progress() === 1) { | |
// ORDER_TL.restart() | |
document.documentElement.style.setProperty('--hue', Math.random() * 360) | |
ORDER_TL.time(0) | |
ORDER_TL.pause() | |
} else if (ORDER_TL.progress() === 0) { | |
ORDER_TL.play() | |
} | |
}) |
<script src="https://unpkg.com/splitting/dist/splitting.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.6/gsap.min.js"></script> |
* | |
box-sizing border-box | |
body | |
min-height 100vh | |
display flex | |
align-items center | |
justify-content center | |
overflow hidden | |
:root | |
--color 'hsl(%s, 80%, 60%)' % var(--hue) | |
.t-shirt | |
height 100% | |
width 100% | |
position absolute | |
top 0 | |
left 0 | |
&__shirt | |
fill var(--color) | |
&__wrapper | |
position relative | |
&__cannon | |
position absolute | |
left 50% | |
top 50% | |
width 10px | |
transform translate(-50%, 0) | |
svg | |
position absolute | |
top 0 | |
left 0 | |
.cannon__shirt path | |
fill var(--color) | |
.cannon__band | |
fill hsl(50, 100%, 50%) | |
.cannon__plastic | |
fill hsla(190, 80%, 80%, 0.35) | |
.cannon__shine | |
fill hsla(0, 0%, 100%, 0.5) | |
.button | |
font-family sans-serif | |
font-weight bold | |
font-size 1rem | |
padding 1rem 2rem | |
padding-left calc(1rem + 50px) | |
position relative | |
border-radius 6px | |
border 0 | |
color hsl(0, 0%, 100%) | |
outline transparent | |
min-width 120px | |
$clip = inset(-1000% -1000% 0 0) | |
// $clip = inset(-1000% -1000% -1000% -1000%) | |
-webkit-clip-path $clip | |
clip-path $clip | |
&__text | |
position relative | |
.dummy | |
color transparent | |
& > .text | |
position absolute | |
top 0 | |
left 0 | |
white-space nowrap | |
.word | |
display inline-block | |
-webkit-clip-path inset(0 0 0 0) | |
clip-path inset(0 0 0 0) | |
.char | |
display inline-block | |
&__shirt | |
position absolute | |
height 32px | |
width 32px | |
top 50% | |
left calc(0.5rem + 25px) | |
transform translate(-50%, -50%) | |
// Don't know why but I needed this little wrapper piece to hide the t-shirt | |
.t-shirt__container | |
position absolute | |
top 0 | |
right 0 | |
bottom 0 | |
left 0 | |
overflow hidden | |
border-radius 6px | |
button | |
--hue 260 | |
cursor pointer | |
background transparent | |
padding 0 | |
border 0 | |
border-radius 6px | |
outline transparent | |
background 'hsl(%s, 46%, %s)' % (var(--hue, 260) calc(var(--lightness, 20) * 1%)) | |
box-shadow 2px 2px 4px 0px #333 | |
transition box-shadow .15s | |
&:active | |
box-shadow 0px 0px 0px 0px #333 |