1
2
3//
4// INK EQUATIONS
5//
6
7// Animation constants.
8var globalSpeed = 1;
9var waveOpacityDecayVelocity = 0.8 / globalSpeed;  // opacity per second.
10var waveInitialOpacity = 0.25;
11var waveLingerOnTouchUp = 0.2;
12var waveMaxRadius = 150;
13
14// TODOs:
15// - rather than max distance to corner, use hypotenuos(sp) (diag)
16// - use quadratic for the fall off, move fast at the beginning,
17// - on cancel, immediately fade out, reverse the direction
18
19function waveRadiusFn(touchDownMs, touchUpMs, ww, hh) {
20  // Convert from ms to s.
21  var touchDown = touchDownMs / 1000;
22  var touchUp = touchUpMs / 1000;
23  var totalElapsed = touchDown + touchUp;
24  var waveRadius = Math.min(Math.max(ww, hh), waveMaxRadius) * 1.1 + 5;
25  var dduration = 1.1 - .2 * (waveRadius / waveMaxRadius);
26  var tt = (totalElapsed / dduration);
27
28  var ssize = waveRadius * (1 - Math.pow(80, -tt));
29  return Math.abs(ssize);
30}
31
32function waveOpacityFn(td, tu) {
33  // Convert from ms to s.
34  var touchDown = td / 1000;
35  var touchUp = tu / 1000;
36  var totalElapsed = touchDown + touchUp;
37
38  if (tu <= 0) {  // before touch up
39    return waveInitialOpacity;
40  }
41  return Math.max(0, waveInitialOpacity - touchUp * waveOpacityDecayVelocity);
42}
43
44function waveOuterOpacityFn(td, tu) {
45  // Convert from ms to s.
46  var touchDown = td / 1000;
47  var touchUp = tu / 1000;
48
49  // Linear increase in background opacity, capped at the opacity
50  // of the wavefront (waveOpacity).
51  var outerOpacity = touchDown * 0.3;
52  var waveOpacity = waveOpacityFn(td, tu);
53  return Math.max(0, Math.min(outerOpacity, waveOpacity));
54
55}
56
57function waveGravityToCenterPercentageFn(td, tu, r) {
58  // Convert from ms to s.
59  var touchDown = td / 1000;
60  var touchUp = tu / 1000;
61  var totalElapsed = touchDown + touchUp;
62
63  return Math.min(1.0, touchUp * 6);
64}
65
66
67// Determines whether the wave should be completely removed.
68function waveDidFinish(wave, radius) {
69  var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp);
70  // Does not linger any more.
71  // var lingerTimeMs = waveLingerOnTouchUp * 1000;
72
73  // If the wave opacity is 0 and the radius exceeds the bounds
74  // of the element, then this is finished.
75  if (waveOpacity < 0.01 && radius >= wave.maxRadius) {
76    return true;
77  }
78  return false;
79};
80
81//
82// DRAWING
83//
84
85function animateIcon() {
86  var el = document.getElementById('button_toolbar0');
87  el.classList.add('animate');
88  setTimeout(function(){
89    el.classList.remove('animate');
90    el.classList.toggle('selected');
91  }, 500);
92}
93
94
95function drawRipple(canvas, x, y, radius, innerColor, outerColor, innerColorAlpha, outerColorAlpha) {
96  var ctx = canvas.getContext('2d');
97  if (outerColor) {
98    ctx.fillStyle = outerColor;
99    ctx.fillRect(0,0,canvas.width, canvas.height);
100  }
101
102  ctx.beginPath();
103  ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
104  ctx.fillStyle = innerColor;
105  ctx.fill();
106}
107
108function drawLabel(canvas, label, fontSize, color, alignment) {
109  var ctx = canvas.getContext('2d');
110  ctx.font= fontSize + 'px Helvetica';
111
112  var metrics = ctx.measureText(label);
113  var width = metrics.width;
114  var height = metrics.height;
115  ctx.fillStyle = color;
116
117  var xPos = (canvas.width/2 - width)/2;
118
119  if (alignment === 'left') { xPos = 16; }
120
121  ctx.fillText(label, xPos, canvas.height/2 - (canvas.height/2 - fontSize +2) / 2);
122}
123
124//
125// BUTTON SETUP
126//
127
128function createWave(elem) {
129  var elementStyle = window.getComputedStyle(elem);
130  var fgColor = elementStyle.color;
131
132  var wave = {
133    waveColor: fgColor,
134    maxRadius: 0,
135    isMouseDown: false,
136    mouseDownStart: 0.0,
137    mouseUpStart: 0.0,
138    tDown: 0,
139    tUp: 0
140  };
141  return wave;
142}
143
144function removeWaveFromScope(scope, wave) {
145  if (scope.waves) {
146    var pos = scope.waves.indexOf(wave);
147    scope.waves.splice(pos, 1);
148  }
149};
150
151
152function setUpPaperByClass( classname ) {
153  var elems = document.querySelectorAll( classname );
154  [].forEach.call( elems, function( el ) {
155      setUpPaper(el);
156  });
157}
158
159function setUpPaper(elem) {
160  var pixelDensity = 2;
161
162  var elementStyle = window.getComputedStyle(elem);
163  var fgColor = elementStyle.color;
164  var bgColor = elementStyle.backgroundColor;
165  elem.width = elem.clientWidth;
166  elem.setAttribute('width', elem.clientWidth * pixelDensity + "px");
167  elem.setAttribute('height', elem.clientHeight * pixelDensity + "px");
168
169  var isButton = elem.classList.contains( 'button' ) || elem.classList.contains( 'button_floating' ) | elem.classList.contains( 'button_menu' );
170  var isToolbarButton =  elem.classList.contains( 'button_toolbar' );
171
172  elem.getContext('2d').scale(pixelDensity, pixelDensity)
173
174  var scope = {
175    backgroundFill: true,
176    element: elem,
177    label: 'Button',
178    waves: [],
179  };
180
181
182  scope.label = elem.getAttribute('value') || elementStyle.content;
183  scope.labelFontSize = elementStyle.fontSize.split("px")[0];
184
185  drawLabel(elem, scope.label, scope.labelFontSize, fgColor, elem.style.textAlign);
186
187
188  //
189  // RENDER FOR EACH FRAME
190  //
191  var onFrame = function() {
192    var shouldRenderNextFrame = false;
193
194    // Clear the canvas
195    var ctx = elem.getContext('2d');
196    ctx.clearRect(0, 0, elem.width, elem.height);
197
198    var deleteTheseWaves = [];
199    // The oldest wave's touch down duration
200    var longestTouchDownDuration = 0;
201    var longestTouchUpDuration = 0;
202    // Save the last known wave color
203    var lastWaveColor = null;
204
205    for (var i = 0; i < scope.waves.length; i++) {
206      var wave = scope.waves[i];
207
208      if (wave.mouseDownStart > 0) {
209        wave.tDown = now() - wave.mouseDownStart;
210      }
211      if (wave.mouseUpStart > 0) {
212        wave.tUp = now() - wave.mouseUpStart;
213      }
214
215      // Determine how long the touch has been up or down.
216      var tUp = wave.tUp;
217      var tDown = wave.tDown;
218      longestTouchDownDuration = Math.max(longestTouchDownDuration, tDown);
219      longestTouchUpDuration = Math.max(longestTouchUpDuration, tUp);
220
221      // Obtain the instantenous size and alpha of the ripple.
222      var radius = waveRadiusFn(tDown, tUp, elem.width, elem.height);
223      var waveAlpha =  waveOpacityFn(tDown, tUp);
224      var waveColor = cssColorWithAlpha(wave.waveColor, waveAlpha);
225      lastWaveColor = wave.waveColor;
226
227      // Position of the ripple.
228      var x = wave.startPosition.x;
229      var y = wave.startPosition.y;
230
231      // Ripple gravitational pull to the center of the canvas.
232      if (wave.endPosition) {
233
234        var translateFraction = waveGravityToCenterPercentageFn(tDown, tUp, wave.maxRadius);
235
236        // This translates from the origin to the center of the view  based on the max dimension of
237        var translateFraction = Math.min(1, radius / wave.containerSize * 2 / Math.sqrt(2) );
238
239        x += translateFraction * (wave.endPosition.x - wave.startPosition.x);
240        y += translateFraction * (wave.endPosition.y - wave.startPosition.y);
241      }
242
243      // If we do a background fill fade too, work out the correct color.
244      var bgFillColor = null;
245      if (scope.backgroundFill) {
246        var bgFillAlpha = waveOuterOpacityFn(tDown, tUp);
247        bgFillColor = cssColorWithAlpha(wave.waveColor, bgFillAlpha);
248      }
249
250      // Draw the ripple.
251      drawRipple(elem, x, y, radius, waveColor, bgFillColor);
252
253      // Determine whether there is any more rendering to be done.
254      var shouldRenderWaveAgain = !waveDidFinish(wave, radius);
255      shouldRenderNextFrame = shouldRenderNextFrame || shouldRenderWaveAgain;
256      if (!shouldRenderWaveAgain) {
257        deleteTheseWaves.push(wave);
258      }
259   }
260
261    if (shouldRenderNextFrame) {
262      window.requestAnimationFrame(onFrame);
263    }  else {
264      // If there is nothing to draw, clear any drawn waves now because
265      // we're not going to get another requestAnimationFrame any more.
266      var ctx = elem.getContext('2d');
267      ctx.clearRect(0, 0, elem.width, elem.height);
268    }
269
270    // Draw the label at the very last point so it is on top of everything.
271    drawLabel(elem, scope.label, scope.labelFontSize, fgColor, elem.style.textAlign);
272
273    for (var i = 0; i < deleteTheseWaves.length; ++i) {
274      var wave = deleteTheseWaves[i];
275      removeWaveFromScope(scope, wave);
276    }
277  };
278
279  //
280  // MOUSE DOWN HANDLER
281  //
282
283  elem.addEventListener('mousedown', function(e) {
284    var wave = createWave(e.target);
285    var elem = scope.element;
286
287    wave.isMouseDown = true;
288    wave.tDown = 0.0;
289    wave.tUp = 0.0;
290    wave.mouseUpStart = 0.0;
291    wave.mouseDownStart = now();
292
293    var width = e.target.width / 2; // Retina canvas
294    var height = e.target.height / 2;
295    var touchX = e.clientX - e.target.offsetLeft - e.target.offsetParent.offsetLeft;
296    var touchY = e.clientY - e.target.offsetTop - e.target.offsetParent.offsetTop;
297    wave.startPosition = {x:touchX, y:touchY};
298
299    if (elem.classList.contains("recenteringTouch")) {
300      wave.endPosition = {x: width / 2,  y: height / 2};
301      wave.slideDistance = dist(wave.startPosition, wave.endPosition);
302    }
303    wave.containerSize = Math.max(width, height);
304    wave.maxRadius = distanceFromPointToFurthestCorner(wave.startPosition, {w: width, h: height});
305    elem.classList.add("activated");
306    scope.waves.push(wave);
307    window.requestAnimationFrame(onFrame);
308    return false;
309  });
310
311  //
312  // MOUSE UP HANDLER
313  //
314
315  elem.addEventListener('mouseup', function(e) {
316    elem.classList.remove("activated");
317
318    for (var i = 0; i < scope.waves.length; i++) {
319      // Declare the next wave that has mouse down to be mouse'ed up.
320      var wave = scope.waves[i];
321      if (wave.isMouseDown) {
322        wave.isMouseDown = false
323        wave.mouseUpStart = now();
324        wave.mouseDownStart = 0;
325        wave.tUp = 0.0;
326        break;
327      }
328    }
329    return false;
330  });
331
332  elem.addEventListener('mouseout', function(e) {
333  elem.classList.remove("activated");
334
335  for (var i = 0; i < scope.waves.length; i++) {
336    // Declare the next wave that has mouse down to be mouse'ed up.
337    var wave = scope.waves[i];
338    if (wave.isMouseDown) {
339      wave.isMouseDown = false
340      wave.mouseUpStart = now();
341      wave.mouseDownStart = 0;
342      wave.tUp = 0.0;
343      wave.cancelled = true;
344      break;
345    }
346  }
347  return false;
348  });
349
350  return scope;
351};
352
353// Shortcuts.
354var pow = Math.pow;
355var now = function() { return new Date().getTime(); };
356
357// Quad beizer where t is between 0 and 1.
358function quadBezier(t, p0, p1, p2, p3) {
359  return pow(1 - t, 3) * p0 +
360         3 * pow(1 - t, 2) * t * p1 +
361         (1 - t) * pow(t, 2) * p2 +
362         pow(t, 3) * p3;
363}
364
365function easeIn(t) {
366  return quadBezier(t, 0.4, 0.0, 1, 1);
367}
368
369function cssColorWithAlpha(cssColor, alpha) {
370    var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
371    if (typeof alpha == 'undefined') {
372        alpha = 1;
373    }
374    if (!parts) {
375      return 'rgba(255, 255, 255, ' + alpha + ')';
376    }
377    return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')';
378}
379
380function dist(p1, p2) {
381  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
382}
383
384function distanceFromPointToFurthestCorner(point, size) {
385  var tl_d = dist(point, {x: 0, y: 0});
386  var tr_d = dist(point, {x: size.w, y: 0});
387  var bl_d = dist(point, {x: 0, y: size.h});
388  var br_d = dist(point, {x: size.w, y: size.h});
389  return Math.max(Math.max(tl_d, tr_d), Math.max(bl_d, br_d));
390}
391
392
393function toggleDialog() {
394  var el = document.getElementById('dialog');
395  el.classList.toggle("visible");
396}
397
398function toggleMenu() {
399  var el = document.getElementById('menu');
400  el.classList.toggle("visible");
401}
402
403
404// Initialize
405
406function init() {
407    setUpPaperByClass( '.paper' );
408}
409
410window.addEventListener('DOMContentLoaded', init, false);
411