// TODO
// Dynamic sizing / breakpoint sizing
// 2 values, with animation to switch between.
// Remove JS reliance wherever possible for decent fallback

mixin starburst()
  .starburst.js-starburst
    -
      var numberOfStars = 20
        , i = 0

    while i++ < numberOfStars
      -
        var scale = (Math.random() * (1 - 0.3) + 0.3).toFixed(2)
          , rotation = ((1 / (numberOfStars + 1)) * i).toFixed(2)
          , opacity = (Math.random() * (1 - 0.3) + 0.3).toFixed(2)

      .starburst__star(style='transform: scale(' + scale + ') rotate(' + rotation + 'turn); opacity: ' + opacity + ';')
        .starburst__star-inner

h2 Progress from empty
p Click to animate

.progress-badge.js-progress-badge

  //- BADGE
  .badge.badge--empty

    .badge__fill
      .badge__fill-inner

    .badge__content
      .badge__content-inner
        .badge__value
          span.badge__previous-value £0
          span.badge__current-value £5
  
  //- STARBURST
  +starburst()

hr

h2 Progress from full
p Click to animate

.progress-badge.js-progress-badge

  //- BADGE
  .badge

    .badge__fill
      .badge__fill-inner

    .badge__content
      .badge__content-inner
        .badge__value
          span.badge__previous-value £5
          span.badge__current-value £10
  
  //- STARBURST
  +starburst()
  

hr

h2 No change
  
.progress-badge

  //- BADGE
  .badge.badge--static

    .badge__fill
      .badge__fill-inner

    .badge__content
      .badge__content-inner
        .badge__value
          span.badge__previous-value £5
          span.badge__current-value £10
  
  //- STARBURST
  +starburst()
View Compiled
//
// SETUP
//

$color--red = #ee4432
$color--white = #fff


//
// PROGRESS BADGE
// Contains badge and starburst
//

.progress-badge
  position relative
  display inline-block


//
// BADGE
//

$badge-size = 150px

$badge-fill-duration = 2000ms
$badge-wave-duration = 1000ms
$badge-pop-duration = 300ms
$badge-pop-delay = $badge-fill-duration - 100ms

$badge-full-inner-offset-1 = 5px
$badge-full-inner-offset-2 = 10px
$badge-empty-inner-offset-1 = 7px
$badge-empty-inner-offset-2 = 10px

.badge // Red
  position relative
  width $badge-size
  height $badge-size
  border-radius 50%
  text-align center
  display table
  vertical-align middle
  color $color--white
  background $color--red
  z-index 2

  .is-filling &
    animation badge-pop $badge-pop-duration cubic-bezier(0.6, 1.5, 0.8, 1.15) 1
  
  .is-filling-from-empty &
    animation-delay $badge-pop-delay
  
  &:before,
  &:after
    position absolute
    content ''
    border-radius 50%
    z-index 10
    
  &:before
    top $badge-full-inner-offset-1
    right $badge-full-inner-offset-1
    bottom $badge-full-inner-offset-1
    left $badge-full-inner-offset-1
    border 2px dotted $color--white
  
  &:after
    top $badge-full-inner-offset-2
    right $badge-full-inner-offset-2
    bottom $badge-full-inner-offset-2
    left $badge-full-inner-offset-2
    border 2px solid $color--white

.badge__content
  display table-cell
  vertical-align middle

.badge__content-inner
  position relative
  
.badge__fill
  position absolute
  top -1px
  right -1px
  bottom -1px
  left -1px
  border-radius 50%
  overflow hidden
  z-index 0
  
  .badge--static &:after
    content ''
    position absolute
    top 0
    right (- $badge-size / 3) // Avoid cutting corners off gradient
    bottom 0
    left (- $badge-size / 3) // Avoid cutting corners off gradient
    background-image linear-gradient(-45deg, transparent 40%, $color--white 40%, $color--white 60%, transparent 60%)
    background-repeat no-repeat
    z-index 1
    opacity 0.3
    transform translateX($badge-size * -1)
    
    animation badge-shine 700ms 500ms ease-in-out
    

.badge__fill-inner
  position absolute
  right 0
  bottom -10px // Hides wave off-screen
  left 0
  top 0
  background $color--red
  transform translateY(100%)
  will-change transform
  
  &:before
    content ''
    position absolute
    left 0
    bottom 100%
    margin-bottom -2px // Hide aliasing gap
    width 400px
    height 12px
    background-repeat repeat-x
    background-image url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iNDAwcHgiIGhlaWdodD0iMTJweCIgdmlld0JveD0iMCAwIDQwMCAxMiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgNDAwIDEyIiB4bWw6c3BhY2U9InByZXNlcnZlIj48cGF0aCBmaWxsPSIjRUU0NDMyIiBkPSJNMzUwLDVjMCwwLTI0LTUtNDkuOC01QzI3NC4zLDAsMjUwLDUsMjUwLDVzLTIyLDUtNTAsNXMtNTAtNS01MC01cy0yNC01LTQ5LjgtNUM3NC4zLDAsNTAsNSw1MCw1cy0yMiw1LTUwLDV2Mmg0MDB2LTJDMzcyLDEwLDM1MCw1LDM1MCw1eiIvPjwvc3ZnPg==')

