summaryrefslogtreecommitdiffstats
path: root/components
diff options
context:
space:
mode:
authorwychen <wychen@chromium.org>2015-04-24 18:03:46 -0700
committerCommit bot <commit-bot@chromium.org>2015-04-25 01:03:44 +0000
commit6349c066f2a16007c0fba1684dfe2a18d40a9e20 (patch)
tree2aec3faf322e1aa638229fc4a45bc8c40ece9860 /components
parent73dfc1c5a87b062a1432713dedb64db2c45dca7d (diff)
downloadchromium_src-6349c066f2a16007c0fba1684dfe2a18d40a9e20.zip
chromium_src-6349c066f2a16007c0fba1684dfe2a18d40a9e20.tar.gz
chromium_src-6349c066f2a16007c0fba1684dfe2a18d40a9e20.tar.bz2
Changing font size with pinch gesture in Reader Mode
When users pinch in Reader Mode, the page would zoom in or out as if it is a normal web page allowing user-zoom. At the end of pinch gesture, the page would do text reflow. These pinch-to-zoom and text reflow effects are not native, but are emulated using CSS and JavaScript. In order to achieve near-native zooming and panning frame rate, fake 3D transform is used so that the layer doesn't repaint for each frame. After the text reflow, the web content shown in the viewport should roughly be the same paragraph before zooming. The control point of font size is the html element, so that both "em" and "rem" are adjusted. Accordingly, font size of body is no longer specified in CSS in unit of pixel. Some CSS styles and animations are updated to fix issues specific to resizing. BUG=445632 Review URL: https://codereview.chromium.org/1009703002 Cr-Commit-Position: refs/heads/master@{#326945}
Diffstat (limited to 'components')
-rw-r--r--components/components_browsertests.isolate1
-rw-r--r--components/dom_distiller/content/distiller_page_web_contents_browsertest.cc49
-rw-r--r--components/dom_distiller/core/css/distilledpage.css9
-rw-r--r--components/dom_distiller/core/javascript/dom_distiller_viewer.js206
-rw-r--r--components/test/data/dom_distiller/pinch_tester.html14
-rw-r--r--components/test/data/dom_distiller/pinch_tester.js388
6 files changed, 662 insertions, 5 deletions
diff --git a/components/components_browsertests.isolate b/components/components_browsertests.isolate
index e60cea1..0419dcd 100644
--- a/components/components_browsertests.isolate
+++ b/components/components_browsertests.isolate
@@ -56,6 +56,7 @@
'variables': {
'files': [
'test/data/',
+ 'dom_distiller/core/javascript/',
'../third_party/dom_distiller_js/dist/test/data/',
],
},
diff --git a/components/dom_distiller/content/distiller_page_web_contents_browsertest.cc b/components/dom_distiller/content/distiller_page_web_contents_browsertest.cc
index d1d2dff..0ba7c61 100644
--- a/components/dom_distiller/content/distiller_page_web_contents_browsertest.cc
+++ b/components/dom_distiller/content/distiller_page_web_contents_browsertest.cc
@@ -5,6 +5,7 @@
#include "base/memory/weak_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "components/dom_distiller/content/distiller_page_web_contents.h"
#include "components/dom_distiller/content/web_contents_main_frame_observer.h"
@@ -100,6 +101,11 @@ class DistillerPageWebContentsTest : public ContentBrowserTest {
quit_closure_.Run();
}
+ void OnJsExecutionDone(base::Closure callback, const base::Value* value) {
+ js_result_.reset(value->DeepCopy());
+ callback.Run();
+ }
+
private:
void AddComponentsResources() {
base::FilePath pak_file;
@@ -119,8 +125,10 @@ class DistillerPageWebContentsTest : public ContentBrowserTest {
void SetUpTestServer() {
base::FilePath path;
PathService::Get(base::DIR_SOURCE_ROOT, &path);
- path = path.AppendASCII("components/test/data/dom_distiller");
- embedded_test_server()->ServeFilesFromDirectory(path);
+ embedded_test_server()->ServeFilesFromDirectory(
+ path.AppendASCII("components/test/data/dom_distiller"));
+ embedded_test_server()->ServeFilesFromDirectory(
+ path.AppendASCII("components/dom_distiller/core/javascript"));
ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady());
}
@@ -133,6 +141,7 @@ class DistillerPageWebContentsTest : public ContentBrowserTest {
DistillerPageWebContents* distiller_page_;
base::Closure quit_closure_;
scoped_ptr<proto::DomDistillerResult> distiller_result_;
+ scoped_ptr<base::Value> js_result_;
};
// Use this class to be able to leak the WebContents, which is needed for when
@@ -458,4 +467,40 @@ IN_PROC_BROWSER_TEST_F(DistillerPageWebContentsTest, TestTitleNeverEmpty) {
}
}
+IN_PROC_BROWSER_TEST_F(DistillerPageWebContentsTest,
+ TestPinch) {
+ // Load the test file in content shell and wait until it has fully loaded.
+ content::WebContents* web_contents = shell()->web_contents();
+ dom_distiller::WebContentsMainFrameObserver::CreateForWebContents(
+ web_contents);
+ base::RunLoop url_loaded_runner;
+ WebContentsMainFrameHelper main_frame_loaded(web_contents,
+ url_loaded_runner.QuitClosure(),
+ true);
+ web_contents->GetController().LoadURL(
+ embedded_test_server()->GetURL("/pinch_tester.html"),
+ content::Referrer(),
+ ui::PAGE_TRANSITION_TYPED,
+ std::string());
+ url_loaded_runner.Run();
+
+ // Execute the JS to run the tests, and wait until it has finished.
+ base::RunLoop run_loop;
+ web_contents->GetMainFrame()->ExecuteJavaScript(
+ base::UTF8ToUTF16("(function() {return pinchtest.run();})();"),
+ base::Bind(&DistillerPageWebContentsTest::OnJsExecutionDone,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+
+ // Convert to dictionary and parse the results.
+ const base::DictionaryValue* dict;
+ ASSERT_TRUE(js_result_);
+ ASSERT_TRUE(js_result_->GetAsDictionary(&dict));
+
+ ASSERT_TRUE(dict->HasKey("success"));
+ bool success;
+ ASSERT_TRUE(dict->GetBoolean("success", &success));
+ EXPECT_TRUE(success);
+}
+
} // namespace dom_distiller
diff --git a/components/dom_distiller/core/css/distilledpage.css b/components/dom_distiller/core/css/distilledpage.css
index fde075d..e0eeccf 100644
--- a/components/dom_distiller/core/css/distilledpage.css
+++ b/components/dom_distiller/core/css/distilledpage.css
@@ -73,9 +73,11 @@ th {
/* Base typography. */
-body,
html {
font-size: 14px;
+}
+
+body {
height: 100%;
line-height: 1.4;
text-rendering: optimizeLegibility;
@@ -154,11 +156,11 @@ h6 {
/* Margins for Show Original link. */
#showOriginal {
- margin: auto 1.296rem 1.296rem 1.296rem;
+ margin: auto 1.296rem 1.296rem 5%;
}
#content {
- margin: 0.2rem;
+ margin: 0.2rem 2.2%;
}
/* Main margins. */
@@ -340,6 +342,7 @@ pre {
}
.feedbackContent {
+ font-size: 14px;
background-color: #4285F4;
clear: both;
padding: 14px;
diff --git a/components/dom_distiller/core/javascript/dom_distiller_viewer.js b/components/dom_distiller/core/javascript/dom_distiller_viewer.js
index 78b6749..4122dc7 100644
--- a/components/dom_distiller/core/javascript/dom_distiller_viewer.js
+++ b/components/dom_distiller/core/javascript/dom_distiller_viewer.js
@@ -163,3 +163,209 @@ document.getElementById('feedbackContainer').addEventListener('animationend',
contentWrap.style.paddingBottom = '0px';
}, true);
+document.getElementById('contentWrap').addEventListener('transitionend',
+ function(e) {
+ var contentWrap = document.getElementById('contentWrap');
+ contentWrap.style.transition = '';
+ }, true);
+
+var pincher = (function() {
+ 'use strict';
+ // When users pinch in Reader Mode, the page would zoom in or out as if it
+ // is a normal web page allowing user-zoom. At the end of pinch gesture, the
+ // page would do text reflow. These pinch-to-zoom and text reflow effects
+ // are not native, but are emulated using CSS and JavaScript.
+ //
+ // In order to achieve near-native zooming and panning frame rate, fake 3D
+ // transform is used so that the layer doesn't repaint for each frame.
+ //
+ // After the text reflow, the web content shown in the viewport should
+ // roughly be the same paragraph before zooming.
+ //
+ // The control point of font size is the html element, so that both "em" and
+ // "rem" are adjusted.
+ //
+ // TODO(wychen): Improve scroll position when elementFromPoint is body.
+
+ var pinching = false;
+ var fontSizeAnchor = 1.0;
+
+ var focusElement = null;
+ var focusPos = 0;
+ var initClientMid;
+
+ var clampedScale = 1;
+
+ var lastSpan;
+ var lastClientMid;
+
+ var scale = 1;
+ var shiftX;
+ var shiftY;
+
+ // The zooming speed relative to pinching speed.
+ const FONT_SCALE_MULTIPLIER = 0.5;
+ const MIN_SPAN_LENGTH = 20;
+
+ // The font size is guaranteed to be in px.
+ var baseSize =
+ parseFloat(getComputedStyle(document.documentElement).fontSize);
+
+ var refreshTransform = function() {
+ var slowedScale = Math.exp(Math.log(scale) * FONT_SCALE_MULTIPLIER);
+ clampedScale = Math.max(0.4, Math.min(2.5, fontSizeAnchor * slowedScale));
+
+ // Use "fake" 3D transform so that the layer is not repainted.
+ // With 2D transform, the frame rate would be much lower.
+ document.body.style.transform =
+ 'translate3d(' + shiftX + 'px,' +
+ shiftY + 'px, 0px)' +
+ 'scale(' + clampedScale/fontSizeAnchor + ')';
+ };
+
+ function endPinch() {
+ pinching = false;
+
+ document.body.style.transformOrigin = '';
+ document.body.style.transform = '';
+ document.documentElement.style.fontSize = clampedScale * baseSize + "px";
+
+ var rect = focusElement.getBoundingClientRect();
+ var targetTop = focusPos * (rect.bottom - rect.top) + rect.top +
+ document.body.scrollTop - (initClientMid.y + shiftY);
+ document.body.scrollTop = targetTop;
+ }
+
+ function touchSpan(e) {
+ var count = e.touches.length;
+ var mid = touchClientMid(e);
+ var sum = 0;
+ for (var i = 0; i < count; i++) {
+ var dx = (e.touches[i].clientX - mid.x);
+ var dy = (e.touches[i].clientY - mid.y);
+ sum += Math.hypot(dx, dy);
+ }
+ // Avoid very small span.
+ return Math.max(MIN_SPAN_LENGTH, sum/count);
+ }
+
+ function touchClientMid(e) {
+ var count = e.touches.length;
+ var sumX = 0;
+ var sumY = 0;
+ for (var i = 0; i < count; i++) {
+ sumX += e.touches[i].clientX;
+ sumY += e.touches[i].clientY;
+ }
+ return {x: sumX/count, y: sumY/count};
+ }
+
+ function touchPageMid(e) {
+ var clientMid = touchClientMid(e);
+ return {x: clientMid.x - e.touches[0].clientX + e.touches[0].pageX,
+ y: clientMid.y - e.touches[0].clientY + e.touches[0].pageY};
+ }
+
+ return {
+ handleTouchStart: function(e) {
+ if (e.touches.length < 2) return;
+ e.preventDefault();
+
+ var span = touchSpan(e);
+ var clientMid = touchClientMid(e);
+
+ if (e.touches.length > 2) {
+ lastSpan = span;
+ lastClientMid = clientMid;
+ refreshTransform();
+ return;
+ }
+
+ scale = 1;
+ shiftX = 0;
+ shiftY = 0;
+
+ pinching = true;
+ fontSizeAnchor =
+ parseFloat(getComputedStyle(document.documentElement).fontSize) /
+ baseSize;
+
+ var pinchOrigin = touchPageMid(e);
+ document.body.style.transformOrigin =
+ pinchOrigin.x + 'px ' + pinchOrigin.y + 'px';
+
+ // Try to preserve the pinching center after text reflow.
+ // This is accurate to the HTML element level.
+ focusElement = document.elementFromPoint(clientMid.x, clientMid.y);
+ var rect = focusElement.getBoundingClientRect();
+ initClientMid = clientMid;
+ focusPos = (initClientMid.y - rect.top) / (rect.bottom - rect.top);
+
+ lastSpan = span;
+ lastClientMid = clientMid;
+
+ refreshTransform();
+ },
+
+ handleTouchMove: function(e) {
+ if (!pinching) return;
+ if (e.touches.length < 2) return;
+ e.preventDefault();
+
+ var span = touchSpan(e);
+ var clientMid = touchClientMid(e);
+
+ scale *= touchSpan(e) / lastSpan;
+ shiftX += clientMid.x - lastClientMid.x;
+ shiftY += clientMid.y - lastClientMid.y;
+
+ refreshTransform();
+
+ lastSpan = span;
+ lastClientMid = clientMid;
+ },
+
+ handleTouchEnd: function(e) {
+ if (!pinching) return;
+ e.preventDefault();
+
+ var span = touchSpan(e);
+ var clientMid = touchClientMid(e);
+
+ if (e.touches.length >= 2) {
+ lastSpan = span;
+ lastClientMid = clientMid;
+ refreshTransform();
+ return;
+ }
+
+ endPinch();
+ },
+
+ handleTouchCancel: function(e) {
+ endPinch();
+ },
+
+ reset: function() {
+ scale = 1;
+ shiftX = 0;
+ shiftY = 0;
+ clampedScale = 1;
+ document.documentElement.style.fontSize = clampedScale * baseSize + "px";
+ },
+
+ status: function() {
+ return {
+ scale: scale,
+ clampedScale: clampedScale,
+ shiftX: shiftX,
+ shiftY: shiftY
+ };
+ }
+ };
+}());
+
+window.addEventListener('touchstart', pincher.handleTouchStart, false);
+window.addEventListener('touchmove', pincher.handleTouchMove, false);
+window.addEventListener('touchend', pincher.handleTouchEnd, false);
+window.addEventListener('touchcancel', pincher.handleTouchCancel, false);
diff --git a/components/test/data/dom_distiller/pinch_tester.html b/components/test/data/dom_distiller/pinch_tester.html
new file mode 100644
index 0000000..9dbb48d
--- /dev/null
+++ b/components/test/data/dom_distiller/pinch_tester.html
@@ -0,0 +1,14 @@
+<html>
+<head><title>Test Page for Pinch to Zoom</title></head>
+<body>
+<div id="contentWrap">
+<p id="showOriginal">Original</p>
+</div>
+<div id="feedbackContainer">
+ <div id="feedbackYes"></div>
+ <div id="feedbackNo"></div>
+</div>
+<script src="dom_distiller_viewer.js"></script>
+<script src="pinch_tester.js"></script>
+</body>
+</html>
diff --git a/components/test/data/dom_distiller/pinch_tester.js b/components/test/data/dom_distiller/pinch_tester.js
new file mode 100644
index 0000000..e2c1df5
--- /dev/null
+++ b/components/test/data/dom_distiller/pinch_tester.js
@@ -0,0 +1,388 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+var pinchtest = (function() {
+ 'use strict';
+
+ function assertTrue(condition, message) {
+ if (!condition) {
+ message = message || "Assertion failed";
+ console.trace();
+ throw new Error(message);
+ }
+ }
+
+ function assertClose(a, b, message) {
+ if (Math.abs(a-b) > 1e-5) {
+ message = message || "Assertion failed";
+ console.log('"', a, '" and "', b, '" are not close.');
+ console.trace();
+ throw new Error(message);
+ }
+ }
+
+ function isEquivalent(a, b) {
+ // Create arrays of property names
+ var aProps = Object.getOwnPropertyNames(a);
+ var bProps = Object.getOwnPropertyNames(b);
+
+ // If number of properties is different,
+ // objects are not equivalent
+ if (aProps.length != bProps.length) {
+ return false;
+ }
+
+ for (var i = 0; i < aProps.length; i++) {
+ var propName = aProps[i];
+
+ // If values of same property are not equal,
+ // objects are not equivalent
+ if (a[propName] !== b[propName]) {
+ return false;
+ }
+ }
+
+ // If we made it this far, objects
+ // are considered equivalent
+ return true;
+ }
+
+ function assertEqual(a, b, message) {
+ if (!isEquivalent(a, b)) {
+ message = message || "Assertion failed";
+ console.log('"', a, '" and "', b, '" are not equal');
+ console.trace();
+ throw new Error(message);
+ }
+ }
+
+ var touch = (function() {
+ 'use strict';
+ var points = {};
+ function lowestID() {
+ var ans = -1;
+ for(var key in points) {
+ ans = Math.max(ans, key);
+ }
+ return ans + 1;
+ }
+ function changeTouchPoint (key, x, y, offsetX, offsetY) {
+ var e = {
+ clientX: x,
+ clientY: y,
+ pageX: x,
+ pageY: y
+ };
+ if (typeof(offsetX) === 'number') {
+ e.clientX += offsetX;
+ }
+ if (typeof(offsetY) === 'number') {
+ e.clientY += offsetY;
+ }
+ points[key] = e;
+ }
+ return {
+ addTouchPoint: function(x, y, offsetX, offsetY) {
+ changeTouchPoint(lowestID(), x, y, offsetX, offsetY);
+ },
+ updateTouchPoint: changeTouchPoint,
+ releaseTouchPoint: function(key) {
+ delete points[key];
+ },
+ events: function() {
+ var arr = [];
+ for(var key in points) {
+ arr.push(points[key]);
+ }
+ return {
+ touches: arr,
+ preventDefault: function(){}
+ };
+ }
+ }
+ });
+
+ function testZoomOut() {
+ pincher.reset();
+ var t = new touch();
+
+ // Make sure start event doesn't change state
+ var oldState = pincher.status();
+ t.addTouchPoint(100, 100);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+ t.addTouchPoint(300, 300);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+
+ // Make sure extra move event doesn't change state
+ pincher.handleTouchMove(t.events());
+ assertEqual(oldState, pincher.status());
+
+ t.updateTouchPoint(0, 150, 150);
+ t.updateTouchPoint(1, 250, 250);
+ pincher.handleTouchMove(t.events());
+ assertTrue(pincher.status().clampedScale < 0.9);
+
+ // Make sure end event doesn't change state
+ oldState = pincher.status();
+ t.releaseTouchPoint(1);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+ t.releaseTouchPoint(0);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+ }
+
+ function testZoomIn() {
+ pincher.reset();
+ var t = new touch();
+
+ var oldState = pincher.status();
+ t.addTouchPoint(150, 150);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+ t.addTouchPoint(250, 250);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+
+ t.updateTouchPoint(0, 100, 100);
+ t.updateTouchPoint(1, 300, 300);
+ pincher.handleTouchMove(t.events());
+ assertTrue(pincher.status().clampedScale > 1.1);
+
+ oldState = pincher.status();
+ t.releaseTouchPoint(1);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+ t.releaseTouchPoint(0);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+ }
+
+ function testZoomOutAndPan() {
+ pincher.reset();
+ var t = new touch();
+ t.addTouchPoint(100, 100);
+ pincher.handleTouchStart(t.events());
+ t.addTouchPoint(300, 300);
+ pincher.handleTouchStart(t.events());
+ t.updateTouchPoint(0, 150, 150);
+ t.updateTouchPoint(1, 250, 250);
+ pincher.handleTouchMove(t.events());
+ t.updateTouchPoint(0, 150, 150, 10, -5);
+ t.updateTouchPoint(1, 250, 250, 10, -5);
+ pincher.handleTouchMove(t.events());
+ t.releaseTouchPoint(1);
+ pincher.handleTouchEnd(t.events());
+ t.releaseTouchPoint(0);
+ pincher.handleTouchEnd(t.events());
+
+ assertClose(pincher.status().shiftX, 10);
+ assertClose(pincher.status().shiftY, -5);
+ assertTrue(pincher.status().clampedScale < 0.9);
+ }
+
+ function testReversible() {
+ pincher.reset();
+ var t = new touch();
+ t.addTouchPoint(100, 100);
+ pincher.handleTouchStart(t.events());
+ t.addTouchPoint(300, 300);
+ pincher.handleTouchStart(t.events());
+ t.updateTouchPoint(0, 0, 0);
+ t.updateTouchPoint(1, 400, 400);
+ pincher.handleTouchMove(t.events());
+ t.releaseTouchPoint(1);
+ pincher.handleTouchEnd(t.events());
+ t.releaseTouchPoint(0);
+ pincher.handleTouchEnd(t.events());
+ t.addTouchPoint(0, 0);
+ pincher.handleTouchStart(t.events());
+ t.addTouchPoint(400, 400);
+ pincher.handleTouchStart(t.events());
+ t.updateTouchPoint(0, 100, 100);
+ t.updateTouchPoint(1, 300, 300);
+ pincher.handleTouchMove(t.events());
+ t.releaseTouchPoint(1);
+ pincher.handleTouchEnd(t.events());
+ t.releaseTouchPoint(0);
+ pincher.handleTouchEnd(t.events());
+ assertClose(pincher.status().clampedScale, 1);
+ }
+
+ function testMultitouchZoomOut() {
+ pincher.reset();
+ var t = new touch();
+
+ var oldState = pincher.status();
+ t.addTouchPoint(100, 100);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+ t.addTouchPoint(300, 300);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+ t.addTouchPoint(100, 300);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+ t.addTouchPoint(300, 100);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+
+ // Multi-touch zoom out.
+ t.updateTouchPoint(0, 150, 150);
+ t.updateTouchPoint(1, 250, 250);
+ t.updateTouchPoint(2, 150, 250);
+ t.updateTouchPoint(3, 250, 150);
+ pincher.handleTouchMove(t.events());
+
+ oldState = pincher.status();
+ t.releaseTouchPoint(3);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+ t.releaseTouchPoint(2);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+ t.releaseTouchPoint(1);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+ t.releaseTouchPoint(0);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+
+ assertTrue(pincher.status().clampedScale < 0.9);
+ }
+
+ function testZoomOutThenMulti() {
+ pincher.reset();
+ var t = new touch();
+
+ var oldState = pincher.status();
+ t.addTouchPoint(100, 100);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+ t.addTouchPoint(300, 300);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+
+ // Zoom out.
+ t.updateTouchPoint(0, 150, 150);
+ t.updateTouchPoint(1, 250, 250);
+ pincher.handleTouchMove(t.events());
+ assertTrue(pincher.status().clampedScale < 0.9);
+
+ // Make sure adding and removing more point doesn't change state
+ oldState = pincher.status();
+ t.addTouchPoint(600, 600);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+ t.releaseTouchPoint(2);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+
+ // More than two fingers.
+ t.addTouchPoint(150, 250);
+ pincher.handleTouchStart(t.events());
+ t.addTouchPoint(250, 150);
+ pincher.handleTouchStart(t.events());
+ assertEqual(oldState, pincher.status());
+
+ t.updateTouchPoint(0, 100, 100);
+ t.updateTouchPoint(1, 300, 300);
+ t.updateTouchPoint(2, 100, 300);
+ t.updateTouchPoint(3, 300, 100);
+ pincher.handleTouchMove(t.events());
+ assertClose(pincher.status().scale, 1);
+
+ oldState = pincher.status();
+ t.releaseTouchPoint(3);
+ t.releaseTouchPoint(2);
+ t.releaseTouchPoint(1);
+ t.releaseTouchPoint(0);
+ pincher.handleTouchEnd(t.events());
+ assertEqual(oldState, pincher.status());
+ }
+
+ function testCancel() {
+ pincher.reset();
+ var t = new touch();
+
+ t.addTouchPoint(100, 100);
+ pincher.handleTouchStart(t.events());
+ t.addTouchPoint(300, 300);
+ pincher.handleTouchStart(t.events());
+ t.updateTouchPoint(0, 150, 150);
+ t.updateTouchPoint(1, 250, 250);
+ pincher.handleTouchMove(t.events());
+ assertTrue(pincher.status().clampedScale < 0.9);
+
+ var oldState = pincher.status();
+ t.releaseTouchPoint(1);
+ t.releaseTouchPoint(0);
+ pincher.handleTouchCancel(t.events());
+ assertEqual(oldState, pincher.status());
+
+ t.addTouchPoint(150, 150);
+ pincher.handleTouchStart(t.events());
+ t.addTouchPoint(250, 250);
+ pincher.handleTouchStart(t.events());
+ t.updateTouchPoint(0, 100, 100);
+ t.updateTouchPoint(1, 300, 300);
+ pincher.handleTouchMove(t.events());
+ assertClose(pincher.status().clampedScale, 1);
+ }
+
+ function testSingularity() {
+ pincher.reset();
+ var t = new touch();
+
+ t.addTouchPoint(100, 100);
+ pincher.handleTouchStart(t.events());
+ t.addTouchPoint(100, 100);
+ pincher.handleTouchStart(t.events());
+ t.updateTouchPoint(0, 150, 150);
+ t.updateTouchPoint(1, 50, 50);
+ pincher.handleTouchMove(t.events());
+ assertTrue(pincher.status().clampedScale > 1.1);
+ assertTrue(pincher.status().clampedScale < 100);
+ assertTrue(pincher.status().scale < 100);
+
+ pincher.handleTouchCancel();
+ }
+
+ function testMinSpan() {
+ pincher.reset();
+ var t = new touch();
+
+ t.addTouchPoint(50, 50);
+ pincher.handleTouchStart(t.events());
+ t.addTouchPoint(150, 150);
+ pincher.handleTouchStart(t.events());
+ t.updateTouchPoint(0, 100, 100);
+ t.updateTouchPoint(1, 100, 100);
+ pincher.handleTouchMove(t.events());
+ assertTrue(pincher.status().clampedScale < 0.9);
+ assertTrue(pincher.status().clampedScale > 0);
+ assertTrue(pincher.status().scale > 0);
+
+ pincher.handleTouchCancel();
+ }
+
+ return {
+ run: function(){
+ testZoomOut();
+ testZoomIn();
+ testZoomOutAndPan();
+ testReversible();
+ testMultitouchZoomOut();
+ testZoomOutThenMulti();
+ testCancel();
+ testSingularity();
+ testMinSpan();
+ pincher.reset();
+
+ return {success: true};
+ }
+ };
+}());