T-shirt cannon playful animated button concept

Your shirt order blasts off from a t-shirt cannon in this playful button concept from Jhey, built with GSAP.

See the Pen T-Shirt Cannon Button 🚀 by Jhey (@jh3y) on CodePen.

Created on April 10, 2020 Updated on April 15, 2020. A Pen by Jhey on CodePen. License.

- 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
<button>
  <div class="button">
    <div class="t-shirt__cannon button__cannon">
      <div class="t-shirt__cannon-content">
        <svg class="cannon__shirt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 16.7 87.1">
          <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)"></path>
          </g>
        </svg>
        <svg class="cannon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 16.7 87.1">
          <g transform="matrix(0 -1.00036 .99247 0 -219.8 98)">
            <path class="cannon__plastic" stroke="#000" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" d="M11.6 222.1h85.7v15.5H11.6z"></path>
            <rect class="cannon__shine" width="20.4" height="1.9" x="63.2" y="223.7" ry="1"></rect>
            <g stroke="#000" stroke-linecap="round" stroke-linejoin="round">
              <path class="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"></path>
            </g>
          </g>
        </svg>
      </div>
    </div>
    <div class="t-shirt__container">
      <div class="t-shirt__wrapper button__shirt">
        <svg class="t-shirt t-shirt--middle" xmlns="http://www.w3.org/2000/svg" width="245" height="230" viewbox="0 0 64.8 60.9">
          <defs>
            <clipPath id="clipMain">
              <rect width="65" height="61"></rect>
            </clipPath>
          </defs>
          <g class="t-shirt__shirt" stroke="#000">
            <g class="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)"></path>
            </g>
          </g>
        </svg>
        <svg class="t-shirt t-shirt--left" xmlns="http://www.w3.org/2000/svg" width="245" height="230" viewbox="0 0 64.8 60.9">
          <defs>
            <clipPath id="clipLeft">
              <rect width="22.5" height="61"></rect>
            </clipPath>
          </defs>
          <g class="t-shirt__shirt" stroke="#000">
            <g class="t-shirt__arm t-shirt__arm--left" clip-path="url(#clipLeft)">
              <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"></path>
            </g>
          </g>
        </svg>
        <svg class="t-shirt t-shirt--right" xmlns="http://www.w3.org/2000/svg" width="245" height="230" viewbox="0 0 64.8 60.9">
          <defs>
            <clipPath id="clipRight">
              <rect x="42.3" width="22.5" height="61"></rect>
            </clipPath>
          </defs>
          <g class="t-shirt__shirt" stroke="#000">
            <g class="t-shirt__arm t-shirt__arm--right" clip-path="url(#clipRight)">
              <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"></path>
            </g>
          </g>
        </svg>
        <svg class="t-shirt t-shirt--bottom" xmlns="http://www.w3.org/2000/svg" width="245" height="230" viewbox="0 0 64.8 60.9">
          <defs>
          </defs>
          <g class="t-shirt__shirt" stroke="#000">
            <g class="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)"></path>
            </g>
          </g>
        </svg>
      </div>
    </div>
    <div class="button__text">
      <div class="dummy">Ordered</div>
      <div class="text text--order" data-splitting="">Order</div>
      <div class="text text--ordered" data-splitting="">Ordered</div>
    </div>
  </div>
</button>

script.js

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()
  }
})

styles

<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
* {
  box-sizing: border-box;
}
body {
  min-height: 100vh;
  display: -webkit-box;
  display: flex;
  -webkit-box-align: center;
          align-items: center;
  -webkit-box-pack: center;
          justify-content: center;
  overflow: hidden;
}
:root {
  --color: hsl(var(--hue), 80%, 60%);
}
.t-shirt {
  height: 100%;
  width: 100%;
  position: absolute;
  top: 0;
  left: 0;
}
.t-shirt__shirt {
  fill: var(--color);
}
.t-shirt__wrapper {
  position: relative;
}
.t-shirt__cannon {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 10px;
  -webkit-transform: translate(-50%, 0);
          transform: translate(-50%, 0);
}
.t-shirt__cannon svg {
  position: absolute;
  top: 0;
  left: 0;
}
.cannon__shirt path {
  fill: var(--color);
}
.cannon__band {
  fill: #ffd500;
}
.cannon__plastic {
  fill: rgba(163,231,245,0.35);
}
.cannon__shine {
  fill: rgba(255,255,255,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: #fff;
  outline: transparent;
  min-width: 120px;
  -webkit-clip-path: inset(-1000% -1000% 0 0);
  clip-path: inset(-1000% -1000% 0 0);
}
.button__text {
  position: relative;
}
.button__text .dummy {
  color: transparent;
}
.button__text > .text {
  position: absolute;
  top: 0;
  left: 0;
  white-space: nowrap;
}
.button .word {
  display: inline-block;
  -webkit-clip-path: inset(0 0 0 0);
  clip-path: inset(0 0 0 0);
}
.button .char {
  display: inline-block;
}
.button__shirt {
  position: absolute;
  height: 32px;
  width: 32px;
  top: 50%;
  left: calc(0.5rem + 25px);
  -webkit-transform: translate(-50%, -50%);
          transform: translate(-50%, -50%);
}
.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(var(--hue, 260), 46%, calc(var(--lightness, 20) * 1%));
  box-shadow: 2px 2px 4px 0px #333;
  -webkit-transition: box-shadow 0.15s;
  transition: box-shadow 0.15s;
}
button:active {
  box-shadow: 0px 0px 0px 0px #333;
}
W3TWEAKS
Latest posts by W3TWEAKS (see all)

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *