public long animate(@NotNull Collection<@NotNull Animation> animations) {
    if (myDisposed.get()) {
      LOG.warn("Animator is already disposed");
      return Long.MAX_VALUE;
    }

    var from = Integer.MAX_VALUE;
    var to = 0;

    for (Animation animation : animations) {
      from = Math.min(animation.getDelay(), from);
      to = Math.max(animation.getDelay() + animation.getDuration(), to);
    }

    final var delay = animations.isEmpty() ? 0 : from;
    final var duration = animations.isEmpty() ? 0 : to - from;

    final var taskId = myRunning.incrementAndGet();

    if (!myIgnorePowerSaveMode && PowerSaveMode.isEnabled()
        || Registry.is("ui.no.bangs.and.whistles", false)
        || RemoteDesktopService.isRemoteSession()
        || duration == 0) {
      myCurrentAnimatorFuture = myService.schedule(() -> {
        if (taskId < myRunning.get()) {
          for (Animation animation : animations) {
            animation.fireEvent(Animation.Phase.CANCELLED);
          }
          return;
        }
        for (Animation animation : animations) {
          try {
            animation.fireEvent(Animation.Phase.SCHEDULED);
            animation.update(1.0);
            animation.fireEvent(Animation.Phase.UPDATED);
            animation.fireEvent(Animation.Phase.EXPIRED);
          }
          catch (Throwable t) {
            LOG.error(t);
          }
        }
        myRunning.compareAndSet(taskId, taskId + 1);
      }, delay, TimeUnit.MILLISECONDS);
      return taskId;
    }

    final var stat = new Statistic(myName, taskId);
    stat.start = System.nanoTime();
    if (myPeriod < 16) { // do not enable this until it's really necessary
      JBAnimatorHelper.requestHighPrecisionTimer(this);
    }

    myCurrentAnimatorFuture = myService.schedule(new Runnable() {
      final Type type = myType;
      final int period = myPeriod;
      final boolean cycle = myCyclic;
      @Nullable FrameCounter frameCounter;
      @NotNull LinkedHashSet<Animation> scheduledAnimations = new LinkedHashSet<>();
      //private final long animationStarted = System.nanoTime();
      private long nextScheduleTime = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delay); // ns

      private void prepareAnimations() {
        frameCounter = create(type, period, duration);
        scheduledAnimations = new LinkedHashSet<>(animations);
        for (Animation animation : scheduledAnimations) {
          animation.fireEvent(Animation.Phase.SCHEDULED);
        }
      }

      private void finalizeRunning() {
        JBAnimatorHelper.cancelHighPrecisionTimer(JBAnimator.this);
        stat.end = System.nanoTime();
        myStatistic = stat;
      }

      @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);
        }
      }
    }, delay, TimeUnit.MILLISECONDS);

    return taskId;
  }
public long animate(@NotNull Collection<@NotNull Animation> animations)