@Override
      public void run() {
        stat.count.incrementAndGet();
        // There's a penalty for run a task.
        // To decrease cumulative penalty difference between real start
        // and expected is subtracted from the next delay
        long wasLate = System.nanoTime() - nextScheduleTime;
        if (wasLate < 0) {
          LOG.warn("Negative animation late value");
          wasLate = 0;
        }
        if (taskId < myRunning.get()) {
          finalizeRunning();
          for (Animation animation : scheduledAnimations) {
            animation.fireEvent(Animation.Phase.CANCELLED);
          }
          return;
        }
        if (frameCounter == null) {
          prepareAnimations();
        }
        long totalFrames = frameCounter.getTotalFrames();
        long currentFrame = Math.min(frameCounter.getNextFrame(cycle), totalFrames);
        long currentDelay = frameCounter.getDelay(currentFrame);
        double timeline = (double) currentFrame / totalFrames;
        if (currentFrame >= totalFrames && cycle) {
          frameCounter = null;
        }
        final var expired = new LinkedList<Animation>();
        for (Animation animation : scheduledAnimations) {
          double start = (double) (animation.getDelay() - delay) / duration;
          double end = start + (double) animation.getDuration() / duration;
          if (start <= timeline) try {
            double current = (timeline - start) / (end - start);
            animation.update(MathUtil.clamp(current, 0.0, 1.0));
            animation.fireEvent(Animation.Phase.UPDATED);
          }
          catch (Throwable t) {
            LOG.error(t);
          }
          if (timeline > end) {
            expired.add(animation);
          }
        }
        expired.forEach(scheduledAnimations::remove);
        boolean isProceed = currentFrame < totalFrames || cycle;
        if (isProceed) {
          long nextDelay = Math.max(TimeUnit.MILLISECONDS.toNanos(currentDelay) - wasLate, TimeUnit.MILLISECONDS.toNanos(1));
          nextScheduleTime = System.nanoTime() + nextDelay;
          myCurrentAnimatorFuture = myService.schedule(this, nextDelay, TimeUnit.NANOSECONDS);
        }
        else {
          // There's a situation when a new task is submitted but current is already in progress.
          // For example, the task can be submitted with Animation#runWhenExpired,
          // but this code synchronously can fire animate, therefore myRunning increases.
          // If this situation happens the current one ID is abandoned,
          // because the value is increased somewhere else.
          myRunning.compareAndSet(taskId, taskId + 1);
          //var debugInfo = "Animation total time is " +
          //                TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - animationStarted) +
          //                " ms; requested time is " +
          //                (delay + duration);
          //LOG.info(debugInfo);
          finalizeRunning();
        }
        // we should fire events after taskId is updated
        // and some final activity is done by calling finalizeRunning
        for (Animation animation : isProceed ? expired : scheduledAnimations) {
          animation.fireEvent(Animation.Phase.EXPIRED);
        }
      }
@Override
      public void run()