Index: pkg/polymer/lib/elements/polymer-scrub/polymer-scrub.html |
diff --git a/pkg/polymer/lib/elements/polymer-scrub/polymer-scrub.html b/pkg/polymer/lib/elements/polymer-scrub/polymer-scrub.html |
new file mode 100644 |
index 0000000000000000000000000000000000000000..572623040f6475fb5479eb1f5f470c9bb98ed47e |
--- /dev/null |
+++ b/pkg/polymer/lib/elements/polymer-scrub/polymer-scrub.html |
@@ -0,0 +1,347 @@ |
+<link rel="import" href="../polymer/polymer.html"> |
+ |
+<polymer-element name="polymer-scrub" attributes="animation target scale scrubType wrap nudgeTime snapPoints snapThreshold startSnap"> |
+ <script> |
+ /** |
+ * TODO: |
+ * - add easing to snap |
+ * - test delay |
+ * - multiple iterations for input animation |
+ * - test CustomEffect |
+ */ |
+ (function() { |
+ var clamp = function(inValue, inMin, inMax) { |
+ return Math.max(inMin, Math.min(inValue, inMax)); |
+ } |
+ |
+ Polymer('polymer-scrub', { |
+ target: null, |
+ animation: null, |
+ scale: 1, |
+ scrubType: 'x', |
+ nudgeTime: 1e-6, |
+ snapPoints: null, |
+ startSnap: 0, |
+ snapThreshold: 0, |
+ wrap: false, |
+ observe: { |
+ target: 'makeScrub', |
+ animation: 'makeScrub', |
+ scale: 'makeScrub', |
+ scrubType: 'makeScrub', |
+ nudgeTime: 'makeScrub', |
+ snapPoints: 'makeScrub', |
+ startSnap: 'makeScrub', |
+ snapThreshold: 'makeScrub', |
+ wrap: 'makeScrub' |
+ }, |
+ get node() { |
+ return this._node; |
+ }, |
+ set node(inNode) { |
+ if (this.node) { |
+ this.enableEvents(this.node, this.scrubEvents, false); |
+ this.node.removeAttribute('touch-action'); |
+ } |
+ this._node = inNode; |
+ // TODO(sorvell): remove when pointer-events does the right thing. |
+ var html = document.querySelector('html'); |
+ if (!html.hasAttribute('touch-action')) { |
+ html.setAttribute('touch-action', 'none'); |
+ } |
+ this.node.setAttribute('touch-action', 'none'); |
+ if (this.node) { |
+ this.scrubEvents = [ |
+ {type: 'click', handler: this.click.bind(this)} |
+ ]; |
+ var moreEvents = []; |
+ if (this.scrubType == 'zoom') { |
+ this.scrubProp = 'zoom'; |
+ moreEvents = [ |
+ {type: 'trackstart', handler: this.trackStart.bind(this)}, |
+ {type: 'track', handler: this.track.bind(this)}, |
+ {type: 'trackend', handler: this.trackEnd.bind(this)} |
+ ]; |
+ } else { |
+ this.directionProp = this.scrubType + 'Direction'; |
+ this.scrubProp = 'd' + this.scrubType; |
+ // TODO(sorvell): include flick. |
+ moreEvents = [ |
+ {type: 'trackstart', handler: this.trackStart.bind(this)}, |
+ {type: 'track', handler: this.track.bind(this)}, |
+ {type: 'trackend', handler: this.trackEnd.bind(this)} |
+ ]; |
+ } |
+ this.scrubEvents = this.scrubEvents.concat(moreEvents); |
+ this.enableEvents(this.node, this.scrubEvents, true); |
+ } |
+ }, |
+ destroy: function() { |
+ if (this.node) { |
+ this.enableEvents(this.node, this.scrubEvents, false); |
+ this.node.removeAttribute('touch-action'); |
+ } |
+ }, |
+ enableEvents: function(inNode, inEventInfos, inEnable) { |
+ var m = inEnable ? 'addEventListener' : 'removeEventListener'; |
+ inEventInfos.forEach(function(info) { |
+ inNode[m](info.type, info.handler); |
+ }) |
+ }, |
+ calcDirection: function(e) { |
+ // note: gesture events don't yet provide direction |
+ if (this.scrubType == 'zoom') { |
+ var d = e.zoom == 1 ? 0 : (e.zoom > 1 ? -1 : 1); |
+ if (this.scrubbing) { |
+ if (!this.scrubbing.direction) { |
+ this.scrubbing.direction = d; |
+ } |
+ return this.scrubbing.direction; |
+ } else { |
+ return d; |
+ } |
+ } else { |
+ return e[this.directionProp]; |
+ } |
+ }, |
+ calcTimeForEvent: function(e) { |
+ var dt = e[this.scrubProp] / this.scrubbing.timeScalar; |
+ if (this.scrubType == 'zoom') { |
+ var z = (e.zoom - 1) * 0.5; |
+ dt = Math.max(-1, Math.min(1, z)); |
+ } |
+ return this.scrubbing.startTime + dt; |
+ }, |
+ calcSnapPoints: function(inFraction) { |
+ var snapPoints = [0]; |
+ snapPoints = snapPoints.concat(this.snapPoints || []); |
+ snapPoints.push(1); |
+ var sign = inFraction >= 0 ? 1 : -1; |
+ var p = Math.abs(inFraction), l; |
+ //console.log('snap', inFraction); |
+ for (var i=0, l=snapPoints.length, prev=0, snap, s, e; i<l; i++) { |
+ snap = snapPoints[i]; |
+ if (prev <= p && snap > p) { |
+ s = sign * prev; |
+ e = sign * snap; |
+ return {start: Math.min(s, e), end: Math.max(s, e)}; |
+ } |
+ prev = snap; |
+ } |
+ return {start: snapPoints[0], end: snapPoints[1]}; |
+ }, |
+ canScrub: function(e) { |
+ return !this.scrubbingStopped && this.calcDirection(e); |
+ }, |
+ maybeScrub: function(e) { |
+ if (this.canScrub(e)) { |
+ this.beginScrub(e); |
+ } |
+ }, |
+ beginScrub: function(e) { |
+ this.scrubEvent = e; |
+ var d = this.calcDirection(e); |
+ this.forward = d < 0; |
+ // invoke(this, "onStartScrub", this); |
+ var length = this.node[this.scrubType == 'y' ? |
+ 'offsetHeight' : 'offsetWidth']; |
+ if (!this.scrubbingStopped) { |
+ this.scrubAnimation(); |
+ this.scrubbing = { |
+ startTime: this.currentTime, |
+ timeScalar: length / (this.duration * this.scale) |
+ } |
+ } |
+ }, |
+ trackStart: function(e) { |
+ this.resetScrub(); |
+ this.maybeScrub(e); |
+ }, |
+ track: function(e) { |
+ if (this.scrubbing) { |
+ this.currentTime = this.calcTimeForEvent(e); |
+ } else { |
+ this.maybeScrub(e); |
+ } |
+ }, |
+ trackEnd: function(e) { |
+ if (this.scrubbing) { |
+ this.playToSnap(this.calcDirection(e)); |
+ this.resetScrub(); |
+ } |
+ this.squelchNextClick(); |
+ }, |
+ click: function(e) { |
+ if (this.scrubClick) { |
+ if (!this.squelchClick) { |
+ this.scrubAnimation(); |
+ this.forward = 1; |
+ this.scrubEvent = e; |
+ // invoke(this, "onStartScrub", this); |
+ if (!this.scrubbingStopped) { |
+ this.playTo(3); |
+ } |
+ this.resetScrub(); |
+ } |
+ e.preventDefault(); |
+ } |
+ }, |
+ squelchNextClick: function() { |
+ this.squelchClick = true; |
+ setTimeout(function() { |
+ this.squelchClick = false; |
+ }.bind(this), 0); |
+ }, |
+ stopScrubbing: function() { |
+ this.scrubbingStopped = true; |
+ this.scrubbing = null; |
+ }, |
+ resetScrub: function() { |
+ this.scrubbing = null; |
+ this.scrubEvent = null; |
+ this.scrubbingStopped = false; |
+ }, |
+ canAnimate: function(inDirection) { |
+ return this.wrap || (inDirection < 0 && this.currentTime != 0) || |
+ (inDirection > 0 && this.currentTime < this.maxTime); |
+ }, |
+ playToSnap: function(inDirection) { |
+ this.scrubAnimation(); |
+ if (this.canAnimate(inDirection)) { |
+ var currentFraction = this.currentTime / this.duration; |
+ var snaps = this.calcSnapPoints(currentFraction); |
+ var reversing = (inDirection*this.scale < 0); |
+ var snap = reversing ? snaps.start : snaps.end; |
+ if (this.snapThreshold) { |
+ var threshold = this.snapThreshold * (snaps.end - snaps.start); |
+ if (reversing) { |
+ snap = (snaps.end - currentFraction < threshold) ? snaps.end : snaps.start; |
+ } else { |
+ snap = (currentFraction - snaps.start < threshold) ? snaps.start : snaps.end; |
+ } |
+ } |
+ this.playTo(snap * this.duration); |
+ } |
+ }, |
+ snapNext: function() { |
+ this.scrubAnimation(); |
+ this.currentTime += this.nudgeTime; |
+ this.playToSnap(this.scale); |
+ }, |
+ snapPrevious: function() { |
+ this.scrubAnimation(); |
+ this.currentTime -= this.nudgeTime; |
+ this.playToSnap(-this.scale); |
+ }, |
+ playTo: function(inTime, inDuration) { |
+ this.scrubAnimation(); |
+ var rate = inDuration ? Math.abs(inTime - t) / inDuration : 1; |
+ var duration = this.animation.specified.duration; |
+ var t = this.currentTime; |
+ var reversing = (inTime < t); |
+ if (reversing) { |
+ this.segment = new SeqGroup([ |
+ new SeqGroup([this.animation], {direction: 'reverse'}) |
+ ], { |
+ duration: (duration - inTime), |
+ playbackRate: rate, |
+ delay: -(duration - t) |
+ }); |
+ } else { |
+ this.segment = new SeqGroup([this.animation], { |
+ duration: inTime, |
+ playbackRate: rate, |
+ delay: -t |
+ }); |
+ } |
+ this.segment.onend = function() { |
+ this.fire('polymer-scrub-animation-end'); |
+ }.bind(this); |
+ document.timeline.play(this.segment); |
+ }, |
+ scrubAnimation: function() { |
+ if (this.animation.parent) { |
+ var reversing = this.animation.parent.specified.direction === 'reverse'; |
+ var segmentDuration = reversing ? |
+ this.animation.specified.duration - this.segment.specified.duration : |
+ this.segment.specified.duration; |
+ // remove animation from segment |
+ this.animation.remove(); |
+ this.segment.parent && this.segment.remove(); |
+ // reset the animation to the last segment duration |
+ document.timeline.play(this.animation).paused = true; |
+ this.currentTime = segmentDuration; |
+ } |
+ }, |
+ /* |
+ addAnimToSegment: function(inDirection, inSnaps) { |
+ var reversing = (inDirection*this.scale < 0); |
+ if (reversing != this.segment._reversing) { |
+ this.segment.reverse(); |
+ // install proper easing |
+ this.segment.timing.timingFunction = reversing ? 'ease-in' : 'ease-out'; |
+ } |
+ var start = inSnaps.start * this.duration, end = inSnaps.end * this.duration; |
+ this.segment.timing.duration = reversing ? this.currentTime - start : |
+ end - this.currentTime; |
+ // TODO(sorvell): delay calculation for reversing must be understood |
+ this.animation.timing.delay = reversing ? -start : -this.currentTime; |
+ this.segment.currentTime = 0; |
+ // TODO(sorvell): bug: must play here to ensure the global tick keeps going |
+ //console.log('segment duration', this.segment.timing.duration, 'animation delay', this.animation.timing.delay); |
+ document.timeline.play(this.animation); |
+ this.segment.add(this.animation); |
+ }, |
+ */ |
+ set currentTime(inTime) { |
+ var t = inTime; |
+ if (this.wrap) { |
+ t = t % this.duration; |
+ if (t < 0) { |
+ t = this.duration + t; |
+ } |
+ } else { |
+ t = clamp(t, 0, this.maxTime); |
+ } |
+ this.animation.player.currentTime = t; |
+ }, |
+ get currentTime() { |
+ // TODO(sorvell): currentTime can drift(?) so make sure not to == duration |
+ // or we'll pop over to next iteration. |
+ return this.wrap ? this.animation.player.currentTime % this.duration : |
+ clamp(this.animation.player.currentTime, 0, this.maxTime); |
+ }, |
+ get duration() { |
+ return this.animation.duration; |
+ }, |
+ get maxTime() { |
+ return this.duration - (this.wrap ? 0 : this.nudgeTime); |
+ }, |
+ makeScrub: function() { |
+ this.destroy(); |
+ if (this.target && this.animation) { |
+ // fallback to scrubbing x if zooming not available. |
+ if (this.scrubType) { |
+ if (!('ontouchstart' in window) && (this.scrubType == 'zoom')) { |
+ this.scrubType = 'x'; |
+ } |
+ } |
+ if (!this.snapPoints) { |
+ this.snapPoints = []; |
+ } |
+ if (this.animation) { |
+ // create a player and pause the animation |
+ document.timeline.play(this.animation).paused = true; |
+ if (this.startSnap) { |
+ this.currentTime = this.startSnap * this.duration; |
+ } |
+ } else { |
+ console.warn('No animation set for scrub'); |
+ } |
+ this.node = this.target; |
+ } |
+ } |
+ }); |
+ })(); |
+ </script> |
+</polymer-element> |