COMPOSITE -Yet another Tumblr photo viewer-

By koyachi Last update Jun 16, 2007 — Installed 500 times.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name       COMPOSITE -Yet another Tumblr photo viewer-
// @namespace  http://d.hatena.ne.jp/koyachi
// @include    http://*.tumblr.com/
// ==/UserScript==
//
// 2007-06-16 t.koyachi
//   Adjustment of timing, prefetch_remain. fader,...
//
// 2007-06-12 t.koyachi
//   Bug fix(href when paused)
//
// 2007-06-11 t.koyachi
//   Fix first 100 photos restirction.
//
// 2007-06-10 t.koyachi
//   First release.
//
// Many classes/functions are based on STROBO(http://userscripts.org/scripts/show/9370).
//

GM_registerMenuCommand('Composite Tumblr Photos', function() {

  var Config = {
//    SELECT_TARGETS_NUM: 100,
    SELECT_TARGETS_NUM: 30,
    SELECT_NUM: 2,
    WIDTH: 400,
    INTERVAL: 1800,
    BOOTTIMEOUT: 1100,
    HISTORY_SIZE: 21, // history(20) + prefetch(1)
    ENTRY_PREFETCH_NUM: 1,
    PAGE_PREFETCH_REMAIN: 7,
    FADER_COLOR_START: 0xFF0000,
    FADER_COLOR_END: 0xFFFFFF
  };

  var KEY_SPACEBAR = 32;
  var KEY_J = 74;
  var KEY_K = 75;
  
  GM_addStyle(<><![CDATA[
                         #lightbox{
                           display: block;
                           position: absolute;
                           left: 0;
                           top: 0;
                           background-color: #fff;
                           width: 100%;
                           height: 100%;
                           overflow : hidden;
                         }
                         
                         #status {
                           display: none;
                           font-size: xx-large;
                           font-weight: bolder;
                           color: #FFF;
                           background-color: #444;
                           width: 100%;
                           padding: 20px;
                         }
                         
                         #info {
                           display: none;
                           position: relative;
                           top: -20px;
                           float: right;
                           color: #fff;
                         }
                         
                         #lightbox img{
                           border : none;
                           xborder: solid 3px;
                           padding: 5px;
                           background-color: #f00;
                         }

                         #lightbox .image{
                           border : none;
                           xborder: solid 3px;
                           padding: 5px;
                           xbackground-color: #f00;
                         }
                         #lightbox .image:hover{
                           padding: 5px;
                           background-color: #f00;
                         }
                         
                         #lightbox .picture_frame {
                           float: left;
                           top: 0px;
                           xxbackground-color: #ccc;
                           width: 400px;
                           height: 400px;
                           padding: 50px;
                         }
                         #lightbox .picture_frame .imagetitle {
                           xdisplay: none;
                           display: block;
                           height: 20px;
                           xxbackground-color: #888;
                           xxwidth: 400px;
                           xxheight: 400px;
                         }
                         #lightbox .picture_frame .placeholder {
                           xxbackground-color: #F00;
                           width: 400px;
                           height: 400px;
                         }
                         ]]></>);
  // ---------------------------------------------------------------------------
  // Tumblr

  Tumblr = {};
