/** @license Animator.js 1.1.9 This library is released under the BSD license: Copyright (c) 2006, Bernard Sumption. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name BernieCode nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // http://www.berniecode.com/writing/animator.html // Applies a sequence of numbers between 0 and 1 to a number of subjects // construct - see setOptions for parameters function Animator(options) { this.setOptions(options); var _this = this; this.timerDelegate = function(){_this.onTimerEvent()}; this.subjects = []; this.subjectScopes = []; this.target = 0; this.state = 0; this.lastTime = null; }; Animator.prototype = { // apply defaults setOptions: function(options) { this.options = Animator.applyDefaults({ interval: 20, // time between animation frames duration: 400, // length of animation onComplete: function(){}, onStep: function(){}, transition: Animator.tx.easeInOut }, options); }, // animate from the current state to provided value seekTo: function(to) { this.seekFromTo(this.state, to); }, // animate from the current state to provided value seekFromTo: function(from, to) { this.target = Math.max(0, Math.min(1, to)); this.state = Math.max(0, Math.min(1, from)); this.lastTime = new Date().getTime(); if (!this.intervalId) { this.intervalId = window.setInterval(this.timerDelegate, this.options.interval); } }, // animate from the current state to provided value jumpTo: function(to) { this.target = this.state = Math.max(0, Math.min(1, to)); this.propagate(); }, // seek to the opposite of the current target toggle: function() { this.seekTo(1 - this.target); }, // add a function or an object with a method setState(state) that will be called with a number // between 0 and 1 on each frame of the animation addSubject: function(subject,scope) { this.subjects[this.subjects.length] = subject; this.subjectScopes[this.subjectScopes.length] = scope; return this; }, // remove all subjects clearSubjects: function() { this.subjects = []; this.subjectScopes = []; }, // forward the current state to the animation subjects propagate: function() { var value = this.options.transition(this.state); for (var i=0; i= Math.abs(this.state - this.target)) { this.state = this.target; } else { this.state += movement; } try { this.propagate(); } finally { this.options.onStep.call(this); if (this.target == this.state) { window.clearInterval(this.intervalId); this.intervalId = null; this.options.onComplete.call(this); } } }, // shortcuts play: function() {this.seekFromTo(0, 1)}, reverse: function() {this.seekFromTo(1, 0)}, // return a string describing this Animator, for debugging inspect: function() { var str = "# 20) return; } }, getStyle: function(state) { state = this.from + ((this.to - this.from) * state); if (this.property == 'filter') return "alpha(opacity=" + Math.round(state*100) + ")"; if (this.property == 'opacity') return state; return Math.round(state) + this.units; }, inspect: function() { return "\t" + this.property + "(" + this.from + this.units + " to " + this.to + this.units + ")\n"; } } // animates a colour based style property between two hex values function ColorStyleSubject(els, property, from, to) { this.els = Animator.makeArray(els); this.property = Animator.camelize(property); this.to = this.expandColor(to); this.from = this.expandColor(from); this.origFrom = from; this.origTo = to; } ColorStyleSubject.prototype = { // parse "#FFFF00" to [256, 256, 0] expandColor: function(color) { var hexColor, red, green, blue; hexColor = ColorStyleSubject.parseColor(color); if (hexColor) { red = parseInt(hexColor.slice(1, 3), 16); green = parseInt(hexColor.slice(3, 5), 16); blue = parseInt(hexColor.slice(5, 7), 16); return [red,green,blue] } if (window.DEBUG) { alert("Invalid colour: '" + color + "'"); } }, getValueForState: function(color, state) { return Math.round(this.from[color] + ((this.to[color] - this.from[color]) * state)); }, setState: function(state) { var color = '#' + ColorStyleSubject.toColorPart(this.getValueForState(0, state)) + ColorStyleSubject.toColorPart(this.getValueForState(1, state)) + ColorStyleSubject.toColorPart(this.getValueForState(2, state)); for (var i=0; i 255) number = 255; var digits = number.toString(16); if (number < 16) return '0' + digits; return digits; } ColorStyleSubject.parseColor.rgbRe = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i; ColorStyleSubject.parseColor.hexRe = /^\#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; // Animates discrete styles, i.e. ones that do not scale but have discrete values // that can't be interpolated function DiscreteStyleSubject(els, property, from, to, threshold) { this.els = Animator.makeArray(els); this.property = Animator.camelize(property); this.from = from; this.to = to; this.threshold = threshold || 0.5; } DiscreteStyleSubject.prototype = { setState: function(state) { var j=0; for (var i=0; i section ? 1 : 0); } if (this.options.rememberance) { document.location.hash = this.rememberanceTexts[section]; } } } /** * * SoundManager 2 Demo: 360-degree / "donut player" * ------------------------------------------------ * http://schillmania.com/projects/soundmanager2/ * * An inline player with a circular UI. * Based on the original SM2 inline player. * Inspired by Apple's preview feature in the * iTunes music store (iPhone), among others. * * Requires SoundManager 2 Javascript API. * Also uses Bernie's Better Animation Class (BSD): * http://www.berniecode.com/writing/animator.html * */ /*jslint white: false, onevar: true, undef: true, nomen: false, eqeqeq: true, plusplus: false, bitwise: true, regexp: false, newcap: true, immed: true */ /*global document, window, soundManager, navigator */ var threeSixtyPlayer, // instance ThreeSixtyPlayer; // constructor (function(window) { function ThreeSixtyPlayer() { var self = this, pl = this, sm = soundManager, // soundManager instance uA = navigator.userAgent, isIE = (uA.match(/msie/i)), isOpera = (uA.match(/opera/i)), isSafari = (uA.match(/safari/i)), isChrome = (uA.match(/chrome/i)), isFirefox = (uA.match(/firefox/i)), isTouchDevice = (uA.match(/ipad|iphone/i)), hasRealCanvas = (typeof window.G_vmlCanvasManager === 'undefined' && typeof document.createElement('canvas').getContext('2d') !== 'undefined'), // I dunno what Opera doesn't like about this. I'm probably doing it wrong. fullCircle = (isOpera||isChrome?359.9:360); // CSS class for ignoring MP3 links this.excludeClass = 'threesixty-exclude'; this.links = []; this.sounds = []; this.soundsByURL = []; this.indexByURL = []; this.lastSound = null; this.lastTouchedSound = null; this.soundCount = 0; this.oUITemplate = null; this.oUIImageMap = null; this.vuMeter = null; this.callbackCount = 0; this.peakDataHistory = []; // 360player configuration options this.config = { playNext: false, // stop after one sound, or play through list until end autoPlay: false, // start playing the first sound right away allowMultiple: false, // let many sounds play at once (false = only one sound playing at a time) loadRingColor: '#ccc', // how much has loaded playRingColor: '#000', // how much has played backgroundRingColor: '#eee', // color shown underneath load + play ("not yet loaded" color) // optional segment/annotation (metadata) stuff.. segmentRingColor: 'rgba(255,255,255,0.33)', // metadata/annotation (segment) colors segmentRingColorAlt: 'rgba(0,0,0,0.1)', loadRingColorMetadata: '#ddd', // "annotations" load color playRingColorMetadata: 'rgba(128,192,256,0.9)', // how much has played when metadata is present circleDiameter: null, // set dynamically according to values from CSS circleRadius: null, animDuration: 500, animTransition: window.Animator.tx.bouncy, // http://www.berniecode.com/writing/animator.html showHMSTime: false, // hours:minutes:seconds vs. seconds-only scaleFont: true, // also set the font size (if possible) while animating the circle // optional: spectrum or EQ graph in canvas (not supported in IE <9, too slow via ExCanvas) useWaveformData: false, waveformDataColor: '#0099ff', waveformDataDownsample: 3, // use only one in X (of a set of 256 values) - 1 means all 256 waveformDataOutside: false, waveformDataConstrain: false, // if true, +ve values only - keep within inside circle waveformDataLineRatio: 0.64, // "spectrum frequency" option useEQData: false, eqDataColor: '#339933', eqDataDownsample: 4, // use only one in X (of 256 values) eqDataOutside: true, eqDataLineRatio: 0.54, // enable "amplifier" (canvas pulses like a speaker) effect usePeakData: true, peakDataColor: '#ff33ff', peakDataOutside: true, peakDataLineRatio: 0.5, useAmplifier: true, // "pulse" like a speaker fontSizeMax: null, // set according to CSS scaleArcWidth: 1, // thickness factor of playback progress ring useFavIcon: false // Experimental (also requires usePeakData: true).. Try to draw a "VU Meter" in the favicon area, if browser supports it (Firefox + Opera as of 2009) }; this.css = { // CSS class names appended to link during various states sDefault: 'sm2_link', // default state sBuffering: 'sm2_buffering', sPlaying: 'sm2_playing', sPaused: 'sm2_paused' }; this.addEventHandler = (typeof window.addEventListener !== 'undefined' ? function(o, evtName, evtHandler) { return o.addEventListener(evtName,evtHandler,false); } : function(o, evtName, evtHandler) { o.attachEvent('on'+evtName,evtHandler); }); this.removeEventHandler = (typeof window.removeEventListener !== 'undefined' ? function(o, evtName, evtHandler) { return o.removeEventListener(evtName,evtHandler,false); } : function(o, evtName, evtHandler) { return o.detachEvent('on'+evtName,evtHandler); }); this.hasClass = function(o,cStr) { return typeof(o.className)!=='undefined'?o.className.match(new RegExp('(\\s|^)'+cStr+'(\\s|$)')):false; }; this.addClass = function(o,cStr) { if (!o || !cStr || self.hasClass(o,cStr)) { return false; } o.className = (o.className?o.className+' ':'')+cStr; }; this.removeClass = function(o,cStr) { if (!o || !cStr || !self.hasClass(o,cStr)) { return false; } o.className = o.className.replace(new RegExp('( '+cStr+')|('+cStr+')','g'),''); }; this.getElementsByClassName = function(className,tagNames,oParent) { var doc = (oParent||document), matches = [], i,j, nodes = []; if (typeof tagNames !== 'undefined' && typeof tagNames !== 'string') { for (i=tagNames.length; i--;) { if (!nodes || !nodes[tagNames[i]]) { nodes[tagNames[i]] = doc.getElementsByTagName(tagNames[i]); } } } else if (tagNames) { nodes = doc.getElementsByTagName(tagNames); } else { nodes = doc.all||doc.getElementsByTagName('*'); } if (typeof(tagNames)!=='string') { for (i=tagNames.length; i--;) { for (j=nodes[tagNames[i]].length; j--;) { if (self.hasClass(nodes[tagNames[i]][j],className)) { matches.push(nodes[tagNames[i]][j]); } } } } else { for (i=0; i 1) { // only catch left-clicks return true; } var o = self.getTheDamnLink(e), sURL, soundURL, thisSound, oContainer, has_vis, diameter; if (o.nodeName.toLowerCase() !== 'a') { o = self.isChildOfNode(o,'a'); if (!o) { return true; } } if (!self.isChildOfClass(o,'ui360')) { // not a link we're interested in return true; } sURL = o.getAttribute('href'); if (!o.href || !sm.canPlayLink(o) || self.hasClass(o,self.excludeClass)) { return true; // pass-thru for non-MP3/non-links } sm._writeDebug('handleClick()'); soundURL = (o.href); thisSound = self.getSoundByURL(soundURL); if (thisSound) { // already exists if (thisSound === self.lastSound) { // and was playing (or paused) thisSound.togglePause(); } else { // different sound thisSound.togglePause(); // start playing current sm._writeDebug('sound different than last sound: '+self.lastSound.id); if (!self.config.allowMultiple && self.lastSound) { self.stopSound(self.lastSound); } } } else { // append some dom shiz, make noise oContainer = o.parentNode; has_vis = (self.getElementsByClassName('ui360-vis','div',oContainer.parentNode).length); // create sound thisSound = sm.createSound({ id:'ui360Sound'+(self.soundCount++), url:soundURL, onplay:self.events.play, onstop:self.events.stop, onpause:self.events.pause, onresume:self.events.resume, onfinish:self.events.finish, onbufferchange:self.events.bufferchange, type:(o.type||null), whileloading:self.events.whileloading, whileplaying:self.events.whileplaying, useWaveformData:(has_vis && self.config.useWaveformData), useEQData:(has_vis && self.config.useEQData), usePeakData:(has_vis && self.config.usePeakData) }); // tack on some custom data diameter = parseInt(self.getElementsByClassName('sm2-360ui','div',oContainer)[0].offsetWidth, 10); thisSound._360data = { oUI360: self.getParentByClassName(o,'ui360'), // the (whole) entire container oLink: o, // DOM node for reference within SM2 object event handlers className: self.css.sPlaying, oUIBox: self.getElementsByClassName('sm2-360ui','div',oContainer)[0], oCanvas: self.getElementsByClassName('sm2-canvas','canvas',oContainer)[0], oButton: self.getElementsByClassName('sm2-360btn','span',oContainer)[0], oTiming: self.getElementsByClassName('sm2-timing','div',oContainer)[0], oCover: self.getElementsByClassName('sm2-cover','div',oContainer)[0], circleDiameter: diameter, circleRadius: diameter/2, lastTime: null, didFinish: null, pauseCount:0, radius:0, fontSize: 1, fontSizeMax: self.config.fontSizeMax, scaleFont: (has_vis && self.config.scaleFont), showHMSTime: has_vis, amplifier: (has_vis && self.config.usePeakData?0.9:1), // TODO: x1 if not being used, else use dynamic "how much to amplify by" value radiusMax: diameter*0.175, // circle radius width:0, widthMax: diameter*0.4, // width of the outer ring lastValues: { bytesLoaded: 0, bytesTotal: 0, position: 0, durationEstimate: 0 }, // used to track "last good known" values before sound finish/reset for anim animating: false, oAnim: new window.Animator({ duration: self.config.animDuration, transition:self.config.animTransition, onComplete: function() { // var thisSound = this; // thisSound._360data.didFinish = false; // reset full circle } }), oAnimProgress: function(nProgress) { var thisSound = this; thisSound._360data.radius = parseInt(thisSound._360data.radiusMax*thisSound._360data.amplifier*nProgress, 10); thisSound._360data.width = parseInt(thisSound._360data.widthMax*thisSound._360data.amplifier*nProgress, 10); if (thisSound._360data.scaleFont && thisSound._360data.fontSizeMax !== null) { thisSound._360data.oTiming.style.fontSize = parseInt(Math.max(1,thisSound._360data.fontSizeMax*nProgress), 10)+'px'; thisSound._360data.oTiming.style.opacity = nProgress; } if (thisSound.paused || thisSound.playState === 0 || thisSound._360data.lastValues.bytesLoaded === 0 || thisSound._360data.lastValues.position === 0) { self.updatePlaying.apply(thisSound); } }, fps: 0 }; // "Metadata" (annotations) if (typeof self.Metadata !== 'undefined' && self.getElementsByClassName('metadata','div',thisSound._360data.oUI360).length) { thisSound._360data.metadata = new self.Metadata(thisSound,self); } // minimize ze font if (thisSound._360data.scaleFont && thisSound._360data.fontSizeMax !== null) { thisSound._360data.oTiming.style.fontSize = '1px'; } // set up ze animation thisSound._360data.oAnim.addSubject(thisSound._360data.oAnimProgress,thisSound); // animate the radius out nice self.refreshCoords(thisSound); self.updatePlaying.apply(thisSound); self.soundsByURL[soundURL] = thisSound; self.sounds.push(thisSound); if (!self.config.allowMultiple && self.lastSound) { self.stopSound(self.lastSound); } thisSound.play(); } self.lastSound = thisSound; // reference for next call if (typeof e !== 'undefined' && typeof e.preventDefault !== 'undefined') { e.preventDefault(); } else if (typeof window.event !== 'undefined') { window.event.returnValue = false; } return false; }; this.fanOut = function(oSound) { var thisSound = oSound; if (thisSound._360data.animating === 1) { return false; } thisSound._360data.animating = 0; soundManager._writeDebug('fanOut: '+thisSound.id+': '+thisSound._360data.oLink.href); thisSound._360data.oAnim.seekTo(1); // play to end window.setTimeout(function() { // oncomplete hack thisSound._360data.animating = 0; },self.config.animDuration+20); }; this.fanIn = function(oSound) { var thisSound = oSound; if (thisSound._360data.animating === -1) { return false; } thisSound._360data.animating = -1; soundManager._writeDebug('fanIn: '+thisSound.id+': '+thisSound._360data.oLink.href); // massive hack thisSound._360data.oAnim.seekTo(0); // play to end window.setTimeout(function() { // reset full 360 fill after animation has completed (oncomplete hack) thisSound._360data.didFinish = false; thisSound._360data.animating = 0; self.resetLastValues(thisSound); }, self.config.animDuration+20); }; this.resetLastValues = function(oSound) { oSound._360data.lastValues.position = 0; }; this.refreshCoords = function(thisSound) { thisSound._360data.canvasXY = self.findXY(thisSound._360data.oCanvas); thisSound._360data.canvasMid = [thisSound._360data.circleRadius,thisSound._360data.circleRadius]; thisSound._360data.canvasMidXY = [thisSound._360data.canvasXY[0]+thisSound._360data.canvasMid[0], thisSound._360data.canvasXY[1]+thisSound._360data.canvasMid[1]]; }; this.stopSound = function(oSound) { soundManager._writeDebug('stopSound: '+oSound.id); soundManager.stop(oSound.id); if (!isTouchDevice) { // iOS 4.2+ security blocks onfinish() -> playNext() if we set a .src in-between(?) soundManager.unload(oSound.id); } }; this.buttonClick = function(e) { var o = e?(e.target?e.target:e.srcElement):window.event.srcElement; self.handleClick({target:self.getParentByClassName(o,'sm2-360ui').nextSibling}); // link next to the nodes we inserted return false; }; this.buttonMouseDown = function(e) { // user might decide to drag from here // watch for mouse move if (!isTouchDevice) { document.onmousemove = function(e) { // should be boundary-checked, really (eg. move 3px first?) self.mouseDown(e); }; } else { self.addEventHandler(document,'touchmove',self.mouseDown); } self.stopEvent(e); return false; }; this.mouseDown = function(e) { if (!isTouchDevice && e.button > 1) { return true; // ignore non-left-click } if (!self.lastSound) { self.stopEvent(e); return false; } var evt = e?e:window.event, target, thisSound, oData; if (isTouchDevice && evt.touches) { evt = evt.touches[0]; } target = (evt.target||evt.srcElement); thisSound = self.getSoundByURL(self.getElementsByClassName('sm2_link','a',self.getParentByClassName(target,'ui360'))[0].href); // self.lastSound; // TODO: In multiple sound case, figure out which sound is involved etc. // just in case, update coordinates (maybe the element moved since last time.) self.lastTouchedSound = thisSound; self.refreshCoords(thisSound); oData = thisSound._360data; self.addClass(oData.oUIBox,'sm2_dragging'); oData.pauseCount = (self.lastTouchedSound.paused?1:0); // self.lastSound.pause(); self.mmh(e?e:window.event); if (isTouchDevice) { self.removeEventHandler(document,'touchmove',self.mouseDown); self.addEventHandler(document,'touchmove',self.mmh); self.addEventHandler(document,'touchend',self.mouseUp); } else { // incredibly old-skool. TODO: Modernize. document.onmousemove = self.mmh; document.onmouseup = self.mouseUp; } self.stopEvent(e); return false; }; this.mouseUp = function(e) { var oData = self.lastTouchedSound._360data; self.removeClass(oData.oUIBox,'sm2_dragging'); if (oData.pauseCount === 0) { self.lastTouchedSound.resume(); } if (!isTouchDevice) { document.onmousemove = null; document.onmouseup = null; } else { self.removeEventHandler(document,'touchmove',self.mmh); self.removeEventHandler(document,'touchend',self.mouseUP); } }; this.mmh = function(e) { if (typeof e === 'undefined') { e = window.event; } var oSound = self.lastTouchedSound, coords = self.getMouseXY(e), x = coords[0], y = coords[1], deltaX = x-oSound._360data.canvasMidXY[0], deltaY = y-oSound._360data.canvasMidXY[1], angle = Math.floor(fullCircle-(self.rad2deg(Math.atan2(deltaX,deltaY))+180)); oSound.setPosition(oSound.durationEstimate*(angle/fullCircle)); self.stopEvent(e); return false; }; // assignMouseDown(); this.drawSolidArc = function(oCanvas, color, radius, width, radians, startAngle, noClear) { // thank you, http://www.snipersystems.co.nz/community/polarclock/tutorial.html var x = radius, y = radius, canvas = oCanvas, ctx, innerRadius, doesntLikeZero, endPoint; if (canvas.getContext){ // use getContext to use the canvas for drawing ctx = canvas.getContext('2d'); } // re-assign canvas as the actual context oCanvas = ctx; if (!noClear) { self.clearCanvas(canvas); } // ctx.restore(); if (color) { ctx.fillStyle = color; } oCanvas.beginPath(); if (isNaN(radians)) { radians = 0; } innerRadius = radius-width; doesntLikeZero = (isOpera || isSafari); // safari 4 doesn't actually seem to mind. if (!doesntLikeZero || (doesntLikeZero && radius > 0)) { oCanvas.arc(0, 0, radius, startAngle, radians, false); endPoint = self.getArcEndpointCoords(innerRadius, radians); oCanvas.lineTo(endPoint.x, endPoint.y); oCanvas.arc(0, 0, innerRadius, radians, startAngle, true); oCanvas.closePath(); oCanvas.fill(); } }; this.getArcEndpointCoords = function(radius, radians) { return { x: radius * Math.cos(radians), y: radius * Math.sin(radians) }; }; this.deg2rad = function(nDeg) { return (nDeg * Math.PI/180); }; this.rad2deg = function(nRad) { return (nRad * 180/Math.PI); }; this.getTime = function(nMSec,bAsString) { // convert milliseconds to mm:ss, return as object literal or string var nSec = Math.floor(nMSec/1000), min = Math.floor(nSec/60), sec = nSec-(min*60); // if (min === 0 && sec === 0) return null; // return 0:00 as null return (bAsString?(min+':'+(sec<10?'0'+sec:sec)):{'min':min,'sec':sec}); }; this.clearCanvas = function(oCanvas) { var canvas = oCanvas, ctx = null, width, height; if (canvas.getContext){ // use getContext to use the canvas for drawing ctx = canvas.getContext('2d'); } width = canvas.offsetWidth; height = canvas.offsetHeight; ctx.clearRect(-(width/2), -(height/2), width, height); }; this.updatePlaying = function() { var timeNow = (this._360data.showHMSTime?self.getTime(this.position,true):parseInt(this.position/1000, 10)); var ringScaleFactor = self.config.scaleArcWidth; if (this.bytesLoaded) { this._360data.lastValues.bytesLoaded = this.bytesLoaded; this._360data.lastValues.bytesTotal = this.bytesTotal; } if (this.position) { this._360data.lastValues.position = this.position; } if (this.durationEstimate) { this._360data.lastValues.durationEstimate = this.durationEstimate; } // background ring self.drawSolidArc(this._360data.oCanvas,self.config.backgroundRingColor,this._360data.width,this._360data.radius * ringScaleFactor,self.deg2rad(fullCircle),false); // loaded ring self.drawSolidArc(this._360data.oCanvas,(this._360data.metadata?self.config.loadRingColorMetadata:self.config.loadRingColor),this._360data.width,this._360data.radius * ringScaleFactor,self.deg2rad(fullCircle*(this._360data.lastValues.bytesLoaded/this._360data.lastValues.bytesTotal)),0,true); // don't draw if 0 (full black circle in Opera) if (this._360data.lastValues.position !== 0) { self.drawSolidArc(this._360data.oCanvas,(this._360data.metadata?self.config.playRingColorMetadata:self.config.playRingColor),this._360data.width,this._360data.radius * ringScaleFactor,self.deg2rad((this._360data.didFinish===1?fullCircle:fullCircle*(this._360data.lastValues.position/this._360data.lastValues.durationEstimate))),0,true); } // metadata goes here if (this._360data.metadata) { this._360data.metadata.events.whileplaying(); } if (timeNow !== this._360data.lastTime) { this._360data.lastTime = timeNow; this._360data.oTiming.innerHTML = timeNow; } // draw spectrum, if applicable if ((this.instanceOptions.useWaveformData || this.instanceOptions.useEQData) && hasRealCanvas) { // IE <9 can render maybe 3 or 4 FPS when including the wave/EQ, so don't bother. self.updateWaveform(this); } if (self.config.useFavIcon && self.vuMeter) { self.vuMeter.updateVU(this); } }; this.updateWaveform = function(oSound) { if ((!self.config.useWaveformData && !self.config.useEQData) || (!sm.features.waveformData && !sm.features.eqData)) { // feature not enabled.. return false; } if (!oSound.waveformData.left.length && !oSound.eqData.length && !oSound.peakData.left) { // no data (or errored out/paused/unavailable?) return false; } /* use for testing the data */ /* for (i=0; i<256; i++) { oSound.eqData[i] = 1-(i/256); } */ var oCanvas = oSound._360data.oCanvas.getContext('2d'), offX = 0, offY = parseInt(oSound._360data.circleDiameter/2, 10), scale = offY/2, // Y axis (+/- this distance from 0) // lineWidth = Math.floor(oSound._360data.circleDiameter-(oSound._360data.circleDiameter*0.175)/(oSound._360data.circleDiameter/255)); // width for each line lineWidth = 1, lineHeight = 1, thisY = 0, offset = offY, i, j, direction, downSample, dataLength, sampleCount, startAngle, endAngle, waveData, innerRadius, perItemAngle, yDiff, eqSamples, playedAngle, iAvg, nPeak; if (self.config.useWaveformData) { // raw waveform downSample = self.config.waveformDataDownsample; // only sample X in 256 (greater number = less sample points) downSample = Math.max(1,downSample); // make sure it's at least 1 dataLength = 256; sampleCount = (dataLength/downSample); startAngle = 0; endAngle = 0; waveData = null; innerRadius = (self.config.waveformDataOutside?1:(self.config.waveformDataConstrain?0.5:0.565)); scale = (self.config.waveformDataOutside?0.7:0.75); perItemAngle = self.deg2rad((360/sampleCount)*self.config.waveformDataLineRatio); // 0.85 = clean pixel lines at 150? // self.deg2rad(360*(Math.max(1,downSample-1))/sampleCount); for (i=0; i16500 Hz), most stuff won't actually use it. sampleCount = (eqSamples/downSample); innerRadius = (self.config.eqDataOutside?1:0.565); direction = (self.config.eqDataOutside?-1:1); scale = (self.config.eqDataOutside?0.5:0.75); startAngle = 0; endAngle = 0; perItemAngle = self.deg2rad((360/sampleCount)*self.config.eqDataLineRatio); // self.deg2rad(360/(sampleCount+1)); playedAngle = self.deg2rad((oSound._360data.didFinish===1?360:360*(oSound._360data.lastValues.position/oSound._360data.lastValues.durationEstimate))); j=0; iAvg = 0; for (i=0; iplayedAngle?self.config.eqDataColor:self.config.playRingColor),oSound._360data.width*innerRadius,oSound._360data.radius*scale*(oSound.eqData.left[i]*direction),endAngle,startAngle,true); } } if (self.config.usePeakData) { if (!oSound._360data.animating) { nPeak = (oSound.peakData.left||oSound.peakData.right); // GIANT HACK: use EQ spectrum data for bass frequencies eqSamples = 3; for (i=0; i', ' ', // note use of imageMap, edit or remove if you use a different-size image. '
', // + Ever-so-slight Safari horizontal alignment tweak '
' ]; }; this.uiTest = function(sClass) { // fake a 360 UI so we can get some numbers from CSS, etc. var oTemplate = document.createElement('div'), oFakeUI, oFakeUIBox, oTemp, fakeDiameter, uiHTML, circleDiameter, circleRadius, fontSizeMax, oTiming; oTemplate.className = 'sm2-360ui'; oFakeUI = document.createElement('div'); oFakeUI.className = 'ui360'+(sClass?' '+sClass:''); // ui360 ui360-vis oFakeUIBox = oFakeUI.appendChild(oTemplate.cloneNode(true)); oFakeUI.style.position = 'absolute'; oFakeUI.style.left = '-9999px'; oTemp = document.body.appendChild(oFakeUI); fakeDiameter = oFakeUIBox.offsetWidth; uiHTML = self.getUIHTML(fakeDiameter); oFakeUIBox.innerHTML = uiHTML[1]+uiHTML[2]+uiHTML[3]; circleDiameter = parseInt(oFakeUIBox.offsetWidth, 10); circleRadius = parseInt(circleDiameter/2, 10); oTiming = self.getElementsByClassName('sm2-timing','div',oTemp)[0]; fontSizeMax = parseInt(self.getStyle(oTiming,'font-size'), 10); if (isNaN(fontSizeMax)) { // getStyle() etc. didn't work. fontSizeMax = null; } // soundManager._writeDebug('diameter, font size: '+circleDiameter+','+fontSizeMax); oFakeUI.parentNode.removeChild(oFakeUI); uiHTML = oFakeUI = oFakeUIBox = oTemp = null; return { circleDiameter: circleDiameter, circleRadius: circleRadius, fontSizeMax: fontSizeMax }; }; this.init = function() { sm._writeDebug('threeSixtyPlayer.init()'); var oItems = self.getElementsByClassName('ui360','div'), i, j, oLinks = [], is_vis = false, foundItems = 0, oCanvas, oCanvasCTX, oCover, diameter, radius, uiData, uiDataVis, oUI, oBtn, o, o2, oID; for (i=0,j=oItems.length; i0) { self.addEventHandler(document,'click',self.handleClick); if (self.config.autoPlay) { self.handleClick({target:self.links[0],preventDefault:function(){}}); } } sm._writeDebug('threeSixtyPlayer.init(): Found '+foundItems+' relevant items.'); if (self.config.useFavIcon && typeof this.VUMeter !== 'undefined') { this.vuMeter = new this.VUMeter(this); } }; } // Optional: VU Meter component ThreeSixtyPlayer.prototype.VUMeter = function(oParent) { var self = oParent, me = this, _head = document.getElementsByTagName('head')[0], isOpera = (navigator.userAgent.match(/opera/i)), isFirefox = (navigator.userAgent.match(/firefox/i)); this.vuMeterData = []; this.vuDataCanvas = null; this.setPageIcon = function(sDataURL) { if (!self.config.useFavIcon || !self.config.usePeakData || !sDataURL) { return false; } var link = document.getElementById('sm2-favicon'); if (link) { _head.removeChild(link); link = null; } if (!link) { link = document.createElement('link'); link.id = 'sm2-favicon'; link.rel = 'shortcut icon'; link.type = 'image/png'; link.href = sDataURL; document.getElementsByTagName('head')[0].appendChild(link); } }; this.resetPageIcon = function() { if (!self.config.useFavIcon) { return false; } var link = document.getElementById('favicon'); if (link) { link.href = '/favicon.ico'; } }; this.updateVU = function(oSound) { if (soundManager.flashVersion >= 9 && self.config.useFavIcon && self.config.usePeakData) { me.setPageIcon(me.vuMeterData[parseInt(16*oSound.peakData.left, 10)][parseInt(16*oSound.peakData.right, 10)]); } }; this.createVUData = function() { var i=0, j=0, canvas = me.vuDataCanvas.getContext('2d'), vuGrad = canvas.createLinearGradient(0, 16, 0, 0), bgGrad = canvas.createLinearGradient(0, 16, 0, 0), outline = 'rgba(0,0,0,0.2)'; vuGrad.addColorStop(0,'rgb(0,192,0)'); vuGrad.addColorStop(0.30,'rgb(0,255,0)'); vuGrad.addColorStop(0.625,'rgb(255,255,0)'); vuGrad.addColorStop(0.85,'rgb(255,0,0)'); bgGrad.addColorStop(0,outline); bgGrad.addColorStop(1,'rgba(0,0,0,0.5)'); for (i=0; i<16; i++) { me.vuMeterData[i] = []; } for (i=0; i<16; i++) { for (j=0; j<16; j++) { // reset/erase canvas me.vuDataCanvas.setAttribute('width',16); me.vuDataCanvas.setAttribute('height',16); // draw new stuffs canvas.fillStyle = bgGrad; canvas.fillRect(0,0,7,15); canvas.fillRect(8,0,7,15); /* // shadow canvas.fillStyle = 'rgba(0,0,0,0.1)'; canvas.fillRect(1,15-i,7,17-(17-i)); canvas.fillRect(9,15-j,7,17-(17-j)); */ canvas.fillStyle = vuGrad; canvas.fillRect(0,15-i,7,16-(16-i)); canvas.fillRect(8,15-j,7,16-(16-j)); // and now, clear out some bits. canvas.clearRect(0,3,16,1); canvas.clearRect(0,7,16,1); canvas.clearRect(0,11,16,1); me.vuMeterData[i][j] = me.vuDataCanvas.toDataURL('image/png'); // for debugging VU images /* var o = document.createElement('img'); o.style.marginRight = '5px'; o.src = vuMeterData[i][j]; document.documentElement.appendChild(o); */ } } }; this.testCanvas = function() { // canvas + toDataURL(); var c = document.createElement('canvas'), ctx = null, ok; if (!c || typeof c.getContext === 'undefined') { return null; } ctx = c.getContext('2d'); if (!ctx || typeof c.toDataURL !== 'function') { return null; } // just in case.. try { ok = c.toDataURL('image/png'); } catch(e) { // no canvas or no toDataURL() return null; } // assume we're all good. return c; }; this.init = function() { if (self.config.useFavIcon) { me.vuDataCanvas = me.testCanvas(); if (me.vuDataCanvas && (isFirefox || isOpera)) { // these browsers support dynamically-updating the favicon me.createVUData(); } else { // browser doesn't support doing this self.config.useFavIcon = false; } } }; this.init(); }; // completely optional: Metadata/annotations/segments code ThreeSixtyPlayer.prototype.Metadata = function(oSound, oParent) { soundManager._wD('Metadata()'); var me = this, oBox = oSound._360data.oUI360, o = oBox.getElementsByTagName('ul')[0], oItems = o.getElementsByTagName('li'), isFirefox = (navigator.userAgent.match(/firefox/i)), isAlt = false, i, oDuration; this.lastWPExec = 0; this.refreshInterval = 250; this.totalTime = 0; this.events = { whileplaying: function() { var width = oSound._360data.width, radius = oSound._360data.radius, fullDuration = (oSound.durationEstimate||(me.totalTime*1000)), isAlt = null, i, j, d; for (i=0,j=me.data.length; ime.refreshInterval) { me.refresh(); me.lastWPExec = d; } } }; this.refresh = function() { // Display info as appropriate var i, j, index = null, now = oSound.position, metadata = oSound._360data.metadata.data; for (i=0, j=metadata.length; i= metadata[i].startTimeMS && now <= metadata[i].endTimeMS) { index = i; break; } } if (index !== metadata.currentItem && index < metadata.length) { // update oSound._360data.oLink.innerHTML = metadata.mainTitle+' '; // self.setPageTitle(metadata[index].title+' | '+metadata.mainTitle); metadata.currentItem = index; } }; this.strToTime = function(sTime) { var segments = sTime.split(':'), seconds = 0, i; for (i=segments.length; i--;) { seconds += parseInt(segments[i], 10)*Math.pow(60,segments.length-1-i); // hours, minutes } return seconds; }; this.data = []; this.data.givenDuration = null; this.data.currentItem = null; this.data.mainTitle = oSound._360data.oLink.innerHTML; for (i=0; i