<commit_before>/*!
* ..Strip.js, uly may2012..
*
* organized like jQuery v1.7.2
*
* most of the Strip methods are intended to work with the "cascading-jsp-style".
*
*/
(function( window, undefined ){ 

var warn = function( msg ) {
  try {
    console.warn( "DEBUG::"+msg );
  }
  catch ( e ) {
    //..
  } 
};

var debug = function( msg ) {
  try {
    console.log( "DEBUG::"+msg );
  }
  catch ( e ) {
    //..
  } 
};

var Strip = (function() {
  debug("Strip..");

// "Local" copy of Strip.
var Strip = function() {
    debug("Strip local init..");
    return new Strip.fn.init();
  },
  Sequence = function() {},
  Shot =     function() {},
  Scene =    function() {},
  Text =     function() {},
  Dialogue = function() {};

/*
*   public methods
*/
Strip.fn = Strip.prototype = {

    constructor: Strip,

    init: function() { 
      return this;
    },

    // (Strip.Sequence) the particular sequence we are representing now.
    sequence: null,

    //
    //  load a sequence/chapter onto our stage.
    //
    loadSequence: function( sequence ) {
      debug( "strip.loadSequence, sequence:"+sequence );
      this.sequence = sequence;
      return this;
    },

    // the html5 canvas we will draw on.
    canvas: null,

    ctx: null,

    /**
    * chainable,
    * add a bunch of images and automagically 
    * setup the internal sequence/scene model.
    *
    * @param images - an array of html5 images.
    */
    addImages: function( images ) {
      debug("..addImages..");
      var sequence = this.sequence || this.newSequence(),
        scene,
        shot,
        image,
        i = 0,
        len = images.length;
      debug( "len:"+len );
//    $.each(images, function(idx, image) {
      for ( ; i<len; i++ ) {
        image = images[i];
        debug( "image:"+image );
        // TODO:newShot @image param
        scene = new Scene.fn.init();
        shot =  new Shot.fn.init();
        window.image3 = image;
        shot = shot.setImage( image );
        //scene.setShot( shot.setImage( image ) );
        scene.setShot( shot );
        sequence.pushScene( scene );
//    });
      }
      this.loadSequence( sequence );
      return this;
    },

    /*
    * draw the previous/next buttons for the strip.
    * @param ctx - the 2d drawing context.
    */
    draw_buttons: function( ctx ) { 
      var h = this.canvas.height,
        w = this.canvas.width,
        bh = this.button_height,
        bw = this.button_width;

      var next_x = 0,
        next_y = h - bh,
        prev_x = w - bw,
        prev_y = h - bh,
        mid_bh = bh / 2,
        mid_bw = bw / 2,
        guess = 20;

      ctx.save();

      ctx.globalAlpha = 0.2;
      ctx.fillStyle = '#0000aa';
      ctx.fillRect( next_x, next_y, bw, bh );
      ctx.fillRect( prev_x, prev_y, bw, bh );

      ctx.globalAlpha = 1;
//    ctx.fillText( 'next -->', prev_x + mid_bw, prev_y + mid_bh );
//    ctx.fillText( '<-- prev', next_x + mid_bw, next_y + mid_bh );

      ctx.fillText( 'next -->', 
        prev_x + mid_bw - guess, 
        prev_y + mid_bh );
      ctx.fillText( '<-- prev', 
        next_x + mid_bw - guess , 
        next_y + mid_bh );
      ctx.restore(); 
    },

    /*
    * dimensions for our Strip next/previous buttons.
    */
    button_height: 25, 
    button_width: 75,
    canvas_height: -1,
    canvas_width: -1,

    /*
    * is point x,y within the bounds of box, xy 1~4
    * where x1,y1 is the top left corner, and it goes clockwise
    * (ie. bottom left corner is designated as x4, y4).
    *
    * TODO:this does not anticipate none orthogonal shapes.
    *
    * @return boolean
    */
    is_coords_within_box: function( x,  y, 
                          x1, y1, ///* top left */
                          x2, y2, ///* top right */
                          x3, y3, ///* bottom right */
                          x4, y4  ///* bottom left */ 
                          ) { 
//    debug( "x,y:"+x+","+y );
//    debug( "x1,y1:"+x1+","+y1 );
//    debug( "x2,y2:"+x2+","+y2 );
//    debug( "x3,y3:"+x3+","+y3 );
//    debug( "x4,y4:"+x4+","+y4 );
      return ( x >= x1
        && x <= x2
        && y >= y1
        && y <= y4 );
    },

    /*
    * is point x,y within the region of a rectangle,
    * the rectangle is described by it's top left coordinates (x1,y1),
    * and it's dimensions of width and height.
    *
    * @param x - the x coordinate of the point of interest.
    * @param y - the y coordinate of the point of interest.
    * @param x1 top left x coordinate of the rectangle.
    * @param y1 top left y coordinate of the rectangle
    * @param w width of the rectangular region.
    * @param h height of the rectangular region.
    *
    * @return boolean
    */
    is_coords_within_rectangle: function( x, y, x1, y1, w, h ) {
      return this.is_coords_within_box( 
        x,    y,  
        x1,   y1,
        x1+w, y1,
        x1+w, y1+h,
        x1,   y1+h ); 
    },

    go_back: function() {
      debug( "go_back" );
      this.previous(); 
      return this;
    },

    go_forward: function() {
      debug( "go_forward" );
      this.next();
      return this;
    },

    // public setter for our Strip's canvas.
    setCanvas: function( canvas ) {
      var strip = this;
      debug("canvas:"+canvas); 

      // TODO:remove listeners...
      canvas.parent_strip = this; // TODO:i feel dirty about this...
      canvas.addEventListener('click', 
        function(e){strip.handle_canvas_click(strip,e)}, false );
      window.addEventListener('keyup', 
        function(e){strip.handle_canvas_keypress(strip,e)}, false ); 
      this.canvas = canvas;
      this.ctx =    canvas.getContext('2d');
      this.canvas_height = canvas.height;
      this.canvas_width =  canvas.width;
      debug("strip.setCanvas, ctx:"+this.ctx); 
      return this;
    },

    /*
    * @private
    * handle canvas mouse clicks.
    *
    * we have to carefully bind the scope here back to strip...
    *
    * @param e - mouse click event.
    * @param strip - the strip handling the mouse click.
    */
    handle_canvas_click: function( strip, e ) {
      debug("handle_canvas_click");
      var x = e.offsetX,
        y = e.offsetY,
        button_height = strip.button_height,
        button_width =  strip.button_width,
        height = strip.canvas_height,
        width = strip.canvas_width;
      window.canvas_click = e;  // TODO:remove

      // TODO abstract button click regions...

      //
      // the <- button on the bottom left.
      //
      if ( strip.is_coords_within_rectangle.apply( strip, [ x, y,
          0, height - button_height,
          button_width, button_height ] ) ) {
        strip.go_back.apply(strip);
      //
      // the -> button on the bottom right.
      //
      } else if ( strip.is_coords_within_rectangle.apply( strip, [ x, y,  
          width - button_width, height - button_height,
          button_width, button_height ] ) ) {
        strip.go_forward.apply(strip);
      } 
    },

    /*
    * @private,
    *
    * handle canvas keyboard presses.
    *
    * @param strip - the passed in strip scope.
    * @param e - the keypress event.
    */
    handle_canvas_keypress: function( strip, e ) {
      debug("handle_canvas_keypress");
      var button_height = strip.button_height,
        button_width =  strip.button_width,
        height = strip.canvas_height,
        width = strip.canvas_width;
      window.keypress_event = e;  // TODO:remove
      console.log( "keyCode:"+e.keyCode );

      //
      // see http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
      //
      switch ( e.keyCode ) {
        case 39:  // -->
        case 32:  // spacebar
          strip.go_forward.apply(strip);
          break;
        case 37:  // <--
        case 8:   // backspace
          strip.go_back.apply(strip);
          break;
        default:
          console.warn( "TODO: handle keyCode:"+e.keyCode );
          break;
      } 
    }, 

    /**
    * Strip.action
    * synonymous with "start".
    */
    action: function() {
      debug("strip.action");
      if ( !this.sequence ) {
        debug( "sequence is null!" );
      }
      this.clear_canvas();
      this.sequence.setContext(this.ctx).action();
      this.draw_buttons(this.ctx);
      return this;
    },

    /**
    * Strip.clear_canvas.
    */
    clear_canvas: function() {
      var ctx = this.ctx,
        h = this.canvas.height,
        w = this.canvas.width;
      ctx.clearRect( 0, 0, w, h );
      ctx.fillStyle = '#808080';
      ctx.fillRect( 0, 0, w, h );
      return this;
    },

    // synonimous with "stop".
    cut: function() {
      this.sequence.cut();
      return this;
    },

    // go to the next scene.
    next: function() { 
      return this.change_scenes(true);
    },

    // go to the previous scene.
    previous: function() { 
      return this.change_scenes(false);
    },

    /*
    * change scenes for our strip, called by next/previous.
    *
    * @param next - boolean, when true will increment the selected scene, 
    *               otherwise it will go backwards.
    */
    change_scenes: function( next ) {
      console.log("change_scenes, next:"+next);
      next = ( typeof next !== 'undefined' )? next : true;

      var strip = this,
        sequence = this.sequence;

      // TODO:move this logic to live under sequence...
      if ( next ) {
        sequence.current_scene_index += 1;
      } else { 
        sequence.current_scene_index -= 1;
      } 
      if ( sequence.current_scene_index > sequence.number_of_scenes - 1 ) {
        sequence.current_scene_index = sequence.number_of_scenes - 1;
      } else if ( sequence.current_scene_index < 0 ) { 
        sequence.current_scene_index = 0;
      }

      // TODO call action instead!
      strip.clear_canvas();
      sequence.load_current_scene(); 
      strip.draw_buttons(strip.ctx);
      return this; 
    },

    newSequence: function() {
      debug("Strip.newSequence");
      return new Sequence.fn.init();
    },

    newShot: function( image ) {
      debug("Strip.newShot");
      var shot = new Shot.fn.init();
      image && shot.setImage( image );
      return shot;
    },

    newScene: function( image, lines ) {
      debug("Strip.newScene()");
      var scene = new Scene.fn.init(),
        strip = this;

      image && scene.setShot(     strip.newShot(     image ) );
      lines && scene.setDialogue( strip.newDialogue( lines ) ); 
      return scene;
    },

    /**
    * add a new scene to our strip, 
    * and automatically include it into our current scene sequence.
    *
    * @return (Scene) - the newly created scene.
    */
    addNewScene: function( image, lines ) { 
      var scene,
        strip = this; 
      scene = strip.newScene( image, lines );
      strip.sequence = strip.sequence || strip.newSequence();
      strip.sequence.pushScene( scene );

      return scene;
    },

    newDialogue: function( lines ) {
      debug("Strip.newDialogue()");
      var dialogue = new Dialogue.fn.init();
      lines && dialogue.addLines( lines );      
      return dialogue;
    },

    newText: function(msg) {
      debug("strip.newText");
      return ( new Text.fn.init() ).setMessage(msg);
    }

  };

//---------------------------------------------------------------------------
/*
*   the Sequence "class".
*   represents an ordered collection of scenes to be drawn by the strip.
*/
Sequence.fn = Sequence.prototype = {

  constructor: Sequence,

  init: function() { 
    return this;
  },

  selected_scene_index: 0, 

  canvas: null,

  // the drawing context.
  ctx: null,  

  current_scene_index: -1, 

  scenes: [],

  action: function() {
    debug("sequence, action");
    if ( this.current_scene_index < 0 ) {
      this.current_scene_index = 0;
    }
    this.load_current_scene();
    return this;
  },

  //
  //  load the currently selected scene by index.
  //
  load_current_scene: function() {
    debug("load_current_scene");
    var scene,
      idx = this.current_scene_index,
      ctx = this.ctx;

    debug( "idx:"+idx );
    scene = this.scenes[ idx ]; 

    debug( "scene:"+scene );
    debug( "ctx:"+ctx );    

    scene.setup( ctx );

    return this;
  },

  // load/setup a scene.
  loadScene: function( scene ) { 
    var i = 0, 
      len = this.scenes.length, 
      scenes = this.scenes,
      selected_scene_index = -1;

    for ( ;i < len; i++ ) {
      if ( scene === scenes[i] ) {  // TODO does this work?  
        selected_scene_index = i; 
      }
    }

    scene.setup( this.ctx );

    return this;
  },

  number_of_scenes: 0,

  // add a scene to the sequence stack.
  pushScene: function( scene ) {
    var sequence = this;
    sequence.scenes = sequence.scenes || [];
    sequence.scenes.push( scene );
    sequence.number_of_scenes = sequence.scenes.length;
    return sequence;
  },

  clearScenes: function() {
    this.scenes = [];
    this.number_of_scenes = 0;
    return this;
  },

  setContext: function( ctx ) { 
    debug( "sequence.setContext, ctx:"+ctx );
    this.ctx = ctx; 
    return this;
  } 
};
//---------------------------------------------------------------------------
/*
* A Scene represents a visual, and possibly some text;
* parent to Shot, and Dialogue.
* 1+ Scenes make up a Sequence.
*/
Scene.fn = Scene.prototype = { 
  constructor: Scene, 
  init: function() { 
    return this;
  },

  setShot: function( shot ) {
    this.shot = shot;
    return this;
  }, 

// TODO, this would be a nice wrapper...
//addWords: function( s ) {
//  dialogue = dialogue || Strip.newDialogue();
//  dialogue.addText( Text..newText( s ) ); //TODO:this will fail
//  return this;
//},

  setup: function( ctx ) {
    debug( "scene.setup, ctx:"+ctx );<commit_msg>added move dialogue forward/backward methods. NEXT:drawing next/previous is iffy. Need to add logic for speech bubbles to "change" their index.<commit_after>/*!
* ..Strip.js, uly may2012..
*
* organized like jQuery v1.7.2
*
* most of the Strip methods are intended to work with the "cascading-jsp-style".
*
*/
(function( window, undefined ){ 

var warn = function( msg ) {
  try {
    console.warn( "DEBUG::"+msg );
  }
  catch ( e ) {
    //..
  } 
};

var debug = function( msg ) {
  try {
    console.log( "DEBUG::"+msg );
  }
  catch ( e ) {
    //..
  } 
};

var Strip = (function() {
  debug("Strip..");

// "Local" copy of Strip.
var Strip = function() {
    debug("Strip local init..");
    return new Strip.fn.init();
  },
  Sequence = function() {},
  Shot =     function() {},
  Scene =    function() {},
  Text =     function() {},
  Dialogue = function() {};

/*
*   public methods
*/
Strip.fn = Strip.prototype = {

    constructor: Strip,

    init: function() { 
      return this;
    },

    // (Strip.Sequence) the particular sequence we are representing now.
    sequence: null,

    //
    //  load a sequence/chapter onto our stage.
    //
    loadSequence: function( sequence ) {
      debug( "strip.loadSequence, sequence:"+sequence );
      this.sequence = sequence;
      return this;
    },

    // the html5 canvas we will draw on.
    canvas: null,

    ctx: null,

    /**
    * chainable,
    * add a bunch of images and automagically 
    * setup the internal sequence/scene model.
    *
    * @param images - an array of html5 images.
    */
    addImages: function( images ) {
      debug("..addImages..");
      var sequence = this.sequence || this.newSequence(),
        scene,
        shot,
        image,
        i = 0,
        len = images.length;
      debug( "len:"+len );
//    $.each(images, function(idx, image) {
      for ( ; i<len; i++ ) {
        image = images[i];
        debug( "image:"+image );
        // TODO:newShot @image param
        scene = new Scene.fn.init();
        shot =  new Shot.fn.init();
        window.image3 = image;
        shot = shot.setImage( image );
        //scene.setShot( shot.setImage( image ) );
        scene.setShot( shot );
        sequence.pushScene( scene );
//    });
      }
      this.loadSequence( sequence );
      return this;
    },

    /*
    * draw the previous/next buttons for the strip.
    * @param ctx - the 2d drawing context.
    */
    draw_buttons: function( ctx ) { 
      var h = this.canvas.height,
        w = this.canvas.width,
        bh = this.button_height,
        bw = this.button_width;

      var next_x = 0,
        next_y = h - bh,
        prev_x = w - bw,
        prev_y = h - bh,
        mid_bh = bh / 2,
        mid_bw = bw / 2,
        guess = 20;

      ctx.save();

      ctx.globalAlpha = 0.2;
      ctx.fillStyle = '#0000aa';
      ctx.fillRect( next_x, next_y, bw, bh );
      ctx.fillRect( prev_x, prev_y, bw, bh );

      ctx.globalAlpha = 1;
//    ctx.fillText( 'next -->', prev_x + mid_bw, prev_y + mid_bh );
//    ctx.fillText( '<-- prev', next_x + mid_bw, next_y + mid_bh );

      ctx.fillText( 'next -->', 
        prev_x + mid_bw - guess, 
        prev_y + mid_bh );
      ctx.fillText( '<-- prev', 
        next_x + mid_bw - guess , 
        next_y + mid_bh );
      ctx.restore(); 
    },

    /*
    * dimensions for our Strip next/previous buttons.
    */
    button_height: 25, 
    button_width: 75,
    canvas_height: -1,
    canvas_width: -1,

    /*
    * is point x,y within the bounds of box, xy 1~4
    * where x1,y1 is the top left corner, and it goes clockwise
    * (ie. bottom left corner is designated as x4, y4).
    *
    * TODO:this does not anticipate none orthogonal shapes.
    *
    * @return boolean
    */
    is_coords_within_box: function( x,  y, 
                          x1, y1, ///* top left */
                          x2, y2, ///* top right */
                          x3, y3, ///* bottom right */
                          x4, y4  ///* bottom left */ 
                          ) { 
//    debug( "x,y:"+x+","+y );
//    debug( "x1,y1:"+x1+","+y1 );
//    debug( "x2,y2:"+x2+","+y2 );
//    debug( "x3,y3:"+x3+","+y3 );
//    debug( "x4,y4:"+x4+","+y4 );
      return ( x >= x1
        && x <= x2
        && y >= y1
        && y <= y4 );
    },

    /*
    * is point x,y within the region of a rectangle,
    * the rectangle is described by it's top left coordinates (x1,y1),
    * and it's dimensions of width and height.
    *
    * @param x - the x coordinate of the point of interest.
    * @param y - the y coordinate of the point of interest.
    * @param x1 top left x coordinate of the rectangle.
    * @param y1 top left y coordinate of the rectangle
    * @param w width of the rectangular region.
    * @param h height of the rectangular region.
    *
    * @return boolean
    */
    is_coords_within_rectangle: function( x, y, x1, y1, w, h ) {
      return this.is_coords_within_box( 
        x,    y,  
        x1,   y1,
        x1+w, y1,
        x1+w, y1+h,
        x1,   y1+h ); 
    },

    go_back: function() {
      debug( "go_back" );
      this.previous(); 
      return this;
    },

    go_forward: function() {
      debug( "go_forward" );
      this.next();
      return this;
    },

    // public setter for our Strip's canvas.
    setCanvas: function( canvas ) {
      var strip = this;
      debug("canvas:"+canvas); 

      // TODO:remove listeners...
      canvas.parent_strip = this; // TODO:i feel dirty about this...
      canvas.addEventListener('click', 
        function(e){strip.handle_canvas_click(strip,e)}, false );
      window.addEventListener('keyup', 
        function(e){strip.handle_canvas_keypress(strip,e)}, false ); 
      this.canvas = canvas;
      this.ctx =    canvas.getContext('2d');
      this.canvas_height = canvas.height;
      this.canvas_width =  canvas.width;
      debug("strip.setCanvas, ctx:"+this.ctx); 
      return this;
    },

    /*
    * @private
    * handle canvas mouse clicks.
    *
    * we have to carefully bind the scope here back to strip...
    *
    * @param e - mouse click event.
    * @param strip - the strip handling the mouse click.
    */
    handle_canvas_click: function( strip, e ) {
      debug("handle_canvas_click");
      var x = e.offsetX,
        y = e.offsetY,
        button_height = strip.button_height,
        button_width =  strip.button_width,
        height = strip.canvas_height,
        width = strip.canvas_width;
      window.canvas_click = e;  // TODO:remove

      // TODO abstract button click regions...

      //
      // the <- button on the bottom left.
      //
      if ( strip.is_coords_within_rectangle.apply( strip, [ x, y,
          0, height - button_height,
          button_width, button_height ] ) ) {
        strip.go_back.apply(strip);
      //
      // the -> button on the bottom right.
      //
      } else if ( strip.is_coords_within_rectangle.apply( strip, [ x, y,  
          width - button_width, height - button_height,
          button_width, button_height ] ) ) {
        strip.go_forward.apply(strip);
      } 
    },

    /*
    * @private,
    *
    * handle canvas keyboard presses.
    *
    * @param strip - the passed in strip scope.
    * @param e - the keypress event.
    */
    handle_canvas_keypress: function( strip, e ) {
      debug("handle_canvas_keypress");
      var button_height = strip.button_height,
        button_width =  strip.button_width,
        height = strip.canvas_height,
        width = strip.canvas_width;
      window.keypress_event = e;  // TODO:remove
      console.log( "keyCode:"+e.keyCode );

      //
      // see http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
      //
      switch ( e.keyCode ) {
        case 39:  // -->
        case 32:  // spacebar
          strip.go_forward.apply(strip);
          break;
        case 37:  // <--
        case 8:   // backspace
          strip.go_back.apply(strip);
          break;
        default:
          console.warn( "TODO: handle keyCode:"+e.keyCode );
          break;
      } 
    }, 

    /**
    * Strip.action
    * synonymous with "start".
    */
    action: function() {
      debug("strip.action");
      if ( !this.sequence ) {
        debug( "sequence is null!" );
      }
      this.clear_canvas();
      this.sequence.setContext(this.ctx).action();
      this.draw_buttons(this.ctx);
      return this;
    },

    /**
    * Strip.clear_canvas.
    */
    clear_canvas: function() {
      var ctx = this.ctx,
        h = this.canvas.height,
        w = this.canvas.width;
      ctx.clearRect( 0, 0, w, h );
      ctx.fillStyle = '#808080';
      ctx.fillRect( 0, 0, w, h );
      return this;
    },

    // synonimous with "stop".
    cut: function() {
      this.sequence.cut();
      return this;
    },

    // go to the next scene.
    next: function() { 
      var sequence = this.sequence;
      if ( sequence.is_at_end_of_current_scene() ) {
        this.change_scenes(true);
      } else {
        sequence.move_dialogue_forward();
      }
      return this; 
    },

    // go to the previous scene.
    previous: function() { 
      var sequence = this.sequence;
      if ( sequence.is_at_beginning_of_current_scene() ) {
        this.change_scenes(false);
      } else {
        sequence.move_dialogue_backward();
      }
      return this;
    },

    /*
    * change scenes for our strip, called by next/previous.
    *
    * @param next - boolean, when true will increment the selected scene, 
    *               otherwise it will go backwards.
    */
    change_scenes: function( next ) {
      console.log("change_scenes, next:"+next);
      next = ( typeof next !== 'undefined' )? next : true;

      var strip = this,
        sequence = this.sequence,
        scene_incrementor = (next)? 1 : -1;

      // TODO:should all of this logic to live under sequence?
      sequence.current_scene_index += scene_incrementor;

      // catch falling off edges
      if ( sequence.current_scene_index > sequence.number_of_scenes - 1 ) {
        sequence.current_scene_index = sequence.number_of_scenes - 1;
      } else if ( sequence.current_scene_index < 0 ) { 
        sequence.current_scene_index = 0;
      } 

      // TODO call action instead!
      strip.clear_canvas();
      sequence.load_current_scene(); 
      strip.draw_buttons(strip.ctx);
      return this; 
    },

    newSequence: function() {
      debug("Strip.newSequence");
      return new Sequence.fn.init();
    },

    newShot: function( image ) {
      debug("Strip.newShot");
      var shot = new Shot.fn.init();
      image && shot.setImage( image );
      return shot;
    },

    newScene: function( image, lines ) {
      debug("Strip.newScene()");
      var scene = new Scene.fn.init(),
        strip = this;

      image && scene.setShot(     strip.newShot(     image ) );
      lines && scene.setDialogue( strip.newDialogue( lines ) ); 
      return scene;
    },

    /**
    * add a new scene to our strip, 
    * and automatically include it into our current scene sequence.
    *
    * @return (Scene) - the newly created scene.
    */
    addNewScene: function( image, lines ) { 
      var scene,
        strip = this; 
      scene = strip.newScene( image, lines );
      strip.sequence = strip.sequence || strip.newSequence();
      strip.sequence.pushScene( scene );

      return scene;
    },

    newDialogue: function( lines ) {
      debug("Strip.newDialogue()");
      var dialogue = new Dialogue.fn.init();
      lines && dialogue.addLines( lines );      
      return dialogue;
    },

    newText: function(msg) {
      debug("strip.newText");
      return ( new Text.fn.init() ).setMessage(msg);
    }

  };

//---------------------------------------------------------------------------
/*
*   the Sequence "class".
*   represents an ordered collection of scenes to be drawn by the strip.
*/
Sequence.fn = Sequence.prototype = {

  constructor: Sequence,

  init: function() { 
    return this;
  },

  selected_scene_index: 0, 

  canvas: null,

  // the drawing context.
  ctx: null,  

  current_scene_index: -1, 

  scenes: [],

  action: function() {
    debug("sequence, action");
    if ( this.current_scene_index < 0 ) {
      this.current_scene_index = 0;
    }
    this.load_current_scene();
    return this;
  },

  /**
  * return the sequence's currently selected scene.
  * @return Scene.
  */
  get_current_scene: function() {
    var scene,
      idx = this.current_scene_index;

    debug( "idx:"+idx );
    scene = this.scenes[ idx ]; 
    debug( "scene:"+scene );

    return scene;
  },

  /**
  * is the currently selected scene in our sequence all played out?
  * in other words, are we ready for the next scene to be displayed?
  *
  * @return boolean
  */ 
  is_at_end_of_current_scene: function() {
    var seq = this;
    return seq.get_current_scene().is_at_end(); 
  },

  /**
  * is the currently selected scene in the sequence at the very beginning?
  *
  * @return boolean
  */ 
  is_at_beginning_of_current_scene: function() {
    var seq = this;
    return seq.get_current_scene().is_at_beginning();
  },

  move_dialogue_forward: function() {
    var seq = this;
    return seq.get_current_scene().move_dialogue_forward();
  },

  move_dialogue_backward: function() {
    var seq = this;
    return seq.get_current_scene().move_dialogue_backward();
  },

  //
  //  load the currently selected scene by index.
  //
  load_current_scene: function() {
    debug("load_current_scene");
    var scene = this.get_current_scene(),
      ctx = this.ctx; 
    debug( "ctx:"+ctx );    
    scene.setup( ctx );

    return this;
  },

  // load/setup a scene.
  loadScene: function( scene ) { 
    var i = 0, 
      len = this.scenes.length, 
      scenes = this.scenes,
      selected_scene_index = -1;

    for ( ;i < len; i++ ) {
      if ( scene === scenes[i] ) {  // TODO does this work?  
        selected_scene_index = i; 
      }
    }

    scene.setup( this.ctx );

    return this;
  },

  number_of_scenes: 0,

  // add a scene to the sequence stack.
  pushScene: function( scene ) {
    var sequence = this;
    sequence.scenes = sequence.scenes || [];
    sequence.scenes.push( scene );
    sequence.number_of_scenes = sequence.scenes.length;
    return sequence;
  },

  clearScenes: function() {
    this.scenes = [];
    this.number_of_scenes = 0;
    return this;
  },

  setContext: function( ctx ) { 
    debug( "sequence.setContext, ctx:"+ctx );
    this.ctx = ctx; 
    return this;
  } 
};
//---------------------------------------------------------------------------
/*
* A Scene represents a visual, and possibly some text;
* parent to Shot, and Dialogue.
* 1+ Scenes make up a Sequence.
*/
Scene.fn = Scene.prototype = { 
  constructor: Scene, 
  init: function() { 
    return this;
  },

  setShot: function( shot ) {
    this.shot = shot;
    return this;
  }, 



  /**
  * is the currently selected scene in our sequence all played out?
  * in other words, are we ready for the next scene to be displayed?
  *
  * @return boolean
  */
  is_at_end: function() {
    var dialogue = this.dialogue;
    if ( 'undefined' === typeof dialogue ) {
      return true; 
    } 
    return dialogue.texts.length - 1 === dialogue.selected_index;
  },

  /**
  * is the currently selected scene in the sequence at the very beginning?
  *
  * @return boolean
  */
  is_at_beginning: function() {
    var dialogue = this.dialogue;
    if ( 'undefined' === typeof dialogue ) {
      return true; 
    }
    return 0 === dialogue.selected_index;
  },

  /**
  * move the dialogue forward by one speech bubble.
  */
  move_dialogue_forward: function() {
    return this.move_dialogue( true );
  },

  /**
  * move the dialogue backward by one speech bubble.
  */
  move_dialogue_backward: function() {
    return this.move_dialogue( false );
  },

  move_dialogue: function( forward ) {
    var dialogue = this.dialogue,
      max_text_index = this.dialogue.texts.length - 1;
    if ( 'undefined' === typeof dialogue ) { 
      return this; 
    }

    if ( forward ) {
      dialogue.selectedIndex += 1;
    } else { 
      dialogue.selectedIndex -= 1;
    }

    if ( dialogue.selectedIndex < 0 ) {
      dialogue.selectedIndex = 0;
    } else if ( dialogue.selectedIndex > max_text_index  ) {
      dialogue.selectedIndex = max_text_index;
    } 

    return this; 
  },

  setup: function( ctx ) {
    debug( "scene.setup, ctx:"+ctx );
{'fraction_non_alphanumeric': 0.14194314359211133, 'fraction_numerical': 0.007750148407097157, 'mean_word_length': 1.9120330356285413, 'pattern_counts': {'":': 0, '<': 24, '<?xml version=': 0, '>': 18, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2791985', 'n_tokens_mistral': 9897, 'n_tokens_neox': 9469, 'n_words': 2486}