2

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

Owen Kelvin
  • 14,054
  • 10
  • 41
  • 74
  • As far as I can tell, the numbers are changed by removing elements and then adding them back in with updated content. This is why the animations aren't playing. Not sure how to fix that, though. – Ouroborus Dec 24 '20 at 21:47
  • ouroboros is correct - you cannot animate what is not there. Therefore, you just only update the actual content of the elements rather than the elements themselves.s – Randy Casburn Dec 24 '20 at 21:57
  • @Ouroborus I have some 'progress' but now the animation is reflecting on all the numbers https://stackblitz.com/edit/angular-ivy-msmuwa?file=src%2Fapp%2Fapp.component.html – Owen Kelvin Dec 25 '20 at 15:58
  • They're still being updated simultaneously, whether they need it or not. – Ouroborus Dec 26 '20 at 00:43

2 Answers2

4

I made a timer similar to that of the 'this' link.(don't go by the fps of image output's fps is way higher)

enter image description here

This will work for u.

var h = document.getElementsByClassName('hours');

var m = document.getElementsByClassName('min');

var s = document.getElementsByClassName('sec');
var now = new Date().getTime();

var distance = now;
var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((distance % (1000 * 60)) / 1000);

var prev_sec = seconds;
var prev_min = minutes;
var prev_hour = hours;
h[0].innerHTML = h[2].innerHTML = parseInt(hours / 10);
h[1].innerHTML = h[3].innerHTML = parseInt(hours % 10);
m[0].innerHTML = m[2].innerHTML = parseInt(minutes / 10);
m[1].innerHTML = m[3].innerHTML = parseInt(minutes % 10);
s[0].innerHTML = s[2].innerHTML = parseInt(seconds / 10);
s[1].innerHTML = s[3].innerHTML = parseInt(seconds % 10);

var x = setInterval(function() {

  // Get today's date and time
  var now = new Date().getTime();

  // Find the distance between now and the count down date
  var distance = now;

  // Time calculations for days, hours, minutes and seconds



  var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
  var seconds = Math.floor((distance % (1000 * 60)) / 1000);



  h[0].innerHTML = h[2].innerHTML = parseInt(hours / 10);
  h[1].innerHTML = h[3].innerHTML = parseInt(hours % 10);
  m[0].innerHTML = m[2].innerHTML = parseInt(minutes / 10);
  m[1].innerHTML = m[3].innerHTML = parseInt(minutes % 10);
  s[0].innerHTML = s[2].innerHTML = parseInt(seconds / 10);
  s[1].innerHTML = s[3].innerHTML = parseInt(seconds % 10);
  h[4].innerHTML = parseInt(prev_hour / 10);
  h[5].innerHTML = parseInt(prev_hour % 10);
  m[4].innerHTML = parseInt(prev_min / 10);
  m[5].innerHTML = parseInt(prev_min % 10);
  s[4].innerHTML = parseInt(prev_sec / 10);
  s[5].innerHTML = parseInt(prev_sec % 10);


  h[2].style.animation = parseInt(prev_hour / 10) != parseInt(hours / 10) ? "reflect 1s infinite" : "stay 1s infinite";
  h[3].style.animation = parseInt(prev_hour % 10) != parseInt(hours % 10) ? "reflect 1s infinite" : "stay 1s infinite";
  m[2].style.animation = parseInt(prev_min / 10) != parseInt(minutes / 10) ? "reflect 1s infinite" : "stay 1s infinite";
  m[3].style.animation = parseInt(prev_min % 10) != parseInt(minutes % 10) ? "reflect 1s infinite" : "stay 1s infinite";
  s[2].style.animation = parseInt(prev_sec / 10) != parseInt(seconds / 10) ? "reflect 1s infinite" : "stay 1s infinite";
  s[3].style.animation = "reflect 1s infinite";


  prev_sec = seconds;
  prev_min = minutes
  prev_hour = hours;
}, 1000);
* {
  font-family: 'arial';
  font-weight: bold;
}

