| /* |
| gpuBenchmarking finishes a gesture by sending a completion callback to the |
| renderer after the final input event. When the renderer receives the callback |
| it requests a new frame. This should flush all input through the system - and |
| DOM events should synchronously run here - and produce a compositor frame. |
| The callback is resolved when the frame is presented to the screen. |
| For methods in this file, the callback is the resolve method of the Promise |
| returned. |
| |
| Example: |
| await mouseMoveTo(10,10); |
| The await returns after the mousemove event fired. |
| |
| Note: |
| Given the event handler runs synchronous code, the await returns after |
| the event handler finished running. |
| */ |
| |
| function waitForCompositorCommit() { |
| return new Promise((resolve) => { |
| if (window.testRunner) { |
| // After doing the composite, we also allow any async tasks to run before |
| // resolving, via setTimeout(). |
| testRunner.updateAllLifecyclePhasesAndCompositeThen(() => { |
| window.setTimeout(resolve, 0); |
| }); |
| } else { |
| // Fall back to just rAF twice. |
| window.requestAnimationFrame(() => { |
| window.requestAnimationFrame(resolve); |
| }); |
| } |
| }); |
| } |
| |
| // Returns a promise that resolves when the given condition is met or rejects |
| // after 200 animation frames. |
| function waitFor(condition, error_message = 'Reaches the maximum frames.') { |
| const MAX_FRAME = 200; |
| return new Promise((resolve, reject) => { |
| function tick(frames) { |
| // We requestAnimationFrame either for 200 frames or until condition is |
| // met. |
| if (frames >= MAX_FRAME) |
| reject(error_message); |
| else if (condition()) |
| resolve(); |
| else |
| requestAnimationFrame(tick.bind(this, frames + 1)); |
| } |
| tick(0); |
| }); |
| } |
| |
| // Returns a promise that only gets resolved when the condition is met. |
| function waitUntil(condition) { |
| return new Promise((resolve, reject) => { |
| function tick() { |
| if (condition()) |
| resolve(); |
| else |
| requestAnimationFrame(tick.bind(this)); |
| } |
| tick(); |
| }); |
| } |
| |
| // Returns a promise that resolves when the given condition holds for 10 |
| // animation frames or rejects if the condition changes to false within 10 |
| // animation frames. |
| function conditionHolds(condition, error_message = 'Condition is not true anymore.') { |
| const MAX_FRAME = 10; |
| return new Promise((resolve, reject) => { |
| function tick(frames) { |
| // We requestAnimationFrame either for 10 frames or until condition is |
| // violated. |
| if (frames >= MAX_FRAME) |
| resolve(); |
| else if (!condition()) |
| reject(error_message); |
| else |
| requestAnimationFrame(tick.bind(this, frames + 1)); |
| } |
| tick(0); |
| }); |
| } |
| |
| // TODO: Frames are animated every 1ms for testing. It may be better to have the |
| // timeout based on time rather than frame count. |
| function waitForAnimationEnd(getValue, max_frame, max_unchanged_frame) { |
| const MAX_FRAME = max_frame; |
| const MAX_UNCHANGED_FRAME = max_unchanged_frame; |
| var last_changed_frame = 0; |
| var last_position = getValue(); |
| return new Promise((resolve, reject) => { |
| function tick(frames) { |
| // We requestAnimationFrame either for MAX_FRAME or until |
| // MAX_UNCHANGED_FRAME with no change have been observed. |
| if (frames >= MAX_FRAME || frames - last_changed_frame > MAX_UNCHANGED_FRAME) { |
| resolve(); |
| } else { |
| current_value = getValue(); |
| if (last_position != current_value) { |
| last_changed_frame = frames; |
| last_position = current_value; |
| } |
| requestAnimationFrame(tick.bind(this, frames + 1)); |
| } |
| } |
| tick(0); |
| }) |
| } |
| |
| // TODO(bokan): Replace uses of the above with this one. |
| function waitForAnimationEndTimeBased(getValue) { |
| // Give up if the animation still isn't done after this many milliseconds. |
| const TIMEOUT_MS = 1000; |
| |
| // If the value is unchanged for this many milliseconds, we consider the |
| // animation ended and return. |
| const END_THRESHOLD_MS = 200; |
| |
| const START_TIME = performance.now(); |
| |
| let last_changed_time = START_TIME; |
| let last_value = getValue(); |
| return new Promise((resolve, reject) => { |
| function tick() { |
| let cur_time = performance.now(); |
| |
| if (cur_time - last_changed_time > END_THRESHOLD_MS) { |
| resolve(); |
| return; |
| } |
| |
| if (cur_time - START_TIME > TIMEOUT_MS) { |
| reject(new Error("Timeout waiting for animation to end")); |
| return; |
| } |
| |
| let current_value = getValue(); |
| if (last_value != current_value) { |
| last_changed_time = cur_time; |
| last_value = current_value; |
| } |
| |
| requestAnimationFrame(tick); |
| } |
| tick(); |
| }) |
| } |
| |
| function waitForScrollEvent(eventTarget) { |
| return new Promise((resolve, reject) => { |
| const scrollListener = () => { |
| eventTarget.removeEventListener('scroll', scrollListener); |
| resolve(); |
| }; |
| eventTarget.addEventListener('scroll', scrollListener); |
| }); |
| } |
| |
| // Event driven scroll promise. This method has the advantage over timing |
| // methods, as it is more forgiving to delays in event dispatch or between |
| // chained smooth scrolls. It has an additional advantage of completing sooner |
| // once the end condition is reached. |
| // The promise is resolved when the result of calling getValue matches the |
| // target value. The timeout timer starts once the first event has been |
| // received. |
| function waitForScrollEnd(eventTarget, getValue, targetValue) { |
| // Give up if the animation still isn't done after this many milliseconds from |
| // the time of the first scroll event. |
| const TIMEOUT_MS = 1000; |
| |
| return new Promise((resolve, reject) => { |
| let timeout = undefined; |
| const scrollListener = () => { |
| if (!timeout) |
| timeout = setTimeout(reject, TIMEOUT_MS); |
| |
| if (getValue() == targetValue) { |
| clearTimeout(timeout); |
| eventTarget.removeEventListener('scroll', scrollListener); |
| resolve(); |
| } |
| }; |
| if (getValue() == targetValue) |
| resolve(); |
| else |
| eventTarget.addEventListener('scroll', scrollListener); |
| }); |
| } |
| |
| // Enums for gesture_source_type parameters in gpuBenchmarking synthetic |
| // gesture methods. Must match C++ side enums in synthetic_gesture_params.h |
| const GestureSourceType = (function() { |
| var isDefined = (window.chrome && chrome.gpuBenchmarking); |
| return { |
| DEFAULT_INPUT: isDefined && chrome.gpuBenchmarking.DEFAULT_INPUT, |
| TOUCH_INPUT: isDefined && chrome.gpuBenchmarking.TOUCH_INPUT, |
| MOUSE_INPUT: isDefined && chrome.gpuBenchmarking.MOUSE_INPUT, |
| TOUCHPAD_INPUT: isDefined && chrome.gpuBenchmarking.TOUCHPAD_INPUT, |
| PEN_INPUT: isDefined && chrome.gpuBenchmarking.PEN_INPUT, |
| ToString: function(value) { |
| if (!isDefined) |
| return 'Synthetic gestures unavailable'; |
| switch (value) { |
| case chrome.gpuBenchmarking.DEFAULT_INPUT: |
| return 'DefaultInput'; |
| case chrome.gpuBenchmarking.TOUCH_INPUT: |
| return 'Touchscreen'; |
| case chrome.gpuBenchmarking.MOUSE_INPUT: |
| return 'MouseWheel/Touchpad'; |
| case chrome.gpuBenchmarking.PEN_INPUT: |
| return 'Pen'; |
| default: |
| return 'Invalid'; |
| } |
| } |
| } |
| })(); |
| |
| // Enums used as input to the |modifier_keys| parameters of methods in this |
| // file like smoothScrollWithXY and wheelTick. |
| const Modifiers = (function() { |
| return { |
| ALT: "Alt", |
| CONTROL: "Control", |
| META: "Meta", |
| SHIFT: "Shift", |
| CAPSLOCK: "CapsLock", |
| NUMLOCK: "NumLock", |
| ALTGRAPH: "AltGraph", |
| } |
| })(); |
| |
| // Enums used as input to the |modifier_buttons| parameters of methods in this |
| // file like smoothScrollWithXY and wheelTick. |
| const Buttons = (function() { |
| return { |
| LEFT: "Left", |
| MIDDLE: "Middle", |
| RIGHT: "Right", |
| BACK: "Back", |
| FORWARD: "Forward", |
| } |
| })(); |
| |
| // Use this for speed to make gestures (effectively) instant. That is, finish |
| // entirely within one Begin|Update|End triplet. This is in physical |
| // pixels/second. |
| // TODO(bokan): This isn't really instant but high enough that it works for |
| // current purposes. This should be replaced with the Infinity value and |
| // the synthetic gesture code modified to guarantee the single update behavior. |
| // https://crbug.com/893608 |
| const SPEED_INSTANT = 400000; |
| |
| // This will be replaced by smoothScrollWithXY. |
| function smoothScroll(pixels_to_scroll, start_x, start_y, gesture_source_type, |
| direction, speed_in_pixels_s, precise_scrolling_deltas, |
| scroll_by_page, cursor_visible, scroll_by_percentage, |
| modifier_keys) { |
| let pixels_to_scroll_x = 0; |
| let pixels_to_scroll_y = 0; |
| if (direction == "down") { |
| pixels_to_scroll_y = pixels_to_scroll; |
| } else if (direction == "up") { |
| pixels_to_scroll_y = -pixels_to_scroll; |
| } else if (direction == "right") { |
| pixels_to_scroll_x = pixels_to_scroll; |
| } else if (direction == "left") { |
| pixels_to_scroll_x = -pixels_to_scroll; |
| } else if (direction == "upleft") { |
| pixels_to_scroll_x = -pixels_to_scroll; |
| pixels_to_scroll_y = -pixels_to_scroll; |
| } else if (direction == "upright") { |
| pixels_to_scroll_x = pixels_to_scroll; |
| pixels_to_scroll_y = -pixels_to_scroll; |
| } else if (direction == "downleft") { |
| pixels_to_scroll_x = -pixels_to_scroll; |
| pixels_to_scroll_y = pixels_to_scroll; |
| } else if (direction == "downright") { |
| pixels_to_scroll_x = pixels_to_scroll; |
| pixels_to_scroll_y = pixels_to_scroll; |
| } |
| return smoothScrollWithXY(pixels_to_scroll_x, pixels_to_scroll_y, start_x, |
| start_y, gesture_source_type, speed_in_pixels_s, |
| precise_scrolling_deltas, scroll_by_page, |
| cursor_visible, scroll_by_percentage, modifier_keys); |
| } |
| |
| // Perform a percent based scroll using smoothScrollWithXY |
| function percentScroll(percent_to_scroll_x, percent_to_scroll_y, start_x, start_y, gesture_source_type) { |
| return smoothScrollWithXY(percent_to_scroll_x, percent_to_scroll_y, start_x, start_y, |
| gesture_source_type, |
| undefined /* speed_in_pixels_s - not defined for percent based scrolls */, |
| false /* precise_scrolling_deltas */, |
| false /* scroll_by_page */, |
| true /* cursor_visible */, |
| true /* scroll_by_percentage */); |
| } |
| |
| // modifier_keys means the keys pressed while doing the mouse wheel scroll, it |
| // should be one of the values in the |Modifiers| or a comma separated string |
| // to specify multiple values. |
| // modifier_buttons means the mouse buttons pressed while doing the mouse wheel |
| // scroll, it should be one of the values in the |Buttons| or a comma separated |
| // string to specify multiple values. |
| function smoothScrollWithXY(pixels_to_scroll_x, pixels_to_scroll_y, start_x, |
| start_y, gesture_source_type, speed_in_pixels_s, |
| precise_scrolling_deltas, scroll_by_page, |
| cursor_visible, scroll_by_percentage, modifier_keys, |
| modifier_buttons) { |
| return new Promise((resolve, reject) => { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| chrome.gpuBenchmarking.smoothScrollByXY(pixels_to_scroll_x, |
| pixels_to_scroll_y, |
| resolve, |
| start_x, |
| start_y, |
| gesture_source_type, |
| speed_in_pixels_s, |
| precise_scrolling_deltas, |
| scroll_by_page, |
| cursor_visible, |
| scroll_by_percentage, |
| modifier_keys, |
| modifier_buttons); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| // modifier_keys means the keys pressed while doing the mouse wheel scroll, it |
| // should be one of the values in the |Modifiers| or a comma separated string |
| // to specify multiple values. |
| // modifier_buttons means the mouse buttons pressed while doing the mouse wheel |
| // scroll, it should be one of the values in the |Buttons| or a comma separated |
| // string to specify multiple values. |
| function wheelTick(scroll_tick_x, scroll_tick_y, center, speed_in_pixels_s, |
| modifier_keys, modifier_buttons) { |
| if (typeof(speed_in_pixels_s) == "undefined") |
| speed_in_pixels_s = SPEED_INSTANT; |
| // Do not allow precise scrolling deltas for tick wheel scroll. |
| return smoothScrollWithXY(scroll_tick_x * pixelsPerTick(), |
| scroll_tick_y * pixelsPerTick(), |
| center.x, center.y, GestureSourceType.MOUSE_INPUT, |
| speed_in_pixels_s, false /* precise_scrolling_deltas */, |
| false /* scroll_by_page */, true /* cursor_visible */, |
| false /* scroll_by_percentage */, modifier_keys, |
| modifier_buttons); |
| } |
| |
| const LEGACY_MOUSE_WHEEL_TICK_MULTIPLIER = 120; |
| |
| // Returns the number of pixels per wheel tick which is a platform specific value. |
| function pixelsPerTick() { |
| // Comes from ui/events/event.cc |
| if (navigator.platform.indexOf("Win") != -1) |
| return 120; |
| |
| if (navigator.platform.indexOf("Mac") != -1 || navigator.platform.indexOf("iPhone") != -1 || |
| navigator.platform.indexOf("iPod") != -1 || navigator.platform.indexOf("iPad") != -1) { |
| return 40; |
| } |
| |
| // Some android devices return android while others return Android. |
| if (navigator.platform.toLowerCase().indexOf("android") != -1) |
| return 64; |
| |
| // Comes from ui/events/event.cc |
| return 53; |
| } |
| |
| // Note: unlike other functions in this file, the |direction| parameter here is |
| // the "finger direction". This means |y| pixels "up" causes the finger to move |
| // up so the page scrolls down (i.e. scrollTop increases). |
| function swipe(pixels_to_scroll, start_x, start_y, direction, speed_in_pixels_s, fling_velocity, gesture_source_type) { |
| return new Promise((resolve, reject) => { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| chrome.gpuBenchmarking.swipe(direction, |
| pixels_to_scroll, |
| resolve, |
| start_x, |
| start_y, |
| speed_in_pixels_s, |
| fling_velocity, |
| gesture_source_type); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| function pinchBy(scale, centerX, centerY, speed_in_pixels_s, gesture_source_type) { |
| return new Promise((resolve, reject) => { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| chrome.gpuBenchmarking.pinchBy(scale, |
| centerX, |
| centerY, |
| resolve, |
| speed_in_pixels_s, |
| gesture_source_type); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| |
| function mouseMoveTo(xPosition, yPosition) { |
| return new Promise(function(resolve, reject) { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| chrome.gpuBenchmarking.pointerActionSequence([ |
| {source: 'mouse', |
| actions: [ |
| { name: 'pointerMove', x: xPosition, y: yPosition }, |
| ]}], resolve); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| function mouseDownAt(xPosition, yPosition) { |
| return new Promise(function(resolve, reject) { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| chrome.gpuBenchmarking.pointerActionSequence([ |
| {source: 'mouse', |
| actions: [ |
| { name: 'pointerDown', x: xPosition, y: yPosition }, |
| ]}], resolve); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| function mouseUpAt(xPosition, yPosition) { |
| return new Promise(function(resolve, reject) { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| chrome.gpuBenchmarking.pointerActionSequence([ |
| {source: 'mouse', |
| actions: [ |
| { name: 'pointerMove', x: xPosition, y: yPosition }, |
| { name: 'pointerUp' }, |
| ]}], resolve); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| // Simulate a mouse click on point. |
| function mouseClickOn(x, y, button = 0 /* left */, keys = '') { |
| return new Promise((resolve, reject) => { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| let pointerActions = [{ |
| source: 'mouse', |
| actions: [ |
| { 'name': 'pointerMove', 'x': x, 'y': y }, |
| { 'name': 'pointerDown', 'x': x, 'y': y, 'button': button, 'keys': keys }, |
| { 'name': 'pointerUp', 'button': button }, |
| ] |
| }]; |
| chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| // Simulate a mouse double click on point. |
| function mouseDoubleClickOn(x, y, button = 0 /* left */, keys = '') { |
| return new Promise((resolve, reject) => { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| let pointerActions = [{ |
| source: 'mouse', |
| actions: [ |
| { 'name': 'pointerMove', 'x': x, 'y': y }, |
| { 'name': 'pointerDown', 'x': x, 'y': y, 'button': button, 'keys': keys }, |
| { 'name': 'pointerUp', 'button': button }, |
| { 'name': 'pointerDown', 'x': x, 'y': y, 'button': button, 'keys': keys }, |
| { 'name': 'pointerUp', 'button': button }, |
| ] |
| }]; |
| chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| // Simulate a mouse press on point for a certain time. |
| function mousePressOn(x, y, t) { |
| return new Promise((resolve, reject) => { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| let pointerActions = [{ |
| source: 'mouse', |
| actions: [ |
| { 'name': 'pointerMove', 'x': x, 'y': y }, |
| { 'name': 'pointerDown', 'x': x, 'y': y }, |
| { 'name': 'pause', duration: t}, |
| { 'name': 'pointerUp' }, |
| ] |
| }]; |
| chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| // Simulate a mouse drag and drop. mouse down at {start_x, start_y}, move to |
| // {end_x, end_y} and release. |
| function mouseDragAndDrop(start_x, start_y, end_x, end_y, button = 0 /* left */, t = 0) { |
| return new Promise((resolve, reject) => { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| let pointerActions = [{ |
| source: 'mouse', |
| actions: [ |
| { 'name': 'pointerMove', 'x': start_x, 'y': start_y }, |
| { 'name': 'pointerDown', 'x': start_x, 'y': start_y, 'button': button }, |
| { 'name': 'pause', 'duration': t}, |
| { 'name': 'pointerMove', 'x': end_x, 'y': end_y }, |
| { 'name': 'pause', 'duration': t}, |
| { 'name': 'pointerUp', 'button': button }, |
| ] |
| }]; |
| chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve); |
| } else { |
| reject('This test requires chrome.gpuBenchmarking'); |
| } |
| }); |
| } |
| |
| // Helper functions used in some of the gesture scroll layouttests. |
| function recordScroll() { |
| scrollEventsOccurred++; |
| } |
| function notScrolled() { |
| return scrolledElement.scrollTop == 0 && scrolledElement.scrollLeft == 0; |
| } |
| function checkScrollOffset() { |
| // To avoid flakiness up to two pixels off per gesture is allowed. |
| var pixels = 2 * (gesturesOccurred + 1); |
| var result = approx_equals(scrolledElement.scrollTop, scrollAmountY[gesturesOccurred], pixels) && |
| approx_equals(scrolledElement.scrollLeft, scrollAmountX[gesturesOccurred], pixels); |
| if (result) |
| gesturesOccurred++; |
| |
| return result; |
| } |
| |
| // This promise gets resolved in iframe onload. |
| var iframeLoadResolve; |
| iframeOnLoadPromise = new Promise(function(resolve) { |
| iframeLoadResolve = resolve; |
| }); |
| |
| // Include run-after-layout-and-paint.js to use this promise. |
| function WaitForlayoutAndPaint() { |
| return new Promise((resolve, reject) => { |
| if (typeof runAfterLayoutAndPaint !== 'undefined') |
| runAfterLayoutAndPaint(resolve); |
| else |
| reject('This test requires run-after-layout-and-paint.js'); |
| }); |
| } |
| |
| function touchTapOn(xPosition, yPosition) { |
| return new Promise(function(resolve, reject) { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| chrome.gpuBenchmarking.pointerActionSequence( [ |
| {source: 'touch', |
| actions: [ |
| { name: 'pointerDown', x: xPosition, y: yPosition }, |
| { name: 'pointerUp' } |
| ]}], resolve); |
| } else { |
| reject(); |
| } |
| }); |
| } |
| |
| function touchDragTo(drag) { |
| const PREVENT_FLING_PAUSE = 40; |
| return new Promise(function(resolve, reject) { |
| if (window.chrome && chrome.gpuBenchmarking) { |
| chrome.gpuBenchmarking.pointerActionSequence( [ |
| {source: 'touch', |
| actions: [ |
| { name: 'pointerDown', x: drag.start_x, y: drag.start_y }, |
| { name: 'pause', duration: PREVENT_FLING_PAUSE }, |
| { name: 'pointerMove', x: drag.end_x, y: drag.end_y}, |
| { name: 'pause', duration: PREVENT_FLING_PAUSE }, |
| { name: 'pointerUp', x: drag.end_x, y: drag.end_y } |
| ]}], resolve); |
| } else { |
| reject(); |
| } |
| }); |
| } |
| |
| function doubleTapAt(xPosition, yPosition) { |
| // This comes from config constants in gesture_detector.cc. |
| const DOUBLE_TAP_MINIMUM_DURATION_MS = 40; |
| |
| return new Promise(function(resolve, reject) { |
| if (!window.chrome || !chrome.gpuBenchmarking) { |
| reject("chrome.gpuBenchmarking not found."); |
| return; |
| } |
| |
| chrome.gpuBenchmarking.pointerActionSequence( [{ |
| source: 'touch', |
| actions: [ |
| { name: 'pointerDown', x: xPosition, y: yPosition }, |
| { name: 'pointerUp' }, |
| { name: 'pause', duration: DOUBLE_TAP_MINIMUM_DURATION_MS }, |
| { name: 'pointerDown', x: xPosition, y: yPosition }, |
| { name: 'pointerUp' } |
| ] |
| }], resolve); |
| }); |
| } |
| |
| function approx_equals(actual, expected, epsilon) { |
| return actual >= expected - epsilon && actual <= expected + epsilon; |
| } |
| |
| function clientToViewport(client_point) { |
| const viewport_point = { |
| x: (client_point.x - visualViewport.offsetLeft) * visualViewport.scale, |
| y: (client_point.y - visualViewport.offsetTop) * visualViewport.scale |
| }; |
| return viewport_point; |
| } |
| |
| // Returns the center point of the given element's rect in visual viewport |
| // coordinates. Returned object is a point with |x| and |y| properties. |
| function elementCenter(element) { |
| const rect = element.getBoundingClientRect(); |
| const center_point = { |
| x: rect.x + rect.width / 2, |
| y: rect.y + rect.height / 2 |
| }; |
| return clientToViewport(center_point); |
| } |
| |
| // Returns a position in the given element with an offset of |x| and |y| from |
| // the element's top-left position. Returned object is a point with |x| and |y| |
| // properties. The returned coordinate is in visual viewport coordinates. |
| function pointInElement(element, x, y) { |
| const rect = element.getBoundingClientRect(); |
| const point = { |
| x: rect.x + x, |
| y: rect.y + y |
| }; |
| return clientToViewport(point); |
| } |
| |
| // Waits for 'time' ms before resolving the promise. |
| function waitForMs(time) { |
| return new Promise((resolve) => { |
| window.setTimeout(function() { resolve(); }, time); |
| }); |
| } |
| |
| // Requests an animation frame. |
| function raf() { |
| return new Promise((resolve) => { |
| requestAnimationFrame(() => { |
| resolve(); |
| }); |
| }); |
| } |