Search code examples
javascripthtmlcssangularangular-animations

How to implement flipclock in angular


I am trying to implement a countdown timer using angular as implemented in this

The problem is that the animations are not being applied on change of values, what I am I missing?

html

<div>
  <div class="flipclock" *ngIf="timer$ | async as timer">
    <div id="container" class="flipclock">
      <ul class="flip " *ngFor="let time of timer">
        <li
          *ngFor="let item of time.split(''); let i = index"
          [class.d1]="i === 1"
          [class.d2]="i === 0"
        >
          <section class="ready">
            <div class="up">
              <div class="shadow"></div>
              <div class="inn">{{ item }}</div>
            </div>
            <div class="down">
              <div class="shadow"></div>
              <div class="inn">{{ item }}</div>
            </div>
          </section>
          <section class="active">
            <div class="up">
              <div class="shadow"></div>
              <div class="inn">{{ item }}</div>
            </div>
            <div class="down">
              <div class="shadow"></div>
              <div class="inn">{{ item }}</div>
            </div>
          </section>
        </li>
      </ul>
    </div>
  </div>
</div>

export class AppComponent  {
  name = 'Angular ' + VERSION.major;
    initialMinutes$ = new BehaviorSubject(30);
  expired$ = new Subject();

  @Input()
  set minutes(val) {
    this.initialMinutes$.next(val);
  }

  timer$ = this.initialMinutes$.pipe(
    switchMap(minutes => timer(0, 1000).pipe(
      map(t => minutes * 60 - t),
      tap(seconds => {
        if (seconds < 0) {
          this.expired$.next();
        }
      }),
      takeUntil(this.expired$),
      map(seconds => ({
        hr: Math.max(Math.floor(seconds / 3600), 0),
        min: Math.max(Math.floor((seconds % 3600) / 60), 0),
        s: (seconds % 60)
      })),
      map(({hr, min, s}) => ([
        hr > 9 ? hr.toString() : '0' + hr.toString(),
        min > 9 ? min.toString() : '0' + min.toString(),
        s > 9 ? s.toString() : '0' + s.toString(),
      ]))
    ))
  );
}

css



.flipclock {
}
.flipclock hr {
  position: absolute;
  left: 0;
  top: 65px;
  width: 100%;
  height: 3px;
  border: 0;
  background: #000;
  z-index: 10;
  opacity: 0;
}
ul.flip {
  position: relative;
  float: left;
  margin: 10px;
  padding: 0;
  width: 90px;
  height: 60px;
  font-size: 60px;
  font-weight: 400;
  line-height: 60px;
}

ul.flip li {
  float: left;
  margin: 0;
  padding: 0;
  width: 49%;
  height: 100%;
  -webkit-perspective: 200px;
  list-style: none;
}

ul.flip li.d1 {
  float: right;
}

ul.flip li section {
  z-index: 1;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;

}

ul.flip li section:first-child {
  z-index: 2;
}

ul.flip li div {
  z-index: 1;
  position: absolute;
  left: 0;
  width: 100%;
  height: 49%;
  overflow: hidden;
}

ul.flip li div .shadow {
  display: block;
  position: absolute;
  width: 100%;
  height: 100%;
  z-index: 2;
}

ul.flip li div.up {
  -webkit-transform-origin: 50% 100%;
  top: 0;
}

ul.flip li div.down {
  -webkit-transform-origin: 50% 0;
  bottom: 0;
}

ul.flip li div div.inn {
  position: absolute;
  left: 0;
  z-index: 1;
  width: 100%;
  height: 200%;
  color: #fff;
  text-shadow: 0 0 2px #fff;
  text-align: center;
  background-color: #000;
  border-radius: 6px;
}

ul.flip li div.up div.inn {
  top: 0;

}

ul.flip li div.down div.inn {
  bottom: 0;
}

/*--------------------------------------
 PLAY
--------------------------------------*/

.play ul section.ready {
  z-index: 3;
}

.play ul section.active {
  -webkit-animation: index .5s .5s linear both;
  z-index: 2;
}

@-webkit-keyframes index {
  0% {
    z-index: 2;
  }
  5% {
    z-index: 4;
  }
  100% {
    z-index: 4;
  }
}

.play ul section.active .down {
  z-index: 2;
  -webkit-animation: flipdown .5s .5s linear both;
}

@-webkit-keyframes flipdown {
  0% {
    -webkit-transform: rotateX(90deg);
  }
  80% {
    -webkit-transform: rotateX(5deg);
  }
  90% {
    -webkit-transform: rotateX(15deg);
  }
  100% {
    -webkit-transform: rotateX(0deg);
  }
}

.play ul section.ready .up {
  z-index: 2;
  -webkit-animation: flipup .5s linear both;
}

@-webkit-keyframes flipup {
  0% {
    -webkit-transform: rotateX(0deg);
  }
  90% {
    -webkit-transform: rotateX(0deg);
  }
  100% {
    -webkit-transform: rotateX(-90deg);
  }
}

/*--------------------------------------
 SHADOW
--------------------------------------*/

.play ul section.ready .up .shadow {
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, .1)), color-stop(100%, rgba(0, 0, 0, 1)));
  background: linear-gradient(to bottom, rgba(0, 0, 0, .1) 0%, rgba(0, 0, 0, 1) 100%);
  -webkit-animation: show .5s linear both;
}

.play ul section.active .up .shadow {
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, .1)), color-stop(100%, rgba(0, 0, 0, 1)));
  background: linear-gradient(to bottom, rgba(0, 0, 0, .1) 0%, rgba(0, 0, 0, 1) 100%);
  -webkit-animation: hide .5s .3s linear both;
}

/*DOWN*/

