/*
Name: KBurnalizer
Author: Rui Pereira
URI: http://iRui.ac/cool-stuff/kburnalizer
This work is licensed under a Creative Commons License
http://creativecommons.org/licenses/by/2.5/
*/
/*
Version 1.0 - 2006/04/09
Version 1.1 - 2006/05/25
Version 2.0 - 2007/01/13
Version 2.1 - 2007/01/28
Changes:
- Supports hiding of all display visuals on the constructor (progress bars, iRui logo, and so on...)
- The background color has been replaced by viewPortBackground, which now:
* No background (transparent): empty string (this is the new default)
* Color: provide color code (e.g. #fff)
* Background image: provide an URL
- New parameter: delayStart. Specifies a delay period before the Slide Show starts.
- Bug fix: kbImage.src only gets assigned once per slide (instead of every frame). This solves memory leaks and improves performance.
Version 2.2 - 2007/03/28
Changes:
- Supports a width of 100% by using the value 0
Version 2.3 - 2007/05/01
Changes:
- Checks if slideShows is already defined from another JS file, to allow multiple file loads
Version 3.0 - 2007/06/07
Changes:
- Fixed bug related to src=null becoming URL of page
- Code cleanups
*/
/*****
Class KBurnalizer
This is the main slideshow class.
In order for the OO design to work, it is *extremely* important that the kbID parameter
matches the "name" of the variable that holds an instance of this objects
If you don't respect this rule, this doesn't work - simple as that :)
******/
function KBurnalizer(kbID, viewPortID, displayText /*optional*/, displayVisuals /*optional*/, viewPortWidth /*optional*/, viewPortHeight /*optional*/, viewPortBackground /*optional - color or URL*/, preload /*optional*/, delayStart /*optional*/, duration /*optional*/, transition /*optional*/, fps /*optional*/) {
// The unique identifier of this object (the variable name)
this.kbID = kbID;
// The ID of the "div" that will be replaced by the slide show
this.viewPortID = viewPortID;
// The specified dimensions of the view port
// This can be optional
this.viewPortWidth = 0;
this.viewPortHeight = 0;
if(viewPortWidth) {
this.viewPortWidth = viewPortWidth;
}
if(viewPortHeight) {
this.viewPortHeight = viewPortHeight;
}
// Set the backgroung color
if(!viewPortBackground) {
this.viewPortBackground = "";
}
else {
this.viewPortBackground = viewPortBackground;
}
// Set the preload flag
// If true, all images are loaded before the slideshow starts (can be a while if there a lot of images...)
// If false (default), as soon as one is available, the slideshow starts
this.preload = false;
if(preload == true) {
this.preload = true;
}
// Set the start delay
if(!delayStart) {
this.delayStart = 0;
}
else {
this.delayStart = delayStart;
}
// Set the framerate
if(!fps) {
this.fps = 25;
}
else {
this.fps = fps;
}
// Set the duration
if(!duration) {
// Default is 5 seg
this.duration = 5000;
}
else {
this.duration = duration;
}
// Set the transition
if(!transition) {
// Default is 2 seg
this.transition = 2000;
}
else {
this.transition = transition;
}
// Set the display text flag
this.displayText = false;
if(displayText == true) {
this.displayText = true;
}
// Set the display visuals flag (loadbar, logo, etc...)
this.displayVisuals = true;
if(displayVisuals == false) {
this.displayVisuals = false;
}
this.delay = 1000 / this.fps;
// Shutdown flag
this.shutdownFlag = false;
// Slideshow started flag
this.started = false;
// Slides
this.slides = new Array();
this.slideIndex;
this.loadedSlides = 0;
// Active objects
this.kbViewport;
this.kbImage = new Array(2);
this.kbTextArea;
this.kbText;
this.kbLoadBar;
this.kbiRui;
// Swap support objects
this.swap = 0;
this.activeSlide = new Array(2);
this.activeFocusArea = new Array(2);;
// Register the init of the slideshow on the onload event of the page
slideShows.push(this);
////////////////////////////////////////////
// Init function of this object
this.init = function() {
// Get the specified viewport
this.kbViewport = document.getElementById(this.viewPortID);
// Sets the dimensions, if needed
if( this.viewPortWidth > 0) {
this.kbViewport.style.width = this.viewPortWidth + "px";
}
else {
this.kbViewport.style.width = "100%";
}
if(this.viewPortHeight > 0) {
this.kbViewport.style.height = this.viewPortHeight + "px";
}
// Set the background
// none (by default), a color or a URL to a image
//
if( viewPortBackground != "" ) {
if( viewPortBackground.substr(0,1) == "#" ) {
this.kbViewport.style.backgroundColor = this.viewPortBackground;
}
else {
this.kbViewport.style.background = "url('" + viewPortBackground + "')";
}
}
// Creates the image objects and places them under the viewport
// There are two to support the swapping process
this.kbImage[0] = document.createElement('img');
this.kbImage[0].className = 'kb-image';
this.kbImage[0].style.zIndex = 10;
this.kbViewport.appendChild(this.kbImage[0]);
this.kbImage[1] = document.createElement('img');
this.kbImage[1].className = 'kb-image';
this.kbImage[1].style.zIndex = -10;
this.kbViewport.appendChild(this.kbImage[1]);
// Text area
this.kbTextArea = document.createElement('div');
this.kbTextArea.className = 'kb-text-area';
this.kbTextArea.style.zIndex = 50;
if(!this.displayText) {
this.kbTextArea.style.visibility="hidden";
}
this.kbViewport.appendChild(this.kbTextArea);
// Text
this.kbText = document.createElement('div');
this.kbText.className = 'kb-text';
this.kbText.style.zIndex = 60;
this.kbViewport.appendChild(this.kbText);
// Load bar
this.kbLoadBar = document.createElement('div');
this.kbLoadBar.className = 'kb-load-bar';
this.kbLoadBar.style.zIndex = 50;
if(this.displayVisuals == true) {
this.kbViewport.appendChild(this.kbLoadBar);
}
// Image counter text
this.kbCounter = document.createElement('div');
this.kbCounter.className = 'kb-counter-text';
this.kbCounter.style.zIndex = 100;
this.kbCounter.innerHTML = "";
this.kbViewport.appendChild(this.kbCounter);
// iRui text
this.kbiRui = document.createElement('div');
this.kbiRui.className = 'kb-irui-text';
this.kbiRui.style.zIndex = 100;
this.kbiRui.innerHTML = " KB ";
if(this.displayVisuals == true) {
this.kbViewport.appendChild(this.kbiRui);
}
// Triggers the loading of all images. From this point on everything happens in parallel... event handlers take care of rest
if(this.slides.length>0) {
for(var i=0; i0) {
this.slideIndex = 0;
this.next();
}
// Just in case a slideshow is started without any slides being loaded
else {
this.kbLoadBar.style.display = "none";
this.kbText.innerHTML = "No Images";
}
}
////////////////////////////////////////////
// Renders a frame
this.renderFrame = function(swap, opacity) {
// Get viewport size
var viewPortWidth = this.kbViewport.clientWidth;
var viewPortHeight = this.kbViewport.clientHeight;
// Get image original dimensions
var originalImageWidth = this.activeSlide[swap].originalImageWidth;
var originalImageHeight = this.activeSlide[swap].originalImageHeight;
// Dimensions of the selected area
var newWidth =this.activeFocusArea[swap].bottomX - this.activeFocusArea[swap].topX;
var newHeight = this.activeFocusArea[swap].bottomY - this.activeFocusArea[swap].topY;
// Zoom level
var zoomX = viewPortWidth * 1.0 / newWidth;
var zoomY = viewPortHeight * 1.0 / newHeight;
// Resize image
this.kbImage[swap].style.width = Math.round((zoomX * originalImageWidth)) + "px";
this.kbImage[swap].style.height = Math.round((zoomY * originalImageHeight)) + "px";
// And displace to put selected area on top left
this.kbImage[swap].style.left = -Math.round((this.activeFocusArea[swap].topX * zoomX)) + "px";
this.kbImage[swap].style.top = -Math.round((this.activeFocusArea[swap].topY * zoomY)) + "px";
// Control the opacity. These three settings should cover most browsers
this.kbImage[swap].style.MozOpacity = opacity;
this.kbImage[swap].style.filter = 'Alpha(opacity=' + (opacity * 100) + ')';
this.kbImage[swap].style.opacity = opacity;
// Only set the image after all the resizing has been done, to avoid artifacts
// And only do it one time!
if(this.kbImage[swap].src != this.activeSlide[swap].image.src) {
this.kbImage[swap].src = this.activeSlide[swap].image.src;
this.kbImage[swap].title = this.activeSlide[swap].description;
// Make sure the new slide is visible
this.kbImage[swap].style.visibility = 'visible';
}
}
////////////////////////////////////////////////////////////
// Calculate the settings for the next frame of the slide
this.calculateNextFrame = function(
swap,
stepsToGo, inStepsToGo, outStepsToGo, opacity,
opacityInc, topXInc, topYInc, bottomXInc, bottomYInc
) {
// If global shutdown is active, stop processing
if(this.shutdownFlag) {
return;
}
// If the slide was forced to stop, do some cleanup
if(this.activeSlide[swap].isStopping()) {
this.kbImage[swap].style.visibility = 'hidden';
this.kbImage[swap].style.zIndex = -10;
this.activeSlide[swap].stopRunning();
return;
}
// Render the current frame
this.renderFrame(swap, opacity);
// If there are frames left for this slide
if(stepsToGo>0) {
// New focus area values
this.activeFocusArea[swap].topX += topXInc;
this.activeFocusArea[swap].topY += topYInc;
this.activeFocusArea[swap].bottomX += bottomXInc;
this.activeFocusArea[swap].bottomY += bottomYInc;
// Fade in block
if(inStepsToGo > 0) {
opacity += opacityInc;
// Don't let opacity reach 1!
// Browsers flicker when changing from "Alpha" mode to standard solid display
if(opacity >= .99) {
opacity = .99;
}
inStepsToGo--;
}
// Fade out block
if(outStepsToGo < 0) {
opacity -= opacityInc;
if(opacity < 0.01) {
opacity = 0.01;
}
}
// The next slide begins when the fade out of the current slide starts
if(outStepsToGo == 0) {
// Move this image back
this.kbImage[swap].style.zIndex = -10;
// Start next slide
this.next();
}
// Reduce steps
outStepsToGo--;
stepsToGo--;
// Calculate the next frame after the delay time
setTimeout(this.kbID + ".calculateNextFrame(" +
swap + "," +
stepsToGo + "," +
inStepsToGo + "," +
outStepsToGo + "," +
opacity + "," +
opacityInc + "," +
topXInc + "," +
topYInc + "," +
bottomXInc + "," +
bottomYInc + ")",
this.delay);
}
else {
// Hide the image when not in use. Speeds up the browser rendering a little bit
this.kbImage[swap].style.visibility = 'hidden';
this.activeSlide[swap].stopRunning();
// No more frames to display in this slide. Die away in peace...
}
}
////////////////////////////////////////////////////////////
// Starts a new slide
this.startSlide = function(swap, duration, transition, startFocusArea, endFocusArea) {
// Calculate the opposite corners displacements
var topDisplacementX = endFocusArea.topX - startFocusArea.topX;
var topDisplacementY = endFocusArea.topY - startFocusArea.topY;
var bottomDisplacementX = endFocusArea.bottomX - startFocusArea.bottomX;
var bottomDisplacementY = endFocusArea.bottomY - startFocusArea.bottomY;
// How many frames will be needed at the current framerate
var durationSteps = Math.round(duration/this.delay);
// How many frames of fade in (half of the transition time)
var transitionStepsIn = Math.round(((transition / 2) / this.delay));
// Fade out lasts as long as fade in. transitionStepsOut holds how many frames need to be
// rendered before the fade out starts
var transitionStepsOut = durationSteps - transitionStepsIn - 1;
// Opacity increments must correspond to how many frames the fade effect lasts
var opacityInc = 1 / transitionStepsIn;
// Opposite corners will move in a linear progression between the start and the end point
var topXInc = topDisplacementX / durationSteps;
var topYInc = topDisplacementY / durationSteps;
var bottomXInc = bottomDisplacementX / durationSteps;
var bottomYInc = bottomDisplacementY / durationSteps;
// Set the working focus area. This will be updated during the rendering process
this.activeFocusArea[swap] = startFocusArea.getCopy();
// Show the image description
this.displayDescription();
// Start the first frame
setTimeout(this.kbID + ".calculateNextFrame(" +
swap + "," +
durationSteps + "," +
transitionStepsIn + "," +
transitionStepsOut + ", 0.01 ," +
opacityInc + "," +
topXInc + "," +
topYInc + "," +
bottomXInc + "," +
bottomYInc + ")",
this.delay);
}
////////////////////////////////////////////////////////////
// Next on the slide show
this.next = function() {
// Make an infinite loop
if(this.slideIndex == this.slides.length) {
this.slideIndex=0;
}
if(this.slideIndex < 0 ) {
this.slideIndex=this.slides.length-1;
}
// This will cycle through the array, skipping any "holes", that is, images that haven't been loaded yet
// This allows the slideshow to run even if all images have not been loaded
while(!this.slides[this.slideIndex].image.loadStatus) {
this.slideIndex++;
// Due to the infinite loop
if(this.slideIndex == this.slides.length) {
this.slideIndex=0;
}
}
// Activate the next slide
this.activeSlide[this.swap] = this.slides[this.slideIndex];
this.activeSlide[this.swap].startRunning();
this.kbImage[this.swap].src = null;
this.kbImage[this.swap].style.zIndex = 10;
// Start the slide
this.startSlide(this.swap, this.duration, this.transition, this.activeSlide[this.swap].getStartFocusArea(this.kbViewport), this.activeSlide[this.swap].getEndFocusArea(this.kbViewport));
// Prepare for the next one
this.slideIndex++;
this.swap = Math.abs(this.swap-1);
}
////////////////////////////////////////////////////////////
// Start the KBurnalizer slideshow
this.start = function() {
setTimeout(this.kbID + ".init()", this.delayStart);
}
////////////////////////////////////////////////////////////
// Add a new slide to the show
this.addSlide = function(src,description,width,height,startFocusArea,endFocusArea) {
var slide = new KBSlide(this,src,description,width,height,startFocusArea, endFocusArea);
this.slides.push(slide);
}
////////////////////////////////////////////////////////////
// Change the slide duration dinamically
// This takes effect on the next slide
this.setSlideDuration = function(duration) {
// The duration can't be lower than the transition time
if(duration>=this.transition) {
this.duration = duration;
}
return this.duration;
}
////////////////////////////////////////////////////////////
// Change the transition time dinamically
// This takes effect on the next slide
this.setTransitionTime = function(transition) {
// The transition can't be higher than the slide duration time
if(transition<=this.duration) {
this.transition = transition;
}
return this.transition;
}
////////////////////////////////////////////////////////////
// Change the frame rate
// This takes effect immediately
// Be very carefull with this! It might make everything slower if you increase too much
this.setFPS = function(fps) {
this.fps = fps;
this.delay = 1000 / this.fps;
}
////////////////////////////////////////////////////////////
// Prints the text of the slide description
this.displayDescription = function() {
if(this.displayText) {
this.kbText.innerHTML = this.activeSlide[this.swap].description;
this.kbCounter.innerHTML = " « [" + padNumber(this.slideIndex+1,this.slides.length) + "/" + this.slides.length + "] » ";
}
}
////////////////////////////////////////////////////////////
// Hides/shows the description area
// Takes effect immediately
this.toggleDisplayDescription = function() {
this.displayText = !this.displayText;
if(!this.displayText) {
this.kbTextArea.style.visibility = "hidden";
this.kbText.style.visibility = "hidden";
this.kbCounter.style.visibility = "hidden";
}
else {
this.kbTextArea.style.visibility = "visible";
this.kbText.style.visibility = "visible";
this.kbCounter.style.visibility = "visible";
}
}
////////////////////////////////////////////////////////////
// Set the shutdown flag to true
// This stops the slideshow immediately
this.shutdown = function() {
this.shutdownFlag = true;
}
////////////////////////////////////////////////////////////
// Skips one slide forward or backward
// Signals active slides to stop
// Uses waitForForcedStop to check periodically if they have stopped
this.skip = function(previous) {
if(this.activeSlide[0]!=null) {
this.activeSlide[0].stopSignal();
}
if(this.activeSlide[1]!=null) {
this.activeSlide[1].stopSignal();
}
if(previous) this.slideIndex -= 2;
this.waitForForcedStop();
}
////////////////////////////////////////////////////////////
// Waits until slides have stopped
this.waitForForcedStop = function() {
if(
((this.activeSlide[0]==null) || (this.activeSlide[0].isStopped()))
&&
((this.activeSlide[1]==null) || (this.activeSlide[1].isStopped()))
) {
this.next();
}
else {
setTimeout(this.kbID + ".waitForForcedStop()", 10);
};
}
}
/******************************************************************************************************
******************************************************************************************************/
/*****
Class KBSlide
This is a slide in a KBurnalizer slideshow. Every photo has
to be wrapped in one of these
******/
function KBSlide(kbObj,src,description,width,height,startFocusArea, endFocusArea) {
// The slideshow object that contains this slideshow
// This is needed for callbacks
this.kbObj = kbObj;
// Image URL
this.src = src;
// Photo description
this.description = description;
// Image width
this.originalImageWidth = width;
this.originalImageHeight = height;
// Focus area to start
this.startFocusArea = startFocusArea;
// Focus area to finish
this.endFocusArea = endFocusArea;
// Image object
this.image = document.createElement("img");
// Images should also know to which slide show they belong, for callbacks
this.image.kbObj = kbObj;
this.image.onerror = kbObj.onerrorHandler; // This might not work in FF if the given URL is invalid (but it should!).
this.image.onload = kbObj.onloadHandler;
this.image.loadStatus = false;
// Current state of the slide
this.runningStatus = STATUS_STOPPED;
// This method loads the image for the slide
this.load = function() {
this.image.src = this.src;
}
// Sets the slide with the "runnning" status
this.startRunning = function() {
this.runningStatus = STATUS_RUNNING;
}
// Signals the slide that it should stop running
this.stopSignal = function() {
if(this.runningStatus == STATUS_RUNNING)
this.runningStatus=STATUS_STOPPING;
}
// Set's the signal has stopped
this.stopRunning = function() {
this.runningStatus = STATUS_STOPPED;
}
this.isStopped = function() {
if(this.runningStatus == STATUS_STOPPED) {
return true;
}
else {
return false;
}
}
this.isStopping = function() {
if(this.runningStatus == STATUS_STOPPING) {
return true;
}
else {
return false;
}
}
// If the area was not specified, choose a random one
this.getStartFocusArea = function(kbViewport) {
if(!this.startFocusArea) {
this.randomizeArea(kbViewport);
}
return this.startFocusArea;
}
// If the area was not specified, choose a random one
this.getEndFocusArea = function(kbViewport) {
if(!this.endFocusArea) {
this.randomizeArea(kbViewport);
}
return this.endFocusArea;
}
// Randomizes the start and end focus area
// To be used when these areas are not provided
this.randomizeArea = function(kbViewport) {
var vpWidth = kbViewport.clientWidth;
var vpHeight = kbViewport.clientHeight;
var imgWidth = this.originalImageWidth;
var imgHeight = this.originalImageHeight;
// We will start by calculating the largest possible rectangle that fits the image
// and is proportional to the viewport
var largestWidth = vpWidth;
var largestHeight = vpHeight;
// What factor we need to multiply to make it as wide as the image?
var scaleFactor = imgWidth * 1.0 / largestWidth;
// First we try to make the rectangle as wide as the image
largestWidth = scaleFactor * largestWidth; // Which is of course the same as largestWidth = imgWidth, but just to make it clear
largestHeight = scaleFactor * largestHeight; // Multiply the height by the same amount
// If the height for any reason is too large, scale everything down
if(largestHeight > imgHeight) {
scaleFactor = imgHeight * 1.0 / largestHeight;
largestWidth = scaleFactor * largestWidth;
largestHeight = scaleFactor * largestHeight;
}
// Ok, now we not everything fits
// Randomize the start size and end size
// between 10% and 30% of the rectangle size
var startRandomScale = 1 - ((Math.random() * 20 + 10) / 100);
var endRandomScale = 1 - ((Math.random() * 20 + 10) / 100);
var startWidth = Math.floor(largestWidth * startRandomScale);
var startHeight = Math.floor(largestHeight * startRandomScale);
var endWidth = Math.floor(largestWidth * endRandomScale);
var endHeight = Math.floor(largestHeight * endRandomScale);
var maximumStartX = imgWidth - startWidth;
var maximumStartY = imgHeight - startHeight;
var maximumEndX = imgWidth - endWidth;
var maximumEndY = imgHeight - endHeight;
var startX = Math.floor( Math.random() * maximumStartX);
var startY = Math.floor( Math.random() * maximumStartY);
var endX = Math.floor( Math.random() * maximumEndX);
var endY = Math.floor( Math.random() * maximumEndY);
this.startFocusArea = new FocusArea(startX,startY,startX+startWidth,startY+startHeight);
this.endFocusArea = new FocusArea(endX,endY,endX+endWidth,endY+endHeight);
}
}
/******************************************************************************************************
******************************************************************************************************/
/*****
Class FocusArea
FocusArea is an auxiliary class that represents a rectangular area in a photo
******/
function FocusArea(topx, topY, bottomX, bottomY) {
this.topX = topx;
this.topY = topY;
this.bottomX = bottomX;
this.bottomY = bottomY;
this.getCopy = function() {
return new FocusArea(this.topX, this.topY, this.bottomX, this.bottomY);
}
}
/******************************************************************************************************
******************************************************************************************************/
// Utils
function padNumber(number, reference) {
number=number + "";
reference=reference + "";
while(number.length is now set to false
function addEvent(obj, evType, fn){
if(obj.addEventListener){
obj.addEventListener(evType, fn, false);
return true;
} else if (obj.attachEvent){
var r = obj.attachEvent('on'+evType, fn);
return r;
} else {
return false;
}
}