You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

775 lines
23 KiB

  1. /* ******************************
  2. Semantic Module: Shape
  3. Author: Jack Lukic
  4. Notes: First Commit March 25, 2013
  5. An experimental plugin for manipulating 3D shapes on a 2D plane
  6. ****************************** */
  7. ;(function ( $, window, document, undefined ) {
  8. $.fn.shape = function(parameters) {
  9. var
  10. $allModules = $(this),
  11. moduleSelector = $allModules.selector || '',
  12. settings = $.extend(true, {}, $.fn.shape.settings, parameters),
  13. // internal aliases
  14. namespace = settings.namespace,
  15. selector = settings.selector,
  16. error = settings.error,
  17. className = settings.className,
  18. // define namespaces for modules
  19. eventNamespace = '.' + namespace,
  20. moduleNamespace = 'module-' + namespace,
  21. time = new Date().getTime(),
  22. performance = [],
  23. query = arguments[0],
  24. methodInvoked = (typeof query == 'string'),
  25. queryArguments = [].slice.call(arguments, 1),
  26. invokedResponse
  27. ;
  28. $allModules
  29. .each(function() {
  30. var
  31. // selector cache
  32. $module = $(this),
  33. $sides = $module.find(selector.sides),
  34. $side = $module.find(selector.side),
  35. // private variables
  36. $activeSide,
  37. $nextSide,
  38. // standard module
  39. element = this,
  40. instance = $module.data(moduleNamespace),
  41. module
  42. ;
  43. module = {
  44. initialize: function() {
  45. module.verbose('Initializing module for', element);
  46. module.set.defaultSide();
  47. module.instantiate();
  48. },
  49. instantiate: function() {
  50. module.verbose('Storing instance of module', module);
  51. instance = module;
  52. $module
  53. .data(moduleNamespace, instance)
  54. ;
  55. },
  56. destroy: function() {
  57. module.verbose('Destroying previous module for', element);
  58. $module
  59. .removeData(moduleNamespace)
  60. .off(eventNamespace)
  61. ;
  62. },
  63. refresh: function() {
  64. module.verbose('Refreshing selector cache for', element);
  65. $module = $(element);
  66. $sides = $(this).find(selector.shape);
  67. $side = $(this).find(selector.side);
  68. },
  69. repaint: function() {
  70. module.verbose('Forcing repaint event');
  71. var
  72. shape = $sides.get(0) || document.createElement('div'),
  73. fakeAssignment = shape.offsetWidth
  74. ;
  75. },
  76. animate: function(propertyObject, callback) {
  77. module.verbose('Animating box with properties', propertyObject);
  78. callback = callback || function(event) {
  79. module.verbose('Executing animation callback');
  80. if(event !== undefined) {
  81. event.stopPropagation();
  82. }
  83. module.reset();
  84. module.set.active();
  85. };
  86. if(settings.useCSS) {
  87. if(module.get.transitionEvent()) {
  88. module.verbose('Starting CSS animation');
  89. $module
  90. .addClass(className.animating)
  91. ;
  92. module.set.stageSize();
  93. module.repaint();
  94. $module
  95. .addClass(className.css)
  96. ;
  97. $activeSide
  98. .addClass(className.hidden)
  99. ;
  100. $sides
  101. .css(propertyObject)
  102. .one(module.get.transitionEvent(), callback)
  103. ;
  104. }
  105. else {
  106. callback();
  107. }
  108. }
  109. else {
  110. // not yet supported until .animate() is extended to allow RotateX/Y
  111. module.verbose('Starting javascript animation');
  112. $module
  113. .addClass(className.animating)
  114. .removeClass(className.css)
  115. ;
  116. module.set.stageSize();
  117. module.repaint();
  118. $activeSide
  119. .animate({
  120. opacity: 0
  121. }, settings.duration, settings.easing)
  122. ;
  123. $sides
  124. .animate(propertyObject, settings.duration, settings.easing, callback)
  125. ;
  126. }
  127. },
  128. queue: function(method) {
  129. module.debug('Queueing animation of', method);
  130. $sides
  131. .one(module.get.transitionEvent(), function() {
  132. module.debug('Executing queued animation');
  133. setTimeout(function(){
  134. $module.shape(method);
  135. }, 0);
  136. })
  137. ;
  138. },
  139. reset: function() {
  140. module.verbose('Animating states reset');
  141. $module
  142. .removeClass(className.css)
  143. .removeClass(className.animating)
  144. .attr('style', '')
  145. .removeAttr('style')
  146. ;
  147. // removeAttr style does not consistently work in safari
  148. $sides
  149. .attr('style', '')
  150. .removeAttr('style')
  151. ;
  152. $side
  153. .attr('style', '')
  154. .removeAttr('style')
  155. .removeClass(className.hidden)
  156. ;
  157. $nextSide
  158. .removeClass(className.animating)
  159. .attr('style', '')
  160. .removeAttr('style')
  161. ;
  162. },
  163. is: {
  164. animating: function() {
  165. return $module.hasClass(className.animating);
  166. }
  167. },
  168. get: {
  169. transform: {
  170. up: function() {
  171. var
  172. translate = {
  173. y: -(($activeSide.outerHeight() - $nextSide.outerHeight()) / 2),
  174. z: -($activeSide.outerHeight() / 2)
  175. }
  176. ;
  177. return {
  178. transform: 'translateY(' + translate.y + 'px) translateZ('+ translate.z + 'px) rotateX(-90deg)'
  179. };
  180. },
  181. down: function() {
  182. var
  183. translate = {
  184. y: -(($activeSide.outerHeight() - $nextSide.outerHeight()) / 2),
  185. z: -($activeSide.outerHeight() / 2)
  186. }
  187. ;
  188. return {
  189. transform: 'translateY(' + translate.y + 'px) translateZ('+ translate.z + 'px) rotateX(90deg)'
  190. };
  191. },
  192. left: function() {
  193. var
  194. translate = {
  195. x : -(($activeSide.outerWidth() - $nextSide.outerWidth()) / 2),
  196. z : -($activeSide.outerWidth() / 2)
  197. }
  198. ;
  199. return {
  200. transform: 'translateX(' + translate.x + 'px) translateZ(' + translate.z + 'px) rotateY(90deg)'
  201. };
  202. },
  203. right: function() {
  204. var
  205. translate = {
  206. x : -(($activeSide.outerWidth() - $nextSide.outerWidth()) / 2),
  207. z : -($activeSide.outerWidth() / 2)
  208. }
  209. ;
  210. return {
  211. transform: 'translateX(' + translate.x + 'px) translateZ(' + translate.z + 'px) rotateY(-90deg)'
  212. };
  213. },
  214. over: function() {
  215. var
  216. translate = {
  217. x : -(($activeSide.outerWidth() - $nextSide.outerWidth()) / 2)
  218. }
  219. ;
  220. return {
  221. transform: 'translateX(' + translate.x + 'px) rotateY(180deg)'
  222. };
  223. },
  224. back: function() {
  225. var
  226. translate = {
  227. x : -(($activeSide.outerWidth() - $nextSide.outerWidth()) / 2)
  228. }
  229. ;
  230. return {
  231. transform: 'translateX(' + translate.x + 'px) rotateY(-180deg)'
  232. };
  233. }
  234. },
  235. transitionEvent: function() {
  236. var
  237. element = document.createElement('element'),
  238. transitions = {
  239. 'transition' :'transitionend',
  240. 'OTransition' :'oTransitionEnd',
  241. 'MozTransition' :'transitionend',
  242. 'WebkitTransition' :'webkitTransitionEnd'
  243. },
  244. transition
  245. ;
  246. for(transition in transitions){
  247. if( element.style[transition] !== undefined ){
  248. return transitions[transition];
  249. }
  250. }
  251. },
  252. nextSide: function() {
  253. return ( $activeSide.next(selector.side).size() > 0 )
  254. ? $activeSide.next(selector.side)
  255. : $module.find(selector.side).first()
  256. ;
  257. }
  258. },
  259. set: {
  260. defaultSide: function() {
  261. $activeSide = $module.find('.' + settings.className.active);
  262. $nextSide = ( $activeSide.next(selector.side).size() > 0 )
  263. ? $activeSide.next(selector.side)
  264. : $module.find(selector.side).first()
  265. ;
  266. module.verbose('Active side set to', $activeSide);
  267. module.verbose('Next side set to', $nextSide);
  268. },
  269. stageSize: function() {
  270. var
  271. stage = {
  272. width : $nextSide.outerWidth(),
  273. height : $nextSide.outerHeight()
  274. }
  275. ;
  276. module.verbose('Resizing stage to fit new content', stage);
  277. $module
  278. .css({
  279. width : stage.width,
  280. height : stage.height
  281. })
  282. ;
  283. },
  284. nextSide: function(selector) {
  285. $nextSide = $module.find(selector);
  286. if($nextSide.size() === 0) {
  287. module.error(error.side);
  288. }
  289. module.verbose('Next side manually set to', $nextSide);
  290. },
  291. active: function() {
  292. module.verbose('Setting new side to active', $nextSide);
  293. $side
  294. .removeClass(className.active)
  295. ;
  296. $nextSide
  297. .addClass(className.active)
  298. ;
  299. $.proxy(settings.onChange, $nextSide)();
  300. module.set.defaultSide();
  301. }
  302. },
  303. flip: {
  304. up: function() {
  305. module.debug('Flipping up', $nextSide);
  306. if( !module.is.animating() ) {
  307. module.stage.above();
  308. module.animate( module.get.transform.up() );
  309. }
  310. else {
  311. module.queue('flip up');
  312. }
  313. },
  314. down: function() {
  315. module.debug('Flipping down', $nextSide);
  316. if( !module.is.animating() ) {
  317. module.stage.below();
  318. module.animate( module.get.transform.down() );
  319. }
  320. else {
  321. module.queue('flip down');
  322. }
  323. },
  324. left: function() {
  325. module.debug('Flipping left', $nextSide);
  326. if( !module.is.animating() ) {
  327. module.stage.left();
  328. module.animate(module.get.transform.left() );
  329. }
  330. else {
  331. module.queue('flip left');
  332. }
  333. },
  334. right: function() {
  335. module.debug('Flipping right', $nextSide);
  336. if( !module.is.animating() ) {
  337. module.stage.right();
  338. module.animate(module.get.transform.right() );
  339. }
  340. else {
  341. module.queue('flip right');
  342. }
  343. },
  344. over: function() {
  345. module.debug('Flipping over', $nextSide);
  346. if( !module.is.animating() ) {
  347. module.stage.behind();
  348. module.animate(module.get.transform.over() );
  349. }
  350. else {
  351. module.queue('flip over');
  352. }
  353. },
  354. back: function() {
  355. module.debug('Flipping back', $nextSide);
  356. if( !module.is.animating() ) {
  357. module.stage.behind();
  358. module.animate(module.get.transform.back() );
  359. }
  360. else {
  361. module.queue('flip back');
  362. }
  363. }
  364. },
  365. stage: {
  366. above: function() {
  367. var
  368. box = {
  369. origin : (($activeSide.outerHeight() - $nextSide.outerHeight()) / 2),
  370. depth : {
  371. active : ($nextSide.outerHeight() / 2),
  372. next : ($activeSide.outerHeight() / 2)
  373. }
  374. }
  375. ;
  376. module.verbose('Setting the initial animation position as above', $nextSide, box);
  377. $activeSide
  378. .css({
  379. 'transform' : 'rotateY(0deg) translateZ(' + box.depth.active + 'px)'
  380. })
  381. ;
  382. $nextSide
  383. .addClass(className.animating)
  384. .css({
  385. 'display' : 'block',
  386. 'top' : box.origin + 'px',
  387. 'transform' : 'rotateX(90deg) translateZ(' + box.depth.next + 'px)'
  388. })
  389. ;
  390. },
  391. below: function() {
  392. var
  393. box = {
  394. origin : (($activeSide.outerHeight() - $nextSide.outerHeight()) / 2),
  395. depth : {
  396. active : ($nextSide.outerHeight() / 2),
  397. next : ($activeSide.outerHeight() / 2)
  398. }
  399. }
  400. ;
  401. module.verbose('Setting the initial animation position as below', $nextSide, box);
  402. $activeSide
  403. .css({
  404. 'transform' : 'rotateY(0deg) translateZ(' + box.depth.active + 'px)'
  405. })
  406. ;
  407. $nextSide
  408. .addClass(className.animating)
  409. .css({
  410. 'display' : 'block',
  411. 'top' : box.origin + 'px',
  412. 'transform' : 'rotateX(-90deg) translateZ(' + box.depth.next + 'px)'
  413. })
  414. ;
  415. },
  416. left: function() {
  417. var
  418. box = {
  419. origin : ( ( $activeSide.outerWidth() - $nextSide.outerWidth() ) / 2),
  420. depth : {
  421. active : ($nextSide.outerWidth() / 2),
  422. next : ($activeSide.outerWidth() / 2)
  423. }
  424. }
  425. ;
  426. module.verbose('Setting the initial animation position as left', $nextSide, box);
  427. $activeSide
  428. .css({
  429. 'transform' : 'rotateY(0deg) translateZ(' + box.depth.active + 'px)'
  430. })
  431. ;
  432. $nextSide
  433. .addClass(className.animating)
  434. .css({
  435. 'display' : 'block',
  436. 'left' : box.origin + 'px',
  437. 'transform' : 'rotateY(-90deg) translateZ(' + box.depth.next + 'px)'
  438. })
  439. ;
  440. },
  441. right: function() {
  442. var
  443. box = {
  444. origin : ( ( $activeSide.outerWidth() - $nextSide.outerWidth() ) / 2),
  445. depth : {
  446. active : ($nextSide.outerWidth() / 2),
  447. next : ($activeSide.outerWidth() / 2)
  448. }
  449. }
  450. ;
  451. module.verbose('Setting the initial animation position as left', $nextSide, box);
  452. $activeSide
  453. .css({
  454. 'transform' : 'rotateY(0deg) translateZ(' + box.depth.active + 'px)'
  455. })
  456. ;
  457. $nextSide
  458. .addClass(className.animating)
  459. .css({
  460. 'display' : 'block',
  461. 'left' : box.origin + 'px',
  462. 'transform' : 'rotateY(90deg) translateZ(' + box.depth.next + 'px)'
  463. })
  464. ;
  465. },
  466. behind: function() {
  467. var
  468. box = {
  469. origin : ( ( $activeSide.outerWidth() - $nextSide.outerWidth() ) / 2),
  470. depth : {
  471. active : ($nextSide.outerWidth() / 2),
  472. next : ($activeSide.outerWidth() / 2)
  473. }
  474. }
  475. ;
  476. module.verbose('Setting the initial animation position as behind', $nextSide, box);
  477. $activeSide
  478. .css({
  479. 'transform' : 'rotateY(0deg)'
  480. })
  481. ;
  482. $nextSide
  483. .addClass(className.animating)
  484. .css({
  485. 'display' : 'block',
  486. 'left' : box.origin + 'px',
  487. 'transform' : 'rotateY(-180deg)'
  488. })
  489. ;
  490. }
  491. },
  492. setting: function(name, value) {
  493. if(value !== undefined) {
  494. if( $.isPlainObject(name) ) {
  495. $.extend(true, settings, name);
  496. }
  497. else {
  498. settings[name] = value;
  499. }
  500. }
  501. else {
  502. return settings[name];
  503. }
  504. },
  505. internal: function(name, value) {
  506. if(value !== undefined) {
  507. if( $.isPlainObject(name) ) {
  508. $.extend(true, module, name);
  509. }
  510. else {
  511. module[name] = value;
  512. }
  513. }
  514. else {
  515. return module[name];
  516. }
  517. },
  518. debug: function() {
  519. if(settings.debug) {
  520. if(settings.performance) {
  521. module.performance.log(arguments);
  522. }
  523. else {
  524. module.debug = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  525. module.debug.apply(console, arguments);
  526. }
  527. }
  528. },
  529. verbose: function() {
  530. if(settings.verbose && settings.debug) {
  531. if(settings.performance) {
  532. module.performance.log(arguments);
  533. }
  534. else {
  535. module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  536. module.verbose.apply(console, arguments);
  537. }
  538. }
  539. },
  540. error: function() {
  541. module.error = Function.prototype.bind.call(console.error, console, settings.moduleName + ':');
  542. module.error.apply(console, arguments);
  543. },
  544. performance: {
  545. log: function(message) {
  546. var
  547. currentTime,
  548. executionTime,
  549. previousTime
  550. ;
  551. if(settings.performance) {
  552. currentTime = new Date().getTime();
  553. previousTime = time || currentTime;
  554. executionTime = currentTime - previousTime;
  555. time = currentTime;
  556. performance.push({
  557. 'Element' : element,
  558. 'Name' : message[0],
  559. 'Arguments' : [].slice.call(message, 1) || '',
  560. 'Execution Time' : executionTime
  561. });
  562. }
  563. clearTimeout(module.performance.timer);
  564. module.performance.timer = setTimeout(module.performance.display, 100);
  565. },
  566. display: function() {
  567. var
  568. title = settings.name + ':',
  569. totalTime = 0
  570. ;
  571. time = false;
  572. clearTimeout(module.performance.timer);
  573. $.each(performance, function(index, data) {
  574. totalTime += data['Execution Time'];
  575. });
  576. title += ' ' + totalTime + 'ms';
  577. if(moduleSelector) {
  578. title += ' \'' + moduleSelector + '\'';
  579. }
  580. if($allModules.size() > 1) {
  581. title += ' ' + '(' + $allModules.size() + ')';
  582. }
  583. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  584. console.groupCollapsed(title);
  585. if(console.table) {
  586. console.table(performance);
  587. }
  588. else {
  589. $.each(performance, function(index, data) {
  590. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  591. });
  592. }
  593. console.groupEnd();
  594. }
  595. performance = [];
  596. }
  597. },
  598. invoke: function(query, passedArguments, context) {
  599. var
  600. maxDepth,
  601. found,
  602. response
  603. ;
  604. passedArguments = passedArguments || queryArguments;
  605. context = element || context;
  606. if(typeof query == 'string' && instance !== undefined) {
  607. query = query.split(/[\. ]/);
  608. maxDepth = query.length - 1;
  609. $.each(query, function(depth, value) {
  610. var camelCaseValue = (depth != maxDepth)
  611. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  612. : query
  613. ;
  614. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  615. instance = instance[value];
  616. }
  617. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  618. instance = instance[camelCaseValue];
  619. }
  620. else if( instance[value] !== undefined ) {
  621. found = instance[value];
  622. return false;
  623. }
  624. else if( instance[camelCaseValue] !== undefined ) {
  625. found = instance[camelCaseValue];
  626. return false;
  627. }
  628. else {
  629. module.error(error.method);
  630. return false;
  631. }
  632. });
  633. }
  634. if ( $.isFunction( found ) ) {
  635. response = found.apply(context, passedArguments);
  636. }
  637. else if(found !== undefined) {
  638. response = found;
  639. }
  640. if($.isArray(invokedResponse)) {
  641. invokedResponse.push(response);
  642. }
  643. else if(typeof invokedResponse == 'string') {
  644. invokedResponse = [invokedResponse, response];
  645. }
  646. else if(response !== undefined) {
  647. invokedResponse = response;
  648. }
  649. return found;
  650. }
  651. };
  652. if(methodInvoked) {
  653. if(instance === undefined) {
  654. module.initialize();
  655. }
  656. module.invoke(query);
  657. }
  658. else {
  659. if(instance !== undefined) {
  660. module.destroy();
  661. }
  662. module.initialize();
  663. }
  664. })
  665. ;
  666. return (invokedResponse !== undefined)
  667. ? invokedResponse
  668. : this
  669. ;
  670. };
  671. $.fn.shape.settings = {
  672. // module info
  673. moduleName : 'Shape Module',
  674. // debug content outputted to console
  675. debug : true,
  676. // verbose debug output
  677. verbose : true,
  678. // performance data output
  679. performance: true,
  680. // event namespace
  681. namespace : 'shape',
  682. // callback occurs on side change
  683. beforeChange : function() {},
  684. onChange : function() {},
  685. // use css animation (currently only true is supported)
  686. useCSS : true,
  687. // animation duration (useful only with future js animations)
  688. duration : 1000,
  689. easing : 'easeInOutQuad',
  690. // possible errors
  691. error: {
  692. side : 'You tried to switch to a side that does not exist.',
  693. method : 'The method you called is not defined'
  694. },
  695. // classnames used
  696. className : {
  697. css : 'css',
  698. animating : 'animating',
  699. hidden : 'hidden',
  700. active : 'active'
  701. },
  702. // selectors used
  703. selector : {
  704. sides : '.sides',
  705. side : '.side'
  706. }
  707. };
  708. })( jQuery, window , document );