OLD | NEW |
(Empty) | |
| 1 <link rel="import" href="../polymer/polymer.html"> |
| 2 |
| 3 <polymer-element name="polymer-scrub" attributes="animation target scale scrubTy
pe wrap nudgeTime snapPoints snapThreshold startSnap"> |
| 4 <script> |
| 5 /** |
| 6 * TODO: |
| 7 * - add easing to snap |
| 8 * - test delay |
| 9 * - multiple iterations for input animation |
| 10 * - test CustomEffect |
| 11 */ |
| 12 (function() { |
| 13 var clamp = function(inValue, inMin, inMax) { |
| 14 return Math.max(inMin, Math.min(inValue, inMax))
; |
| 15 } |
| 16 |
| 17 Polymer('polymer-scrub', { |
| 18 target: null, |
| 19 animation: null, |
| 20 scale: 1, |
| 21 scrubType: 'x', |
| 22 nudgeTime: 1e-6, |
| 23 snapPoints: null, |
| 24 startSnap: 0, |
| 25 snapThreshold: 0, |
| 26 wrap: false, |
| 27 observe: { |
| 28 target: 'makeScrub', |
| 29 animation: 'makeScrub', |
| 30 scale: 'makeScrub', |
| 31 scrubType: 'makeScrub', |
| 32 nudgeTime: 'makeScrub', |
| 33 snapPoints: 'makeScrub', |
| 34 startSnap: 'makeScrub', |
| 35 snapThreshold: 'makeScrub', |
| 36 wrap: 'makeScrub' |
| 37 }, |
| 38 get node() { |
| 39 return this._node; |
| 40 }, |
| 41 set node(inNode) { |
| 42 if (this.node) { |
| 43 this.enableEvents(this.node, thi
s.scrubEvents, false); |
| 44 this.node.removeAttribute('touch
-action'); |
| 45 } |
| 46 this._node = inNode; |
| 47 // TODO(sorvell): remove when pointer-ev
ents does the right thing. |
| 48 var html = document.querySelector('html'
); |
| 49 if (!html.hasAttribute('touch-action'))
{ |
| 50 html.setAttribute('touch-action'
, 'none'); |
| 51 } |
| 52 this.node.setAttribute('touch-action', '
none'); |
| 53 if (this.node) { |
| 54 this.scrubEvents = [ |
| 55 {type: 'click', handler:
this.click.bind(this)} |
| 56 ]; |
| 57 var moreEvents = []; |
| 58 if (this.scrubType == 'zoom') { |
| 59 this.scrubProp = 'zoom'; |
| 60 moreEvents = [ |
| 61 {type: 'tracksta
rt', handler: this.trackStart.bind(this)}, |
| 62 {type: 'track',
handler: this.track.bind(this)}, |
| 63 {type: 'trackend
', handler: this.trackEnd.bind(this)} |
| 64 ]; |
| 65 } else { |
| 66 this.directionProp = thi
s.scrubType + 'Direction'; |
| 67 this.scrubProp = 'd' + t
his.scrubType; |
| 68 // TODO(sorvell): includ
e flick. |
| 69 moreEvents = [ |
| 70 {type: 'tracksta
rt', handler: this.trackStart.bind(this)}, |
| 71 {type: 'track',
handler: this.track.bind(this)}, |
| 72 {type: 'trackend
', handler: this.trackEnd.bind(this)} |
| 73 ]; |
| 74 } |
| 75 this.scrubEvents = this.scrubEve
nts.concat(moreEvents); |
| 76 this.enableEvents(this.node, thi
s.scrubEvents, true); |
| 77 } |
| 78 }, |
| 79 destroy: function() { |
| 80 if (this.node) { |
| 81 this.enableEvents(this.node, thi
s.scrubEvents, false); |
| 82 this.node.removeAttribute('touch
-action'); |
| 83 } |
| 84 }, |
| 85 enableEvents: function(inNode, inEventInfos, inE
nable) { |
| 86 var m = inEnable ? 'addEventListener' :
'removeEventListener'; |
| 87 inEventInfos.forEach(function(info) { |
| 88 inNode[m](info.type, info.handle
r); |
| 89 }) |
| 90 }, |
| 91 calcDirection: function(e) { |
| 92 // note: gesture events don't yet provid
e direction |
| 93 if (this.scrubType == 'zoom') { |
| 94 var d = e.zoom == 1 ? 0 : (e.zoo
m > 1 ? -1 : 1); |
| 95 if (this.scrubbing) { |
| 96 if (!this.scrubbing.dire
ction) { |
| 97 this.scrubbing.d
irection = d; |
| 98 } |
| 99 return this.scrubbing.di
rection; |
| 100 } else { |
| 101 return d; |
| 102 } |
| 103 } else { |
| 104 return e[this.directionProp]; |
| 105 } |
| 106 }, |
| 107 calcTimeForEvent: function(e) { |
| 108 var dt = e[this.scrubProp] / this.scrubb
ing.timeScalar; |
| 109 if (this.scrubType == 'zoom') { |
| 110 var z = (e.zoom - 1) * 0.5; |
| 111 dt = Math.max(-1, Math.min(1, z)
); |
| 112 } |
| 113 return this.scrubbing.startTime + dt; |
| 114 }, |
| 115 calcSnapPoints: function(inFraction) { |
| 116 var snapPoints = [0]; |
| 117 snapPoints = snapPoints.concat(this.snap
Points || []); |
| 118 snapPoints.push(1); |
| 119 var sign = inFraction >= 0 ? 1 : -1; |
| 120 var p = Math.abs(inFraction), l; |
| 121 //console.log('snap', inFraction); |
| 122 for (var i=0, l=snapPoints.length, prev=
0, snap, s, e; i<l; i++) { |
| 123 snap = snapPoints[i]; |
| 124 if (prev <= p && snap > p) { |
| 125 s = sign * prev; |
| 126 e = sign * snap; |
| 127 return {start: Math.min(
s, e), end: Math.max(s, e)}; |
| 128 } |
| 129 prev = snap; |
| 130 } |
| 131 return {start: snapPoints[0], end: snapP
oints[1]}; |
| 132 }, |
| 133 canScrub: function(e) { |
| 134 return !this.scrubbingStopped && this.ca
lcDirection(e); |
| 135 }, |
| 136 maybeScrub: function(e) { |
| 137 if (this.canScrub(e)) { |
| 138 this.beginScrub(e); |
| 139 } |
| 140 }, |
| 141 beginScrub: function(e) { |
| 142 this.scrubEvent = e; |
| 143 var d = this.calcDirection(e); |
| 144 this.forward = d < 0; |
| 145 // invoke(this, "onStartScrub", this); |
| 146 var length = this.node[this.scrubType ==
'y' ? |
| 147 'offsetHeight' : 'offset
Width']; |
| 148 if (!this.scrubbingStopped) { |
| 149 this.scrubAnimation(); |
| 150 this.scrubbing = { |
| 151 startTime: this.currentT
ime, |
| 152 timeScalar: length / (th
is.duration * this.scale) |
| 153 } |
| 154 } |
| 155 }, |
| 156 trackStart: function(e) { |
| 157 this.resetScrub(); |
| 158 this.maybeScrub(e); |
| 159 }, |
| 160 track: function(e) { |
| 161 if (this.scrubbing) { |
| 162 this.currentTime = this.calcTime
ForEvent(e); |
| 163 } else { |
| 164 this.maybeScrub(e); |
| 165 } |
| 166 }, |
| 167 trackEnd: function(e) { |
| 168 if (this.scrubbing) { |
| 169 this.playToSnap(this.calcDirecti
on(e)); |
| 170 this.resetScrub(); |
| 171 } |
| 172 this.squelchNextClick(); |
| 173 }, |
| 174 click: function(e) { |
| 175 if (this.scrubClick) { |
| 176 if (!this.squelchClick) { |
| 177 this.scrubAnimation(); |
| 178 this.forward = 1; |
| 179 this.scrubEvent = e; |
| 180 // invoke(this, "onStart
Scrub", this); |
| 181 if (!this.scrubbingStopp
ed) { |
| 182 this.playTo(3); |
| 183 } |
| 184 this.resetScrub(); |
| 185 } |
| 186 e.preventDefault(); |
| 187 } |
| 188 }, |
| 189 squelchNextClick: function() { |
| 190 this.squelchClick = true; |
| 191 setTimeout(function() { |
| 192 this.squelchClick = false; |
| 193 }.bind(this), 0); |
| 194 }, |
| 195 stopScrubbing: function() { |
| 196 this.scrubbingStopped = true; |
| 197 this.scrubbing = null; |
| 198 }, |
| 199 resetScrub: function() { |
| 200 this.scrubbing = null; |
| 201 this.scrubEvent = null; |
| 202 this.scrubbingStopped = false; |
| 203 }, |
| 204 canAnimate: function(inDirection) { |
| 205 return this.wrap || (inDirection < 0 &&
this.currentTime != 0) || |
| 206 (inDirection > 0 && this.current
Time < this.maxTime); |
| 207 }, |
| 208 playToSnap: function(inDirection) { |
| 209 this.scrubAnimation(); |
| 210 if (this.canAnimate(inDirection)) { |
| 211 var currentFraction = this.curre
ntTime / this.duration; |
| 212 var snaps = this.calcSnapPoints(
currentFraction); |
| 213 var reversing = (inDirection*thi
s.scale < 0); |
| 214 var snap = reversing ? snaps.sta
rt : snaps.end; |
| 215 if (this.snapThreshold) { |
| 216 var threshold = this.sna
pThreshold * (snaps.end - snaps.start); |
| 217 if (reversing) { |
| 218 snap = (snaps.en
d - currentFraction < threshold) ? snaps.end : snaps.start; |
| 219 } else { |
| 220 snap = (currentF
raction - snaps.start < threshold) ? snaps.start : snaps.end; |
| 221 } |
| 222 } |
| 223 this.playTo(snap * this.duration
); |
| 224 } |
| 225 }, |
| 226 snapNext: function() { |
| 227 this.scrubAnimation(); |
| 228 this.currentTime += this.nudgeTime; |
| 229 this.playToSnap(this.scale); |
| 230 }, |
| 231 snapPrevious: function() { |
| 232 this.scrubAnimation(); |
| 233 this.currentTime -= this.nudgeTime; |
| 234 this.playToSnap(-this.scale); |
| 235 }, |
| 236 playTo: function(inTime, inDuration) { |
| 237 this.scrubAnimation(); |
| 238 var rate = inDuration ? Math.abs(inTime
- t) / inDuration : 1; |
| 239 var duration = this.animation.specified.
duration; |
| 240 var t = this.currentTime; |
| 241 var reversing = (inTime < t); |
| 242 if (reversing) { |
| 243 this.segment = new SeqGroup([ |
| 244 new SeqGroup([this.anima
tion], {direction: 'reverse'}) |
| 245 ], { |
| 246 duration: (duration - in
Time), |
| 247 playbackRate: rate, |
| 248 delay: -(duration - t) |
| 249 }); |
| 250 } else { |
| 251 this.segment = new SeqGroup([thi
s.animation], { |
| 252 duration: inTime, |
| 253 playbackRate: rate, |
| 254 delay: -t |
| 255 }); |
| 256 } |
| 257 this.segment.onend = function() { |
| 258 this.fire('polymer-scrub-animati
on-end'); |
| 259 }.bind(this); |
| 260 document.timeline.play(this.segment); |
| 261 }, |
| 262 scrubAnimation: function() { |
| 263 if (this.animation.parent) { |
| 264 var reversing = this.animation.p
arent.specified.direction === 'reverse'; |
| 265 var segmentDuration = reversing
? |
| 266 this.animation.specified
.duration - this.segment.specified.duration : |
| 267 this.segment.specified.d
uration; |
| 268 // remove animation from segment |
| 269 this.animation.remove(); |
| 270 this.segment.parent && this.segm
ent.remove(); |
| 271 // reset the animation to the la
st segment duration |
| 272 document.timeline.play(this.anim
ation).paused = true; |
| 273 this.currentTime = segmentDurati
on; |
| 274 } |
| 275 }, |
| 276 /* |
| 277 addAnimToSegment: function(inDirection, inSnaps)
{ |
| 278 var reversing = (inDirection*this.scale
< 0); |
| 279 if (reversing != this.segment._reversing
) { |
| 280 this.segment.reverse(); |
| 281 // install proper easing |
| 282 this.segment.timing.timingFuncti
on = reversing ? 'ease-in' : 'ease-out'; |
| 283 } |
| 284 var start = inSnaps.start * this.duratio
n, end = inSnaps.end * this.duration; |
| 285 this.segment.timing.duration = reversing
? this.currentTime - start : |
| 286 end - this.currentTime; |
| 287 // TODO(sorvell): delay calculation for
reversing must be understood |
| 288 this.animation.timing.delay = reversing
? -start : -this.currentTime; |
| 289 this.segment.currentTime = 0; |
| 290 // TODO(sorvell): bug: must play here to
ensure the global tick keeps going |
| 291 //console.log('segment duration', this.s
egment.timing.duration, 'animation delay', this.animation.timing.delay); |
| 292 document.timeline.play(this.animation); |
| 293 this.segment.add(this.animation); |
| 294 }, |
| 295 */ |
| 296 set currentTime(inTime) { |
| 297 var t = inTime; |
| 298 if (this.wrap) { |
| 299 t = t % this.duration; |
| 300 if (t < 0) { |
| 301 t = this.duration + t; |
| 302 } |
| 303 } else { |
| 304 t = clamp(t, 0, this.maxTime); |
| 305 } |
| 306 this.animation.player.currentTime = t; |
| 307 }, |
| 308 get currentTime() { |
| 309 // TODO(sorvell): currentTime can drift(
?) so make sure not to == duration |
| 310 // or we'll pop over to next iteration. |
| 311 return this.wrap ? this.animation.player
.currentTime % this.duration : |
| 312 clamp(this.animation.player.curr
entTime, 0, this.maxTime); |
| 313 }, |
| 314 get duration() { |
| 315 return this.animation.duration; |
| 316 }, |
| 317 get maxTime() { |
| 318 return this.duration - (this.wrap ? 0 :
this.nudgeTime); |
| 319 }, |
| 320 makeScrub: function() { |
| 321 this.destroy(); |
| 322 if (this.target && this.animation) { |
| 323 // fallback to scrubbing x if zo
oming not available. |
| 324 if (this.scrubType) { |
| 325 if (!('ontouchstart' in
window) && (this.scrubType == 'zoom')) { |
| 326 this.scrubType =
'x'; |
| 327 } |
| 328 } |
| 329 if (!this.snapPoints) { |
| 330 this.snapPoints = []; |
| 331 } |
| 332 if (this.animation) { |
| 333 // create a player and p
ause the animation |
| 334 document.timeline.play(t
his.animation).paused = true; |
| 335 if (this.startSnap) { |
| 336 this.currentTime
= this.startSnap * this.duration; |
| 337 } |
| 338 } else { |
| 339 console.warn('No animati
on set for scrub'); |
| 340 } |
| 341 this.node = this.target; |
| 342 } |
| 343 } |
| 344 }); |
| 345 })(); |
| 346 </script> |
| 347 </polymer-element> |
OLD | NEW |