Skip to content
Snippets Groups Projects
spniEpilogue.js 89.1 KiB
Newer Older
  • Learn to ignore specific revisions
  •     if (scene.background) {
    
          scene.background = scene.background.charAt(0) === '/' ? scene.background.substring(1) : this.epilogue.player.base_folder + scene.background;
    
          this.fetchImage(scene.background);
        }
        for (var j = 0; j < scene.directives.length; j++) {
          var directive = scene.directives[j];
          if (directive.src) {
    
            directive.src = directive.src.charAt(0) === '/' ? directive.src.substring(1) : this.epilogue.player.base_folder + directive.src;
    
            this.fetchImage(directive.src);
          }
    
          
          if (directive.keyframes) {
              for (var k = 0; k < directive.keyframes.length; k++) {
                var keyframe = directive.keyframes[k];
                if (keyframe.src && keyframe !== directive) {
    
                  keyframe.src = keyframe.src.charAt(0) === '/' ? keyframe.src.substring(1) : this.epilogue.player.base_folder + keyframe.src;
    
                  this.fetchImage(keyframe.src);
                }
              }
    
    spnati_edit's avatar
    spnati_edit committed
          }
    
        }
      }
      this.readyToLoad = true;
      this.onLoadComplete();
    }
    
    /**
     * Called whenever all images being pre-fetched have been returned (which isn't necessarily when the total number of images that will be pre-fetched have been requested)
     * This is a workaround for IE11 not supporting promises
     */
    EpiloguePlayer.prototype.onLoadComplete = function () {
    
      $("#epilogue-progress").text(Math.floor(this.loadedImages / Math.max(1, this.totalImages) * 100) + "%");
    
      if (this.loadingImages > 0) { return; }
    
      if (this.readyToLoad) {
    
        $("#epilogue-spinner").hide();
    
        var container = $("#epilogue-container");
        this.views.push(new SceneView(container, 0, this.assetMap));
        this.views.push(new SceneView(container, 1, this.assetMap));
        container.append($("<div id='scene-fade' class='epilogue-overlay' style='z-index: 10000'></div>")); //scene transition overlay
    
        this.loaded = true;
    
        this.advanceScene();
    
    spnati_edit's avatar
    spnati_edit committed
        window.requestAnimationFrame(this.loop.bind(this));
    
    /**
     * Fetches an image asset ahead of time so it's ready before we need it
     * @param {string} path URL for image
     */
    EpiloguePlayer.prototype.fetchImage = function (path) {
      var img = new Image();
      this.loadingImages++;
    
      var $this = this;
      img.onload = img.onerror = function () {
        $this.assetMap[path] = img;
        $this.loadingImages--;
    
        $this.onLoadComplete();
      };
      img.src = path;
    }
    
    EpiloguePlayer.prototype.destroy = function () {
    
      for (var i = 0; i < this.views.length; i++) {
        this.views[i].destroy();
    
    spnati_edit's avatar
    spnati_edit committed
    
    
      $("#scene-fade").remove();
    
      EpiloguePlayer.prototype.layer = 0;
    
    EpiloguePlayer.prototype.hasMoreDirectives = function () {
      return this.sceneIndex < this.epilogue.scenes.length - 1 || this.directiveIndex < this.activeScene.directives.length - 1;
    }
    
    EpiloguePlayer.prototype.hasPreviousDirectives = function () {
    
    spnati_edit's avatar
    spnati_edit committed
      return this.sceneIndex > 0 || this.directiveIndex > 0;
    
    EpiloguePlayer.prototype.loop = function (timestamp) {
      var elapsed = timestamp - this.lastUpdate;
    
    
      if (this.activeTransition) {
        this.activeTransition.update(elapsed);
        if (this.activeTransition.isComplete()) {
          this.activeTransition = null;
        }
      }
    
      for (var i = 0; i < this.views.length; i++) {
        if (this.views[i].isActive()) {
          this.update(elapsed);
          this.draw();
          break;
        }
    
      this.lastUpdate = timestamp;
    
    spnati_edit's avatar
    spnati_edit committed
      window.requestAnimationFrame(this.loop.bind(this));
    
    }
    
    EpiloguePlayer.prototype.update = function (elapsed) {
      var nonLoopingCount = 0;
    
      for (var i = 0; i < this.views.length; i++) {
        nonLoopingCount += this.views[i].update(elapsed);
    
      }
      if (nonLoopingCount === 0 && this.waitingForAnims) {
        this.advanceDirective();
      }
    }
    
    EpiloguePlayer.prototype.draw = function () {
    
      for (var i = 0; i < this.views.length; i++) {
        this.views[i].draw();
    
    /** Advances to the next scene if there is one */
    EpiloguePlayer.prototype.advanceScene = function () {
      this.sceneIndex++;
      if (this.sceneIndex < this.epilogue.scenes.length) {
        this.setupScene(this.sceneIndex);
      }
    }
    
    
    EpiloguePlayer.prototype.layer = 0;
    
    EpiloguePlayer.prototype.setupScene = function (index, skipTransition) {
      var lastScene = this.activeScene;
    
      this.lastUpdate = performance.now();
    
      this.activeScene = this.epilogue.scenes[index];
    
    
      var view = this.activeScene.view = this.views[this.viewIndex];
      this.viewIndex = (this.viewIndex + 1) % this.views.length;
      this.directiveIndex = -1;
    
      view.setup(this.activeScene, index, this.epilogue, skipTransition ? null : lastScene);
    
    spnati_edit's avatar
    spnati_edit committed
    
    
      //fit the viewport based on the scene's aspect ratio and the window size
      this.resizeViewport();
    
    
      //scene transition effect
      if (lastScene) {
        if (lastScene.transition && this.activeScene && !skipTransition) {
          this.activeTransition = new SceneTransition(lastScene.view, this.activeScene.view, lastScene.transition, $("#scene-fade"));
        }
        if (!this.activeTransition) {
          lastScene.view.cleanup();
        }
    
      }
    
      this.performDirective();
    }
    
    EpiloguePlayer.prototype.resizeViewport = function () {
      if (!this.activeScene) {
        return;
      }
    
    
      for (var i = 0; i < this.views.length; i++) {
        this.views[i].resize();
    
      }
    
      this.draw();
    }
    
    EpiloguePlayer.prototype.advanceDirective = function () {
    
      if (this.activeTransition) { return; } //prevent advancing during a scene transition
      this.waitingForAnims = false;
      this.activeScene.view.haltAnimations(false);
    
      this.performDirective();
    }
    
    EpiloguePlayer.prototype.performDirective = function () {
    
      if (this.sceneIndex >= this.epilogue.scenes.length) { return; }
    
      this.directiveIndex++;
      if (this.directiveIndex < this.activeScene.directives.length) {
    
        var view = this.activeScene.view;
    
        var directive = this.activeScene.directives[this.directiveIndex];
    
        directive.action = null;
        if (directive.delay && directive.type !== "pause" && directive.type !== "wait") {
          view.pendDirective(this, directive, directive.delay);
        }
        else {
          if (view.runDirective(this, directive)) {
    
        }
        this.performDirective();
      }
      else {
        this.advanceScene();
      }
    }
    
    /**
     * Reverts all changes up until the last "pause" directive
     */
    EpiloguePlayer.prototype.revertDirective = function () {
    
      if (this.activeTransition) { return; }
    
      this.activeScene.view.haltAnimations(false);
    
    
      var canRevert = (this.sceneIndex > 0);
      if (!canRevert) {
        //on the initial scene, make sure there is a pause directive to revert to. Otherwise we can't rewind any further
        for (var i = this.directiveIndex - 1; i >= 0; i--) {
          if (this.activeScene.directives[i].type === "pause") {
            canRevert = true;
            break;
    
      }
    
      if (!canRevert) { return; }
    
    
      var currentIndex = this.directiveIndex;
      for (var i = currentIndex - 1; i >= 0; i--) {
    
        this.directiveIndex = i;
        var directive = this.activeScene.directives[i];
        if (directive.action) {
          directive.action.revert(directive, directive.action.context);
        }
    
        if (i < currentIndex - 1 && directive.type === "pause") {
    
          return;
        }
      }
    
      //reached the start of the scene, so time to back up an entire scene
    
    
      if (this.sceneIndex >= this.epilogue.scenes.length) {
        this.sceneIndex--; //the last scene had finished, so back up an extra time to move past that scene
      }
    
    
      //it would be better to make scene setup/teardown an undoable action, but for a quick and dirty method for now, just fast forward the whole scene to its last pause
      this.sceneIndex--;
    
      this.setupScene(this.sceneIndex, true);
    
      if (!this.activeTransition) {
        var pauseIndex;
        for (pauseIndex = this.activeScene.directives.length - 1; pauseIndex >= 0; pauseIndex--) {
          if (this.activeScene.directives[pauseIndex].type === "pause") {
            break;
          }
        }
        while (this.directiveIndex < pauseIndex) {
          this.advanceDirective();
    
    fromHex = function (hex) {
      var value = parseInt(hex.substring(1), 16);
      var r = (value & 0xff0000) >> 16;
      var g = (value & 0x00ff00) >> 8;
      var b = (value & 0x0000ff);
      return [r, g, b];
    
    toHexPiece = function (v) {
      var hex = Math.round(v).toString(16);
      if (hex.length < 2) {
        hex = "0" + hex;
    
    spnati_edit's avatar
    spnati_edit committed
    
    
    toHex = function (rgb) {
      return "#" + this.toHexPiece(rgb[0]) + this.toHexPiece(rgb[1]) + this.toHexPiece(rgb[2]);
    }
    
    EpiloguePlayer.prototype.awaitAnims = function (directive, context) {
      for (var i = 0; i < this.views.length; i++) {
        if (this.views[i].isAnimRunning()) {
          this.waitingForAnims = true;
          return;
    
      this.advanceDirective();
    }
    
    function SceneView(container, index, assetMap) {
      this.scene = null;
      this.index = index;
    
      this.pendingDirectives = [];
    
      this.anims = [];
      this.camera = null;
      this.assetMap = assetMap;
      this.sceneObjects = {};
    
      this.viewportWidth = 0;
      this.viewportHeight = 0;
      this.particlePool = [];
    
      var viewport = this.$viewport = $("<div id='epilogue-viewport" + index + "' class='epilogue-viewport'></div>");
      this.$canvas = $("<div id='epilogue-canvas" + index + "' class='epilogue-canvas'></div>");
      viewport.append(this.$canvas);
      this.$overlay = $("<div id='epilogue-overlay" + index + "' class='epilogue-overlay'></div>");
      viewport.append(this.$overlay);
      this.$textContainer = $("<div id='epilogue-content" + index + "' class='epilogue-content'></div>");
      viewport.append(this.$textContainer);
      this.overlay = { rgb: [0, 0, 0], a: 0 };
      container.append(this.$viewport);
      viewport.hide();
    }
    
    SceneView.prototype.cleanup = function () {
      this.haltAnimations(true);
    
      //clear old textboxes
      this.$textContainer.empty();
    
    
      //clear old images
      for (var obj in this.sceneObjects) {
    
      this.sceneObjects = {};
    
      //hide until needed again
      this.$viewport.hide();
    }
    
    SceneView.prototype.destroy = function () {
      this.cleanup();
    
    
    SceneView.prototype.runDirective = function (epiloguePlayer, directive) {
      switch (directive.type) {
        case "sprite":
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.addSprite.bind(this), this.removeSceneObject.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.addText.bind(this), this.removeText.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.clearText.bind(this), this.restoreText.bind(this));
    
          break;
        case "clear-all":
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.clearAllText.bind(this), this.restoreText.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.moveSprite.bind(this), this.returnSprite.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.moveCamera.bind(this), this.returnCamera.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.fade.bind(this), this.restoreOverlay.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.stopAnimation.bind(this), this.restoreAnimation.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, epiloguePlayer.awaitAnims.bind(epiloguePlayer), function () { });
    
          return true;
        case "pause":
          return true;
        case "remove":
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.hideSceneObject.bind(this), this.showSceneObject.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.addEmitter.bind(this), this.removeSceneObject.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, this.burstParticles.bind(this), this.clearParticles.bind(this));
    
    spnati_edit's avatar
    spnati_edit committed
          this.addAction(directive, function () { }, function () { });
    
          break;
      }
      return false;
    }
    
    /**
     * Adds an undoable action to the history
     * @param {any} context Context to pass to do and undo functions
     * @param {Function} doFunc Function to perform the directive
     * @param {Function} undoFunc Function to undo the directive
     */
    
    spnati_edit's avatar
    spnati_edit committed
    SceneView.prototype.addAction = function (directive, doFunc, undoFunc) {
    
      var context = {}; //contextual information for the do action to store off that the revert action can refer to
    
    spnati_edit's avatar
    spnati_edit committed
      var action = { directive: directive, context: context, perform: doFunc, revert: undoFunc };
    
      directive.action = action;
      action.perform(directive, context);
    }
    
    SceneView.prototype.pendDirective = function (epiloguePlayer, directive, delay) {
      var info = { epiloguePlayer: epiloguePlayer, directive: directive };
      info.handle = setTimeout(this.runPendedDirective.bind(this), delay, info, true);
      this.pendingDirectives.push(info);
    }
    
    SceneView.prototype.runPendedDirective = function (info, remove) {
      info.handle = 0;
      this.runDirective(info.epiloguePlayer, info.directive);
      if (remove) {
        var index = this.pendingDirectives.indexOf(info);
        this.pendingDirectives.splice(index, 1);
      }
    }
    
    
    SceneView.prototype.updateOverlay = function (id, last, next, t) {
      if (typeof next.color !== "undefined") {
        var rgb1 = fromHex(last.color);
        var rgb2 = fromHex(next.color);
    
        var rgb = [0, 0, 0];
        for (var i = 0; i < rgb.length; i++) {
          rgb[i] = lerp(rgb1[i], rgb2[i], t);
    
      var alpha = lerp(last.alpha, next.alpha, t);
    
    SceneView.prototype.setOverlay = function (color, alpha) {
      if (typeof color !== "undefined") {
        this.overlay.rgb = color;
      }
      this.overlay.a = alpha;
      this.$overlay.css({
        "opacity": alpha / 100,
        "background-color": toHex(this.overlay.rgb)
    
    spnati_edit's avatar
    spnati_edit committed
      });
    
    SceneView.prototype.isActive = function () {
      if (this.anims.length > 0) {
        return true;
      }
      for (var obj in this.sceneObjects) {
        var sceneObj = this.sceneObjects[obj];
        if (sceneObj instanceof Emitter && (sceneObj.activeParticles.length > 0 || sceneObj.rate > 0)) {
          return true;
        }
      }
      return false;
    
    SceneView.prototype.update = function (elapsed) {
    
      var nonLoopingCount = this.pendingDirectives.length;
    
      for (var obj in this.sceneObjects) {
        this.sceneObjects[obj].update(elapsed);
    
    
      for (var i = this.anims.length - 1; i >= 0; i--) {
        var anim = this.anims[i];
        anim.update(elapsed);
        if (anim.isComplete()) {
          this.anims.splice(i, 1);
        }
        else {
          if (!anim.looped) {
            nonLoopingCount++;
          }
        }
    
    SceneView.prototype.draw = function () {
      for (var obj in this.sceneObjects) {
        this.sceneObjects[obj].draw();
    
    SceneView.prototype.drawObject = function (obj) {
      if (!obj.element) { return; }
      var properties = [
        "scale(" + this.viewportWidth / this.scene.width * this.camera.zoom + ")",
        "translate(" + this.toViewX(obj.x) + ", " + this.toViewY(obj.y) + ")"
      ];
      var transform = properties.join(" ");
    
      $(obj.element).css({
        "transform": transform,
        "transform-origin": "top left",
        "opacity": obj.alpha / 100,
      });
      $(obj.rotElement).css({
    
    spnati_edit's avatar
    spnati_edit committed
        "transform": "rotate(" + obj.rotation + "deg) scale(" + obj.scalex + ", " + obj.scaley + ") skew(" + obj.skewx + "deg, " + obj.skewy + "deg)",
    
      });
    }
    
    SceneView.prototype.toViewX = function (x) {
      var sceneWidth = this.camera.width;
      var offset = sceneWidth / this.camera.zoom / 2 - sceneWidth / 2 + x - this.camera.x;
      return offset + "px";
    }
    
    SceneView.prototype.toViewY = function (y) {
      var sceneHeight = this.camera.height;
      var offset = sceneHeight / this.camera.zoom / 2 - sceneHeight / 2 + y - this.camera.y;
      return offset + "px";
    }
    
    SceneView.prototype.setup = function (scene, sceneIndex, epilogue, lastScene) {
      this.scene = scene;
    
      //copy the overlay values from the previous scene
      if (lastScene) {
        this.setOverlay(lastScene.view.overlay.rgb, lastScene.view.overlay.a);
      }
    
      else {
        //otherwise clear them completely
        this.setOverlay([0, 0, 0], 0);
      }
    
    
      if (!scene.width) {
        //if no scene dimensions were provided, use the background image's dimensions
        var backgroundImg = this.assetMap[scene.background];
        if (backgroundImg) {
          scene.width = backgroundImg.naturalWidth;
          scene.height = backgroundImg.naturalHeight;
          scene.aspectRatio = backgroundImg.naturalWidth / backgroundImg.naturalHeight;
    
          //backwards compatibility: for really skinny ratios, we probably don't want to use it since it'll make textboxes really squished. Use the first scene's instead
          if (sceneIndex > 0) {
            var previousScene = epilogue.scenes[0];
            if (scene.aspectRatio < 0.5) {
              scene.width = previousScene.width;
              scene.height = previousScene.height;
              scene.aspectRatio = previousScene.aspectRatio;
            }
    
    spnati_edit's avatar
    spnati_edit committed
          }
    
        }
      }
    
      this.camera = {
        x: isNaN(scene.x) ? 0 : toSceneX(scene.x, scene),
        y: isNaN(scene.y) ? 0 : toSceneY(scene.y, scene),
        width: scene.width,
        height: scene.height,
        zoom: scene.zoom || 1,
      }
    
      this.initOverlay(scene.overlayColor, scene.overlayAlpha);
    
      if (scene.background) {
        this.addBackground(scene.background);
      }
      this.$viewport.css({
        "background-color": scene.color,
        "z-index": EpiloguePlayer.prototype.layer++,
      });
      this.$viewport.show();
    }
    
    SceneView.prototype.initOverlay = function (rgb, a) {
      var alpha;
      if (!this.overlay.rgb) {
        this.setOverlay([0, 0, 0], 0);
      }
      if (a) {
        alpha = parseInt(a, 10);
        if (typeof alpha === "undefined") {
          alpha = 100;
        }
      }
      else {
        alpha = this.overlay.a || 0;
      }
      if (rgb) {
        this.setOverlay(fromHex(rgb), alpha);
      }
    }
    
    SceneView.prototype.resize = function () {
      if (!this.scene) {
        return;
      }
      var windowHeight = $(window).height();
      var windowWidth = $(window).width();
    
      var viewWidth = this.scene.aspectRatio * windowHeight;
      var width = viewWidth;
      var height = windowHeight;
      if (viewWidth > windowWidth) {
        //take full width of window
        width = windowWidth;
        height = windowWidth / this.scene.aspectRatio;
      }
    
      width = Math.ceil(width);
      height = Math.ceil(height);
      this.viewportWidth = width;
      this.viewportHeight = height;
      this.$viewport.width(width);
      this.$viewport.height(height);
    
    
      for (var id in this.textObjects) {
        var box = this.textObjects[id];
        var directive = box.data("directive");
        this.applyTextDirective(directive, box);
      }
    
    }
    
    SceneView.prototype.haltAnimations = function (haltLooping) {
    
      for (var i = 0; i < this.pendingDirectives.length; i++) {
        var info = this.pendingDirectives[i];
        if (info.handle) {
          clearTimeout(info.handle);
          this.runPendedDirective(info, false);
        }
      }
      this.pendingDirectives = [];
    
    
      var animloop = this.anims.slice();
      var j = 0;
      for (var i = 0; i < animloop.length; i++) {
        if (haltLooping || !animloop[i].looped) {
          animloop[i].halt();
          this.anims.splice(j, 1);
        }
        else {
          j++;
        }
      }
      this.draw();
    }
    
    SceneView.prototype.addBackground = function (background) {
      var img = this.assetMap[background];
      this.addImage("background", background, { x: 0, y: 0, width: img.naturalWidth + "px", height: img.naturalHeight + "px" });
    }
    
    SceneView.prototype.addImage = function (id, src, args) {
      var img = document.createElement("img");
      img.src = this.assetMap[src].src;
    
    spnati_edit's avatar
    spnati_edit committed
      var obj = new SceneObject(id, img, this, args);
      obj.setImage(src);
      this.addSceneObject(obj);
    
    }
    
    SceneView.prototype.addSprite = function (directive) {
      this.addImage(directive.id, directive.src, directive);
    }
    
    SceneView.prototype.addSceneObject = function (obj) {
      this.sceneObjects[obj.id] = obj;
      if (obj.element) {
        this.$canvas.append(obj.element);
      }
      this.draw();
    }
    
    SceneView.prototype.removeSceneObject = function (directive) {
    
      this.sceneObjects[directive.id].destroy();
    
      delete this.sceneObjects[directive.id];
    }
    
    SceneView.prototype.hideSceneObject = function (directive, context) {
    
    spnati_edit's avatar
    spnati_edit committed
      var sceneObject = context.object = this.sceneObjects[directive.id];
    
      context.anims = {};
      if (context.object) {
        $(context.object.element).hide();
        this.stopAnimation(directive, context.anims);
    
    spnati_edit's avatar
    spnati_edit committed
        context.rate = sceneObject.rate;
        sceneObject.rate = 0;
        if (!sceneObject instanceof Emitter) {
            delete this.sceneObjects[directive.id];
        }
    
      }
    }
    
    SceneView.prototype.showSceneObject = function (directive, context) {
      var obj = context.object;
      if (obj) {
        this.sceneObjects[directive.id] = obj;
    
    spnati_edit's avatar
    spnati_edit committed
        this.sceneObjects[directive.id].rate = context.rate;
    
        this.restoreAnimation(directive, context.anims);
        $(obj.element).show();
      }
    }
    
    SceneView.prototype.addText = function (directive, context) {
    
      context.id = id;
      this.lastTextId = id;
    
    
        //reuse the DOM element if one of the same ID already exists
        context.oldDirective = box.data("directive");
      }
      else {
    
        box = $(document.createElement('div')).addClass('bordered dialogue-bubble');
    
        //attach new div element to the content div
        this.$textContainer.append(box[0]);
    
        box.data("id", id);
        this.textObjects[id] = box;
    
      }
      this.applyTextDirective(directive, box);
    }
    
    SceneView.prototype.removeText = function (directive, context) {
      this.lastTextId = context.id;
    
      var box = this.textObjects[directive.id];
    
      if (context.oldDirective) {
    
        this.applyTextDirective(context.oldDirective, box);
    
        this.$textContainer.get(0).removeChild(box[0]);
        delete this.textObjects[directive.id];
    
      }
    }
    
    SceneView.prototype.applyTextDirective = function (directive, box) {
    
      var content = expandDialogue(directive.text, null, players[HUMAN_PLAYER]);
    
      box.html('<span>' + content + '</span>');
      box.addClass(directive.arrow)
    
      box.attr('style', directive.css);
    
    
      //use css to position the box
      box.css('left', directive.x);
      box.css('top', directive.y);
      box.css('width', directive.width);
    
    
      var arrowHeight = (directive.arrow === "arrow-up" || directive.arrow === "arrow-down" ? 15 : 0);
      var arrowWidth = (directive.arrow === "arrow-left" || directive.arrow === "arrow-right" ? 15 : 0);
      switch (directive.alignmenty) {
        case "center":
          var height = box.height() + arrowHeight;
          var top = box.position().top;
          box.css("top", (top - height / 2) + "px");
          break;
        case "bottom":
          var height = box.height() + arrowHeight;
          var top = box.position().top;
          box.css("top", (top - height) + "px");
          break;
      }
      switch (directive.alignmentx) {
        case "center":
          var width = box.width() + arrowWidth;
          var left = box.position().left;
          box.css("left", (left - width / 2) + "px");
          break;
        case "right":
          var width = box.width() + arrowWidth;
          var left = box.position().left;
          box.css("left", (left - width) + "px");
          break;
      }
    
    
      box.data("directive", directive);
    }
    
    
    SceneView.prototype.clearAllText = function (directive, context) {
    
      var $this = this;
    
      context = context || {};
    
      for (var box in this.textObjects) {
        this.clearText({ id: this.textObjects[box].data("id") }, context, true);
      }
      this.textObjects = {};
    
    SceneView.prototype.clearText = function (directive, context, keepObject) {
    
      context.boxes = context.boxes || [];
      var boxContext = {};
      context.boxes.push(boxContext);
    
    
      var id = directive.id || this.lastTextId;
      boxContext.id = lastTextId = id;
      var box = this.textObjects[id];
    
    
      boxContext.directive = box.data("directive");
    
      this.$textContainer.get(0).removeChild(box[0]);
    
      if (!keepObject) {
        delete this.textObjects[id];
      }
    
    SceneView.prototype.restoreText = function (directive, context) {
    
      for (var i = 0; i < context.boxes.length; i++) {
        var boxContext = context.boxes[i];
        var id = this.lastTextId = boxContext.id;
        var directive = boxContext.directive;
        directive.id = id;
        this.addText(directive, {});
    
    spnati_edit's avatar
    spnati_edit committed
    SceneView.prototype.interpolate = function (obj, prop, last, next, t, mode) {
    
      var current = obj[prop];
      var start = last[prop];
      var end = next[prop];
    
    spnati_edit's avatar
    spnati_edit committed
      if (mode !== "none" && (typeof start === "undefined" || isNaN(start) || typeof end === "undefined" || isNaN(end))) {
    
    spnati_edit's avatar
    spnati_edit committed
      mode = mode || next.interpolation || "linear";
      obj[prop] = interpolationModes[mode](prop, start, end, t, last.keyframes, last.index);
    
    SceneView.prototype.updateObject = function (id, last, next, t) {
      var obj = this.sceneObjects[id];
      obj.interpolateProperties(last, next, t);
    
    SceneView.prototype.addAnimation = function (anim) {
    
      this.anims.push(anim);
    
    SceneView.prototype.moveSprite = function (directive, context) {
    
      var sprite = this.sceneObjects[directive.id];
      if (sprite) {
        var frames = directive.keyframes.slice();
        context.x = sprite.x;
        context.y = sprite.y;
        context.rotation = sprite.rotation;
    
        context.scalex = sprite.scalex;
        context.scaley = sprite.scaley;
    
    spnati_edit's avatar
    spnati_edit committed
        context.skewx = sprite.skewx;
        context.skewy = sprite.skewy;
    
        context.alpha = sprite.alpha;
    
    spnati_edit's avatar
    spnati_edit committed
        context.src = sprite.src;
    
        frames.unshift(context);
    
    spnati_edit's avatar
    spnati_edit committed
        context.anim = this.addAnimation(new Animation(directive.id, frames, this.updateObject.bind(this), directive.loop, directive.ease, directive.clamp, directive.iterations));
    
    SceneView.prototype.returnSprite = function (directive, context) {
    
      var sprite = this.sceneObjects[directive.id];
      if (sprite) {
        if (typeof context.x !== "undefined") {
          sprite.x = context.x;
        }
        if (typeof context.y !== "undefined") {
          sprite.y = context.y;
        }
        if (typeof context.rotation !== "undefined") {
          sprite.rotation = context.rotation;
        }
    
        if (typeof context.scalex !== "undefined") {
          sprite.scalex = context.scalex;
        }
        if (typeof context.scaley !== "undefined") {
          sprite.scaley = context.scaley;
    
    spnati_edit's avatar
    spnati_edit committed
        if (typeof context.skewx !== "undefined") {
          sprite.skewx = context.skewx;
        }
        if (typeof context.skewy !== "undefined") {
          sprite.skewy = context.skewy;
        }
    
        if (typeof context.alpha !== "undefined") {
          sprite.alpha = context.alpha;
        }
    
    spnati_edit's avatar
    spnati_edit committed
        if (typeof context.src !== "undefined") {
          sprite.setImage(context.src);
        }
    
    SceneView.prototype.removeAnimation = function (anim) {
      if (anim) {
        var index = this.anims.indexOf(anim);
        if (index >= 0) {
          this.anims.splice(index, 1);
        }
      }
    }
    
    
    SceneView.prototype.updateCamera = function (id, last, next, t) {
    
      this.interpolate(this.camera, "x", last, next, t);
      this.interpolate(this.camera, "y", last, next, t);
      if (last.zoom && next.zoom) {
        this.camera.zoom = lerp(last.zoom, next.zoom, t);
      }
    }
    
    
    SceneView.prototype.moveCamera = function (directive, context) {
    
      var frames = directive.keyframes.slice();
      context.x = this.camera.x;
      context.y = this.camera.y;
      context.zoom = this.camera.zoom;
      frames.unshift(context);
    
    spnati_edit's avatar
    spnati_edit committed
      context.anim = this.addAnimation(new Animation("camera", frames, this.updateCamera.bind(this), directive.loop, directive.ease, directive.clamp, directive.iterations));
    
    SceneView.prototype.returnCamera = function (directive, context) {
    
      if (typeof context.x !== "undefined") {
        this.camera.x = context.x;
      }
      if (typeof context.y !== "undefined") {
        this.camera.y = context.y;
      }
      if (context.zoom) {
        this.camera.zoom = context.zoom;
      }
    
    SceneView.prototype.fade = function (directive, context) {
      var color = toHex(this.scene.view.overlay.rgb);
    
      var frames = directive.keyframes.slice();
      context.color = color;
    
      context.alpha = this.scene.view.overlay.a;
    
      frames.unshift(context);
    
    spnati_edit's avatar
    spnati_edit committed
      context.anim = this.addAnimation(new Animation("fade", frames, this.updateOverlay.bind(this), directive.loop, directive.ease, directive.clamp, directive.iterations));
    
    SceneView.prototype.restoreOverlay = function (directive, context) {
    
      this.setOverlay(context.color, context.alpha);
    
    SceneView.prototype.isAnimRunning = function () {
    
      if (this.pendingDirectives.length > 0) {
        return true;
      }
    
      for (var i = 0; i < this.anims.length; i++) {
        if (!this.anims[i].looped) {
    
    SceneView.prototype.stopAnimation = function (directive, context) {
    
      var anim;
      var id = directive.id;
      context.haltedAnims = [];
      for (var i = this.anims.length - 1; i >= 0; i--) {
        anim = this.anims[i];
        if (anim.id === id) {
          anim.halt();
          this.anims.splice(i, 1);
          context.haltedAnims.push(anim);
          this.draw();
    
    SceneView.prototype.restoreAnimation = function (directive, context) {
    
      var haltedAnims = context.haltedAnims;
      for (var i = 0; i < haltedAnims.length; i++) {
        var anim = haltedAnims[i];
        anim.elapsed = 0;
    
        this.addAnimation(anim);
      }
    }
    
    SceneView.prototype.addEmitter = function (directive, context) {
      var element;
    
      if (directive.src) {
        var srcImg = this.assetMap[directive.src];
        directive.width = directive.width || srcImg.naturalWidth;
        directive.height = directive.height || srcImg.naturalHeight;
      }
    
      this.addSceneObject(new Emitter(directive.id, element, this, directive, this.particlePool));
    }
    
    
    SceneView.prototype.burstParticles = function (directive, context) {
      var emitter = this.sceneObjects[directive.id];
      if (emitter && emitter.emit) {
        context.emitter = emitter;
        for (var i = 0; i < directive.count; i++) {
          emitter.emit();
        }
      }
    }
    
    SceneView.prototype.clearParticles = function (directive, context) {
      var emitter = context.emitter;
      if (emitter) {
        context.emitter = emitter;
        for (var i = 0; i < directive.count; i++) {
          emitter.killParticles();
        }
      }
    }
    
    
    function RandomParameter(startValue, endValue) {
      this.start = startValue;
      this.end = endValue;
    }