/* global soundManager, document, console, window */ (function(window) { /** @license * SoundManager 2: "Turntable UI": Base and API * Copyright (c) 2015, Scott Schiller. All rights reserved. * http://www.schillmania.com/projects/soundmanager2/ * Code provided under BSD license. * http://schillmania.com/projects/soundmanager2/license.txt */ 'use strict'; var turntables = [], turntablesById = {}, // CSS selector for top-level DOM node turntableSelector = '.turntable', tt_prefix = 'tt_', idCounter = 0, utils; /** * Slightly hackish: Turntable event callbacks. * Override globally by setting turntables.on = {}, or individually by turntables[0].on = {} etc. */ turntables.on = {/* stop: function(tt) { console.log('turntable stopped', tt); }, start: function(tt) { console.log('turntable started', tt); }, powerOn: function(tt) { console.log('turntable powerOn', tt); }, powerOff: function(tt) { console.log('turntable powerOff', tt); }*/ }; function Turntable(o, options) { var api, css, dom, data, id, methods; // DOM ID + SM2 sound reference id = (tt_prefix + idCounter); idCounter++; options = options || {}; function add(className) { utils.css.add(dom.o, className); } function remove(className) { utils.css.remove(dom.o, className); } function callback(method) { if (method) { // fire callback, passing current turntable object if (api.on && api.on[method]) { api.on[method](api); } else if (turntables.on[method]) { turntables.on[method](api); } } } function applyTonearmAngle() { if (data.tonearm.angle >= 0) { dom.tonearm.style[utils.features.transform.prop] = 'rotate(' + data.tonearm.angle + 'deg)'; } } // TODO: use mixin if (options.hideLabelWithArtwork === undefined) { options.hideLabelWithArtwork = true; } css = { power: { turntable: 'power-on', motor: 'motor-on' }, turntable: { hasArtwork: 'has-artwork', hasRecord: 'has-record', hasSlipmat: 'has-slipmat', hideLabelWithArtwork: 'hide-label-with-artwork' } }; data = { power: { turntable: false, motor: false, motorVelocity: 0 }, tonearm: { angle: 0, maxAngle: 42, minAngle: 0 }, record: { hasArtwork: false } }; methods = { start: function() { if (data.power.turntable && !data.power.motor) { data.power.motor = true; add(css.power.motor); callback('start'); } }, stop: function() { if (data.power.motor) { data.power.motor = false; remove(css.power.motor); callback('stop'); } }, toggle: function() { if (data.power.motor) { methods.stop(); } else { methods.start(); } }, powerOn: function() { if (!data.power.turntable) { data.power.turntable = true; add(css.power.turntable); callback('powerOn'); } }, powerOff: function() { if (data.power.turntable) { data.power.turntable = false; remove(css.power.turntable); callback('powerOff'); } // no power = no motor, too. methods.stop(); }, powerToggle: function() { if (!data.power.turntable) { methods.powerOn(); } else { methods.powerOff(); } }, setTonearmAngle: function(angle) { if (!isNaN(angle)) { data.tonearm.angle = Math.max(data.tonearm.minAngle, Math.min(data.tonearm.maxAngle, angle)); if (utils.features.transform.prop) { if (utils.features.getAnimationFrame) { utils.features.getAnimationFrame(applyTonearmAngle); } else { applyTonearmAngle(); } } } }, addSlipmat: function() { add(css.turntable.hasSlipmat); }, removeSlipmat: function() { remove(css.turntable.hasSlipmat); }, toggleSlipmat: function() { utils.css.toggle(dom.o, css.turntable.hasSlipmat); }, addRecord: function() { add(css.turntable.hasRecord); }, removeRecord: function() { remove(css.turntable.hasRecord); }, toggleRecord: function() { utils.css.toggle(dom.o, css.turntable.hasRecord); }, setArtwork: function(url) { if (url) { dom.record.style.backgroundImage = 'url(' + url + ')'; if (!data.record.hasArtwork) { add(css.turntable.hasArtwork); data.record.hasArtwork = true; } } else if (data.record.hasArtwork) { dom.record.style.backgroundImage = 'none'; remove(css.turntable.hasArtwork); data.record.hasArtwork = false; } if (options.hideLabelWithArtwork) { add(css.turntable.hideLabelWithArtwork); } else { remove(css.turntable.hideLabelWithArtwork); } } }; function handleMethod(e) { var target, action; target = e.target; if (target) { action = target.getAttribute('data-method'); if (action && methods[action]) { methods[action](e); } } } function preventDefault(e) { if (e.target && e.target.nodeName === 'A') { utils.events.preventDefault(e); return false; } return true; } function assignEvents() { utils.events.add(dom.o, 'mousedown', handleMethod); utils.events.add(dom.o, 'click', preventDefault); } function initDOM() { dom = { o: o, platter: utils.dom.get(o, '.platter'), record: utils.dom.get(o, '.record'), slipmat: utils.dom.get(o, '.slipmat'), tonearm: utils.dom.get(o, '.tonearm') }; // inherit ID if (!dom.o.id) { dom.o.id = id; } else { id = dom.o.id; } } function init() { initDOM(); assignEvents(); // in a moment... window.setTimeout(methods.powerToggle, 500); } init(); // public interface api = { id: id, // css: css, data: data, methods: methods, on: {} // per-turntable event overrides you can specify; see turntables.on for examples }; return api; } // common JS helpers utils = { array: (function() { function compare(property) { var result; return function(a, b) { if (a[property] < b[property]) { result = -1; } else if (a[property] > b[property]) { result = 1; } else { result = 0; } return result; }; } function shuffle(array) { // Fisher-Yates shuffle algo var i, j, temp; for (i = array.length - 1; i > 0; i--) { j = Math.floor(Math.random() * (i + 1)); temp = array[i]; array[i] = array[j]; array[j] = temp; } return array; } return { compare: compare, shuffle: shuffle }; }()), css: (function() { function hasClass(o, cStr) { return (o.className !== undefined ? !!new RegExp('(^|\\s)' + cStr + '(\\s|$)').test(o.className) : false); } function addClass(o, cStr) { if (!o || !cStr || hasClass(o, cStr)) { return; // safety net } o.className = (o.className ? o.className + ' ' : '') + cStr; } function removeClass(o, cStr) { if (!o || !cStr || !hasClass(o, cStr)) { return; } o.className = o.className.replace(new RegExp('( ' + cStr + ')|(' + cStr + ')', 'g'), ''); } function swapClass(o, cStr1, cStr2) { var tmpClass = { className: o.className }; removeClass(tmpClass, cStr1); addClass(tmpClass, cStr2); o.className = tmpClass.className; } function toggleClass(o, cStr) { var found, method; found = hasClass(o, cStr); method = (found ? removeClass : addClass); method(o, cStr); // indicate the new state... return !found; } return { has: hasClass, add: addClass, remove: removeClass, swap: swapClass, toggle: toggleClass }; }()), dom: (function() { function getAll(param1, param2) { var node, selector, results; if (arguments.length === 1) { // .selector case node = document.documentElement; // first param is actually the selector selector = param1; } else { // node, .selector node = param1; selector = param2; } // sorry, IE 7 users; IE 8+ required. if (node && node.querySelectorAll) { results = node.querySelectorAll(selector); } return results; } function get(/* parentNode, selector */) { var results = getAll.apply(this, arguments); // hackish: if an array, return the last item. if (results && results.length) { return results[results.length - 1]; } // handle "not found" case return results && results.length === 0 ? null : results; } return { get: get, getAll: getAll }; }()), position: (function() { function getOffX(o) { // http://www.xs4all.nl/~ppk/js/findpos.html var curleft = 0; if (o.offsetParent) { while (o.offsetParent) { curleft += o.offsetLeft; o = o.offsetParent; } } else if (o.x) { curleft += o.x; } return curleft; } function getOffY(o) { // http://www.xs4all.nl/~ppk/js/findpos.html var curtop = 0; if (o.offsetParent) { while (o.offsetParent) { curtop += o.offsetTop; o = o.offsetParent; } } else if (o.y) { curtop += o.y; } return curtop; } return { getOffX: getOffX, getOffY: getOffY }; }()), style: (function() { function get(node, styleProp) { // http://www.quirksmode.org/dom/getstyles.html var value; if (node.currentStyle) { value = node.currentStyle[styleProp]; } else if (window.getComputedStyle) { value = document.defaultView.getComputedStyle(node, null).getPropertyValue(styleProp); } return value; } return { get: get }; }()), events: (function() { var add, remove, preventDefault; add = function(o, evtName, evtHandler) { // return an object with a convenient detach method. var eventObject = { detach: function() { return remove(o, evtName, evtHandler); } }; if (window.addEventListener) { o.addEventListener(evtName, evtHandler, false); } else { o.attachEvent('on' + evtName, evtHandler); } return eventObject; }; remove = (window.removeEventListener !== undefined ? function(o, evtName, evtHandler) { return o.removeEventListener(evtName, evtHandler, false); } : function(o, evtName, evtHandler) { return o.detachEvent('on' + evtName, evtHandler); }); preventDefault = function(e) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; e.cancelBubble = true; } return false; }; return { add: add, preventDefault: preventDefault, remove: remove }; }()), features: (function() { var getAnimationFrame, localAnimationFrame, localFeatures, prop, styles, testDiv, transform; testDiv = document.createElement('div'); /** * hat tip: paul irish * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ * https://gist.github.com/838785 */ localAnimationFrame = (window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || null); // apply to window, avoid "illegal invocation" errors in Chrome getAnimationFrame = localAnimationFrame ? function() { return localAnimationFrame.apply(window, arguments); } : null; function has(propName) { // test for feature support var result = testDiv.style[propName]; return (result !== undefined ? propName : null); } // note local scope. localFeatures = { transform: { ie: has('-ms-transform'), opera: has('OTransform'), webkit: has('webkitTransform'), w3: has('transform'), prop: null // the normalized property value }, rotate: { has3D: false, prop: null }, getAnimationFrame: getAnimationFrame }; localFeatures.transform.prop = ( localFeatures.transform.w3 || localFeatures.transform.moz || localFeatures.transform.webkit || localFeatures.transform.ie || localFeatures.transform.opera ); function attempt(style) { try { testDiv.style[transform] = style; } catch(e) { // that *definitely* didn't work. return false; } // if we can read back the style, it should be cool. return !!testDiv.style[transform]; } if (localFeatures.transform.prop) { // try to derive the rotate/3D support. transform = localFeatures.transform.prop; styles = { css_2d: 'rotate(0deg)', css_3d: 'rotate3d(0,0,0,0deg)' }; if (attempt(styles.css_3d)) { localFeatures.rotate.has3D = true; prop = 'rotate3d'; } else if (attempt(styles.css_2d)) { prop = 'rotate'; } localFeatures.rotate.prop = prop; } testDiv = null; return localFeatures; }()) }; soundManager.setup({ // Trade-off: higher UI responsiveness (play/progress bar), but may use more CPU. html5PollingInterval: 50, flashVersion: 9 }); soundManager.onready(function() { var nodes, i, j, tt; nodes = utils.dom.getAll(turntableSelector); if (nodes && nodes.length) { for (i = 0, j = nodes.length; i < j; i++) { tt = new Turntable(nodes[i]); turntables.push(tt); turntablesById[tt.id] = tt; } } }); window.turntables = turntables; window.turntablesById = turntablesById; // for external reference by demos, etc. window.turntables.utils = utils; }(window));