.main_Timer {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.time {
  clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
}

.time span,
.time_reflection span,
.time_prev span {
  color: white;
  font-size: 120px;
  background-color: #252525;
  border-radius: 10px;
  padding: 5px 10px 0px 10px;
  transition: 0.4s;
  text-align: center;
  margin-right: 3px;
}

@keyframes time {
  0% {
    filter: brightness(70%);
  }
  90%,
  100% {
    filter: brightness(100%);
  }
}

.line_hor {
  position: absolute;
  background-color: white;
  width: 650px;
  height: 2px;
  margin-top: -72px;
  z-index: 10;
}

.perspective {
  position: absolute;
  perspective: 1400px;
  perspective-origin: 10% 0;
  z-index: 5;
  margin-top: -143px;
}

.perspective .time_reflection {
  position: absolute;
  transform-style: preserve-3d;
  display: flex;
}

.time_reflection span {
  display: inline-block;
  clip-path: polygon(0 50%, 100% 50%, 100% 100%, 0 100%);
}

.time_prev {
  display: flex;
  margin-top: -143px;
  position: absolute;
  clip-path: polygon(0 50%, 100% 50%, 100% 100%, 0 100%);
  z-index: 4;
  filter: brightness(50%);
}

.time_prev span {}

@keyframes stay {
  from {
    transform: rotateX(0deg);
  }
  to {
    transform: rotateX(0deg);
  }
}

@keyframes reflect {
  0% {
    transform: rotateX(130deg);
    opacity: 1;
  }
  45% {
    transform: rotateX(0deg);
  }
  50% {
    transform: rotateX(7deg);
  }
  53% {
    transform: rotateX(0deg);
  }
  56% {
    transform: rotateX(5deg);
  }
  60% {
    transform: rotateX(0deg);
  }
  95% {
    transform: rotateX(0deg);
  }
  100% {
    transform: rotateX(0deg);
  }
}

.hours:nth-child(2n),
.min:nth-child(2n),
.sec:nth-child(2n) {
  margin-right: 25px;
}
<div class="main_Timer">
  <div class="clock">

    <div class="time">

      <span class="hours">0</span><span class="hours">0</span><span class="min">0</span><span class="min">0</span><span class="sec">0</span><span class="sec">0</span>
    </div>

  </div>
  <div class="perspective">
    <div class="time_reflection">

      <span class="hours">0</span><span class="hours">0</span><span class="min">0</span><span class="min">0</span><span class="sec">0</span><span class="sec">0</span>
    </div>
  </div>
  <div class="time_prev">

    <span class="hours">0</span><span class="hours">0</span><span class="min">0</span><span class="min">0</span><span class="sec">0</span><span class="sec">0</span>
  </div>
  <div class="line_hor"></div>
</div>
ac_mmi
  • 2,302
  • 1
  • 5
  • 14
  • Thanks for your reply unfortunately what I am trying to avoid is accessing dom elements using dom api, eg `document.getElementsByClassName`. The idea behind me using `Observables` is that I am merging the result of http calls to this, something like -> GetAContest->extractPeriod->ExtractTime->mapToTimer->ReduceTimer->IfTimerReachesO->AutoSubmit; – Owen Kelvin Dec 27 '20 at 18:29
  • @OwenKelvin so its like during online test the timer reached max time ,so it would fire submit button. – ac_mmi Dec 28 '20 at 05:53
  • Basically I need to be able to use the element like ` – Owen Kelvin Dec 28 '20 at 06:16
  • 1
    @OwenKelvin you need to store previous value and compare it with the present time those with same digits would remain same and others would give turning animation – ac_mmi Dec 28 '20 at 06:34
  • Let me try the above, sounds like the way to go – Owen Kelvin Dec 28 '20 at 06:36
  • @OwenKelvin you could see in my Js part i compared the prev value to the present one and based on the comparision assigned the animation – ac_mmi Dec 28 '20 at 06:38
  • @Ac_mmi, Just do it! :) – Eliseo Dec 30 '20 at 08:34
  • @Eliseo what should i do ? Elaborate please – ac_mmi Dec 30 '20 at 10:09
  • see my answer (the last part -in the Update-, when use your .css). It's only declare the animation, and bind the animation. the answer has a final stackblitz: https://stackblitz.com/edit/angular-ivy-uxbept?file=src%2Fapp%2Fapp.component.ts – Eliseo Dec 30 '20 at 10:27
  • @Eliseo i just started Angular thats why made timer in old school manner – ac_mmi Dec 30 '20 at 10:33
  • 1
    The animations is a "hard part" of Angular, we can start in the oficials docs:https://angular.io/guide/animations. But the idea is defined an "animation". To do it we define a "trigger" (the name of the animation `flip`), with a "transaction" (when you want that the animation happens `*=>*` mean any change) and declare the "animate" -that can has keyframes-. when in an HTML element you write `[@flip]="variable"`. In our case, each change of variable, execute the animation defined. NOTE I love your "animation" – Eliseo Dec 30 '20 at 10:41
1

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]}))
        })
      )
    )
  );
Eliseo
  • 50,109
  • 4
  • 29
  • 67