.badge__value
  font-size rem(50)
  line-height 1

.badge__previous-value
  
  .is-filling &
    animation badge-hide-previous-value 150ms linear 1 forwards
  
  .is-filling-from-empty &
    animation-delay $badge-fill-duration

.badge__current-value
  
  display none

  .is-filling &
    animation badge-show-current-value 150ms linear 1 forwards
  
  .is-filling-from-empty &
    animation-delay $badge-fill-duration
  
.badge__label
  font-size rem(14)

  

.badge--empty
  color $color--red
  border 4px solid $color--red
  background $color--white
  
  &:before
    top $badge-empty-inner-offset-1
    right $badge-empty-inner-offset-1
    bottom $badge-empty-inner-offset-1
    left $badge-empty-inner-offset-1
    border 1px dotted $color--red
  
  &:after
    top $badge-empty-inner-offset-2
    right $badge-empty-inner-offset-2
    bottom $badge-empty-inner-offset-2
    left $badge-empty-inner-offset-2
    border 1px solid $color--red
    
  .badge__fill-inner
    .is-filling-from-empty &
      animation badge-wave-fill $badge-fill-duration linear 1 forwards
      will-change transform
    .is-filling-from-empty &:before
      animation badge-wave-move $badge-wave-duration linear 2 // Just enough loops to outlast the fill animation

@keyframes badge-shine
  100%
    transform translateX($badge-size)

@keyframes badge-wave-move
  100%
    transform translateX(-200px)
    
@keyframes badge-wave-fill
  100%
    transform translateY(0)
    
@keyframes badge-pop
  50%
    transform scale(0.5)
  100%
    transform scale(1)

@keyframes badge-hide-previous-value
  0%
    display block
    opacity 1
  100%
    display none
    opacity 0
    
@keyframes badge-show-current-value
  0%
    display none
    opacity 0
  100%
    display block
    opacity 1

//
// STARBURST
//
    
$starburst-duration = 1200ms
$starburst-delay = 150ms

.starburst
  position absolute
  top 50%
  left 50%
  transform translate(-12.5px, -30px)
  z-index 1
  
  .is-filling &
    animation starburst-fall $starburst-duration $starburst-delay cubic-bezier(0, 0, 0.7, 0) 1
    
  .is-filling-from-empty &
    animation-delay 2000ms

.starburst__star
  position absolute
  
.starburst__star-inner
  width 25px
  height 25px
  background-size cover
  background-image url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDIwIDIwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48cG9seWdvbiBmaWxsPSIjRUY1NzQ3IiBwb2ludHM9IjEwLDAgMTMuMSw2LjYgMjAsNy42IDE1LDEyLjggMTYuMiwyMCAxMCwxNi42IDMuOCwyMCA1LDEyLjggMCw3LjYgNi45LDYuNiAiLz48L3N2Zz4=')

  .is-filling &
    animation star-move $starburst-duration $starburst-delay cubic-bezier(0, 0, 0.2, 0.8) 1

  .is-filling-from-empty &
    animation-delay 2000ms

@keyframes star-move
  0%
    transform translateX(0)
  100%
    transform translateX(300px)

@keyframes starburst-fall
  0%
    opacity 1
  100%
    opacity 0
    transform translateY(30px)


//
// DEMO STYLES
//

*
  box-sizing border-box
  
body
  width 300px
  margin 0 auto
  padding rem(50) rem(25)
  text-align center
  font-family 'Oswald', sans-serif
  
hr
  margin rem(30) 0
View Compiled
var $badges = $('.js-progress-badge')
  , $window = $(window)

$badges.on('click', function () {
  incrementBadge($(this))
})


function incrementBadge ($progressBadge) {

  var $badge = $progressBadge.find('.badge')
    , $badgeValue = $progressBadge.find('.js-badge-value')
    , badgeValueTarget = $badgeValue.data('target')
    , fillingFromEmpty = $badge.hasClass('badge--empty')
    , $starburst = $progressBadge.find('.js-starburst')

  $progressBadge.removeClass('badge--static')
  $progressBadge.addClass('is-filling')

  if (fillingFromEmpty) {
    $progressBadge.addClass('is-filling-from-empty')
  }
  
  $progressBadge.on('animationend', function () {
    $badge.addClass('badge--static badge--filled')
    $badge.removeClass('badge--empty')
  })

  $starburst.on('animationend', function () {
    $progressBadge.removeClass('is-filling is-filling-from-empty')
  })
  
}

External CSS

  1. https://fonts.googleapis.com/css?family=Oswald
  2. https://codepen.io/jackbrewer/pen/zvojNZ.stylus

External JavaScript

  1. https://code.jquery.com/jquery-2.2.4.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.13.1/lodash.min.js