Home Reference Source

scripts/core/anim.js

import {Vector2, Matrix4, Quat, Vector3, Vector4} from '../path-controller/util/vectormath.js';
import * as math from '../path-controller/util/math.js'
import {color2css, css2color, parsepx} from './ui_theme.js';
import {Curve1D, getCurve} from "../path-controller/curve/curve1d.js";
import * as util from '../path-controller/util/util.js';

class Task {
  constructor(taskcb) {
    this.task = taskcb;
    this.start = util.time_ms();
    this.done = false;
  }
}

export class AnimManager {
  constructor() {
    this.tasks = [];
    this.timer = undefined;

    //all animation tasks that last longer then 10 seconds are terminated
    this.timeOut = 10*1000.0;
  }

  stop() {
    if (this.timer !== undefined) {
      window.clearInterval(this.timer);
      this.timer = undefined;
    }
  }

  add(task) {
    this.tasks.push(new Task(task));
  }

  remove(task) {
    for (let t of this.tasks) {
      if (t.task === task) {
        t.dead = true;
        this.tasks.remove(t);
        return;
      }
    }
  }

  start() {
    this.timer = window.setInterval(() => {
      for (let t of this.tasks) {
        try {
          t.task();
        } catch (error) {
          t.done = true;
          util.print_stack(error);
        }

        if (util.time_ms() - t.start > this.timeOut) {
          t.dead = true;
        }
      }

      for (let i=0; i<this.tasks.length; i++) {
        if (this.tasks[i].done) {
          let t = this.tasks[i];
          this.tasks.remove(t);
          i--;

          try {
            if (t.task.onend) {
              t.task.onend();
            }
          } catch (error) {
            util.print_stack(error);
          }
        }
      }
    }, 1000/40.0);
  }
}


export const manager = new AnimManager();
manager.start();

export class AbstractCommand {
  constructor() {
    this.cbs = [];
    this.end_cbs = [];
  }

  start(animator, done) {

  }

  exec(animator, done) {

  }
}

export class WaitCommand extends AbstractCommand {
  constructor(ms) {
    super();
    this.ms = ms;
  }

  start(animator, done) {
    this.time = animator.time;
  }

  exec(animator, done) {
    if (animator.time - this.time > this.ms) {
      done();
    }
  }
}

export class GoToCommand extends AbstractCommand {
  constructor(obj, key, value, time, curve="ease") {
    super();

    this.object = obj;
    this.key = key;
    this.value = value;
    this.ms = time;

    if (typeof curve === "string") {
      this.curve = new (getCurve(curve))();
    } else {
      this.curve = curve;
    }
  }

  start(animator, done) {
    this.time = animator.time;

    let value = this.object[this.key];

    if (Array.isArray(value)) {
      this.startValue = util.list(value);
    } else {
      this.startValue = value;
    }
  }

  exec(animator, done) {
    let t = animator.time - this.time;
    let ms = this.ms;

    if (t > ms) {
      done();
      t = ms;
    }

    t /= ms;

    t = this.curve.evaluate(t);

    if (Array.isArray(this.startValue)) {
      let value = this.object[this.key];

      for (let i=0; i<this.startValue.length; i++) {
        if (value[i] === undefined || this.value[i] === undefined) {
          continue;
        }

        value[i] = this.startValue[i] + (this.value[i] - this.startValue[i]) * t;
      }
    } else {
      this.object[this.key] = this.startValue + (this.value - this.startValue) * t;

    }
  }
}

export class SetCommand extends AbstractCommand {
  constructor(obj, key, val) {
    super();

    this.object = obj;
    this.key = key;
    this.value = val;
  }

  start(animator, done) {
    this.object[key] = val;
    done();
  }
}

export class Command {
  constructor(type, args) {
    this.args = args;
    this.cbs = [];
  }
}

export class Animator {
  constructor(owner, method = "update") {
    this.on_tick = this.on_tick.bind(this);
    this.on_tick.onend = () => {
      if (this.onend) {
        this.onend();
      }
    }

    this.commands = [];
    this.owner = owner;
    this._done = false;
    this.method = method;
    this.onend = null;
    this.first = true;

    this.time = 0.0;
    this.last = util.time_ms();

    this.bind(owner);
  }

  bind(owner) {
    this._done = false;
    this.owner = owner;
    //this.owner[this.method].after(this.on_tick);
    manager.add(this.on_tick);
  }

  wait(ms) {
    this.commands.push(new WaitCommand(ms));
    return this;
  }

  goto(key, val, timeMs, curve = "ease") {
    let cmd = new GoToCommand(this.owner, key, val, timeMs, curve);
    this.commands.push(cmd);
    return this;
  }

  set(key, val, time) {
    let cmd = new SetCommand(this.owner, key, val);
    this.commands.push(cmd);
    return this;
  }

  while(cb) {
    this.commands[this.commands.length - 1].cbs.push(cb);
    return this;
  }

  then(cb) {
    this.commands[this.commands.length-1].end_cbs.push(cb);
    return this;
  }

  end() {
    if (this._done) {
      return;
    }

    this._done = true;
    manager.remove(this.on_tick);
    //this.owner.update.remove(this.on_tick);

    if (this.onend) {
      this.onend();
    }
  }

  on_tick() {
    if (this._done) {
      throw new Error("animation wasn't properly cleaned up");
    }

    let dt = util.time_ms() - this.last;
    this.time += dt;
    this.last = util.time_ms();

    if (this.commands.length === 0) {
      this.end();
      return
    }

    let cmd = this.commands[0];
    let done = false;

    function donecb() {
      done = true;
    }

    if (this.first) {
      this.first = false;
      //console.log(cmd, cmd.start)
      cmd.start(this, donecb);
    }

    try {
      cmd.exec(this, donecb);
    } catch (error) {
      done = true;
      util.print_stack(error);
    }

    for (let cb of this.commands[0].cbs) {
      try {
        cb();
      } catch (error) {
        util.print_stack(error);
      }
    }

    if (done) {
      for (let cb of this.commands[0].end_cbs) {
        try {
          cb();
        } catch (error) {
          util.print_stack(error);
        }
      }
      while (this.commands.length > 0) {
        this.commands.shift();

        done = false;

        if (this.commands.length > 0) {
          this.commands[0].start(this, donecb);
        }

        if (!done) {
          break;
        }
      }
    }
  }
}