• jQuery Reel 1.3

    • Quick Start All other additions since 1.2 are marked red. Notably:
      • AMD Compatibility
      • Shy Initialization
      • Responsiveness (w/ Event)
      • Gyroscope Support
      • Event interface
      • Data Values in URLs
    • Methods
      • .reel([options])
      • .reel(data, [value])
      • .reel(event, [arguments])
      • .unreel()
    • Options
      • annotations
      • area
      • attr
      • brake
      • clickfree
      • cw
      • cursor
      • delay
      • directional
      • draggable
      • duration
      • entry
      • footage
      • frame
      • framelock
      • frames
      • graph
      • hint
      • horizontal
      • image
      • images
      • indicator
      • inversed
      • klass
      • laziness
      • loops
      • monitor
      • opening
      • orbital
      • orientable
      • path
      • preload
      • preloader
      • rebound
      • responsive
      • revolution
      • row
      • rowlock
      • rows
      • scrollable
      • shy
      • spacing
      • speed
      • steppable
      • stitched
      • suffix
      • tempo
      • throwable
      • timeout
      • velocity
      • vertical
      • wheelable
    • Events
      • down
      • loaded
      • opening
      • openingDone
      • orient
      • pan
      • pause
      • play
      • preload
      • preloaded
      • prepare
      • reach
      • resize
      • setup
      • stepLeft
      • stepRight
      • stepUp
      • stepDown
      • stop
      • teardown
      • up
      • wheel
    • Global Namespace
      • cdn
      • cost
      • def
      • fn
      • instances
      • leader
      • math
      • normal
      • preload
      • re
      • resize_gauge
      • scan()
      • sequence()
      • substitute()
      • substitutes
      • version
    • Examples
    • Tests
    /**
              @@@@@@@@@@@@@@
          @@@@@@@@@@@@@@@@@@@@@@
        @@@@@@@@          @@@@@@@@
      @@@@@@@                @@@@@@@
     @@@@@@@                  @@@@@@@
     @@@@@@@                  @@@@@@@
     @@@@@@@@     @          @@@@@@@@
      @@@@@@@@@  @@@       @@@@@@@@@
       @@@@@@@@@@@@@@   @@@@@@@@@@@
         @@@@@@@@@@@@@    @@@@@@@
           @@@@@@@@@@@@     @@@
              @@@@@@
             @@@@
            @@
     *
     * jQuery Reel
     * ===========
     * The 360° plugin for jQuery
     *
     * @license Copyright (c) 2009-2013 Petr Vostrel (http://petr.vostrel.cz/)
     * Licensed under the MIT License (LICENSE.txt).
     *
     * jQuery Reel
     * http://reel360.org
     * Version: 1.3.0
     * Updated: 2013-11-04
     *
     * Requires jQuery 1.6.2 or higher
     */
    
    /*
     * CDN
     * ---
     * - http://code.vostrel.net/jquery.reel-bundle.js (recommended)
     * - http://code.vostrel.net/jquery.reel.js
     * - http://code.vostrel.net/jquery.reel-debug.js
     * - or http://code.vostrel.net/jquery.reel-edge.js if you feel like it ;)
     *
     * Optional Plugins
     * ----------------
     * - jQuery.mouseWheel [B] (Brandon Aaron, http://plugins.jquery.com/project/mousewheel)
     * - or jQuery.event.special.wheel (Three Dub Media, http://blog.threedubmedia.com/2008/08/eventspecialwheel.html)
     *
     * [B] Marked plugins are contained (with permissions) in the "bundle" version from the CDN
     */
    
    (function(factory){

  • ¶

    [NEW] AMD Compatibility

  • Reel registers as an asynchronous module with dependency on jQuery for AMD compatible script loaders. Besides that it also complies with CommonJS module definition for Node and such. Of course, no fancy script loader is necessary and good old plain script tag still works too.

      var
        amd= typeof define == 'function' && define.amd && (define(['jquery'], factory) || true),
        commonjs= !amd && typeof module == 'object' && typeof module.exports == 'object' && (module.exports= factory),
        plain= !amd && !commonjs && factory()
    
    })(function(){ return jQuery.reel || (function($, window, document, undefined){

  • ¶

    jQuery

  • One vital requirement is the correct jQuery. Reel requires at least version 1.6.2 and a make sure check is made at the very beginning.

      if (!$) return;
      var
        version= $ && $().jquery.split(/\./)
      if (!version || +(twochar(version[0])+twochar(version[1])+twochar(version[2] || '')) < 10602)
        return error('Too old jQuery library. Please upgrade your jQuery to version 1.6.2 or higher');

  • ¶

    Global Namespace

  • $.reel (or jQuery.reel) namespace is provided for storage of all Reel belongings. It is locally referenced as just reel for speedier access.

      var
        reel= $.reel= {
  • ¶

    $.reel.version

    String (major.minor.patch), since 1.1

          version: '1.3.0',
  • ¶

    Options

  • When calling .reel() method you have plenty of options (far too many) available. You collect them into one hash and supply them with your call.

    Example: Initiate a non-looping Reel with 12 frames:

    .reel({
      frames: 12,
      looping: false
    })

    All options are optional and if omitted, default value is used instead. Defaults are being housed as members of $.reel.def hash. If you customize any default value therein, all subsequent .reel() calls will use the new value as default.

    Example: Change default initial frame to 5th:

    $.reel.def.frame = 5

  • ¶

    $.reel.def

    Object, since 1.1

          def: {
  • ¶

    Basic Definition

    Reel is just fine with you not setting any options, however if you don't have 36 frames or beginning at frame 1, you will want to set total number of frames and pick a different starting frame.


  • ¶

    frame Option

    Number (frames), since 1.0

            frame:                  1,
  • ¶

    frames Option

    Number (frames), since 1.0

            frames:                36,
  • ~~~

    Another common characteristics of any Reel is whether it loops and covers entire 360° or not.


  • ¶

    loops Option

    Boolean, since 1.0

            loops:               true,
  • ¶

    Interaction

    Using boolean switches many user interaction aspects can be turned on and off. You can disable the mouse wheel control with wheelable, the drag & throw action with throwable, disallow the dragging completely with draggable, on touch devices you can disable the browser's decision to scroll the page instead of Reel script and you can of course disable the stepping of Reel by clicking on either half of the image with steppable.

    You can even enable clickfree operation, which will cause Reel to bind to mouse enter/leave events instead of mouse down/up, thus allowing a click-free dragging.


  • ¶

    clickfree Option

    Boolean, since 1.1

            clickfree:          false,
  • ¶

    draggable Option

    Boolean, since 1.1

            draggable:           true,
  • ¶

    scrollable Option

    Boolean, since 1.2

            scrollable:          true,
  • ¶

    steppable Option

    Boolean, since 1.2

            steppable:           true,
  • ¶

    throwable Option

    Boolean, since 1.1; or Number, since 1.2.1

            throwable:           true,
  • ¶

    wheelable Option

    Boolean, since 1.1

            wheelable:           true,
  • ¶

    [NEW] Gyroscope Support

    When enabled allows gyro-enabled devices (iPad2 for example) to control rotational position using the device's attitude in space. In this more, Reel directly maps the 360° range of your gyro's primary (alpha) axis directly to the value of fraction.

    orientable Option

    [NEW] Boolean, since 1.3

            orientable:         false,
  • ¶

    Order of Images

    Reel presumes counter-clockwise order of the pictures taken. If the nearer facing side doesn't follow your cursor/finger, you did clockwise. Use the cw option to correct this.


  • ¶

    cw Option

    Boolean, since 1.1

            cw:                 false,
  • ¶

    Sensitivity

    In Reel sensitivity is set through the revolution parameter, which represents horizontal dragging distance one must cover to perform one full revolution. By default this value is calculated based on the setup you have - it is either twice the width of the image or half the width of stitched panorama. You may also set your own.

    Optionally revolution can be set as an Object with x member for horizontal revolution and/or y member for vertical revolution in multi-row movies.


  • ¶

    revolution Option

    Number (pixels) or Object, since 1.1, Object support since 1.2

            revolution:     undefined,
  • ¶

    Rectilinear Panorama

    The easiest of all is the stitched panorama mode. For this mode, instead of the sprite, a single seamlessly stitched stretched image is used and the view scrolls the image. This mode is triggered by setting a pixel width of the stitched image.


  • ¶

    stitched Option

    Number (pixels), since 1.0

            stitched:               0,
  • ¶

    Directional Mode

    As you may have noticed on Reel's homepage or in example/object-movie-directional-sprite when you drag the arrow will point to either direction. In such directional mode, the sprite is actually 2 in 1 - one file contains two sprites one tightly following the other, one for visually going one way (A) and one for the other (B).

    A01 A02 A03 A04 A05 A06
    A07 A08 A09 A10 A11 A12
    A13 A14 A15 B01 B02 B03
    B04 B05 B06 B07 B08 B09
    B10 B11 B12 B13 B14 B15

    Switching between A and B frames is based on direction of the drag. Directional mode isn't limited to sprites only, sequences also apply. The figure below shows the very same setup like the above figure, only translated into actual frames of the sequence.

    001 002 003 004 005 006
    007 008 009 010 011 012
    013 014 015 016 017 018
    019 020 021 022 023 024
    025 026 027 028 029 030

    Frame 016 represents the B01 so it actually is first frame of the other direction.


  • ¶

    directional Option

    Boolean, since 1.1

            directional:        false,
  • ¶

    Multi-Row Mode

    As example/object-movie-multirow-sequence very nicely demonstrates, in multi-row arrangement you can perform two-axis manipulation allowing you to add one or more vertical angles. Think of it as a layered cake, each new elevation of the camera during shooting creates one layer of the cake - - a row. One plain horizontal object movie full spin is one row:

    A01 A02 A03 A04 A05 A06
    A07 A08 A09 A10 A11 A12
    A13 A14 A15

    Second row tightly follows after the first one:

    A01 A02 A03 A04 A05 A06
    A07 A08 A09 A10 A11 A12
    A13 A14 A15 B01 B02 B03
    B04 B05 B06 B07 B08 B09
    B10 B11 B12 B13 B14 B15
    C01...

    This way you stack up any number of rows you wish and set the initial row to start with. Again, not limited to sprites, sequences also apply.


  • ¶

    row Option

    Number (rows), since 1.1

            row:                    1,
  • ¶

    rows Option

    Number (rows), since 1.1

            rows:                   0,
  • ¶

    [NEW] Multi-Row Locks

    Optionally you can apply a lock on either of the two axes with rowlock and/or framelock. That will disable direct mouse interaction and will leave using of .reel() the only way of changing position on the locked axis.


  • ¶

    rowlock Option

    [NEW] Boolean, since 1.3

            rowlock:            false,
  • ¶

    framelock Option

    [NEW] Boolean, since 1.3

            framelock:          false,
  • ¶

    Dual-Orbit Mode

    Special form of multi-axis movie is the dual-axis mode. In this mode the object offers two plain spins - horizontal and vertical orbits combined together crossing each other at the frame forming sort of a cross if envisioned. example/object-movie-dual-orbit-sequence demonstrates this setup. When the phone in the example is facing you (marked in the example with green square in the top right), you are at the center. That is within the distance (in frames) defined by the orbital option. Translation from horizontal to vertical orbit can be achieved on this sweet-spot. By default horizontal orbit is chosen first, unless vertical option is used against.

    In case the image doesn't follow the vertical drag, you may have your vertical orbit inversed.

    Technically it is just a two-layer movie:

    A01 A02 A03 A04 A05 A06
    A07 A08 A09 A10 A11 A12
    A13 A14 A15 B01 B02 B03
    B04 B05 B06 B07 B08 B09
    B10 B11 B12 B13 B14 B15

  • ¶

    orbital Option

    Number (frames), since 1.1

            orbital:                0,
  • ¶

    vertical Option

    Boolean, since 1.1

            vertical:           false,
  • ¶

    inversed Option

    Boolean, since 1.1

            inversed:           false,
  • ¶

    Sprite Layout

    For both object movies and panoramas Reel presumes you use a combined Sprite to hold all your frames in a single file. This powerful technique of using a sheet of several individual images has many advantages in terms of compactness, loading, caching, etc. However, know your enemy, be also aware of the limitations, which stem from memory limits of mobile (learn more in FAQ).

    Inside the sprite, individual frames are laid down one by one, to the right of the previous one in a straight Line:

    01 02 03 04 05 06
    07...

    Horizontal length of the line is reffered to as footage. Unless frames spacing in pixels is set, edges of frames must touch.

    01 02 03 04 05 06
    07 08 09 10 11 12
    13 14 15 16 17 18
    19 20 21 22 23 24
    25 26 27 28 29 30
    31 32 33 34 35 36

    This is what you'll get by calling .reel() without any options. All frames laid out 6 in line. By default nicely even 6 x 6 grid like, which also inherits the aspect ratio of your frames.

    By setting horizontal to false, instead of forming lines, frames are expected to form Columns. All starts at the top left corner in both cases.

    01 07 13 19 25 31
    02 08 14 20 26 32
    03 09 15 21 27 33
    04 10 16 22 28 34
    05 11 17 23 29 35
    06 12 18 24 30 36

    URL for the sprite image file is being build from the name of the original <img> src image by adding a suffix to it. By default this results in "object-reel.jpg" for "object.jpg". You can also take full control over the sprite image URL that will be used.


  • ¶

    footage Option

    Number (frames), since 1.0

            footage:                6,
  • ¶

    spacing Option

    Number (pixels), since 1.0

            spacing:                0,
  • ¶

    horizontal Option

    Boolean, since 1.0

            horizontal:          true,
  • ¶

    suffix Option

    String, since 1.0

            suffix:           '-reel',
  • ¶

    image Option

    String, since 1.1

            image:          undefined,
  • ¶

    Sequence

    Collection of individual frame images is called Sequence and it this way one HTTP request per frame is made carried out as opposed to sprite with one request per entire sprite. Define it with string like: "image_###.jpg". The # placeholders will be replaced with a numeric +1 counter padded to the placeholders length. Learn more about sequences.

    In case you work with hashed filenames like 64bc654d21cb.jpg, where no counter element can be indentified, or you prefer direct control, images can also accept array of plain URL strings.

    All images are retrieved from a specified path.


  • ¶

    images Option

    [IMPROVED] String or Array, since 1.1

            images:                '',
  • ¶

    path Option

    String (URL path), since 1.1

            path:                  '',
  • ¶

    Images Preload Order

    Given sequence images can be additionally reordered to achieve a perceived faster preloading. Value given to preload option must match a name of a pre-registered function within $.reel.preload object. There are two functions built-in:

    • "fidelity" - non-linear way that ensures even spreading of preloaded images around the entire revolution leaving the gaps in-between as small as possible. This results in a gradually increasing fidelity of the image rather than having one large shrinking gap. This is the default behavior.
    • "linear" - linear order of preloading

  • ¶

    preload Option

    String, since 1.2

            preload:       'fidelity',
  • ¶

    [NEW] Shy Initialization

    Sometimes, on-demand activation is desirable in order to conserve device resources or bandwidth especially with multiple instances on a single page. If so, enable shy mode, in which Reel will hold up the initialization process until the image is clicked by the user. Alternativelly you can initialize shy instance by triggering "setup" event.


  • ¶

    shy Option

    [NEW] Boolean, since 1.3

            shy:                false,
  • ¶

    Animation

    Your object movie or a panorama can perform an autonomous sustained motion in one direction. When speed is set in revolutions per second (Hz), after a given delay the instance will animate and advance frames by itself.

    t
    |-------›|-----------›
      Delay    Animation

    Start and resume of animation happens when given timeout has elapsed since user became idle.

    t
    |-----------›|= == ==  = === = = |          |-----------›
      Animation    User interaction    Timeout    Animation

    When a scene doesn't loop (see loops) and the animation reaches one end, it stays there for a while and then reversing the direction of the animation it bounces back towards the other end. The time spent on the edge can be customized with rebound.


  • ¶

    speed Option

    Number (Hz), since 1.1

            speed:                  0,
  • ¶

    delay Option

    Number (seconds), since 1.1

            delay:                  0,
  • ¶

    timeout Option

    Number (seconds), since 1.1

            timeout:                2,
  • ¶

    duration Option

    Number (seconds), since 1.3

            duration:       undefined,
  • ¶

    rebound Option

    Number (seconds), since 1.1

            rebound:              0.5,
  • ¶

    Opening Animation

    Chance is you want the object to spin a little to attract attention and then stop and wait for the user to engage. This is called "opening animation" and it is performed for given number of seconds (opening) at dedicated entry speed. The entry speed defaults to value of speed option. After the opening animation has passed, regular animation procedure begins starting with the delay (if any).

    t
    |--------›|-------›|-----------›
      Opening   Delay    Animation

  • ¶

    entry Option

    Number (Hz), since 1.1

            entry:          undefined,
  • ¶

    opening Option

    Number (seconds), since 1.1

            opening:                0,
  • ¶

    Momentum

    Often also called inertial motion is a result of measuring a velocity of dragging. This velocity builds up momentum, so when a drag is released, the image still retains the momentum and continues to spin on itself. Naturally the momentum soon wears off as brake is being applied.

    One can utilize this momentum for a different kind of an opening animation. By setting initial velocity, the instance gets artificial momentum and spins to slow down to stop.


  • ¶

    brake Option

    Number, since 1.1, where it also has a different default 0.5

            brake:               0.23,
  • ¶

    velocity Option

    Number, since 1.2

            velocity:               0,
  • ¶

    Ticker

    For purposes of animation, Reel starts and maintains a timer device which emits ticks timing all animations. There is only one ticker running in the document and all instances subscribe to this one ticker. Ticker is equipped with a mechanism, which compensates for the measured costs of running Reels to ensure the ticker ticks on time. The tempo (in Hz) of the ticker can be specified.

    Please note, that ticker is synchronized with a leader, the oldest living instance on page, and adjusts to his tempo.


  • ¶

    tempo Option

    Number (Hz, ticks per second), since 1.1

            tempo:                 36,
  • ~~~

    Since many mobile devices are sometimes considerably underpowered in comparison with desktops, they often can keep up with the 36 Hz rhythm. In Reel these are called lazy devices and everything mobile qualifies as lazy for the sake of the battery and interaction fluency. The ticker is under-clocked for them by a laziness factor, which is used to divide the tempo. Default laziness of 6 will effectively use 6 Hz instead (6 = 36 / 6) on lazy devices.


  • ¶

    laziness Option

    Number, since 1.1

            laziness:               6,
  • ¶

    Customization

    You can customize Reel on both functional and visual front. The most visible thing you can customize is probably the cursor, size of the preloader, perhaps add visual indicator(s) of Reel's position within the range. You can also set custom hint for the tooltip, which appears when you mouse over the image area. Last but not least you can also add custom class name klass to the instance.


  • ¶

    cursor Option

    String, since 1.2

            cursor:         undefined,
  • ¶

    hint Option

    String, since 1.0

            hint:                  '',
  • ¶

    indicator Option

    Number (pixels), since 1.0

            indicator:              0,
  • ¶

    klass Option

    String, since 1.0

            klass:                 '',
  • ¶

    preloader Option

    Number (pixels), since 1.1

            preloader:              2,
  • ~~~

    You can use custom attributes (attr) on the node - it accepts the same name-value pairs object jQuery .attr() does. In case you want to delegate full interaction control over the instance to some other DOM element(s) on page, you can with area.


  • ¶

    area Option

    jQuery, since 1.1

            area:           undefined,
  • ¶

    attr Option

    Object, since 1.2

            attr:                  {},
  • ¶

    Annotations

    To further visually describe your scene you can place all kinds of in-picture HTML annotations by defining an annotations object. Learn more about Annotations in a dedicated article.


  • ¶

    annotations Option

    Object, since 1.2

            annotations:    undefined,
  • ¶

    [NEW] Responsiveness

    By default, dimensions of Reel are fixed and pixel-match the dimensions of the original image and the responsive mode is disabled. Using responsive option you can enable responsiveness. In such a case Reel will adopt dimensions of its parent container element and scale all relevant data store values accordingly. The scale applied is stored in "ratio" data key, where 1.0 means 100% or no scale.

    To take full advantage of this, you can setup your URLs to contain actual dimensions and serve images in appropriate detail. Learn more about data values in URLs.


  • ¶

    responsive Option

    [NEW] Boolean, since 1.3

            responsive:         false,
  • ¶

    Mathematics

    When reeling, instance conforms to a graph function, which defines whether it will loop ($.reel.math.hatch) or it won't ($.reel.math.envelope). My math is far from flawless and I'm sure there are much better ways how to handle things. the graph option is there for you shall you need it. It accepts a function, which should process given criteria and return a fraction of 1.

    function( x, start, revolution, lo, hi, cwness, y ){
      return fraction  // 0.0 - 1.0
    }

  • ¶

    graph Option

    Function, since 1.1

            graph:          undefined,
  • ¶

    Monitor

    Specify a string data key and you will see its real-time value dumped in the upper-left corner of the viewport. Its visual can be customized by CSS using .jquery-reel-monitor selector.


  • ¶

    monitor Option

    String (data key), since 1.1

            monitor:        undefined
    
          },

  • ¶

    [NEW] Quick Start

  • For basic Reel initialization, you don't even need to write any Javascript! All it takes is to add class name "reel" to your <img> HTML tag, assign an unique id attribute and decorate the tag with configuration data attributes. Result of which will be interactive Reel projection.

    <img src="some/image.jpg" width="300" height="200"
      id="my_image"
      class="reel"
      data-images="some/images/01.jpg, some/images/02.jpg"
      data-speed="0.5">

    All otherwise Javascript options are made available as HTML data-* attributes.

    Only the annotations option doesn't work this way. To quickly create annotations, simply have any HTML node (<div> prefferably) anywhere in the DOM, assign it class name "reel-annotation", an unique id attribute and add configuration data attributes.

    <div id="my_annotation"
      class="reel-annotation"
      data-for="my_image"
      data-x="120"
      data-y="60">
      Whatever HTML I'd like to have in the annotation
    </div>

    Most important is the data-for attribute, which references target Reel instance by id.


  • Responsible for discovery and subsequent conversion of data-configured Reel images is $.reel.scan() method, which is being called automagically when the DOM becomes ready. Under normal circumstances you don't need to scan by yourself.

    It however comes in handy to re-scan when you happen to inject a data-configured Reel <img> into already ready DOM.


  • ¶

    $.reel.scan() Method

    [NEW] returns jQuery, since 1.3

          scan: function(){
            return $(dot(klass)+':not('+dot(overlay_klass)+' > '+dot(klass)+')').each(function(ix, image){
              var
                $image= $(image),
                options= $image.data(),
                images= options.images= soft_array(options.images),
                annotations= {}
              $(dot(annotation_klass)+'[data-for='+$image.attr(_id_)+']').each(function(ix, annotation){
                var
                  $annotation= $(annotation),
                  def= $annotation.data(),
                  x= def.x= numerize_array(soft_array(def.x)),
                  y= def.y= numerize_array(soft_array(def.y)),
                  id= $annotation.attr(_id_),
                  node= def.node= $annotation.removeData()
                annotations[id] = def;
              });
              options.annotations = annotations;
              $image.removeData().reel(options);
            });
          },

  • ¶

    Methods

  • Reel's methods extend jQuery core functions with members of its $.reel.fn object. Despite Reel being a typical one-method plug-in with its .reel() function, for convenience it also offers its bipolar twin .unreel().


  • ¶

    $.reel.fn

    returns Object, since 1.1

          fn: {

  • ¶

    Construction

  • .reel() method is the core of Reel and similar to some jQuery functions, it has adaptive interface. It either builds, reads & writes data or causes events.


  • ¶

    .reel( [options] ) Method

    returns jQuery, since 1.0

            reel: function(){
              var
                args= arguments,
                t= $(this),
                data= t.data(),
                name= args[0] || {},
                value= args[1]
  • The main core of this procedure is rather bulky, so let's skip it for now and instead let me introduce the other uses first.


  • ¶

    [NEW] Control Events

  • Event messages are what ties and binds all Reel's internal working components together. Besides being able to binding to any of these events from your script and react on Reel status changes (e.g. position), you can also trigger some of them in order to control Reel's attitude.

    You can:

    • control the playback of animated Reels with play, pause or stop
    • step the Reel in any direction with stepRight, stepLeft, stepUp, stepDown,
    • reach certain frame with reach

    Triggering Reel's control event is as simple as passing the desired event name to .reel().

    Example: Stop the animation in progress:

    .reel(':stop')

    Think of .reel() as a convenient shortcut to and synonym for .trigger(), only prefix the event name with :. Of course you can use simple .trigger() instead and without the colon.


  • ¶

    .reel( event, [arguments] )

    returns jQuery, since 1.3

              if (typeof name != 'object'){
    
                if (name.slice(0, 1) == ':'){
                  return t.trigger(name.slice(1), value);
                }

  • ¶

    Data

  • Reel stores all its inner state values with the standard DOM data interface interface while adding an additional change-detecting event layer, which makes Reel entirely data-driven.

    Example: Find out on what frame a Reel instance currently is:

    .reel('frame') // Returns the frame number

    This time think of .reel(data) as a synonym for .data(). Note, that you can therefore easily inspect the entire datastore with .data() (without arguments). Use it for debugging only. For real-time data watch use monitor option instead of manually hooking into the data.


  • ¶

    .reel( data )

    can return anything, since 1.2

                else{
                  if (args.length == 1){
                    return data[name]
                  }
  • ¶

    Write Access

    You can store any value the very same way by passing the value as the second function argument.

    Example: Jump to frame 12:

    .reel('frame', 12)

    Only a handful of data keys is suitable for external manipulation. These include area, backwards, brake, fraction, frame, playing, reeling, row, speed, stopped, velocity and vertical. Use the rest of the keys for reading only, you can mess up easily changing them.


  • ¶

    .reel( data, value )

    returns jQuery, since 1.2

                  else{
                    if (value !== undefined){
                      reel.normal[name] && (value= reel.normal[name](value, data));
  • ¶

    Changes

    Any value that does not equal (===) the old value is considered new value and in such a case Reel will trigger a change event to announce the change. The event type takes form of keyChange, where key will be the data key/name you've just assigned.

    Example: Setting "frame" to 12 in the above example will trigger "frameChange".

    Some of these change events (like frame or fraction) have a default handler attached.

    You can easily bind to any of the data key change with standard event binding methods.

    Example: React on instance being manipulated or not:

    .bind('reelingChange', function(evnt, nothing, reeling){
      if (reeling) console.log('Rock & reel!')
      else console.log('Not reeling...')
    })

  • The handler function will be executed every time the value changes and it will be supplied with three arguments. First one is the event object as usual, second is undefined and the third will be the actual value. In this case it was a boolean type value. If the second argument is not undefined it is the backward compatible "before" event triggered from outside Reel.

                      if (data[name] === undefined) data[name]= value
                      else if (data[name] !== value) t.trigger(name+'Change', [ undefined, data[name]= value ]);
                    }
                    return t
                  }
                }
              }

  • ¶

    Construction Core

  • Now, back to the procedure of constructing a Reel instance and binding its event handlers.

    Establish local opt object made by extending the defaults.

              else{
    
              var
                opt= $.extend({}, reel.def, name),
  • Limit the given jQuery collection to just <img> tags with src attribute and dimensions defined.

                applicable= t.filter(_img_).unreel().filter(function(){
                  var
                    $this= $(this),
                    attr= opt.attr,
                    src= attr.src || $this.attr(_src_),
                    width= attr.width || $this.attr(_height_) || $this.width(),
                    height= attr.height || $this.attr(_width_) || $this.height()
                  if (!src) return error('`src` attribute missing on target image');
                  if (!width || !height) return error('Dimension(s) of the target image unknown');
                  return true;
                }),
                instances= []
    
              applicable.each(function(){
                var
                  t= $(this),
  • Quick data interface

                  set= function(name, value){ return t.reel(name, value) && get(name) },
                  get= function(name){ return t.data(name) },
    
                  on= {

  • ¶

    Initialization

  • This internally called private pseudo-handler:

    • initiates all data store keys,
    • binds to ticker
    • and triggers "setup" Event when finished.
                    setup: function(e){
                      if (t.hasClass(klass) && t.parent().hasClass(overlay_klass)) return;
                      set(_options_, opt);
                      var
                        attr= {
                          src: t.attr(_src_),
                          width: t.attr(_width_) || null,
                          height: t.attr(_height_) || null,
                          style: t.attr(_style_) || null,
                          'class': t.attr(_class_) || null
                        },
                        src= t.attr(opt.attr).attr(_src_),
                        id= set(_id_, t.attr(_id_) || t.attr(_id_, klass+'-'+(+new Date())).attr(_id_)),
                        data= $.extend({}, t.data()),
                        images= set(_images_, opt.images || []),
                        stitched= set(_stitched_, opt.stitched),
                        is_sprite= !images.length || stitched,
                        responsive= set(_responsive_, opt.responsive && (knows_background_size ? true : !is_sprite)),
                        truescale= set(_truescale_, {}),
                        loops= opt.loops,
                        orbital= opt.orbital,
                        revolution= opt.revolution,
                        rows= opt.rows,
                        footage= set(_footage_, min(opt.footage, opt.frames)),
                        spacing= set(_spacing_, opt.spacing),
                        width= set(_width_, +t.attr(_width_) || t.width()),
                        height= set(_height_, +t.attr(_height_) || t.height()),
                        shy= set(_shy_, opt.shy),
                        frames= set(_frames_, orbital && footage || rows <= 1 && images.length || opt.frames),
                        multirow= rows > 1 || orbital,
                        revolution_x= set(_revolution_, axis('x', revolution) || stitched / 2 || width * 2),
                        revolution_y= set(_revolution_y_, !multirow ? 0 : (axis('y', revolution) || (rows > 3 ? height : height / (5 - rows)))),
                        rows= stitched ? 1 : ceil(frames / footage),
                        stitched_travel= set(_stitched_travel_, stitched - (loops ? 0 : width)),
                        stitched_shift= set(_stitched_shift_, 0),
                        stage_id= hash(id+opt.suffix),
                        img_class= t.attr(_class_),
                        classes= !img_class ? __ : img_class+___,
                        $overlay= $(tag(_div_), { id: stage_id.substr(1), 'class': classes+___+overlay_klass+___+frame_klass+'0' }),
                        $instance= t.wrap($overlay.addClass(opt.klass)).addClass(klass),
                        instances_count= instances.push(add_instance($instance)[0]),
                        $overlay= $instance.parent().bind(on.instance)
                      set(_image_, images.length ? __ : opt.image || src.replace(reel.re.image, '$1' + opt.suffix + '.$2'));
                      set(_cache_, $(tag(_div_), { 'class': cache_klass }).appendTo('body'));
                      set(_area_, $()),
                      set(_cached_, []);
                      set(_frame_, null);
                      set(_fraction_, null);
                      set(_row_, opt.row);
                      set(_tier_, 0);
                      set(_rows_, rows);
                      set(_rowlock_, opt.rowlock);
                      set(_framelock_, opt.framelock);
                      set(_departure_, set(_destination_, set(_distance_, 0)));
                      set(_bit_, 1 / frames);
                      set(_stage_, stage_id);
                      set(_backwards_, set(_speed_, opt.speed) < 0);
                      set(_loading_, false);
                      set(_velocity_, 0);
                      set(_vertical_, opt.vertical);
                      set(_preloaded_, 0);
                      set(_cwish_, negative_when(1, !opt.cw && !stitched));
                      set(_clicked_location_, {});
                      set(_clicked_, false);
                      set(_clicked_on_, set(_clicked_tier_, 0));
                      set(_lo_, set(_hi_, 0));
                      set(_reeling_, false);
                      set(_reeled_, false);
                      set(_opening_, false);
                      set(_brake_, opt.brake);
                      set(_center_, !!orbital);
                      set(_tempo_, opt.tempo / (reel.lazy? opt.laziness : 1));
                      set(_opening_ticks_, -1);
                      set(_ticks_, -1);
                      set(_annotations_, opt.annotations || $overlay.unbind(dot(_annotations_)) && {});
                      set(_ratio_, 1);
                      set(_backup_, {
                        attr: attr,
                        data: data
                      });
                      opt.steppable || $overlay.unbind('up.steppable');
                      opt.indicator || $overlay.unbind('.indicator');
                      css(__, { overflow: _hidden_, position: 'relative' });
                      responsive || css(__, { width: width, height: height });
                      responsive && $.each(responsive_keys, function(i, key){ truescale[key]= get(key) });
                      css(____+___+dot(klass), { display: _block_ });
                      css(dot(cache_klass), { position: 'fixed', left: px(-100), top: px(-100) }, true);
                      css(dot(cache_klass)+___+_img_, { position: _absolute_, width: 10, height: 10 }, true);
                      pool.bind(on.pool);
                      t.trigger(shy ? 'prepare' : 'setup')
                    },

  • ¶

    Events

  • Reel is completely event-driven meaning there are many events, which can be called (triggered). By binding event handler to any of the events you can easily hook on to any event to inject your custom behavior where and when this event was triggered.

    Example: Make #image element reel and execute some code right after the newly created instance is initialized and completely loaded:

    $("#image")
    .reel()
    .bind("loaded", function(ev){
      // Your code
    })

    Events bound to all individual instances.

                    instance: {
  • ¶

    teardown Event

    Event, since 1.1

    This event does do how it sounds like. It will teardown an instance with all its belongings leaving no trace.

    • It reconstructs the original <img> element,
    • wipes out the data store,
    • removes instance stylesheet
    • and unbinds all its events.
                      teardown: function(e){
                        var
                          backup= t.data(_backup_)
                        t.parent().unbind(on.instance);
                        if (get(_shy_)) t.parent().unbind(_click_, shy_setup)
                        else get(_style_).remove() && get(_area_).unbind(ns);
                        get(_cache_).empty();
                        clearTimeout(delay);
                        clearTimeout(gauge_delay);
                        $(window).unbind(_resize_, slow_gauge);
                        $(window).unbind(ns);
                        pool.unbind(on.pool);
                        pools.unbind(pns);
                        t.siblings().unbind(ns).remove();
                        no_bias();
                        t.removeAttr('onloaded');
                        remove_instance(t.unbind(ns).removeData().unwrap().attr(backup.attr).data(backup.data));
                        t.attr(_style_) == __ && t.removeAttr(_style_);
                      },
  • ¶

    setup Event

    Event, since 1.0

    "setup" Event continues with what has been started by the private on.setup() handler.

    • It prepares all additional on-stage DOM elements
    • and cursors for the instance stylesheet.
                      setup: function(e){
                        var
                          $overlay= t.parent().append(preloader()),
                          $area= set(_area_, $(opt.area || $overlay )),
                          multirow= opt.rows > 1,
                          cursor= opt.cursor,
                          cursor_up= cursor == _hand_ ? drag_cursor : cursor || reel_cursor,
                          cursor_down= cursor == _hand_ ? drag_cursor_down+___+'!important' : undefined
                        css(___+dot(klass), { MozUserSelect: _none_, WebkitUserSelect: _none_, MozTransform: 'translateZ(0)' });
                        t.unbind(_click_, shy_setup);
                        $area
                          .bind(_touchstart_, press)
                          .bind(opt.clickfree ? _mouseenter_ : _mousedown_, press)
                          .bind(opt.wheelable ? _mousewheel_ : null, wheel)
                          .bind(_dragstart_, function(){ return false })
                        css(__, { cursor: cdn(cursor_up) });
                        css(dot(loading_klass), { cursor: 'wait' });
                        css(dot(panning_klass)+____+dot(panning_klass)+' *', { cursor: cdn(cursor_down || cursor_up) }, true);
                        if (get(_responsive_)){
                          css(___+dot(klass), { width: '100%', height: _auto_ });
                          $(window).bind(_resize_, slow_gauge);
                        }
                        function press(e){ return t.trigger('down', [pointer(e).clientX, pointer(e).clientY, e]) && e.give }
                        function wheel(e, delta){ return !delta || t.trigger('wheel', [delta, e]) && e.give }
                        opt.hint && $area.attr('title', opt.hint);
                        opt.indicator && $overlay.append(indicator('x'));
                        multirow && opt.indicator && $overlay.append(indicator('y'));
                        opt.monitor && $overlay.append($monitor= $(tag(_div_), { 'class': monitor_klass }))
                                    && css(___+dot(monitor_klass), { position: _absolute_, left: 0, top: 0 });
                      },
  • ¶

    preload Event

    Event, since 1.1

    Reel keeps a cache of all images it needs for its operation. Either a sprite or all sequence images. It first determines the order of requesting the images and then asynchronously loads all of them.

    • It preloads all frames and sprites.
                      preload: function(e){
                        var
                          $overlay= t.parent(),
                          images= get(_images_),
                          is_sprite= !images.length,
                          order= reel.preload[opt.preload] || reel.preload[reel.def.preload],
                          preload= is_sprite ? [get(_image_)] : order(images.slice(0), opt, get),
                          to_load= preload.length,
                          preloaded= set(_preloaded_, is_sprite ? 0.5 : 0),
                          simultaneous= 0,
                          $cache= get(_cache_).empty(),
                          uris= []
                        $overlay.addClass(loading_klass);
  • It also finalizes the instance stylesheet and prepends it to the head.

                        set(_style_, get(_style_) || $('<'+_style_+' type="text/css">'+css.rules.join('\n')+'</'+_style_+'>').prependTo(_head_));
                        set(_loading_, true);
                        t.trigger('stop');
                        opt.responsive && gauge();
                        t.trigger('resize', true);
                        while(preload.length){
                          var
                            uri= reel.substitute(opt.path+preload.shift(), get),
                            $img= $(tag(_img_)).data(_src_, uri).appendTo($cache)
  • Each image, which finishes the load triggers "preloaded" Event.

                          $img.bind('load error abort', function(e){
                            e.type != 'load' && t.trigger(e.type);
                            return !detached($overlay) && t.trigger('preloaded') && load() && false;
                          });
                          uris.push(uri);
                        }
                        setTimeout(function(){ while (++simultaneous < reel.concurrent_requests) load(); }, 0);
                        set(_cached_, uris);
                        set(_shy_, false);
                        function load(){
                          var
                            $img= $cache.children(':not([src]):first')
                          return $img.attr(_src_, $img.data(_src_))
                        }
                      },
  • ¶

    preloaded Event

    Event, since 1.1

    This event is fired by every preloaded image and adjusts the preloader indicator's target position. Once all images are preloaded, "loaded" Event is triggered.

                      preloaded: function(e){
                        var
                          images= get(_images_).length || 1,
                          preloaded= set(_preloaded_, min(get(_preloaded_) + 1, images))
                        if (preloaded === 1) var
                          frame= t.trigger('frameChange', [undefined, get(_frame_)])
                        if (preloaded === images){
                          t.parent().removeClass(loading_klass);
                          t.trigger('loaded');
                        }
                      },
  • ¶

    loaded Event

    Event, since 1.1

    "loaded" Event is the one announcing when the instance is "locked and loaded". At this time, all is prepared, preloaded and configured for user interaction or animation.

                      loaded: function(e){
                        get(_images_).length > 1 || t.css({ backgroundImage: url(reel.substitute(opt.path+get(_image_), get)) }).attr({ src: cdn(transparent) });
                        get(_stitched_) && t.attr({ src: cdn(transparent) });
                        get(_reeled_) || set(_velocity_, opt.velocity || 0);
                        set(_loading_, false);
                        loaded= true;
                      },
  • ¶

    prepare Event

    [NEW] Event, since 1.3

    In case of shy activation, "prepare" event is called instead of the full "setup". It lefts the target image untouched waiting to be clicked to actually setup.

                      prepare: function(e){
                        t.css('display', _block_).parent().one(_click_, shy_setup);
                      },

  • ¶

    Animation Events

  • ¶

    opening Event

    Event, since 1.1

    When opening animation is configured for the instance, "opening" event engages the animation by pre-calculating some of its properties, which will make the tick handler

                      opening: function(e){
                      /*
                      - initiates opening animation
                      - or simply plays the reel when without opening
                      */
                        if (!opt.opening) return t.trigger('openingDone');
                        var
                          opening= set(_opening_, true),
                          stopped= set(_stopped_, !get(_speed_)),
                          speed= opt.entry || opt.speed,
                          end= get(_fraction_),
                          duration= opt.opening,
                          start= set(_fraction_, end - speed * duration),
                          ticks= set(_opening_ticks_, ceil(duration * leader(_tempo_)))
                      },
  • ¶

    openingDone Event

    Event, since 1.1

    "openingDone" is fired onceWhen opening animation is configured for the instance, "opening" event engages the animation by pre-calculating some of its properties, which will make the tick handler

                      openingDone: function(e){
                        var
                          playing= set(_playing_, false),
                          opening= set(_opening_, false),
                          evnt= _tick_+dot(_opening_)
                        pool.unbind(evnt, on.pool[evnt]);
                        opt.orientable && $(window).bind(_deviceorientation_, orient);
                        if (opt.delay > 0) delay= setTimeout(function(){ t.trigger('play') }, opt.delay * 1000)
                        else t.trigger('play');
                        function orient(e){ return t.trigger('orient', [gyro(e).alpha, gyro(e).beta, gyro(e).gamma, e]) && e.give }
                      },

  • ¶

    Playback Control Events

  • ¶

    play Event

    Event, since 1.1

    "play" event can optionally accept a speed parameter (in Hz) to change the animation speed on the fly.

                      play: function(e, speed){
                        var
                          speed= speed ? set(_speed_, speed) : (get(_speed_) * negative_when(1, get(_backwards_))),
                          duration= opt.duration,
                          ticks= duration && set(_ticks_, ceil(duration * leader(_tempo_))),
                          backwards= set(_backwards_, speed < 0),
                          playing= set(_playing_, !!speed),
                          stopped= set(_stopped_, !playing)
                        idle();
                      },
  • ¶

    reach Event

    [NEW] Event, since 1.3

    Use this event to instruct Reel to play and reach a given frame. "reach" event requires target parameter, which is the frame to which Reel should animate to and stop. Optional speed parameter allows for custom speed independent on the regular speed.

                      reach: function(e, target, speed){
                        if (target == get(_frame_)) return;
                        var
                          frames= get(_frames_),
                          row= set(_row_, ceil(target / frames)),
                          departure= set(_departure_, get(_frame_)),
                          target= set(_destination_, target),
                          shortest = set(_distance_, reel.math.distance(departure, target, frames)),
                          speed= abs(speed || get(_speed_)) * negative_when(1, shortest < 0)
                        t.trigger('play', speed);
                      },
  • ¶

    pause Event

    Event, since 1.1

    Triggering "pause" event will halt the playback for a time period designated by the timeout option. After this timenout, the playback is resumed again.

                      pause: function(e){
                        unidle();
                      },
  • ¶

    stop Event

    Event, since 1.1

    After "stop" event is triggered, the playback stops and stays still until "play"ed again.

                      stop: function(e){
                        var
                          stopped= set(_stopped_, true),
                          playing= set(_playing_, !stopped)
                      },

  • ¶

    Human Interaction Events

  • ¶

    down Event

    Event, since 1.1

    Marks the very beginning of touch or mouse interaction. It receives x and y coordinates in arguments.

    • It calibrates the center point (origin),
    • considers user active not idle,
    • flags the <html> tag with .reel-panning class name
    • and binds dragging events for move and lift. These are usually bound to the pool (document itself) to get a consistent treating regardless the event target element. However in click-free mode, it binds directly to the instance.
                      down: function(e, x, y, ev){
                        if (!opt.clickfree && ev && ev.button !== undefined && ev.button != DRAG_BUTTON) return;
                        if (opt.draggable){
                          var
                            clicked= set(_clicked_, get(_frame_)),
                            clickfree= opt.clickfree,
                            velocity= set(_velocity_, 0),
                            $area= clickfree ? get(_area_) : pools,
                            origin= last= recenter_mouse(get(_revolution_), x, y)
                          unidle();
                          no_bias();
                          panned= 0;
                          $(_html_, pools).addClass(panning_klass);
                          $area
                            .bind(_touchmove_+___+_mousemove_, drag)
                            .bind(_touchend_+___+_touchcancel_, lift)
                            .bind(clickfree ? _mouseleave_ : _mouseup_, lift)
                        }
                        function drag(e){ return t.trigger('pan', [pointer(e).clientX, pointer(e).clientY, e]) && e.give }
                        function lift(e){ return t.trigger('up', [e]) && e.give }
                      },
  • ¶

    up Event

    Event, since 1.1

    This marks the termination of user's interaction. She either released the mouse button or lift the finger of the touch screen. This event handler:

    • calculates the velocity of the drag at that very moment,
    • removes the .reel-panning class from <body>
    • and unbinds dragging events from the pool.
                      up: function(e, ev){
                        var
                          clicked= set(_clicked_, false),
                          reeling= set(_reeling_, false),
                          throwable = opt.throwable,
                          biases= abs(bias[0] + bias[1]) / 60,
                          velocity= set(_velocity_, !throwable ? 0 : throwable === true ? biases : min(throwable, biases)),
                          brakes= braking= velocity ? 1 : 0
                        unidle();
                        no_bias();
                        $(_html_, pools).removeClass(panning_klass);
                        (opt.clickfree ? get(_area_) : pools).unbind(pns);
                      },
  • ¶

    pan Event

    [RENAMED] Event, since 1.2

    Regardles the actual source of movement (mouse or touch), this event is always triggered in response and similar to the "down" Event it receives x and y coordinates in arguments and in addition it is passed a reference to the original browser event. This handler:

    • syncs with timer to achieve good performance,
    • calculates the distance from drag center and applies graph on it to get fraction,
    • recenters the drag when dragged over limits,
    • detects the direction of the motion
    • and builds up inertial motion bias.

    Historically pan was once called slide (conflicted with Mootools - GH-51) or drag (that conflicted with MSIE).

                      pan: function(e, x, y, ev){
                        if (opt.draggable && slidable){
                          slidable= false;
                          unidle();
                          var
                            rows= opt.rows,
                            orbital= opt.orbital,
                            scrollable= !get(_reeling_) && rows <= 1 && !orbital && opt.scrollable,
                            delta= { x: x - last.x, y: y - last.y },
                            abs_delta= { x: abs(delta.x), y: abs(delta.y) }
                          if (ev && scrollable && abs_delta.x < abs_delta.y) return ev.give = true;
                          if (abs_delta.x > 0 || abs_delta.y > 0){
                            ev && (ev.give = false);
                            panned= max(abs_delta.x, abs_delta.y);
                            last= { x: x, y: y };
                            var
                              revolution= get(_revolution_),
                              origin= get(_clicked_location_),
                              vertical= get(_vertical_)
                            if (!get(_framelock_)) var
                              fraction= set(_fraction_, graph(vertical ? y - origin.y : x - origin.x, get(_clicked_on_), revolution, get(_lo_), get(_hi_), get(_cwish_), vertical ? y - origin.y : x - origin.x)),
                              reeling= set(_reeling_, get(_reeling_) || get(_frame_) != get(_clicked_)),
                              motion= to_bias(vertical ? delta.y : delta.x || 0),
                              backwards= motion && set(_backwards_, motion < 0)
                            if (orbital && get(_center_)) var
                              vertical= set(_vertical_, abs(y - origin.y) > abs(x - origin.x)),
                              origin= recenter_mouse(revolution, x, y)
                            if (rows > 1 && !get(_rowlock_)) var
                              revolution_y= get(_revolution_y_),
                              start= get(_clicked_tier_),
                              lo= - start * revolution_y,
                              tier= set(_tier_, reel.math.envelope(y - origin.y, start, revolution_y, lo, lo + revolution_y, -1))
                            if (!(fraction % 1) && !opt.loops) var
                              origin= recenter_mouse(revolution, x, y)
                          }
                        }
                      },
  • ¶

    wheel Event

    Event, since 1.0

    Maps Reel to mouse wheel position change event which is provided by a nifty plug-in written by Brandon Aaron - the Mousewheel plug-in, which you will need to enable the mousewheel wheel for reeling. You can also choose to use Wheel Special Event plug-in by Three Dub Media instead. Either way "wheel" Event handler receives the positive or negative wheeled distance in arguments. This event:

    • calculates wheel input delta and adjusts the fraction using the graph,
    • recenters the "drag" each and every time,
    • detects motion direction
    • and nullifies the velocity.
                      wheel: function(e, distance, ev){
                        if (!distance) return;
                        wheeled= true;
                        var
                          delta= ceil(math.sqrt(abs(distance)) / 2),
                          delta= negative_when(delta, distance > 0),
                          revolution= 0.0833 * get(_revolution_), // Wheel's revolution is 1/12 of full revolution
                          origin= recenter_mouse(revolution),
                          backwards= delta && set(_backwards_, delta < 0),
                          velocity= set(_velocity_, 0),
                          fraction= set(_fraction_, graph(delta, get(_clicked_on_), revolution, get(_lo_), get(_hi_), get(_cwish_)))
                        ev && ev.preventDefault();
                        ev && (ev.give = false);
                        unidle();
                        t.trigger('up', [ev]);
                      },
  • ¶

    orient Event

    [NEW] Event, since 1.3

    Maps Reel to device orientation event which is provided by some touch enabled devices with gyroscope inside. Event handler receives all three device orientation angles in arguments. This event:

    • maps alpha angle directly to fraction
                      orient: function(e, alpha, beta, gamma, ev){
                        if (!slidable || operated) return;
                        oriented= true;
                        var
                          alpha_fraction= alpha / 360
                          fraction= set(_fraction_, +((opt.stitched || opt.cw ? 1 - alpha_fraction : alpha_fraction)).toFixed(2))
                        slidable = false;
                      },

  • ¶

    Data Change Events

  • Besides Reel being event-driven, it also is data-driven respectively data-change-driven meaning that there is a mechanism in place, which detects real changes in data being stored with .reel(name, value). Learn more about data changes.

    These data change bindings are for internal use only and you don't ever trigger them per se, you change data and that will trigger respective change event. If the value being stored is the same as the one already stored, nothing will be triggered.

    Example: Change Reel's current frame:

    .reel("frame", 15)

    Change events always receive the actual data key value in the third argument.

    Example: Log each viewed frame number into the developers console:

    .bind("frameChange", function(e, d, frame){
        console.log(frame)
    })

  • ¶

    fractionChange Event

    Event, since 1.0

    Internally Reel doesn't really work with the frames you set it up with. It uses fraction, which is a numeric value ranging from 0.0 to 1.0. When fraction changes this handler basically calculates and sets new value of frame.

                      fractionChange: function(e, nil, fraction){
                        if (nil !== undefined) return;
                        var
                          frame= 1 + floor(fraction / get(_bit_)),
                          multirow= opt.rows > 1,
                          orbital= opt.orbital,
                          center= set(_center_, !!orbital && (frame <= orbital || frame >= get(_footage_) - orbital + 2))
                        if (multirow) var
                          frame= frame + (get(_row_) - 1) * get(_frames_)
                        var
                          frame= set(_frame_, frame)
                      },
  • ¶

    tierChange Event

    Event, since 1.2

    The situation of tier is very much similar to the one of fraction. In multi-row movies, tier is an internal value for the vertical axis. Its value also ranges from 0.0 to 1.0. Handler calculates and sets new value of frame.

                      tierChange: function(e, nil, tier){
                        if (nil === undefined) var
                          row= set(_row_, round(interpolate(tier, 1, opt.rows))),
                          frames= get(_frames_),
                          frame= get(_frame_) % frames || frames,
                          frame= set(_frame_, frame + row * frames - frames)
                      },
  • ¶

    rowChange Event

    Event, since 1.1

    The physical vertical position of Reel is expressed in rows and ranges from 1 to the total number of rows defined with rows. This handler only converts row value to tier and sets it.

                      rowChange: function(e, nil, row){
                        if (nil === undefined) var
                          tier= may_set(_tier_, undefined, row, opt.rows)
                      },
  • ¶

    frameChange Event

    Event, since 1.0

    The physical horizontal position of Reel is expressed in frames and ranges from 1 to the total number of frames configured with frames. This handler converts row value to tier and sets it. This default handler:

    • flags the instance's outter wrapper with .frame-X class name (where X is the actual frame number),
    • calculates and eventually sets fraction (and tier for multi-rows) from given frame,
    • for sequences, it switches the <img>'s src to the right frame
    • and for sprites it recalculates sprite's 'background position shift and applies it.
                      frameChange: function(e, nil, frame){
                        if (nil !== undefined) return;
                        this.className= this.className.replace(reel.re.frame_klass, frame_klass + frame);
                        var
                          frames= get(_frames_),
                          rows= opt.rows,
                          path= opt.path,
                          base= frame % frames || frames,
                          frame_row= (frame - base) / frames + 1,
                          frame_tier= (frame_row - 1) / (rows - 1),
                          row= get(_row_),
                          tier= !rows ? get(_tier_) : may_set(_tier_, frame_tier, row, rows),
                          fraction= may_set(_fraction_, undefined, base, frames),
                          footage= get(_footage_)
                        if (opt.orbital && get(_vertical_)) var
                          frame= opt.inversed ? footage + 1 - frame : frame,
                          frame= frame + footage
                        var
                          stitched= get(_stitched_),
                          images= get(_images_),
                          is_sprite= !images.length || stitched
                        if (!is_sprite){
                          get(_responsive_) && gauge();
                          get(_preloaded_) && t.attr({ src: reen(reel.substitute(path + images[frame - 1], get)) });
                        }else{
                          var
                            spacing= get(_spacing_),
                            width= get(_width_),
                            height= get(_height_)
                          if (!stitched) var
                            horizontal= opt.horizontal,
                            minor= (frame % footage) - 1,
                            minor= minor < 0 ? footage - 1 : minor,
                            major= floor((frame - 0.1) / footage),
                            major= major + (rows > 1 ? 0 : (get(_backwards_) ? 0 : !opt.directional ? 0 : get(_rows_))),
                            a= major * ((horizontal ? height : width) + spacing),
                            b= minor * ((horizontal ? width : height) + spacing),
                            shift= images.length ? [0, 0] : horizontal ? [px(-b), px(-a)] : [px(-a), px(-b)]
                          else{
                            var
                              x= set(_stitched_shift_, round(interpolate(fraction, 0, get(_stitched_travel_))) % stitched),
                              y= rows <= 1 ? 0 : (height + spacing) * (rows - row),
                              shift= [px(-x), px(-y)],
                              image= images.length > 1 && images[row - 1],
                              fullpath= reel.substitute(path + image, get)
                            image && t.css('backgroundImage').search(fullpath) < 0 && t.css({ backgroundImage: url(fullpath) })
                          }
                          t.css({ backgroundPosition: shift.join(___) })
                        }
                      },
  • This extra binding takes care of watching frame position while animating the "reach" event.

                      'frameChange.reach': function(e, nil, frame){
                        if (!get(_destination_) || nil !== undefined) return;
                        var
                          travelled= reel.math.distance(get(_departure_), frame, get(_frames_)),
                          onorover= abs(travelled) >= abs(get(_distance_))
                        if (!onorover) return;
                        set(_frame_, set(_destination_));
                        set(_destination_, set(_distance_, set(_departure_, 0)));
                        t.trigger('stop');
                      },
  • ~~~

    When image or images is changed on the fly, this handler resets the loading cache and triggers new preload sequence. Images are actually switched only after the new image is fully loaded.

                      'imageChange imagesChange': function(e, nil, image){
                        t.trigger('preload');
                      },

  • ¶

    Indicator

  • When configured with the indicator option, Reel adds to the scene a visual indicator in a form of a black rectangle traveling along the bottom edge of the image. It bears two distinct messages:

    • its horizontal position accurately reflects actual fraction
    • and its width reflect one frame's share on the whole (more frames mean narrower indicator).
                      'fractionChange.indicator': function(e, nil, fraction){
                        if (opt.indicator && nil === undefined) var
                          size= opt.indicator,
                          orbital= opt.orbital,
                          travel= orbital && get(_vertical_) ? get(_height_) : get(_width_),
                          slots= orbital ? get(_footage_) : opt.images.length || get(_frames_),
                          weight= ceil(travel / slots),
                          travel= travel - weight,
                          indicate= round(interpolate(fraction, 0, travel)),
                          indicate= !opt.cw || get(_stitched_) ? indicate : travel - indicate,
                          $indicator= indicator.$x.css(get(_vertical_)
                          ? { left: 0, top: px(indicate), bottom: _auto_, width: size, height: weight }
                          : { bottom: 0, left: px(indicate), top: _auto_, width: weight, height: size })
                      },
  • For multi-row object movies, there's a second indicator sticked to the left edge and communicates:

    • its vertical position accurately reflects actual tier
    • and its height reflect one row's share on the whole (more rows mean narrower indicator).
                      'tierChange.indicator': function(e, nil, tier){
                        if (opt.rows > 1 && opt.indicator && nil === undefined) var
                          travel= get(_height_),
                          size= opt.indicator,
                          weight= ceil(travel / opt.rows),
                          travel= travel - weight,
                          indicate= round(tier * travel),
                          $yindicator= indicator.$y.css({ left: 0, top: indicate, width: size, height: weight })
                      },
  • Indicators are bound to fraction or row changes, meaning they alone can consume more CPU resources than the entire Reel scene. Use them for development only.


  • ¶

    Annotations

  • If you want to annotate features of your scene to better describe the subject, there's annotations for you. Annotations feature allows you to place virtually any HTML content over or into the image and have its position and visibility synchronized with the position of Reel. These two easy looking handlers do a lot more than to fit in here.

    Learn more about Annotations in the wiki, where a great care has been taken to in-depth explain this new exciting functionality.

                      'setup.annotations': function(e){
                        var
                          $overlay= t.parent()
                        $.each(get(_annotations_), function(ida, note){
                          var
                            $note= typeof note.node == _string_ ? $(note.node) : note.node || {},
                            $note= $note.jquery ? $note : $(tag(_div_), $note),
                            $note= $note.attr({ id: ida }).addClass(annotation_klass),
                            $image= note.image ? $(tag(_img_), note.image) : $(),
                            $link= note.link ? $(tag('a'), note.link).click(function(){ t.trigger('up.annotations', { target: $link }); }) : $()
                          css(hash(ida), { display: _none_, position: _absolute_ }, true);
                          note.image || note.link && $note.append($link);
                          note.link || note.image && $note.append($image);
                          note.link && note.image && $note.append($link.append($image));
                          $note.appendTo($overlay);
                        });
                      },
                      'prepare.annotations': function(e){
                        $.each(get(_annotations_), function(ida, note){
                          $(hash(ida)).hide();
                        });
                      },
                      'frameChange.annotations': function(e, nil, frame){
                        if (!get(_preloaded_) || nil !== undefined) return;
                        var
                          width= get(_width_),
                          stitched= get(_stitched_),
                          ss= get(_stitched_shift_)
                        $.each(get(_annotations_), function(ida, note){
                          var
                            $note= $(hash(ida)),
                            start= note.start || 1,
                            end= note.end,
                            frame= frame || get(_frame_),
                            offset= frame - 1,
                            at= note.at ? (note.at[offset] == '+') : false,
                            offset= note.at ? offset : offset - start + 1,
                            x= typeof note.x!=_object_ ? note.x : note.x[offset],
                            y= typeof note.y!=_object_ ? note.y : note.y[offset],
                            placed= x !== undefined && y !== undefined,
                            visible= placed && (note.at ? at : (offset >= 0 && (!end || offset <= end - start)))
                          if (stitched) var
                            on_edge= x < width && ss > stitched - width,
                            after_edge= x > stitched - width && ss >= 0 && ss < width,
                            x= !on_edge ? x : x + stitched,
                            x= !after_edge ? x : x - stitched,
                            x= x - ss
                          if (get(_responsive_)) var
                            ratio= get(_ratio_),
                            x= x && x * ratio,
                            y= y && y * ratio
                          var
                            style= { display: visible ? _block_:_none_, left: px(x), top: px(y) }
                          $note.css(style);
                        });
                      },
                      'up.annotations': function(e, ev){
                        if (panned > 10 || wheeled) return;
                        var
                          $target= $(ev.target),
                          $link= ($target.is('a') ? $target : $target.parents('a')),
                          href= $link.attr('href')
                        href && (panned= 10);
                      },

  • ¶

    Click Stepping Events

  • For devices without drag support or for developers, who want to use some sort of left & right buttons on their site to control your instance from outside, Reel supports ordinary click with added detection of left half or right half and resulting triggering of stepLeft and stepRight events respectively.

    This behavior can be disabled by the steppable option.

                      'up.steppable': function(e, ev){
                        if (panned || wheeled) return;
                        t.trigger(get(_clicked_location_).x - t.offset().left > 0.5 * get(_width_) ? 'stepRight' : 'stepLeft')
                      },
                      'stepLeft stepRight': function(e){
                        unidle();
                      },
  • ¶

    stepLeft Event

    Event, since 1.2

                      stepLeft: function(e){
                        set(_backwards_, false);
                        set(_fraction_, get(_fraction_) - get(_bit_) * get(_cwish_));
                      },
  • ¶

    stepRight Event

    Event, since 1.2

                      stepRight: function(e){
                        set(_backwards_, true);
                        set(_fraction_, get(_fraction_) + get(_bit_) * get(_cwish_));
                      },
  • ¶

    stepUp Event

    [NEW] Event, since 1.3

                      stepUp: function(e){
                        set(_row_, get(_row_) - 1);
                      },
  • ¶

    stepDown Event

    [NEW] Event, since 1.3

                      stepDown: function(e){
                        set(_row_, get(_row_) + 1);
                      },

  • ¶

    [NEW] Responsive Events

  • In responsive mode in case of parent's size change, in addition to actual recalculations, the instance starts to emit throttled resize events. This handler in turn emulates images changes event leading to reload of frames.


  • ¶

    resize Event

    [NEW] Event, since 1.3

                      resize: function(e, force){
                        if (get(_loading_) && !force) return;
                        var
                          stitched= get(_stitched_),
                          spacing= get(_spacing_),
                          height= get(_height_),
                          is_sprite= !get(_images_).length || stitched,
                          rows= opt.rows || 1,
                          size= get(_images_).length
                            ? !stitched ? undefined : px(stitched)+___+px(height)
                            : stitched && px(stitched)+___+px((height + spacing) * rows - spacing)
                            || px((get(_width_) + spacing) * get(_footage_) - spacing)+___+px((height + spacing) * get(_rows_) * rows * (opt.directional? 2:1) - spacing)
                        t.css({
                          height: is_sprite ? px(height) : null,
                          backgroundSize: size || null
                        });
                        force || t.trigger('imagesChange');
                      },

  • ¶

    Follow-up Events

  • When some event as a result triggers another event, it preferably is not triggered directly, because it would disallow preventing the event propagation / chaining to happen. Instead a followup handler is bound to the first event and it triggers the second one.

                      'setup.fu': function(e){
                        var
                          frame= set(_frame_, opt.frame + (get(_row_) - 1) * get(_frames_))
                        t.trigger('preload')
                      },
                      'wheel.fu': function(){ wheeled= false },
                      'clean.fu': function(){ t.trigger('teardown') },
                      'loaded.fu': function(){ t.trigger('opening') }
                    },

  • ¶

    Tick Handlers

  • As opposed to the events bound to the instance itself, there is a ticker in place, which emits tick.reel event on the document level by default every 1/36 of a second and drives all the animations. Three handlers currently bind each instance to the tick.

                    pool: {
  • This handler has a responsibility of continuously updating the preloading indicator until all images are loaded and to unbind itself then.

                      'tick.reel.preload': function(e){
                        if (!(loaded || get(_loading_)) || get(_shy_)) return;
                        var
                          width= get(_width_),
                          current= number(preloader.$.css(_width_)),
                          images= get(_images_).length || 1,
                          target= round(1 / images * get(_preloaded_) * width)
                        preloader.$.css({ width: current + (target - current) / 3 + 1 })
                        if (get(_preloaded_) === images && current > width - 1){
                          loaded= false;
                          preloader.$.fadeOut(300, function(){ preloader.$.css({ opacity: 1, width: 0 }) });
                        }
                      },
  • This handler binds to the document's ticks at all times, regardless the situation. It serves several tasks:

    • keeps track of how long the instance is being operated by the user,
    • or for how long it is braking the velocity inertia,
    • decreases gained velocity by applying power of the brake option,
    • flags the instance as slidable again, so that pan event handler can be executed again,
    • updates the monitor value,
    • bounces off the edges for non-looping panoramas,
    • and most importantly it animates the Reel if speed is configured.
                      'tick.reel': function(e){
                        if (get(_shy_)) return;
                        var
                          velocity= get(_velocity_),
                          leader_tempo= leader(_tempo_),
                          monitor= opt.monitor
                        if (!reel.intense && offscreen()) return;
                        if (braking) var
                          braked= velocity - (get(_brake_) / leader_tempo * braking),
                          velocity= set(_velocity_, braked > 0.1 ? braked : (braking= operated= 0))
                        monitor && $monitor.text(get(monitor));
                        velocity && braking++;
                        operated && operated++;
                        to_bias(0);
                        slidable= true;
                        if (operated && !velocity) return mute(e);
                        if (get(_clicked_)) return mute(e, unidle());
                        if (get(_opening_ticks_) > 0) return;
                        if (!opt.loops && opt.rebound) var
                          edgy= !operated && !(get(_fraction_) % 1) ? on_edge++ : (on_edge= 0),
                          bounce= on_edge >= opt.rebound * 1000 / leader_tempo,
                          backwards= bounce && set(_backwards_, !get(_backwards_))
                        var
                          direction= get(_cwish_) * negative_when(1, get(_backwards_)),
                          ticks= get(_ticks_),
                          step= (!get(_playing_) || oriented || !ticks ? velocity : abs(get(_speed_)) + velocity) / leader(_tempo_),
                          fraction= set(_fraction_, get(_fraction_) - step * direction),
                          ticks= !opt.duration ? ticks : ticks > 0 && set(_ticks_, ticks - 1)
                        !ticks && get(_playing_) && t.trigger('stop');
                      },
  • This handler performs the opening animation duty when during it the normal animation is halted until the opening finishes.

                      'tick.reel.opening': function(e){
                        if (!get(_opening_)) return;
                        var
                          speed= opt.entry || opt.speed,
                          step= speed / leader(_tempo_) * (opt.cw? -1:1),
                          ticks= set(_opening_ticks_, get(_opening_ticks_) - 1),
                          fraction= set(_fraction_, get(_fraction_) + step)
                        ticks || t.trigger('openingDone');
                      }
                    }
                  },
    
                  loaded= false,

  • ¶

    Instance Private Helpers

    • Events propagation stopper / muter
                  mute= function(e, result){ return e.stopImmediatePropagation() || result },
    • Shy initialization helper
                  shy_setup= function(){ t.trigger('setup') },
    • User idle control
                  operated,
                  braking= 0,
                  idle= function(){ return operated= 0 },
                  unidle= function(){
                    clearTimeout(delay);
                    pool.unbind(_tick_+dot(_opening_), on.pool[_tick_+dot(_opening_)]);
                    set(_opening_ticks_, 0);
                    set(_reeled_, true);
                    return operated= -opt.timeout * leader(_tempo_)
                  },
                  panned= 0,
                  wheeled= false,
                  oriented= false,
    • Constructors of UI elements
                  $monitor= $(),
                  preloader= function(){
                    css(___+dot(preloader_klass), {
                      position: _absolute_,
                      left: 0, bottom: 0,
                      height: opt.preloader,
                      overflow: _hidden_,
                      backgroundColor: '#000'
                    });
                    return preloader.$= $(tag(_div_), { 'class': preloader_klass })
                  },
                  indicator= function(axis){
                    css(___+dot(indicator_klass)+dot(axis), {
                      position: _absolute_,
                      width: 0, height: 0,
                      overflow: _hidden_,
                      backgroundColor: '#000'
                    });
                    return indicator['$'+axis]= $(tag(_div_), { 'class': indicator_klass+___+axis })
                  },
    • CSS rules & stylesheet
                  css= function(selector, definition, global){
                    var
                      stage= global ? __ : get(_stage_),
                      selector= selector.replace(/^/, stage).replace(____, ____+stage)
                    return css.rules.push(selector+cssize(definition)) && definition
                    function cssize(values){
                      var rules= [];
                      $.each(values, function(key, value){ rules.push(key.replace(/([A-Z])/g, '-$1').toLowerCase()+':'+px(value)+';') });
                      return '{'+rules.join(__)+'}'
                    }
                  },
                  $style,
    • Off screen detection (vertical only for performance)
                  offscreen= function(){
                    var
                      height= get(_height_),
                      width= get(_width_),
                      rect= t[0].getBoundingClientRect()
                    return rect.top < -height
                        || rect.left < -width
                        || rect.right > width + $(window).width()
                        || rect.bottom > height + $(window).height()
                  },
    • Inertia rotation control
                  on_edge= 0,
                  last= { x: 0, y: 0 },
                  to_bias= function(value){ return bias.push(value) && bias.shift() && value },
                  no_bias= function(){ return bias= [0,0] },
                  bias= no_bias(),
    • Graph function to be used
                  graph= opt.graph || reel.math[opt.loops ? 'hatch' : 'envelope'],
                  normal= reel.normal,
    • Response to the size changes in responsive mode
                  slow_gauge= function(){
                    clearTimeout(gauge_delay);
                    gauge_delay= setTimeout(gauge, reel.resize_gauge);
                  },
                  gauge= function(){
                    if (t.width() == get(_width_)) return;
                    var
                      truescale= get(_truescale_),
                      ratio= set(_ratio_, t.width() / truescale.width)
                    $.each(truescale, function(key, value){ set(key, round(value * ratio)) })
                    t.trigger('resize');
                  },
    • Delay timer pointers
                  delay, // openingDone's delayed play
                  gauge_delay, // slow_gauge's throttle
    • Interaction graph's zero point reset
                  recenter_mouse= function(revolution, x, y){
                    var
                      fraction= set(_clicked_on_, get(_fraction_)),
                      tier= set(_clicked_tier_, get(_tier_)),
                      loops= opt.loops,
                      lo= set(_lo_, loops ? 0 : - fraction * revolution),
                      hi= set(_hi_, loops ? revolution : revolution - fraction * revolution)
                    return x !== undefined && set(_clicked_location_, { x: x, y: y }) || undefined
                  },
                  slidable= true,
  • ~~~

    Data interface used to set fraction and tier with the value recalculated through their cousin keys (frame for fraction and row for tier). This value is actually set only if it does make a difference in the cousin value.

                  may_set= function(key, value, cousin, maximum){
                    if (!maximum) return;
                    var
                      current= get(key) || 0,
                      recalculated= value !== undefined ? value : (cousin - 1) / (maximum - 1),
                      recalculated= key != _fraction_ ? recalculated : min( recalculated, 0.9999),
                      worthy= +abs(current - recalculated).toFixed(8) >= +(1 / (maximum - 1)).toFixed(8),
                      value= worthy ? set(key, recalculated) : value || current
                    return value
                  },
  • ~~~

    Global events are bound to the pool (document), but to make it work inside an <iframe> we need to bind to the parent document too to maintain the dragging even outside the area of the <iframe>.

                  pools= pool
                try{ if (pool[0] != top.document) pools= pool.add(top.document) }
                catch(e){}
  • A private flag $iframe is established to indicate Reel being viewed inside <iframe>.

                var
                  $iframe= top === self ? $() : (function sense_iframe($ifr){
                    $('iframe', pools.last()).each(function(){
                      try{ if ($(this).contents().find(_head_).html() == $(_head_).html()) return ($ifr= $(this)) && false }
                      catch(e){}
                    })
                    return $ifr
                  })()
                css.rules= [];
                on.setup();
              });
  • ~~~

    Reel maintains a ticker, which guides all animations. There's only one ticker per document and all instances bind to it. Ticker's mechanism measures and compares times before and after the tick.reel event trigger to estimate the time spent on executing tick.reel's handlers. The actual timeout time is then adjusted by the amount to run as close to expected tempo as possible.

              ticker= ticker || (function tick(){
                var
                  start= +new Date(),
                  tempo= leader(_tempo_)
                if (!tempo) return ticker= null;
                pool.trigger(_tick_);
                reel.cost= (+new Date() + reel.cost - start) / 2;
                return ticker= setTimeout(tick, max(4, 1000 / tempo - reel.cost));
              })();
    
              return $(instances);
              }
            },

  • ¶

    Destruction

  • The evil-twin of .reel(). Tears down and wipes off entire instance.


  • ¶

    .unreel() Method

    returns jQuery, since 1.2

            unreel: function(){
              return this.trigger('teardown');
            }
          },

  • ¶

    Regular Expressions

  • Few regular expressions is used here and there mostly for options validation and verification levels of user agent's capabilities.


  • ¶

    $.reel.re

    RegExp, since 1.1

          re: {
            /* Valid image file format */
            image:         /^(.*)\.(jpg|jpeg|png|gif)\??.*$/i,
            /* User agent failsafe stack */
            ua: [
                           /(msie|opera|firefox|chrome|safari)[ \/:]([\d.]+)/i,
                           /(webkit)\/([\d.]+)/i,
                           /(mozilla)\/([\d.]+)/i
            ],
            /* Array in a string (comma-separated values) */
            array:         / *, */,
            /* Lazy (low-CPU mobile devices) */
            lazy_agent:    /\(iphone|ipod|android|fennec|blackberry/i,
            /* Format of frame class flag on the instance */
            frame_klass:   /frame-\d+/,
            /* Mask for substitutions in URL */
            substitution:  /(@([A-Z]))/g,
            /* Used for cross-browser detection of Regexp no match situation */
            no_match:      /^(undefined|)$/,
            /* [Sequence](#Sequence) string format */
            sequence:      /(^[^#|]*([#]+)[^#|]*)($|[|]([0-9]+)\.\.([0-9]+))($|[|]([0-9]+)$)/
          },

  • ¶

    Content Delivery Network

  • CDN is used for distributing mouse cursors to all instances running world-wide. It runs on Google cloud infrastructure. If you want to ease up on the servers, please consider setting up your own location with the cursors.


  • ¶

    $.reel.cdn

    String (URL path), since 1.1

          cdn: 'http://code.vostrel.net/',

  • ¶

    Math Behind

  • Surprisingly there's very little math behind Reel. Two equations (graph functions), which drive Reel motion and receive the same set of options.


  • ¶

    $.reel.math

    Object, since 1.1

          math: {
  • 1 |  ********
      |          **
      |            **
      |              **
      |                **
      |                  ********
    0  ----------------------------›
            envelope: function(x, start, revolution, lo, hi, cwness, y){
              return start + min_max(lo, hi, - x * cwness) / revolution
            },
  • 1 |        **          **
      |          **          **
      |            **          **
      |  **          **
      |    **          **
      |      **          **
    0  ----------------------------›
            hatch: function(x, start, revolution, lo, hi, cwness, y){
              var
                x= (x < lo ? hi : 0) + x % hi, // Looping
                fraction= start + (- x * cwness) / revolution
              return fraction - floor(fraction)
            },
  • Plus equation for interpolating fraction (and tier) value into frame and row.

            interpolate: function(fraction, lo, hi){
              return lo + fraction * (hi - lo)
            },
  • And one for calculation of the shortest frame distance from start to the end.

            distance: function(start, end, total){
              var
                half= total / 2,
                d= end - start
              return d < -half ? d + total : d > half ? d - total : d
            }
          },

  • ¶

    Preloading Modes

  • Reel doesn't load frames in a linear manner from first to last (alhough it can if configured that way with the preload option). Reel will take the linear configured sequence and hand it over to one of $.reel.preload functions, along with reference to options and the RO data intearface, and it expects the function to reorder the incoming Array and return it back.


  • ¶

    $.reel.preload

    Object, since 1.2

          preload: {
  • The best (and default) option is the fidelity processor, which is designed for a faster and better perceived loading.

    Example

            fidelity: function(sequence, opt, get){
              var
                orbital= opt.orbital,
                rows= orbital ? 2 : opt.rows || 1,
                frames= orbital ? get(_footage_) : get(_frames_),
                start= (opt.row-1) * frames,
                values= new Array().concat(sequence),
                present= new Array(sequence.length + 1),
                priority= rows < 2 ? [] : values.slice(start, start + frames)
              return spread(priority, 1, start).concat(spread(values, rows, 0))
    
              function spread(sequence, rows, offset){
                if (!sequence.length) return [];
                var
                  order= [],
                  passes= 4 * rows,
                  start= opt.frame,
                  frames= sequence.length,
                  plus= true,
                  granule= frames / passes
                for(var i= 0; i < passes; i++)
                  add(start + round(i * granule));
                while(granule > 1)
                  for(var i= 0, length= order.length, granule= granule / 2, p= plus= !plus; i < length; i++)
                    add(order[i] + (plus? 1:-1) * round(granule));
                for(var i=0; i <= frames; i++) add(i);
                for(var i= 0; i < order.length; i++)
                  order[i]= sequence[order[i] - 1];
                return order
                function add(frame){
                  while(!(frame >= 1 && frame <= frames))
                    frame+= frame < 1 ? +frames : -frames;
                  return present[offset + frame] || (present[offset + frame]= !!order.push(frame))
                }
              }
            },
  • You can opt for a linear loading order too, but that has a drawback of leaving large gap of unloaded frames.

            linear: function(sequence, opt, get){
              return sequence
            }
          },

  • ¶

    [NEW] Data Values in URLs

  • Reel will process each and every image resource URL and substitute special markup with actual values from the data store. Marks made of @ character followed by upper case letter will be substituted with values either directly from data store (@W and @H for width and height) or calculated (@T is substituted with momentary timestamp in milliseconds). Markup can appear anywhere in the folder name, file name or the query params (also in path) and even multiple times.

    Comes handy in product configurators and works magic in conjunction with responsive option.

    Example: Following URLs:

    image.jpg?size=@Wx@H
    pic/@W/@H/rabbit.png
    image.php?nocache=@T

    ... will come out like this for Reel 320 pixels wide and 180 high:

    image.jpg?size=320x180
    pic/320/180/rabbit.png
    image.php?nocache=1377604502788

  • ¶

    $.reel.substitute()

    [NEW] Function, since 1.3

          substitute: function(uri, get){
            return uri.replace(reel.re.substitution, function(match, mark, key){
              return typeof reel.substitutes[key] == 'function'
                          ? reel.substitutes[key](get) : substitution_keys[key]
                          ? get(substitution_keys[key]) : mark;
            });
          },
  • ¶

    $.reel.substitutes

    [NEW] Object of Functions, since 1.3

          substitutes: {
            T: function(get){ return +new Date() }
          },

  • ¶

    Data Value Normalization

  • On all data values being stored with .reel() an attempt is made to normalize the value. Like for example normalization of frame 55 when there's just 45 frames total. These are the built-in normalizations. Normalization function has the same name as the data key it is assigned to and is given the raw value in arguments, along with reference to the instances data object, and it has to return the normalized value.


  • ¶

    $.reel.normal

    Object, since 1.2

          normal: {
            fraction: function(fraction, data){
              if (fraction === null) return fraction;
              return data[_options_].loops ? fraction - floor(fraction) : min_max(0, 1, fraction)
            },
            tier: function(tier, data){
              if (tier === null) return tier;
              return min_max(0, 1, tier)
            },
            row: function(row, data){
              if (row === null) return row;
              return round(min_max(1, data[_options_].rows, row))
            },
            frame: function(frame, data){
              if (frame === null) return frame;
              var
                opt= data[_options_],
                frames= data[_frames_] * (opt.orbital ? 2 : opt.rows || 1),
                result= round(opt.loops ? frame % frames || frames : min_max(1, frames, frame))
              return result < 0 ? result + frames : result
            },
            images: function(images, data){
              var
                sequence= reel.re.sequence.exec(images),
                result= !sequence ? images : reel.sequence(sequence, data[_options_])
              return result;
            }
          },

  • ¶

    Sequence Build-up

  • When configured with a String value for images like image##.jpg, it first has to be converted into an actual Array by engaging the counter placeholder.


  • ¶

    $.reel.sequence()

    Function, since 1.2

          sequence: function(sequence, opt){
            if (sequence.length <= 1) return opt.images;
            var
              images= [],
              orbital= opt.orbital,
              url= sequence[1],
              placeholder= sequence[2],
              start= sequence[4],
              start= reel.re.no_match.test(start+__) ? 1 : +start,
              rows= orbital ? 2 : opt.rows || 1,
              frames= orbital ? opt.footage : opt.stitched ? 1 : opt.frames,
              end= +(sequence[5] || rows * frames),
              total= end - start,
              increment= +sequence[7] || 1,
              counter= 0
            while(counter <= total){
              images.push(url.replace(placeholder, pad((start + counter + __), placeholder.length, '0')));
              counter+= increment;
            }
            return images;
          },

  • ¶

    Reel Instances

  • $.reel.instances holds an inventory of all running instances in the DOM document.


  • ¶

    $.reel.instances

    jQuery, since 1.1

          instances: $(),
  • For ticker-synchronization-related purposes Reel maintains a reference to the leaders data object all the time.


  • ¶

    $.reel.leader

    Object (DOM data), since 1.1

          leader: leader,
  • $.reel.resize_gauge specifies a throttling interval for triggering of resize events, in milliseconds.


  • ¶

    $.reel.resize_gauge

    [NEW] Number, since 1.3

          resize_gauge: 300,
  • $.reel.concurrent_requests specifies how many preloading requests will run simultaneously.


  • ¶

    $.reel.concurrent_requests

    [NEW] Number, since 1.3

          concurrent_requests: 4,
  • $.reel.cost holds document-wide costs in miliseconds of running all Reel instances. It is used to adjust actual timeout of the ticker.


  • ¶

    $.reel.cost

    Number, since 1.1

          cost: 0
        },

  • ¶

    Private-scoped Variables

  •     pool= $(document),
        client= navigator.userAgent,
        browser= reel.re.ua[0].exec(client) || reel.re.ua[1].exec(client) || reel.re.ua[2].exec(client),
        browser_version= +browser[2].split('.').slice(0,2).join('.'),
        ie= browser[1] == 'MSIE',
        knows_data_urls= !(ie && browser_version < 8),
        knows_background_size= !(ie && browser_version < 9),
        ticker,

  • ¶

    CSS Class Names

  • These are all the class names assigned by Reel to various DOM elements during initialization of the UI and they all share same base "reel", which in isolation also is the class of the <img> node you converted into Reel.

        klass= 'reel',
  • Rest of the class names only extend this base class forming for example .reel-overlay, a class assigned to the outter instance wrapper (<img>'s injected parent).

        overlay_klass= klass + '-overlay',
        cache_klass= klass + '-cache',
        indicator_klass= klass + '-indicator',
        preloader_klass= klass + '-preloader',
        monitor_klass= klass + '-monitor',
        annotation_klass= klass + '-annotation',
        panning_klass= klass + '-panning',
        loading_klass= klass + '-loading',
  • The instance wrapper is flagged with actual frame number using a this class.

    Example: Reel on frame 10 will carry a class name frame-10.

        frame_klass= 'frame-',

  • ¶

    Shortcuts And Minification Cache

  • Several math functions are referenced inside the private scope to yield smaller filesize when the code is minified.

        math= Math,
        round= math.round, floor= math.floor, ceil= math.ceil,
        min= math.min, max= math.max, abs= math.abs,
        number= parseInt,
        interpolate= reel.math.interpolate,
  • For the very same reason all storage key Strings are cached into local vars.

        _annotations_= 'annotations', _area_= 'area', _auto_= 'auto',
        _backup_= 'backup', _backwards_= 'backwards', _bit_= 'bit', _brake_= 'brake',
        _cache_= 'cache', _cached_=_cache_+'d', _center_= 'center', _class_= 'class', _click_= 'click',
        _clicked_= _click_+'ed', _clicked_location_= _clicked_+'_location', _clicked_on_= _clicked_+'_on',
        _clicked_tier_= _clicked_+'_tier', _cwish_= 'cwish',
        _departure_= 'departure', _destination_= 'destination', _distance_= 'distance',
        _footage_= 'footage', _fraction_= 'fraction', _frame_= 'frame', _framelock_= 'framelock', _frames_= 'frames',
        _height_= 'height', _hi_= 'hi', _hidden_= 'hidden',
        _image_= 'image', _images_= 'images',
        _lo_= 'lo', _loading_= 'loading',
        _mouse_= 'mouse',
        _opening_= 'opening', _opening_ticks_= _opening_+'_ticks', _options_= 'options',
        _playing_= 'playing', _preloaded_= 'preloaded',
        _ratio_= 'ratio', _reeling_= 'reeling', _reeled_= 'reeled', _responsive_= 'responsive', _revolution_= 'revolution',
        _revolution_y_= 'revolution_y', _row_= 'row', _rowlock_= 'rowlock', _rows_= 'rows',
        _shy_= 'shy', _spacing_= 'spacing', _speed_= 'speed', _src_= 'src', _stage_= 'stage', _stitched_= 'stitched',
        _stitched_shift_= _stitched_+'_shift', _stitched_travel_= _stitched_+'_travel', _stopped_= 'stopped',
        _style_= 'style',
        _tempo_= 'tempo', _ticks_= 'ticks', _tier_= 'tier', _touch_= 'touch', _truescale_= 'truescale',
        _velocity_= 'velocity', _vertical_= 'vertical',
        _width_= 'width',
  • And the same goes for browser events too.

        ns= dot(klass),
        pns= dot('pan') + ns,
        _deviceorientation_= 'deviceorientation'+ns, _dragstart_= 'dragstart'+ns,
        _mousedown_= _mouse_+'down'+ns, _mouseenter_= _mouse_+'enter'+ns, _mouseleave_= _mouse_+'leave'+pns,
        _mousemove_= _mouse_+'move'+pns, _mouseup_= _mouse_+'up'+pns, _mousewheel_= _mouse_+'wheel'+ns,
        _tick_= 'tick'+ns, _touchcancel_= _touch_+'cancel'+pns, _touchend_= _touch_+'end'+pns,
        _touchstart_= _touch_+'start'+ns, _touchmove_= _touch_+'move'+pns,
        _resize_= 'resize'+ns,
  • And some other frequently used Strings.

        __= '', ___= ' ', ____=',', _absolute_= 'absolute', _block_= 'block', _cdn_= '@CDN@', _div_= 'div',
        _hand_= 'hand', _head_= 'head', _html_= 'html', _id_= 'id',
        _img_= 'img', _jquery_reel_= 'jquery.'+klass, _move_= 'move', _none_= 'none', _object_= 'object',
        _preload_= 'preload', _string_= 'string',
  • Collection of data keys holding scalable pixel values responsive to the scale ratio

        responsive_keys= [_width_, _height_, _spacing_, _revolution_, _revolution_y_, _stitched_, _stitched_shift_, _stitched_travel_],
        substitution_keys= { W: _width_, H: _height_ },

  • ¶

    Image Resources

  • Alhough we do what we can to hide the fact, Reel actually needs a few image resources to support some of its actions. First, we may need a transparent image for the original <img> to uncover the sprite applied to its background. This one is embedded in the code as it is very small.

        transparent= knows_data_urls ? embedded('CAAIAIAAAAAAAAAAACH5BAEAAAAALAAAAAAIAAgAAAIHhI+py+1dAAA7') : _cdn_+'blank.gif',
  • Proper cross-browser cursors however need to come in an odd format, which essentially is not compressed at all and this means bigger filesize. While it is no more than ~15k, it is unfit for embedding directly here, so a CDN is employed to retrieve the images from in an effective gzipped and cachable manner.

        reel_cursor= url(_cdn_+_jquery_reel_+'.cur')+____+_move_,
        drag_cursor= url(_cdn_+_jquery_reel_+'-drag.cur')+____+_move_,
        drag_cursor_down= url(_cdn_+_jquery_reel_+'-drag-down.cur')+____+_move_,
  • ~~~

    We then only route around MSIE's left button identification quirk (IE 8- reports left as right).

        lazy= reel.lazy= (reel.re.lazy_agent).test(client),
    
        DRAG_BUTTON= ie && browser_version < 9 ? 1 : 0,
  • ~~~

    So far jQuery doesn't have a proper built-in mechanism to detect/report DOM node removal. But internally, jQuery calls $.cleanData() to flush the DOM data and minimize memory leaks. Reel wraps this function and as a result clean event handler is triggered for every element. Note, that the clean event does not bubble.

        cleanData= $.cleanData,
        cleanDataEvent= $.cleanData= function(elements){
          $(elements).each(function(){ $(this).triggerHandler('clean'); });
          return cleanData.apply(this, arguments);
        }
  • Expose plugin functions as jQuery methods, do the initial global scan for data-configured <img> tags to become enhanced and export the entire namespace module.

      $.extend($.fn, reel.fn) && $(reel.scan);
      return reel;
  • Bunch of very useful helpers.

      function add_instance($instance){ return (reel.instances.push($instance[0])) && $instance }
      function remove_instance($instance){ return (reel.instances= reel.instances.not(hash($instance.attr(_id_)))) && $instance }
      function leader(key){ return reel.instances.first().data(key) }
      function embedded(image){ return 'data:image/gif;base64,R0lGODlh' + image }
      function tag(string){ return '<' + string + '/>' }
      function dot(string){ return '.' + (string || '') }
      function cdn(path){ return path.replace(_cdn_, reel.cdn) }
      function url(location){ return 'url(\'' + reen(location) + '\')' }
      function axis(key, value){ return typeof value == _object_ ? value[key] : value }
      function min_max(minimum, maximum, number){ return max(minimum, min(maximum, number)) }
      function negative_when(value, condition){ return abs(value) * (condition ? -1 : 1) }
      function pointer(e){ return e.touch || e.originalEvent.touches && e.originalEvent.touches[0] || e }
      function gyro(e){ return e.originalEvent }
      function px(value){ return value === undefined ? 0 : typeof value == _string_ ? value : value + 'px' }
      function hash(value){ return '#' + value }
      function pad(string, len, fill){ while (string.length < len) string= fill + string; return string }
      function twochar(string){ return pad(string, 2, '0') }
      function reen(uri){ return encodeURI(decodeURI(uri)) }
      function soft_array(string){ return reel.re.array.exec(string) ? string.split(reel.re.array) : string }
      function detached($node){ return !$node.parents(_html_).length }
      function numerize_array(array){ return typeof array == _string_ ? array : $.each(array, function(ix, it){ array[ix]= it ? +it : undefined }) }
      function error(message){ try{ console.error('[ Reel ] '+message) }catch(e){} }
    })(jQuery, window, document);
    
    });