UNPKG

sliderjs

Version:

lightweight slideshow engine using CSS, Canvas and WebGL Transitions

1,834 lines (1,620 loc) 74.1 kB
/*! Copyright 2010-2012 Gaetan Renaudeau http://sliderjs.org/ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ window.sliderjs = {}; window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })(); (function (ns) { // Matches dashed string for camelizing var rdashAlpha = /-([a-z]|[0-9])/ig, rmsPrefix = /^-ms-/, stringToListSplit = /[, ]+/, fcamelCase = function( all, letter ) { return ( letter + "" ).toUpperCase(); }; /* * util functions. * This internal implementation is light and native but can fail on some old browser. * Hence, you could override them with a plugin (jQuery, Prototype, ...) */ var $ = ns.util = { version: 'native', /// DOM /// html: function (element, html) { if(element) element.innerHTML = html; }, // add only the first node of html in element and return this node. append: function (element, html) { var div = document.createElement("div"); div.innerHTML = html; return element.appendChild(div.firstChild); }, find: function (element, css) { return element.querySelectorAll(css); }, addClass: function (element, c) { element.classList.add(c); }, removeClass: function (element, c) { element.classList.remove(c); }, camelCase: function (string) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); }, css: function (element, prop, value) { prop = $.camelCase(prop); if(arguments.length==1) return element.style[prop]; element.style[prop] = value; }, show: function (element) { element.style.display = "block"; }, hide: function (element) { element.style.display = "none"; }, width: function (element, w) { if (arguments.length==1) return element.style.width; element.style.width = w; }, height: function (element, h) { if (arguments.length==1) return element.style.height; element.style.height = h; }, on: function (element, eventName, fun) { element.addEventListener(eventName, fun); }, off: function (element, eventName, fun) { element.removeEventListener(eventName, fun); }, /// Utils /// indexOf: function (t, a) { if (t.indexOf) return t.indexOf(a); for(var i=0; i<t.length; i++) if(t[i]==a) return i; return -1; }, // Make a pub/sub event system (https://gist.github.com/1000193) makeEvent: function (_){return { pub:function (a,b,c,d){for(d=-1,c=[].concat(_[a]);c[++d];)c[d](b)}, sub:function (a,b){(_[a]||(_[a]=[])).push(b)}, del:function (a,b){if(_[a]){var i = $.indexOf(_[a], b);i>=0 && _[a].splice(i, 1);}} }}, each: function (a,f){ if(a.forEach) a.forEach(f); else for(var i=0; i<a.length; ++i) f(a[i], i, a); }, map: function (a,f){ var t=[]; $.each(a, function (v,i,l){ t[i]=f(v,i,l); }); return t; }, extend: function (obj) { for(var a=1; a<arguments.length; ++a) { var source = arguments[a]; for (var prop in source) if (source[prop] !== void 0) obj[prop] = source[prop]; } return obj; }, // difference between two arrays (a - b) dif: function (a, b) { var t = []; for (var i = 0; i<a.length; ++i) if ($.indexOf(b, a[i]) < 0) t.push(a[i]); return t; }, nop: function () {}, now: function (){ return +new Date() }, uuid: function (n){ return function (){ return ++n; } }(0), // Simplified version of "Simple JavaScript Templating" by John Resig - http://ejohn.org/ - MIT Licensed tmpl: function (str){ return new Function("obj","var p=[];with(obj||{}){p.push('"+ str.replace(/[\r\t\n]/g, " ") .split("<%").join("\t") .replace(/((^|%>)[^\t]*)'/g, "$1\r") .replace(/\t=(.*?)%>/g, "',$1,'") .split("\t").join("');") .split("%>").join("p.push('") .split("\r").join("\\'") +"');}return p.join('');"); }, getList: function (o) { return typeof(o)=="string" ? o.split(stringToListSplit) : (o instanceof Array ? o : []) }, createRegistrable: function() { var keys = [], hashmap = {}; return function(name, o){ switch (arguments.length) { case 0: return keys; case 1: return hashmap[name]; case 2: keys.push(name); hashmap[name]=o; }}; }, parseTime: function(time) { var typ = typeof time; if (typ == "number") return time; if (typ == "string") { var i = time.indexOf("ms"); if (i>0) return parseFloat(time.substring(0, i)); i = time.indexOf("s"); if (i>0) return 1000*parseFloat(time.substring(0, i)); } return NaN; } } }(window.sliderjs)); (function (ns) { /** * slider.Slider(nodeContainer, { ... }) */ ns.Slider = function (container, options) { return new Slider(container, options); } /** * Slider Constructor */ var Slider = function (container, options) { var self = this, $ = ns.util, T, // selector templating E, // pub/sub event system modules = {}; // store modules if(!container) throw "container node required in first argument"; if(!options) options = {}; // Init events E = $.makeEvent({}); self.trigger = function (event, data) { E.pub(event, data); return self; } self.on = function (event, callback) { E.sub(event, callback); return self; } self.off = function (event, callback) { E.del(event, callback); return self; } self.once = function (event, callback) { var removeCallback = function () { self.off(event, removeCallback).off(event, callback); } return self.on(event, callback).on(event, removeCallback); } // Init templating self.T = T = new SelectorTemplating(container, function () { self.trigger("templated", container); }); // opt method self.opt = function (optname, newValue) { var value = options[optname]; if(arguments.length==1) return value; if (modules[optname]) newValue = modules[optname].m.accept(newValue, value); options[optname] = newValue; var message = { key: optname, old: value, value: newValue }; self.trigger(optname+"Changed", message); return self; } // Get a module self.module = function (name) { var module = modules[name]; return module && module.m; } // Init modules self.modules = (function () { var list = function (name) { var t = []; for (var name in modules) t.push(name); return t; } list.start = function (names) { $.each($.getList(names), function (name) { var moduleFunction = ns.modules(name), onChanged, module; if (!moduleFunction) throw "module "+name+" not found."; modules[name] && this.stop(name); // Create module module = $.extend({ //extend the module with module defaults init: $.nop, change: $.nop, destroy: $.nop, accept: function (value) { return value }, fn: function (val) { var args = [name]; arguments.length && args.push(val); return self.opt.apply(self, args); } }, moduleFunction(new ns.Sandbox(self, name), $)); // Create module function self[name] = module.fn; // Set default options[name] = module.accept(name in options ? options[name] : module.def); // Bind change self.on(name+"Changed", onChanged = function (o) { module.change(o.value, o.old) }); // Init module module.init(); modules[name] = { m: module, c: onChanged }; self.trigger(name+"Started"); }); T.compile(); return self; } list.stop = function (names) { $.each($.getList(names), function (name) { var data = modules[name]; if (data) { data.m.destroy(); self.off(name+"Changed", data.c); delete options[name]; delete self[name]; delete modules[name]; self.trigger(name+"Stopped"); } }); T.compile(); return self; } return list; }()); // start all modules var modulesList = ns.modules(); var all = options.modules ? $.getList(options.modules) : modulesList; for (var o in options) { // add all options given in arguments if ($.indexOf(all, o) <= 0 && $.indexOf(modulesList, o) >= 0) all.push(o); } all = $.dif(all, $.getList(options.exceptModules)); self.modules.start(all); } /** * Static module creation * a sliderjs module define the behaviour of an option. * the name of a module MUST be the same as the related option name. */ ns.modules = sliderjs.util.createRegistrable(); /** * The sandbox given to the module. * This is the interface between a module and a slider instance. */ ns.Sandbox = function (slider, modulename) { var self = this; self.template = slider.T; // some proxified methods self.on = function () { slider.on.apply(slider, arguments); return self } self.once = function () { slider.once.apply(slider, arguments); return self } self.off = function () { slider.off.apply(slider, arguments); return self } self.trigger = function () { slider.trigger.apply(slider, arguments); return self } self.opt = function (key, value) { if(arguments.length==1) return slider.opt(key); slider.opt(key, value); return self; } // .value([v]) is a shortcut to .opt(modulename, [v]) self.value = function (value) { var args = [ modulename ]; arguments.length && args.push(value); return self.opt.apply(self, args); } // Request a module function self.ask = function (modulename, funname) { var module = slider.module(modulename); if(module) return module[funname]; } } }(window.sliderjs)); (function ($) { /** * SelectorTemplating.js * License: GPL v3 * Author: gaetanrenaudeau.fr - 2012 * Link: https://gist.github.com/gists/1731611 */ this.SelectorTemplating = function (container, onTemplated) { var self = this, rootTemplate, // type of Arguments fragments = []; // Array of { selector: ".container", tmpl: function(i){ return "html"; } } self.add = function (containerSelector, template, callback, priority) { fragments.push({ tmpl: template, cb: callback, selector: containerSelector, priority: priority || 0 }); return self; } self.remove = function (template) { var i = $.indexOf($.map(fragments, function (t) { return t.tmpl }), template); if (i >= 0) fragments.splice(i, 1); return self; } self.refresh = function (template) { self.compile(); } // Template the whole thing self.compile = function () { var cbs = [], i = 0, waitList, el, nodes, created, somethingChangedThisLoop; // empty the container self.destroy(); // all fragments goes to a waitlist, we need to append them all. // When a selector matches nodes, we append the template in nodes and remove from the waitlist. waitList = [].concat(fragments); waitList.sort(function (a, b) { return (a.priority != b.priority) ? (b.priority - a.priority) : // higher priority first (a.selector < b.selector ? -1 : 1); // try to put root selector first (guess) }); while (waitList.length) { el = waitList[i]; // Try to find the container nodes = !el.selector ? [container] : $.find(container, el.selector); if (nodes.length) { somethingChangedThisLoop = true; // template into each containers. created = $.map(nodes, function(node, i) { return $.append(node, el.tmpl(i)); }); el.nodes = created; // remove from the waitlist waitList.splice(i, 1); i = 0; } else { // continue the loop if we have waitList and something has changed the last loop. if (++i >= waitList.length) { i = 0; if(!somethingChangedThisLoop) break; somethingChangedThisLoop = false; } } } $.each(fragments, function (template) { template.cb && template.cb(template.nodes, container); }); onTemplated && onTemplated(); return self; } // Clean the template self.destroy = function () { $.html(container, ''); $.each(fragments, function (template) { template.nodes = []; }); return self; } } }(sliderjs.util)); //~~~ canvas transition sliderjs.modules("canvasTransition", function (sandbox, $) { var mode, container, canvas, ctx, images = [], renderId, drawing = false, slide, supported = function () { if (navigator && navigator.userAgent) { var m = navigator.userAgent.match(/Android ([0-9\.]+)/); if (m && parseFloat(m[1])<2.2) return false; // Old android versions suck with Canvas } var c = document.createElement("canvas"); return !!(c.getContext && c.getContext("2d")); }(), canvasTransitionSandbox, defaultValue = { commutative: true, params: {} }, template = $.tmpl('<canvas class="slides"></canvas>'); var CanvasRenderHelper = { clippedTransition: function (clipFunction) { return function (o, params) { var self = this; var c = self.ctx; self.drawImage(o.from); c.save(); c.beginPath(); clipFunction.call(self, o, c, params); c.clip(); self.drawImage(o.to); c.restore(); } } }; function findHelper (t) { for(var k in CanvasRenderHelper) if(t[k]) return CanvasRenderHelper[k](t[k]); } function syncHeight (h) { canvas.height = h; !drawing && drawCurrentImage(); } function syncWidth (w) { canvas.width = w; !drawing && drawCurrentImage(); } function onHeightChanged (o) { canvas && syncHeight(o.value); } function onWidthChanged (o) { canvas && syncWidth(o.value); } function drawCurrentImage () { var image = images[slide ? slide.value : sandbox.opt("slide")]; image && drawImage(image); } function drawImage (img) { var w = canvas.width; ctx.drawImage(img, 0, 0, w, w*img.height/img.width); } function clean () { ctx.clearRect(0, 0, canvas.width, canvas.height); } function CanvasTransitionSandbox() { var self = this; self.sandbox = sandbox; self.$ = $; self.canvas = canvas; self.ctx = ctx; self.drawImage = drawImage; self.clean = clean; } function onTemplated (nodes, container) { canvas = nodes[0]; if (!canvas || !canvas.getContext) return; syncWidth(sandbox.opt("width")); syncHeight(sandbox.opt("height")); ctx = canvas.getContext("2d"); updateDOM(); canvasTransitionSandbox = new CanvasTransitionSandbox(); } function updateDOM () { if (!canvas) return; if (mode=="canvas") { $.show(canvas); drawCurrentImage(); } else { $.hide(canvas); } } function startRender (fromSlide, toSlide, transition) { var transitionStart = $.now(), transitionDuration = sandbox.opt('duration') || 0, transitionFunction = transition.render, transitionEasing = sandbox.opt('easing'), data = transition.init && transition.init.call(canvasTransitionSandbox, fromSlide, toSlide), from = images[fromSlide], to = images[toSlide], reverse = transition.commutative && fromSlide > toSlide; transitionEasing = transitionEasing && transitionEasing.get || function (t) { return t }; if (!from || !to) return; if (reverse) { var tmp = to; to = from; from = tmp; } drawing = true; (function render (id) { var now = $.now(); if(id != renderId || now < transitionStart) return; var p = (now-transitionStart)/transitionDuration; if (p < 1) { requestAnimFrame(function () { render(id) }, canvas); p = transitionEasing(p); transitionFunction.call(canvasTransitionSandbox, { from: from, to: to, progress: reverse ? 1-p : p, data: data }, transition.params); } else { drawing = false; clean(); drawImage(reverse ? from : to); sandbox.trigger("transitionEnd", { mode: "canvas" }); } } (renderId = $.uuid())); } function onSlideChanged (o) { slide = o; if (mode=="canvas") ctx && startRender(o.old, o.value, sandbox.value()); } function onImagesLoaded (o) { images = o.images; updateDOM(); } function onTransitionMode (m) { if (m == mode) return; // still the same mode mode = m; updateDOM(); } return { init: function () { sandbox.template.add(".sliderjs div.slides", template, onTemplated); sandbox. on("slideChanged", onSlideChanged). on("heightChanged", onHeightChanged). on("widthChanged", onWidthChanged). on("imagesLoaded", onImagesLoaded). on("transitionMode", onTransitionMode); this.change(sandbox.value()); }, destroy: function () { sandbox.template.remove(template); sandbox. off("slideChanged", onSlideChanged). off("heightChanged", onHeightChanged). off("widthChanged", onWidthChanged). off("imagesLoaded", onImagesLoaded). off("transitionMode", onTransitionMode); }, accept: function (value, old) { if (!value) return value; if(typeof(value)=="string") value = { name: value }; var t = sliderjs.canvasTransitions(value.name); if (!t) t = value; // Check if a canvas render helper is used if (!t.render) t.render = findHelper(t); value = $.extend(defaultValue, value, t, value); if(!value.render) throw new Error("no canvas transition was found."); return value; }, change: function (value) { if (value===undefined) return; updateDOM(); sandbox.trigger("transitionMode", "canvas"); } } }); /** * define a new canvas transition */ sliderjs.canvasTransitions = sliderjs.util.createRegistrable(); /*~~~cssTransition~~~ Handle CSS transitions * * GET: get the current CSS transition. * SET: change the current transition to the given CSS transition * - string: the name of the transition. It is defined in the transitions/css/{name}.* file and is related to the ("tr-"+name) CSS class. * * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("cssTransition", function (sandbox, $) { var node, mode, prefix = "tr-", imageNodes, slideNodes, vendors = ["-webkit-", "-moz-", "-ms-", "-o-", ""]; function syncTransition () { var duration = sandbox.opt("duration") || 0; var ease = sandbox.opt("easing"); if (ease) ease = ease.bezier; slideNodes && $.each(slideNodes, function (node) { $.each(vendors, function (vendor) { $.css(node, vendor+"transition-duration", duration+"ms"); ease && $.css(node, vendor+"transition-timing-function", "cubic-bezier("+ease+")"); }); }); } function syncClass (value, old) { if (!node) return; $.removeClass(node, prefix+"clear") old && $.removeClass(node, prefix+old); value && $.addClass(node, prefix+value); } function onDurationChanged (o) { syncTransition(); } function onEasingChanged (o) { syncTransition(); } function onTransitionMode (m) { if (m == mode) return; // still the same mode if ((mode=m) != "css") syncClass("clear", sandbox.value()); updateDOM(); } var browser = (function(ua){ if (ua.match(/.*(Chrome|Safari).*/)) return "webkit"; if (ua.match(/.*Firefox.*/)) return "firefox"; if (navigator.appName === "Opera") return "opera"; return "webkit"; }(navigator.userAgent||"")); var transitionend = { webkit: "webkitTransitionEnd", opera: "oTransitionEnd", firefox: "transitionend" }[browser]; function bindTransitionEnd(n) { $.on(n, transitionend, function(e) { var c = e.target.className; if (c.indexOf("slide")!=-1 && c.indexOf("current")!=-1 && e.propertyName=="z-index") { sandbox.trigger("transitionEnd", { mode: "css" }); } }); } function updateDOM () { syncTransition (); if (mode=="css") $.each(imageNodes||[], $.show); else $.each(imageNodes||[], $.hide); } function onTemplated (container) { node = $.find(container, ".sliderjs")[0]; slideNodes = $.find(container, ".slide"); imageNodes = $.find(container, ".slide img"); syncClass(mode=="css" ? sandbox.value() : "clear"); updateDOM(); bindTransitionEnd(node); } return { accept: function (value) { return value && typeof(value)=="object" ? value.name : value; }, init: function () { sandbox. on("templated", onTemplated). on("durationChanged", onDurationChanged). on("easingChanged", onEasingChanged). on("transitionMode", onTransitionMode); this.change(sandbox.value()); }, destroy: function () { sandbox. off("templated", onTemplated). off("durationChanged", onDurationChanged). off("easingChanged", onEasingChanged). off("transitionMode", onTransitionMode); }, change: function (value, old) { if (value===undefined) return; syncClass(value, old); sandbox.trigger("transitionMode", "css"); } } }); sliderjs.cssTransitions = sliderjs.util.createRegistrable(); /**~~~duration~~~ handle the transition duration - the animation between 2 slides. * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("duration", function(sandbox, $) { return { def: 1000, accept: $.parseTime } }); /**~~~easing~~~ define the transition easing function * * GET an object with a .get(x) method and a .bezier array field. * .get is the CSS transition timing function * .bezier the bezier curve representing this easing function * * SET can be: * - a predefined easing (linear, ease-in, ease-out, ...) * - a JS function * - a [ X1, Y1, X2, Y2 ] array representing the bezier curve transition easing function * * <renaudeau.gaetan@gmail.com> - 2012 */ (function () { var Easing = { "ease": [0.25, 0.1, 0.25, 1.0], "linear": [0.00, 0.0, 1.00, 1.0], "ease-in": [0.42, 0.0, 1.00, 1.0], "ease-out": [0.00, 0.0, 0.58, 1.0], "ease-in-out": [0.42, 0.0, 0.58, 1.0] } sliderjs.modules("easing", function (sandbox, $) { return { def: "ease", accept: function (value) { if (typeof(value) == "function") return { get: value }; if (typeof(value) == "string") value = { bezier: Easing[value] }; else if (value instanceof Array) value = { bezier: value }; if (value && value.bezier && !value.get) { var b = value.bezier; value.get = new KeySpline(b[0], b[1], b[2], b[3]).get; } return value; } } }); /** * KeySpline - use bezier curve for transition easing function * is inspired from Firefox's nsSMILKeySpline.cpp * Usage: * var spline = new KeySpline(0.25, 0.1, 0.25, 1.0) * spline.get(x) => returns the easing value | x must be in [0, 1] range */ function KeySpline (mX1, mY1, mX2, mY2) { this.get = function(aX) { if (mX1 == mY1 && mX2 == mY2) return aX; // linear return CalcBezier(GetTForX(aX), mY1, mY2); } function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } function C(aA1) { return 3.0 * aA1; } // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. function CalcBezier(aT, aA1, aA2) { return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT; } // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. function GetSlope(aT, aA1, aA2) { return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } function GetTForX(aX) { // Newton raphson iteration var aGuessT = aX; for (var i = 0; i < 4; ++i) { var currentSlope = GetSlope(aGuessT, mX1, mX2); if (currentSlope == 0.0) return aGuessT; var currentX = CalcBezier(aGuessT, mX1, mX2) - aX; aGuessT -= currentX / currentSlope; } return aGuessT; } } }()); /*~~~fullscreen~~~ * * GET: * SET: * * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("fullscreen", function (sandbox, $) { var cl = "fullscreen"; var oldW, oldH; var node; var nativeFullScreenSupported = isFullscreen() !== undefined; function requestFullScreen (node) { if (node.requestFullScreen) return node.requestFullScreen(); if (node.webkitRequestFullScreen) return node.webkitRequestFullScreen(); if (node.mozRequestFullScreen) return node.mozRequestFullScreen(); } function cancelFullScreen () { if (document.cancelFullScreen) return document.cancelFullScreen(); if (document.webkitCancelFullScreen) return document.webkitCancelFullScreen(); if (document.mozCancelFullScreen) return document.mozCancelFullScreen(); } function isFullscreen () { return (document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen); } function onWindowResize () { var w = window.innerWidth, h = window.innerHeight; sandbox.opt("width", w).opt("height", h); } function setFullScreen (on) { if(on) { oldW = sandbox.opt("width"); oldH = sandbox.opt("height"); $.addClass(node, cl); onWindowResize(); $.off(window, "resize", onWindowResize); $.on(window, "resize", onWindowResize); } else { oldW && sandbox.opt("width", oldW); oldH && sandbox.opt("height", oldH); $.removeClass(node, cl); $.off(window, "resize", onWindowResize); } } function get () { if (nativeFullScreenSupported) return isFullscreen(); return sandbox.opt(); } function set (value) { if (!node) return; if ( get() == value ) return; if (nativeFullScreenSupported) { if (value) { requestFullScreen(node); } else { cancelFullScreen(); } } else { setFullScreen(value); } } function onTemplated (container) { node = $.find(container, ".sliderjs")[0]; set(sandbox.value()); $.on(node, "webkitfullscreenchange", onFullScreenEventChange); $.on(node, "mozfullscreenchange", onFullScreenEventChange); $.on(node, "fullscreenchange", onFullScreenEventChange); } function onFullScreenEventChange (e) { if (isFullscreen()) { setFullScreen(true); } else { setFullScreen(false); } } function onKeydown (e) { if (e.keyCode == 13) { setFullScreen(false); } } return { def: false, init: function () { sandbox.on("templated", onTemplated); $.on(window, "keydown", onKeydown); }, destroy: function () { sandbox.off("templated", onTemplated); $.off(window, "keydown", onKeydown); }, change: function (value) { set(value); } } }); /*~~~fullscreenControl~~~ * * GET: * SET: * * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("fullscreenControl", function (sandbox, $) { var defaultValue = { tmpl: $.tmpl('<a class="fullscreenButton icon" href="javascript:;"></a>') }; function template () { var v = sandbox.value(); return v.tmpl($.extend({}, v, defaultValue)); } function onTemplated (nodes, container) { var node = nodes[0]; if (!node) return; $.on(node, "click", function (e) { e.preventDefault(); sandbox.opt("fullscreen", !sandbox.opt("fullscreen")); }); } function onKeyDown (e) { if (e.keyCode == 27 && sandbox.opt("fullscreen")) { sandbox.opt("fullscreen", false); } } return { init: function () { sandbox.template.add(".sliderjs div.left", template, onTemplated, 9); $.on(window, "keydown", onKeyDown); }, accept: function (value) { return $.extend({}, defaultValue, value); }, change: function (value, old) { sandbox.template.refresh(template); }, destroy: function () { sandbox.template.remove(template); $.off(window, "keydown", onKeyDown); } } }); //~~~ glsl transition sliderjs.modules("glslTransition", function (sandbox, $) { var mode, container, drawing = false, canvas, gl, program, supported = function () { var c = document.createElement("canvas"); return !!getWebGLContext(c); }(), locations = {}, // cache for locations slide, images = [], defaultValue = { commutative: true, params: {} }; var template = $.tmpl('<canvas class="slides"></canvas>'); function syncHeight (h) { canvas.height = h; gl && syncViewport(); !drawing && drawCurrentImage(); } function syncWidth (w) { canvas.width = w; gl && syncViewport(); !drawing && drawCurrentImage(); } function onHeightChanged (o) { canvas && syncHeight(o.value); } function onWidthChanged (o) { canvas && syncWidth(o.value); } function onSlideChanged (o) { slide = o; mode=="glsl" && gl && startRender(o.old, o.value, sandbox.value()); } function onImagesLoaded (o) { images = o.images; updateDOM(); } function onTransitionMode (m) { if (m == mode) return; // still the same mode mode = m; updateDOM(); } function onTemplated (nodes, container) { canvas = nodes[0]; if (!canvas || !canvas.getContext) return; $.on(canvas, "webglcontextlost", function (evt) { console.log("webglcontextlost"); gl = null; evt.preventDefault(); }); $.on(canvas, "webglcontextrestored", function (evt) { console.log("webglcontextrestored"); gl = getWebGLContext(canvas); loadTransition(sandbox.value()); }); syncWidth(sandbox.opt("width")); syncHeight(sandbox.opt("height")); updateDOM(); gl = getWebGLContext(canvas); if (!gl) { window["console"] && console.log("Unable to get a WebGL context."); } else { loadTransition(sandbox.value()); } } function updateDOM () { if (!canvas) return; if (mode=="glsl") { $.show(canvas); drawCurrentImage(); } else { $.hide(canvas); } } function setRectangle (x, y, width, height) { var x1 = x, x2 = x + width, y1 = y, y2 = y + height; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]), gl.STATIC_DRAW); } function loadTransition (transition) { if(!transition) return; var w = canvas.width, h = canvas.height; // Clean old program if (program) { gl.deleteProgram(program); program = null; locations = {}; } // Create new program program = loadProgram([ loadShader('attribute vec2 position;attribute vec2 texCoord_in;uniform vec2 resolution;varying vec2 texCoord;void main() {vec2 zeroToOne = position / resolution;vec2 zeroToTwo = zeroToOne * 2.0;vec2 clipSpace = zeroToTwo - 1.0;gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);texCoord = texCoord_in;}', gl.VERTEX_SHADER), loadShader(transition.shader, gl.FRAGMENT_SHADER) ]); gl.useProgram(program); // bind custom params bindParams(transition.params); // buffer var texCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]), gl.STATIC_DRAW); // texCoord var texCoordLocation = gl.getAttribLocation(program, "texCoord_in"); gl.enableVertexAttribArray(texCoordLocation); gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); // position var buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); var positionLocation = gl.getAttribLocation(program, "position"); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); syncViewport(); drawCurrentImage(); } function syncViewport () { var w = canvas.width, h = canvas.height; gl.viewport(0, 0, w, h); var resolutionLocation = gl.getUniformLocation(program, "resolution"); gl.uniform2f(resolutionLocation, w, h); var img = images[sandbox.opt("slide")]; if (img) h = Math.floor(w * img.height / img.width); setRectangle(0, 0, w, h); } // custom params: only handle types: vecN[float], float function bindParams (params) { for (var k in params) { var value = params[k]; var loc = gl.getUniformLocation(program, k); if (value instanceof Array) { if (typeof(value[0])=="number") { var fname = "uniform"+value.length+"f"; gl[fname] && gl[fname].apply(this, [loc].concat(value)); } } else if (typeof(value)=="number") { gl.uniform1f(loc, value); } } } function loadShader (shaderSource, shaderType) { var shader = gl.createShader(shaderType); gl.shaderSource(shader, shaderSource); gl.compileShader(shader); var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); if (!compiled) { lastError = gl.getShaderInfoLog(shader); throw shader + "':" + lastError; gl.deleteShader(shader); return null; } return shader; } function loadProgram (shaders) { var program = gl.createProgram(); $.each(shaders, function (shader) { gl.attachShader(program, shader); }); gl.linkProgram(program); var linked = gl.getProgramParameter(program, gl.LINK_STATUS); if (!linked) { throw "Linking error:" + gl.getProgramInfoLog (program); gl.deleteProgram(program); return null; } return program; } function createTexture (image) { var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // FIXME: the following line throw an error for cross domain images... gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return texture; } function setImages (from, to) { if (!from) return false; if (!to) return false; var fromTexture = createTexture(from), toTexture = createTexture(to); if(!locations.from) locations.from = gl.getUniformLocation(program, "from"); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, fromTexture); gl.uniform1i(locations.from, 0); if(!locations.to) locations.to = gl.getUniformLocation(program, "to"); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, toTexture); gl.uniform1i(locations.to, 1); syncViewport(); return true; } function setProgress (p) { if(!locations.progress) locations.progress = gl.getUniformLocation(program, "progress"); gl.uniform1f(locations.progress, p); } function drawImage (image) { setProgress(0); setImages(image, image) && draw(); } function drawCurrentImage () { var image = images[slide ? slide.value : sandbox.opt("slide")]; image && gl && drawImage(image); } function draw () { gl.drawArrays(gl.TRIANGLES, 0, 6); } function getWebGLContext (canvas) { if (!canvas.getContext) return; var names = ["webgl", "experimental-webgl"]; for (var i = 0; i < names.length; ++i) { try { var ctx = canvas.getContext(names[i]); if (ctx) return ctx; } catch(e) { } } } function startRender (fromSlide, toSlide, transition) { var renderId = $.uuid(), transitionStart = $.now(), transitionDuration = sandbox.opt('duration') || 0, transitionEasing = sandbox.opt('easing'), reverse = transition.commutative && fromSlide > toSlide; transitionEasing = transitionEasing && transitionEasing.get || function (t) { return t }; if (!setImages(images[reverse ? toSlide : fromSlide], images[reverse ? fromSlide : toSlide])) return; drawing = true; (function render (id) { var now = $.now(); if(id != renderId || now < transitionStart) return; var p = (now-transitionStart)/transitionDuration; if (p<1) { requestAnimFrame(function () { render(id) }, canvas); p = transitionEasing(p); setProgress(reverse ? 1-p : p); draw(); } else { drawing = false; drawCurrentImage(); sandbox.trigger("transitionEnd", { mode: "glsl" }); } } (renderId)); } return { init: function () { sandbox.template.add(".sliderjs div.slides", template, onTemplated); sandbox. on("slideChanged", onSlideChanged). on("heightChanged", onHeightChanged). on("widthChanged", onWidthChanged). on("imagesLoaded", onImagesLoaded). on("transitionMode", onTransitionMode); this.change(sandbox.value()); }, destroy: function () { sandbox.template.remove(template); sandbox. off("slideChanged", onSlideChanged). off("heightChanged", onHeightChanged). off("widthChanged", onWidthChanged). off("imagesLoaded", onImagesLoaded). off("transitionMode", onTransitionMode); }, accept: function (value, old) { if (!value) return value; if (typeof(value) == "string") value = { name: value }; value = $.extend({}, defaultValue, value, sliderjs.glslTransitions(value.name), value); if(!value.shader) throw new Error("no shader was found."); return value; }, change: function (value) { if (value===undefined) return; updateDOM(); gl && loadTransition(sandbox.value()); sandbox.trigger("transitionMode", "glsl"); }, supported: function () { return supported } } }); /** * define a new fragment shader */ sliderjs.glslTransitions = sliderjs.util.createRegistrable(); /*~~~height~~~ handle the height of the slider * * GET: the current height (integer) * SET: a new height (integer) * * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("height", function (sandbox, $) { return { def: 430 } }); /*~~~loader~~~ handle loading display * * GET: a templating object * SET: * - a string describing the strategy of the loader : "waitingAll", "waitingFirst", "waitingCurrent" * - a templating object to patch. (if you want to modify the loader template) * * <renaudeau.gaetan@gmail.com> - 2012 */ // passing a string will have different kind of loader (percent, image, ...) sliderjs.modules("loader", function (sandbox, $) { var node, sliderjs, event, state, id = -1, loading = true; var defaultValue = { tmpl: $.tmpl('<div class="loader"><p><span class="spinner"></span> <span class="percent"><%= percent %></span> %</p></div>'), percent: 0, onLoading: function (state, node) { $.html($.find(node, ".percent")[0], ""+Math.floor(100*state.sum/state.total)); }, mode: "waitingAll" } function template () { var v = sandbox.value(); return v.tmpl(v); } function syncHeight (h) { if (!node) return; var hpx = h ? h+"px" : ""; $.height(node, hpx); } function onHeightChanged (o) { syncHeight(o.value); } function onTemplated (nodes, container) { node = nodes[0]; sliderjs = $.find(container, ".sliderjs")[0]; syncHeight(sandbox.opt("height")); loading && startLoading(); } function eventForStrategy (s) { if(s=="waitingFirst") return "firstSlideLoaded"; if(s=="waitingCurrent") return "currentSlideLoaded"; if(s=="waitingAll") return "imagesLoaded"; return null; } function loadingForStrategy (s) { switch (s) { case "waitingFirst": case "waitingCurrent": case "waitingAll": return true; } return false; } function endLoading () { sliderjs && $.removeClass(sliderjs, "loading"); sandbox.trigger("loaderLoaded"); state = null; } function startLoading () { sliderjs && $.addClass(sliderjs, "loading"); sandbox.trigger("loaderLoading"); } function syncLoader (o) { if (!sliderjs) return; if (state && (state.sum == state.total)) { endLoading(); } else { var onLoading = sandbox.value().onLoading; state && onLoading && onLoading(state, sliderjs); if(o.id !== id) { startLoading(); id = o.id; } } } function onImagesLoading (o) { if (o) state = o.state; loading && syncLoader(o); } function onImagesLoaded (o) { if (o) state = o.state; loading && syncLoader(o); } function handler (o) { endLoading(); } function syncMode (mode) { loading = loadingForStrategy(mode); event = eventForStrategy(mode); sandbox.off(event, handler).once(event, handler); } return { def: defaultValue, init: function () { sandbox.template.add(".sliderjs", template, onTemplated, 10); sandbox. on("heightChanged", onHeightChanged). on("imagesLoading", onImagesLoading). on("imagesLoaded", onImagesLoaded); var value = sandbox.value(); value.mode && syncMode(value.mode); }, destroy: function () { sandbox.template.remove(template); sandbox. off("heightChanged", onHeightChanged). off("imagesLoading", onImagesLoading). off("imagesLoaded", onImagesLoaded); }, accept: function (value) { if (typeof(value) == "string") value = { mode: value }; return $.extend(defaultValue, value); }, change: function (value, old) { sandbox.template.refresh(template); value.mode && syncMode(value.mode); } } }); /*~~~loop~~~ start / stop the auto slide mode with a specific duration * * GET: the millisecond time for the current loop (0 is the loop is stopped) * SET: * - an integer: set the duration between each slides (in ms) and start the loop * - "start": start the loop with the latest set duration (or the default) * - "stop": stop the loop if running * * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("loop", function (sandbox, $) { var defaultValue = 5000, lastHumanNav = 0, timeout; function onPagesClicked () { lastHumanNav = $.now(); } function loop () { if($.now()-lastHumanNav > 2000) sandbox.opt("slide", "next"); iterateLoop(); } function iterateLoop () { timeout = setTimeout(loop, sandbox.value()); } function stop () { timeout && clearTimeout(timeout); timeout = null; } function start () { lastHumanNav = 0; stop(); iterateLoop(); } return { def: "start", init: function () { sandbox.on("pagesClicked", onPagesClicked); this.change(sandbox.value()); }, destroy: function () { stop(); sandbox.off("pagesClicked", onPagesClicked); }, accept: function (value, old) { if(value == "start") return defaultValue; if(value == "stop") return 0; value = $.parseTime(value); if (!isNaN(value) && value > 0) return defaultValue = value; return 0; }, change: function (value, old) { if(value) start(); else stop(); } } }); /*~~~loopBar~~~ * * GET: * SET: * * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("loopBar", function (sandbox, $) { var interval, lastSlide = $.now(), oldP, node, inner, loop = sandbox.opt("loop"), defaultValue = { width: 100, tmpl: $.tmpl('<span class="loopBar" style="width: <%= width %>px;"><span class="loopBar-inner"></span></span>') }; function template () { var v = sandbox.value(); return v.tmpl(v); } function onTemplated (nodes, container) { node = nodes[0]; if (!node) return; inner = $.find(node, ".loopBar-inner")[0]; render(); } function onLoopChanged (o) { loop = o.value; lastSlide = $.now(); inner && updateProgress(); } function onSlideChanged (o) { lastSlide = $.now(); inner && updateProgress(); } function updateProgress () { var p = loop ? (Math.max(0, Math.min(1, ($.now()-lastSlide)/loop))) : 0; (p !== oldP) && $.width(inner, Math.round((oldP=p)*10000)/100+"%"); } function render () { if (inner) { requestAnimFrame(render, inner); updateProgress(); } } return { init: function () { sandbox.template.add(".sliderjs .header", template, onTemplated, -6); sandbox. on("loopChanged", onLoopChanged). on("slideChanged", onSlideChanged); render(); }, accept: function (value) { return $.extend({}, defaultValue, value); }, change: function (value, old) { sandbox.template.refresh(template); }, destroy: function () { sandbox.template.remove(template); sandbox. off("loopChanged", onLoopChanged). off("slideChanged", onSlideChanged); } } }); /*~~~nextControl~~~ * * GET: * SET: * * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("nextControl", function (sandbox, $) { var defaultValue = { text: "→", tmpl: $.tmpl('<a class="nextSlide" href="javascript:;"><%= text %></a>') }; function template () { var v = sandbox.value(); return v.tmpl(v); } function onTemplated (nodes, container) { var node = nodes[0]; if (!node) return; $.on(node, "click", function (e) { e.preventDefault(); sandbox.opt("slide", "next").trigger("pagesClicked"); }); } return { init: function () { sandbox.template.add(".sliderjs .footer", template, onTemplated, -9); }, accept: function (value) { return $.extend({}, defaultValue, value); }, change: function (value, old) { sandbox.template.refresh(template); }, destroy: function () { sandbox.template.remove(template); } } }); /*~~~pages~~~ handle navigation * * GET: a templating object * SET: a templating object * * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("pages", function (sandbox, $) { var node, pages, defaultValue = { tmpl: $.tmpl('<span class="pages"> <% if(obj.slides){for(var i=0;i<slides.length;++i){var s=slides[i]; %> <a href="<%= s.image %>" class="page"><%= i+1 %></a> <% }} %> </span>') }; function template () { var v = sandbox.value(); return v.tmpl($.extend({}, v, { slides: sandbox.opt("slides") })); } function onTemplated (nodes) { node = nodes[0]; if (!node) return; pages = $.find(node, ".page"); $.each(pages, function (n, i) { var slide = i; $.on(n, "click", function (e) { e.preventDefault(); sandbox.opt("slide", slide).trigger("pagesClicked"); }); }); var current = pages[sandbox.opt("slide")]; current && $.addClass(current, "current"); } function onSlidesChanged (o) { sandbox.template.refresh(template); } function onSlideChanged (o) { if(!pages) return; var old = pages[o.old]; var value = pages[o.value]; old && $.removeClass(old, "current"); value && $.addClass(value, "current"); } return { def: defaultValue, init: function () { sandbox.template.add(".sliderjs .footer", template, onTemplated, -9); sandbox. on("slidesChanged", onSlidesChanged). on("slideChanged", onSlideChanged); }, destroy: function () { sandbox.template.remove(template); sandbox. off("slidesChanged", onSlidesChanged). off("slideChanged", onSlideChanged); }, accept: function (value) { return $.extend(defaultValue, value); }, change: function (value, old) { sandbox.template.refresh(template); } } }); /*~~~playPauseControl~~~ * * GET: * SET: * * <renaudeau.gaetan@gmail.com> - 2012 */ sliderjs.modules("playPauseControl", function (sandbox, $) { var node, defaultValue = { tmpl: $.tmpl('<a class="playPauseControl icon" href="javascript:;"></a>') }; function template () { var v = sandbox.value(); return v.tmpl($.extend({}, v, defaultValue)); } function running () { return sandbox.opt("loop")>0; } function syncButton () { if (!node) return; if (running()) { $.addClass(node, "pause"); $.removeClass(node, "play"); } else { $.addClass(node, "play"); $.removeClass(node, "pause"); } } function onTemplated (nodes, container) { node = nodes[0]; if (!node) return; syncButton(); $.on(node, "click", function (e) { e.preventDefault(); sandbox.opt("loop", !running