spin.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /**
  2. * Copyright (c) 2011-2014 Felix Gnass
  3. * Licensed under the MIT license
  4. */
  5. (function(root, factory) {
  6. /* CommonJS */
  7. if (typeof exports == 'object') module.exports = factory()
  8. /* AMD module */
  9. else if (typeof define == 'function' && define.amd) define(factory)
  10. /* Browser global */
  11. else root.Spinner = factory()
  12. }
  13. (this, function() {
  14. "use strict";
  15. var prefixes = ['webkit', 'Moz', 'ms', 'O'] /* Vendor prefixes */
  16. , animations = {} /* Animation rules keyed by their name */
  17. , useCssAnimations /* Whether to use CSS animations or setTimeout */
  18. /**
  19. * Utility function to create elements. If no tag name is given,
  20. * a DIV is created. Optionally properties can be passed.
  21. */
  22. function createEl(tag, prop) {
  23. var el = document.createElement(tag || 'div')
  24. , n
  25. for(n in prop) el[n] = prop[n]
  26. return el
  27. }
  28. /**
  29. * Appends children and returns the parent.
  30. */
  31. function ins(parent /* child1, child2, ...*/) {
  32. for (var i=1, n=arguments.length; i<n; i++)
  33. parent.appendChild(arguments[i])
  34. return parent
  35. }
  36. /**
  37. * Insert a new stylesheet to hold the @keyframe or VML rules.
  38. */
  39. var sheet = (function() {
  40. var el = createEl('style', {type : 'text/css'})
  41. ins(document.getElementsByTagName('head')[0], el)
  42. return el.sheet || el.styleSheet
  43. }())
  44. /**
  45. * Creates an opacity keyframe animation rule and returns its name.
  46. * Since most mobile Webkits have timing issues with animation-delay,
  47. * we create separate rules for each line/segment.
  48. */
  49. function addAnimation(alpha, trail, i, lines) {
  50. var name = ['opacity', trail, ~~(alpha*100), i, lines].join('-')
  51. , start = 0.01 + i/lines * 100
  52. , z = Math.max(1 - (1-alpha) / trail * (100-start), alpha)
  53. , prefix = useCssAnimations.substring(0, useCssAnimations.indexOf('Animation')).toLowerCase()
  54. , pre = prefix && '-' + prefix + '-' || ''
  55. if (!animations[name]) {
  56. sheet.insertRule(
  57. '@' + pre + 'keyframes ' + name + '{' +
  58. '0%{opacity:' + z + '}' +
  59. start + '%{opacity:' + alpha + '}' +
  60. (start+0.01) + '%{opacity:1}' +
  61. (start+trail) % 100 + '%{opacity:' + alpha + '}' +
  62. '100%{opacity:' + z + '}' +
  63. '}', sheet.cssRules.length)
  64. animations[name] = 1
  65. }
  66. return name
  67. }
  68. /**
  69. * Tries various vendor prefixes and returns the first supported property.
  70. */
  71. function vendor(el, prop) {
  72. var s = el.style
  73. , pp
  74. , i
  75. prop = prop.charAt(0).toUpperCase() + prop.slice(1)
  76. for(i=0; i<prefixes.length; i++) {
  77. pp = prefixes[i]+prop
  78. if(s[pp] !== undefined) return pp
  79. }
  80. if(s[prop] !== undefined) return prop
  81. }
  82. /**
  83. * Sets multiple style properties at once.
  84. */
  85. function css(el, prop) {
  86. for (var n in prop)
  87. el.style[vendor(el, n)||n] = prop[n]
  88. return el
  89. }
  90. /**
  91. * Fills in default values.
  92. */
  93. function merge(obj) {
  94. for (var i=1; i < arguments.length; i++) {
  95. var def = arguments[i]
  96. for (var n in def)
  97. if (obj[n] === undefined) obj[n] = def[n]
  98. }
  99. return obj
  100. }
  101. /**
  102. * Returns the line color from the given string or array.
  103. */
  104. function getColor(color, idx) {
  105. return typeof color == 'string' ? color : color[idx % color.length]
  106. }
  107. // Built-in defaults
  108. var defaults = {
  109. lines: 12, // The number of lines to draw
  110. length: 7, // The length of each line
  111. width: 5, // The line thickness
  112. radius: 10, // The radius of the inner circle
  113. rotate: 0, // Rotation offset
  114. corners: 1, // Roundness (0..1)
  115. color: '#000', // #rgb or #rrggbb
  116. direction: 1, // 1: clockwise, -1: counterclockwise
  117. speed: 1, // Rounds per second
  118. trail: 100, // Afterglow percentage
  119. opacity: 1/4, // Opacity of the lines
  120. fps: 20, // Frames per second when using setTimeout()
  121. zIndex: 2e9, // Use a high z-index by default
  122. className: 'spinner', // CSS class to assign to the element
  123. top: '50%', // center vertically
  124. left: '50%', // center horizontally
  125. position: 'absolute' // element position
  126. }
  127. /** The constructor */
  128. function Spinner(o) {
  129. this.opts = merge(o || {}, Spinner.defaults, defaults)
  130. }
  131. // Global defaults that override the built-ins:
  132. Spinner.defaults = {}
  133. merge(Spinner.prototype, {
  134. /**
  135. * Adds the spinner to the given target element. If this instance is already
  136. * spinning, it is automatically removed from its previous target b calling
  137. * stop() internally.
  138. */
  139. spin: function(target) {
  140. this.stop()
  141. var self = this
  142. , o = self.opts
  143. , el = self.el = css(createEl(0, {className: o.className}), {position: o.position, width: 0, zIndex: o.zIndex})
  144. css(el, {
  145. left: o.left,
  146. top: o.top
  147. })
  148. if (target) {
  149. target.insertBefore(el, target.firstChild||null)
  150. }
  151. el.setAttribute('role', 'progressbar')
  152. self.lines(el, self.opts)
  153. if (!useCssAnimations) {
  154. // No CSS animation support, use setTimeout() instead
  155. var i = 0
  156. , start = (o.lines - 1) * (1 - o.direction) / 2
  157. , alpha
  158. , fps = o.fps
  159. , f = fps/o.speed
  160. , ostep = (1-o.opacity) / (f*o.trail / 100)
  161. , astep = f/o.lines
  162. ;(function anim() {
  163. i++;
  164. for (var j = 0; j < o.lines; j++) {
  165. alpha = Math.max(1 - (i + (o.lines - j) * astep) % f * ostep, o.opacity)
  166. self.opacity(el, j * o.direction + start, alpha, o)
  167. }
  168. self.timeout = self.el && setTimeout(anim, ~~(1000/fps))
  169. })()
  170. }
  171. return self
  172. },
  173. /**
  174. * Stops and removes the Spinner.
  175. */
  176. stop: function() {
  177. var el = this.el
  178. if (el) {
  179. clearTimeout(this.timeout)
  180. if (el.parentNode) el.parentNode.removeChild(el)
  181. this.el = undefined
  182. }
  183. return this
  184. },
  185. /**
  186. * Internal method that draws the individual lines. Will be overwritten
  187. * in VML fallback mode below.
  188. */
  189. lines: function(el, o) {
  190. var i = 0
  191. , start = (o.lines - 1) * (1 - o.direction) / 2
  192. , seg
  193. function fill(color, shadow) {
  194. return css(createEl(), {
  195. position: 'absolute',
  196. width: (o.length+o.width) + 'px',
  197. height: o.width + 'px',
  198. background: color,
  199. boxShadow: shadow,
  200. transformOrigin: 'left',
  201. transform: 'rotate(' + ~~(360/o.lines*i+o.rotate) + 'deg) translate(' + o.radius+'px' +',0)',
  202. borderRadius: (o.corners * o.width>>1) + 'px'
  203. })
  204. }
  205. for (; i < o.lines; i++) {
  206. seg = css(createEl(), {
  207. position: 'absolute',
  208. top: 1+~(o.width/2) + 'px',
  209. transform: o.hwaccel ? 'translate3d(0,0,0)' : '',
  210. opacity: o.opacity,
  211. animation: useCssAnimations && addAnimation(o.opacity, o.trail, start + i * o.direction, o.lines) + ' ' + 1/o.speed + 's linear infinite'
  212. })
  213. if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'}))
  214. ins(el, ins(seg, fill(getColor(o.color, i), '0 0 1px rgba(0,0,0,.1)')))
  215. }
  216. return el
  217. },
  218. /**
  219. * Internal method that adjusts the opacity of a single line.
  220. * Will be overwritten in VML fallback mode below.
  221. */
  222. opacity: function(el, i, val) {
  223. if (i < el.childNodes.length) el.childNodes[i].style.opacity = val
  224. }
  225. })
  226. function initVML() {
  227. /* Utility function to create a VML tag */
  228. function vml(tag, attr) {
  229. return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr)
  230. }
  231. // No CSS transforms but VML support, add a CSS rule for VML elements:
  232. sheet.addRule('.spin-vml', 'behavior:url(#default#VML)')
  233. Spinner.prototype.lines = function(el, o) {
  234. var r = o.length+o.width
  235. , s = 2*r
  236. function grp() {
  237. return css(
  238. vml('group', {
  239. coordsize: s + ' ' + s,
  240. coordorigin: -r + ' ' + -r
  241. }),
  242. { width: s, height: s }
  243. )
  244. }
  245. var margin = -(o.width+o.length)*2 + 'px'
  246. , g = css(grp(), {position: 'absolute', top: margin, left: margin})
  247. , i
  248. function seg(i, dx, filter) {
  249. ins(g,
  250. ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}),
  251. ins(css(vml('roundrect', {arcsize: o.corners}), {
  252. width: r,
  253. height: o.width,
  254. left: o.radius,
  255. top: -o.width>>1,
  256. filter: filter
  257. }),
  258. vml('fill', {color: getColor(o.color, i), opacity: o.opacity}),
  259. vml('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change
  260. )
  261. )
  262. )
  263. }
  264. if (o.shadow)
  265. for (i = 1; i <= o.lines; i++)
  266. seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)')
  267. for (i = 1; i <= o.lines; i++) seg(i)
  268. return ins(el, g)
  269. }
  270. Spinner.prototype.opacity = function(el, i, val, o) {
  271. var c = el.firstChild
  272. o = o.shadow && o.lines || 0
  273. if (c && i+o < c.childNodes.length) {
  274. c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild
  275. if (c) c.opacity = val
  276. }
  277. }
  278. }
  279. var probe = css(createEl('group'), {behavior: 'url(#default#VML)'})
  280. if (!vendor(probe, 'transform') && probe.adj) initVML()
  281. else useCssAnimations = vendor(probe, 'animation')
  282. return Spinner
  283. }));