//  Tumblr.PAGE_LIMIT = 50;
  Tumblr.PAGE_LIMIT = 20;
  Tumblr.photo = function(start, count, size, callback){
    Tumblr.read('photo', start, count, function(res){
      log('Tumblr.photo');
      var imgs = [];
      res.forEach(function(doc){
        log('Tumblr.photo,foreach');
        var links = $x('//post', doc).map(function(post){
          return post.getAttribute('url');
        });
        var srcs = $x('//photo-url[@max-width="'+size+'"]',
                      doc).map(function(photo){
                        return photo.textContent;
                      });
        
        links.forEach(function(link, i){
          imgs.push({link:link, src:srcs[i]});
        });
      });
      
      callback(imgs);
    });
  };
  
  Tumblr.read = function(type, start, count, callback){
    log('Tumblr.read');
    var pages = Tumblr.split(start, count);
    var res = [];
    
    (function me(){
      var page = pages.shift();
      var url = Tumblr.buildURL({
        type : type,
        start : page[0],
        num : page[1],
      }); 
      log('Tumblr.read.me ' + url);
      
      ajax(url, function(text){
        res.push(xml(text));
        if(pages.length){
          me();
        } else {
          callback(res);
        }
      });
    })();
  };
  
  Tumblr.getTotal = function(type, callback){
    var url = Tumblr.buildURL({
      type : type,
      start : 0,
      num : 0,
    }); 
    
    ajax(url, function(text){
      callback(xml(text).getElementsByTagName('posts')[0].getAttribute('total'));
    });
  };
  
  Tumblr.split = function(start, count){
    var res = [];
    var limit = Tumblr.PAGE_LIMIT;
    for(var i=0,len=Math.ceil(count/limit) ; i<len ; i++){
      //res.push([start + i * limit, start + limit]);
      res.push([start + i * limit, limit]);
    }
    count%limit && (res[res.length-1][1] = count%limit);
    return res;
  };
  
  Tumblr.buildURL = function(params){
    return Tumblr.getEndPoint() + queryString(params);
  };
  
  Tumblr.getEndPoint = function(){
    return 'http://'+location.host+'/api/read?';
  };

  // ---------------------------------------------------------------------------
  // History(FIFO)

  function History(size) {
    this.list = [];
    this.size = size;
  };
  History.prototype = {
    push: function(obj) {
      if (this.list.length >= this.size) {
        var first = this.list.shift();
      }
      var new_object = obj;
      this.list.push(new_object);
    }
    ,
    // reference
    getFromLast: function(index) {
      var list = this.list;
      index = (index > (list.length -1)) ? list.length - 1 : index;
      return list[(list.length - 1) - index];
    }
    ,
    deleteFromLast: function(index) {
      var list = this.list;
      this.list = list.slice(0, list.length - index);
    }
    ,
    length: function() {
      return this.list.length;
    }
  };

  // ---------------------------------------------------------------------------
  // BetterRandom

  function BetterRandom(size) {
    this.pool = [];
    for (var i = 0; i < size; i++) {
      this.pool[i] = Math.random();
    }
  };
  BetterRandom.prototype = {
    process: function(max) {
      var result;
      var i = this.pool.length - 1;
      var tmp_i = i;
      var tmp_v = this.pool[i];
      i = Math.floor(this.pool.length * this.pool[i]);
      result = Math.floor(this.pool[i] * max);
      this.pool[i] = Math.random();;
      //log('random ' + [max, i, tmp_i, tmp_v, result, this.pool[i]].join(', '));
      return result;
    }
  };

  // ---------------------------------------------------------------------------
  // Fader

  function Fader(elm, startColor, stopColor, num, interval, lastAction) {
    this.index = 0;
    this.elm = elm; //document.getElementById(id);;
    this.colors = [];
    var interval = interval || 150;
    var start = {
      r: (startColor & 0x00FF0000) >> 16,
      g: (startColor & 0x0000FF00) >> 8,
      b: (startColor & 0x000000FF)
    };
    var end = {
      r: (stopColor & 0x00FF0000) >> 16,
      g: (stopColor & 0x0000FF00) >> 8,
      b: (stopColor & 0x000000FF)
    };
    var diff = {
      r: end.r - start.r,
      g: end.g - start.g,
      b: end.b - start.b
    };

    for (var i = 0; i < num; i++) {
      var rate = i / (num - 1);
      rate = rate * rate * rate * rate; // via amachang
      this.colors[i] =
        '#' +
        (  ((start.r + diff.r * rate) & 0xFF) << 16
         | ((start.g + diff.g * rate) & 0xFF) << 8
         | ((start.b + diff.b * rate) & 0xFF)).toString(16);
    }
    this.colors[num - 1] = '#' + stopColor.toString(16);

    lastAction = (lastAction) ? lastAction : function(){};
    var self = this;
    function fade() {
      self.elm.style.backgroundColor = self.colors[self.index];
      self.index++;
      if (self.index <= self.colors.length) {
        setTimeout(fade, interval);
        if (self.index == self.colors.length) {
          lastAction();
        }
      }
    }
    setTimeout(fade, interval);
  };

  // ---------------------------------------------------------------------------
  // Composition

  function Composition() {
    this.elmLightbox = {};
    this.elmStatus = {};
    this.elmInfo = {};
    this.elmImageTitles = [];
    this.elmImageHolders = [];
    this.elmImages = [];
    this.elmPreFetchedImages = {};
    this.tid = null;
    this.status = 'STOPPED';
    this.setHistory = new History(Config.HISTORY_SIZE);
    this.cursorFromLast = 0;
    this.selectedList = [];
    this.pageRequestCount = 0;

    this.random;

    this.next = bind(this, 'next');
    this.keyhandler = bind(this, 'keyhandler');
    this.onmouseover = bind(this, 'onmouseover');
    this.onmouseout = bind(this, 'onmouseout');
    //this.fadeTimers = [];
  };

  Composition.swapBody = swapper(document.body, E('body', {id:'CP_body'}));

  keys(Config).forEach(function(key) {
    Composition[key] = Config[key]
  });

  Composition.STATUS = {
    STOPPED: {
      next: 'PLAYING',
      proc: 'next'
    },
    PLAYING: {
      next: 'STOPPED',
      proc: 'pause'
    }
  }

  Composition.prototype = {
    select: function(num) {
      log('select');
      var results = [];
      var select_num = Math.min((Composition.SELECT_TARGETS_NUM) * this.pageRequestCount, this.total);
      log('  select_num ' + select_num);
      for (var i = 0; i < num; i++) {
        var index = this.random.process(select_num);
        log('    selected_index ' + index);
        results.push(this.imgs[index]);

        var image = new Image();
        image.src= this.imgs[index].src;
        image.className = 'image';
        this.elmPreFetchedImages[image.src] = image;
      }
      if (this.cursorFromLast != 0) {
        // delete old entries
        this.setHistory.deleteFromLast(this.cursorFromLast);
        this.cursorFromLast = 0;
      }
      this.selectedList.push(results);
      this.setHistory.push(results);
      return results;
    }
    ,
    setupLightbox: function(image_num) {
      this.elmLightbox = this.parent.appendChild(E('div'));
      this.elmLightbox.id = 'lightbox';
      window.addEventListener('keydown', this.keyhandler, false);

      this.elmStatus = this.elmLightbox.appendChild(E('div'));
      this.elmStatus.id = 'status';
      this.elmStatus.innerHTML = 'COMPOSITE';

      this.elmInfo = this.elmLightbox.appendChild(E('a'));
      this.elmInfo.id = 'info';
      this.elmInfo.innerHTML = 'info';
      this.elmInfo.href = 'http://userscripts.org/scripts/show/9758';

      for (var i = 0; i < image_num; i++) {
        var elmImageHolder = this.elmLightbox.appendChild(E('div'));
        elmImageHolder.id = 'imgae_' + i;
        elmImageHolder.className = 'picture_frame';

        var elmImageTitle = elmImageHolder.appendChild(E('a'));
        elmImageTitle.className = 'imagetitle';
        this.elmImageTitles.push(elmImageTitle);

        var elmPlaceholder = elmImageHolder.appendChild(E('div'));
        elmPlaceholder.className = 'placeholder';
        this.elmImageHolders.push(elmPlaceholder);
      }
      this.elmStatus.style.display = 'block';
    }
    ,

    keyhandler: function(e) {
      switch (e.keyCode) {
      case KEY_SPACEBAR:
        this.toggle();
        break;
      case KEY_K:
        if (this.status == 'STOPPED') {
          this.reverseHistory();
          log('curosr = ' + this.cursorFromLast);
        }
        break;
      case KEY_J:
        if (this.status == 'STOPPED') {
          this.forwardHistory();
          log('curosr = ' + this.cursorFromLast);
        }
        break;
      default:
        break;
      }
    }
    ,
    onmouseout: function(e) {
      this.hideImageTitle(e);
    }
    ,
    onmouseover: function(e) {
      if (this.status == 'STOPPED') {
        this.showImageTitle(e);
        var fader = new Fader(e.target,
                              Composition.FADER_COLOR_START,
                              Composition.FADER_COLOR_END,
                              8,
                              100);
      }
    }
    ,
    forwardHistory: function() {
      var prefetched_num = Composition.ENTRY_PREFETCH_NUM;
      this.cursorFromLast--;
      if (this.cursorFromLast <= prefetched_num)
        this.cursorFromLast = prefetched_num;

      var kind2HUI_offset = 1;
      this.elmStatus.innerHTML
        = 'pause' + ': forward set-history ['
        + (this.cursorFromLast - prefetched_num + kind2HUI_offset) + ']';
      var imgs = this.setHistory.getFromLast(this.cursorFromLast);
      this.swap(imgs);
    }
    ,
    reverseHistory: function() {
      var prefetched_num = Composition.ENTRY_PREFETCH_NUM;
      this.cursorFromLast++;
      if (this.cursorFromLast >= Composition.HISTORY_SIZE)
        this.cursorFromLast = Composition.HISTORY_SIZE - 1;
      if (this.cursorFromLast >= this.setHistory.length())
        this.cursorFromLast = this.setHistory.length() - 1;
      if (this.cursorFromLast == 1)
        this.cursorFromLast++;

      var kind2HUI_offset = 1;
      this.elmStatus.innerHTML
        = 'pause' + ': reverse set-history ['
        + (this.cursorFromLast - prefetched_num + kind2HUI_offset) + ']';
      var imgs = this.setHistory.getFromLast(this.cursorFromLast);
      this.swap(imgs);
    }
    ,
    toggle: function() {
      var current = Composition.STATUS[this.status];
      this.status = current.next;
      this[current.proc]();
    }
    ,
    imageSetIndex: function(e) {
      var img_width = 400;
      var img_padding = 5;
      var place_padding = 50;
      var left_space = (place_padding + img_padding) * 2 + img_width;
      var right_left = left_space;// + place_padding + img_padding;
      return (e.screenX > right_left) ? 1 : 0;
    }
    ,
    showImageTitle: function(e) {
      var cursorOffset = (this.cursorFromLast == 0) ? 1 : 0;
      var imgs = this.setHistory.getFromLast(this.cursorFromLast + cursorOffset);
      var i = this.imageSetIndex(e);
      this.elmImageTitles[i].innerHTML = imgs[i].link;
      this.elmImageTitles[i].href = imgs[i].link;
      this.elmImageTitles[i].style.display = 'block';
    }
    ,
    hideImageTitle: function(e) {
      var i = this.imageSetIndex(e);
      var self = this;
      setTimeout(function(){
        self.elmImageTitles[i].innerHTML = '';
      }, 1200)
    }
    ,
    pause: function() {
      log('pause');
      this.elmStatus.innerHTML = 'pause';
      this.elmStatus.style.display = 'block';
      this.elmInfo.style.display = 'block';
      clearTimeout(this.tid);
      this.tid = null;
      for (var i = 0; i < this.elmImages.length; i++) {
        var fader = new Fader(this.elmImages[i],
                              Composition.FADER_COLOR_START,
                              Composition.FADER_COLOR_END,
                              8,
                              100);
      }
    }
    ,
    next: function() {
      var self = this;
      if (self.tid != null)
        return;
      log('next');
      this.elmStatus.innerHTML = '';
      this.elmStatus.style.display = 'none';
      this.elmInfo.style.display = 'none';
      var imgs = self.select(Composition.SELECT_NUM);
      this.tid = setInterval(function() {
        self.swap(imgs);
        imgs = self.select(Composition.SELECT_NUM);

        var page_count = Math.min(Composition.SELECT_TARGETS_NUM, Tumblr.PAGE_LIMIT);
        if ((page_count - self.selectedList.length) <= Composition.PAGE_PREFETCH_REMAIN) {
          log('  before call Tumblr.photo in "next"');
          log('    ' + [ self.pageRequestCount,
                         page_count,
                         self.selectedList.length,
                         Composition.PAGE_PREFETCH_REMAIN ].join(', '));
          self.selectedList = [];
          Tumblr.photo(self.pageRequestCount * page_count,
                       Math.min(self.pageRequestCount + Composition.SELECT_TARGETS_NUM, self.total),
                       Composition.WIDTH, function(imgs) {
            extend(self.imgs, imgs);
            self.pageRequestCount++;
          });
        }
      }, Composition.INTERVAL);
    }
    ,
    swap: function(imgs) {
      var self = this;
      var i = 0;
      imgs.forEach(function(img) {
        log('  img.src = ' + img.src);
        self.elmImages[i].src = img.src;
        i++;
      });
    }
    ,
    run: function(total, imgs) {
      var self = this;
      self.imgs = imgs;
      self.total = total;
      self.random = new BetterRandom(total);
      self.elmStatus.style.display = 'none';
      var imgs = this.select(Composition.SELECT_NUM);
      log(imgs);
      var i = 0;
      log('before imgs.forEach');
      imgs.forEach(function(img) {
        log('  img.src = ' + img.src);
        var image = new Image();
        image.src= img.src;
        image.className = 'image';
        var id = 'image_' + i;
        image.id = id;
        self.elmImages[i] = self.elmImageHolders[i].appendChild(image);
        self.elmImages[i].addEventListener('mouseover', function(e){self.onmouseover(e)}, false);
        self.elmImages[i].addEventListener('mouseout', function(e){self.onmouseout(e)}, false);
        var fader = new Fader(self.elmImages[i],
                              Composition.FADER_COLOR_START,
                              Composition.FADER_COLOR_END,
                              8,
                              100
        );
        i++;
      });
    }
    ,
    bootstrap: function() {
      Composition.swapBody();
      this.parent = document.getElementById('CP_body');
      this.setupLightbox(Composition.SELECT_NUM);
    }
  }

  // ---------------------------------------------------------------------------
  // Main

  var composition;
  composition = new Composition();
  unsafeWindow.composition = composition; // FOR DEBUG
  composition.bootstrap();

  var self = composition;
  Tumblr.getTotal('photo', function(total){
    Tumblr.photo(0, Math.min(Composition.SELECT_TARGETS_NUM, total), Composition.WIDTH, function(imgs){
      log('Tumblr.main.callback');
      var us = unsafeWindow.Survey = {
        total: total,
        imgs: imgs
      };
      self.pageRequestCount = 1;
      composition.run(total, imgs);
      setTimeout(function() {composition.toggle()}, Composition.BOOTTIMEOUT);
    });
  });

  // ---------------------------------------------------------------------------
  // Utility

  // via MochiKit.Base
  function keys(obj) {
    var rval = [];
    for (var prop in obj) {
      rval.push(prop);
    }
    return rval;
  }

  function extend(self, obj, /* optional */skip) {        
    // Extend an array with an array-like object starting
    // from the skip index
    if (!skip) {
      skip = 0;
    }
    if (obj) {
      // allow iterable fall-through, but skip the full isArrayLike
      // check for speed, this is called often.
      var l = obj.length;
      if (typeof(l) != 'number' /* !isArrayLike(obj) */) {
        if (typeof(MochiKit.Iter) != "undefined") {
          obj = MochiKit.Iter.list(obj);
          l = obj.length;
        } else {
          throw new TypeError("Argument not an array-like and MochiKit.Iter not present");
        }
      }
      if (!self) {
        self = [];
      }
      for (var i = skip; i < l; i++) {
        self.push(obj[i]);
      }
    }
    // This mutates, but it's convenient to return because
    // it's often used like a constructor when turning some
    // ghetto array-like to a real array
    return self;
  }

  function queryString(params){
    var qeries = [];
    for(var key in params)
      qeries.push(key + '='+ encodeURIComponent(params[key]));
    return qeries.join('&');
  }
  
  function bind(obj, func) {
    func = (func instanceof Function)? func : obj[func];
    return function() {
      func.apply(obj, arguments);
    }
  }
  
  function log() {
    if (unsafeWindow.console) {
      unsafeWindow.console.log.apply(unsafeWindow.console,
                                     Array.slice(arguments));
    }
  }

  function ajax(url, onload){
    GM_xmlhttpRequest({
      method : 'get',
      url : url,
      onload : function(res){
        onload(res.responseText);
      }
    });
  }

  function removeElement(elm){
    return elm.parentNode.removeChild(elm);
  }
  
  function insertBefore(target, node){
    return target.parentNode.insertBefore(node, target);
  }
  
  function insertAfter(target, node){
    return target.parentNode.insertBefore(node, target.nextSibling);
  }

  function swapper(elmOld, elmNew) {
    var toggle = function(){
      insertBefore(elmOld, elmNew);
      removeElement(elmOld);
      var temp = elmOld;
      elmOld = elmNew;
      elmNew = temp;
    }
    return toggle;
  }
  
  function E() {
    var tag = Array.prototype.shift.call(arguments);
    var elm = document.createElement(tag);
    var text = [];
    Array.prototype.forEach.call(arguments, function(value){
      if(!value)
        return;
      if(value && value.nodeType){
        elm.appendChild(value);
        return;
      }
      switch (typeof(value)) {
      case 'string':
      case 'number':
        elm.appendChild(document.createTextNode(value));
        break;
      default:
        for(var key in value) {
          var attr = value[key];
          switch(key){
          case 'class':
            elm.className = attr;
          case 'style':
            elm.style.cssText = attr;
          default:
            elm.setAttribute(key, attr);
          }
        };
        break;
      }
    });
    return elm;
  }
  function xml(text){
    return (new DOMParser).parseFromString(text, "application/xml");
  }
  
  function $x(exp, context) {
    if (!context) context = document;
    var resolver = function (prefix) {
      var o = document.createNSResolver(context)(prefix);
      return o ? o
               : (document.contentType == "text/html")
                 ? ""
                 : "http://www.w3.org/1999/xhtml";
    }
    var exp = document.createExpression(exp, resolver);
    
    var result = exp.evaluate(context, XPathResult.ANY_TYPE, null);
    switch (result.resultType) {
    case XPathResult.STRING_TYPE : return result.stringValue;
    case XPathResult.NUMBER_TYPE : return result.numberValue;
    case XPathResult.BOOLEAN_TYPE: return result.booleanValue;
    case XPathResult.UNORDERED_NODE_ITERATOR_TYPE: {
      result = exp.evaluate(context,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
      var ret = [];
      for (var i = 0, len = result.snapshotLength; i < len ; i++) {
        ret.push(result.snapshotItem(i));
      }
      return ret;
      }
    }
    return null;
  }
  
})