Files
kupshop/web/templates/koza/static/js/jquery.reel.js
2025-08-02 16:30:27 +02:00

2822 lines
122 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@ @@@@@@@@
@@@@@@@ @@@@@@@
@@@@@@@ @@@@@@@
@@@@@@@ @@@@@@@
@@@@@@@@ @ @@@@@@@@
@@@@@@@@@ @@@ @@@@@@@@@
@@@@@@@@@@@@@@ @@@@@@@@@@@
@@@@@@@@@@@@@ @@@@@@@
@@@@@@@@@@@@ @@@
@@@@@@
@@@@
@@
*
* 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][1] compatible script loaders.
// Besides that it also complies with [CommonJS][2] module definition for Node and such.
// Of course, no fancy script loader is necessary and good old plain script tag still works too.
//
// [1]:http://en.wikipedia.org/wiki/Asynchronous_module_definition
// [2]:http://en.wikipedia.org/wiki/CommonJS
//
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`][1]
// 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.
//
// [1]:../example/object-movie-directional-sprite/
//
// ---
// #### `directional` Option ####
// `Boolean`, since 1.1
//
directional: false,
// ### Multi-Row Mode ######
//
// As [`example/object-movie-multirow-sequence`][1] 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.
//
// [1]:../example/object-movie-multirow-sequence/
//
// ---
// #### `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`][1] 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
//
// [1]:../example/object-movie-dual-orbit-sequence/
//
// ---
// #### `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](https://github.com/pisi/Reel/wiki/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](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`](#loops-Option)) 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][1] in a dedicated article.
//
// [1]:https://github.com/pisi/Reel/wiki/Annotations
//
// ---
// #### `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](#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](#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](#Data) or [causes events](#Control-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](#Construction-Core) is rather bulky, so let's skip it for now
// and instead let me introduce the other uses first.
// --------------------
// [NEW] Control Events
// --------------------
//
// [Event][1] 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`](#play-Event), [`pause`](#pause-Event)
// or [`stop`](#stop-Event)
// * step the Reel in any direction with [`stepRight`](#stepRight-Event), [`stepLeft`](#stepLeft-Event),
// [`stepUp`](#stepUp-Event), [`stepDown`](#stepDown-Event),
// * reach certain frame with [`reach`](#reach-Event)
//
// 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()`][2], only prefix
// the event name with `:`. Of course you can use simple `.trigger()` instead and without the colon.
//
//
// [1]:http://api.jquery.com/category/events/event-object/
// [2]:http://api.jquery.com/trigger
//
// ---
// #### `.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][1] interface
// while adding an additional change-detecting event layer, which makes Reel entirely data-driven.
//
// [1]:http://api.jquery.com/data
//
// _**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`](#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 _`key`_`Change`, 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](#Construction) 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](#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](#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][1])
// or `drag` (that conflicted with MSIE).
//
// [1]:https://github.com/pisi/Reel/issues/51
//
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][1], which you will need to enable
// the mousewheel wheel for reeling. You can also choose to use [Wheel Special Event
// plug-in][2] 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.
//
// [1]:https://github.com/brandonaaron/jquery-mousewheel
// [2]:http://blog.threedubmedia.com/2008/08/eventspecialwheel.html
//
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](#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`](#rows-Option). 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`](#frames-Option).
// 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`](#indicator-Option) 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][1] in the wiki, where a great care has been taken
// to in-depth explain this new exciting functionality.
//
// [1]:https://github.com/pisi/Reel/wiki/Annotations
//
'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`](#steppable-Option) 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](#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`](#brake-Option) option,
// - flags the instance as `slidable` again, so that `pan` event handler can be executed
// again,
// - updates the [`monitor`](#monitor-Option) value,
// - bounces off the edges for non-looping panoramas,
// - and most importantly it animates the Reel if [`speed`](#speed-Option) 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][1] 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.
//
// [1]:https://github.com/pisi/Reel/wiki/CDN
//
// ---
// ### `$.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`](#preload-Option) 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](https://camo.githubapp.com/74b73060a50f3cbaf522ec31530d34e3fa5cbcb9/687474703a2f2f6a71756572792e766f737472656c2e637a2f7265656c2f7363617474657265642e6c6f6164696e672e676966)
//
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`](#path-Option)) and even multiple times.
//
// Comes handy in product configurators
// and works magic in conjunction with [responsive](#responsive-Option) 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 `Function`s, 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`](#images-Option) 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`](#Content-Delivery-Network) 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);
});