.play ul section.ready .down .shadow {
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, 1)), color-stop(100%, rgba(0, 0, 0, .1)));
  background: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, .1) 100%);
  -webkit-animation: show .5s linear both;
}

.play ul section.active .down .shadow {
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, 1)), color-stop(100%, rgba(0, 0, 0, .1)));
  background: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, .1) 100%);
  -webkit-animation: hide .5s .3s linear both;
}

@-webkit-keyframes show {
  0% {
    opacity: 0;
  }
  90% {
    opacity: .10;
  }
  100% {
    opacity: 1;
  }
}

@-webkit-keyframes hide {
  0% {
    opacity: 1;
  }
  80% {
    opacity: .20;
  }
  100% {
    opacity: 0;
  }
}

See this Demo on stackblitz

Edit 1

I have managed to get animations to reflect but it is now reflecting on all the items

enter link description here


Solution

  • We can do it using angular animations. One aproach (it's diferent than your css) is make a "flip-vertical" from border botton. In one face you has a number, and in the other face another number. Well, really we need that ot has the full number else half of the number.

    Imagine you has some like

    <div class="content">
        <div class="flip">
            <div class="up">
                <div>{{oldvalue}}</div>
            </div>
            <div class="down">
                <div>
                    <div>{{oldvalue}}</div>
                </div>
            </div>
        </div>
    </div>
    

    the .css is like

    .content {
      font-family: "Droid Sans Mono", monospace;
      height: 60px;
      display:inline-block;
      margin-left:10px;
    }
    .flip {
      position: relative;
      height: 60px;
      width: 45px;
    }
    .up,
    .down {
      text-align: center;
      height: 30px;
      overflow: hidden;
    }
    .up > div,
    .down > div {
      font-size: 50px;
      font-weight: 800;
      line-height: 60px;
      align-self: center;
    }
    .down > div > div {
      margin-top: -30px;
    }
    

    With this you has a number in two halfs

    We can then use a clasic flip vertical

    <div class="content">
        <div class="flip-card">
            <div class="flip-card-inner" [@flip]="value">
                <div class="flip-card-front">
                    <div class="up">
                        <div>{{oldvalue}}</div>
                    </div>
                </div>
                <div class="flip-card-back">
                    <div class="down">
                        <div>
                            <div>{{newvalue}}</div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    

    the .css

    .flip-card {
      perspective: 300px;
      position: relative;
      height: 30px;
      width: 45px;
      box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
    }
    
    .flip-card-inner {
      width: 100%;
      height: 100%;
      text-align: center;
      transform-style: preserve-3d;
      -ms-transform-origin: 50% 100%; /* IE 9 */
      transform-origin: 50% 100%; /* IE 9 */
    }
    
    .flip-card-front,
    .flip-card-back {
      position: absolute;
      width: 100%;
      height: 100%;
      -webkit-backface-visibility: hidden;
      backface-visibility: hidden;
      overflow: hidden;
    }
    .flip-card-back {
      transform: rotateX(180deg);
    }
    

    The animation is really simple

      animations:[
        trigger("flip",[
          transition('*=>*',[
          animate(".6s",keyframes([
            style({transform:"rotateX(0deg)",offset: 0}),
            style({transform:"rotateX(-90deg)",offset: .5}),
            style({transform:"rotateX(-180deg)",offset: 1}),
          ]))
        ])
      ])]
    

    Well, the full digit it's now the two before divs

    <div class="content">
        <div style="position:absolute">
            <--here the digit-->
        </div>
        <div style="position:absolute">
            <---here the flip-card--->
        </div>
    </div>
    

    You can see the first stackblitz, see how when you click the button, it's animate the digit

    Well, how use all this. As Ac_mmi say you need has the old and the new value. You can use your code adding a map to return an array of six numbers, a pipe pairwise to get the old and the value and a map to return an array of object with the two properties:

    map(val => val.map(i => i.split("")).reduce((a, b) => [...a, ...b], [])),
    pairwise(),
    map(([old,value])=>{
      return value.map((x,index)=>({value:x,old:old[index]}))
    })
    

    Then we make a simple loop over [0,1,2,3,4,5] to get the numbers

    <ng-container *ngIf="timer$ |async as timer">
      <div class="content" *ngFor="let i of [0,1,2,3,4,5]">
      ....
    </ng-container>
    

    The finnal stackblitz

    Update

    Using the class and animation provided by @Ac_mmi, you defined an animation like

      animations:[
        trigger("flip",[
          transition('*=>*',[
          animate(".6s",keyframes([
            style({transform:"rotateX(130deg)",offset: 0}),
            style({transform:"rotateX(0deg)",offset: .45}),
            style({transform:"rotateX(7deg)",offset: .50}),
            style({transform:"rotateX(0deg)",offset: .53}),
            style({transform:"rotateX(5deg)",offset: .56}),
            style({transform:"rotateX(0deg)",offset: .60}),
            style({transform:"rotateX(0deg)",offset: .95}),
            style({transform:"rotateX(0deg)",offset: 1}),
          ]))
        ])
      ])]
    

    and the elements that are animated becomes like, e.g. for the "hours"

    <span class="hours" [@flip]="timer[0].value">
       {{timer[0].value}}
     </span>
    

    You has another stackblitz

    Update2 to get hours, minutes and secons, we can use formatDate -the function use Angular for DatePipe- and the timer becomes more easy:

      timer$ = this.initialMinutes$.pipe(
        map(minutes => minutes * 60000 + new Date().getTime()),
        switchMap(minutes =>
          timer(0, 500).pipe(
            map(t => formatDate((minutes - new Date().getTime()),"HHmmss","en-US","+0000").split('')),
            takeUntil(this.expired$),
            pairwise(),
            map(([old,value])=>{
              return value.map((x,index)=>({value:x,old:old[index]}))
            })
          )
        )
      );