forked from GoogleChrome/lighthouse
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
core(tap-targets): helper functions for working with ClientRects (Goo…
- Loading branch information
1 parent
302c04e
commit a7e8ffb
Showing
5 changed files
with
696 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
/** | ||
* @license Copyright 2018 Google Inc. All Rights Reserved. | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
*/ | ||
'use strict'; | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rect | ||
* @param {{x:number, y:number}} point | ||
*/ | ||
// We sometimes run this as a part of a gatherer script injected into the page, so prevent | ||
// renaming the function for code coverage. | ||
/* istanbul ignore next */ | ||
function rectContainsPoint(rect, {x, y}) { | ||
return rect.left <= x && rect.right >= x && rect.top <= y && rect.bottom >= y; | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rect1 | ||
* @param {LH.Artifacts.Rect} rect2 | ||
*/ | ||
// We sometimes run this as a part of a gatherer script injected into the page, so prevent | ||
// renaming the function for code coverage. | ||
/* istanbul ignore next */ | ||
function rectContains(rect1, rect2) { | ||
return ( | ||
// top left corner | ||
rectContainsPoint(rect1, { | ||
x: rect2.left, | ||
y: rect2.top, | ||
}) && | ||
// top right corner | ||
rectContainsPoint(rect1, { | ||
x: rect2.right, | ||
y: rect2.top, | ||
}) && | ||
// bottom left corner | ||
rectContainsPoint(rect1, { | ||
x: rect2.left, | ||
y: rect2.bottom, | ||
}) && | ||
// bottom right corner | ||
rectContainsPoint(rect1, { | ||
x: rect2.right, | ||
y: rect2.bottom, | ||
}) | ||
); | ||
} | ||
|
||
|
||
const rectContainsString = ` | ||
${rectContainsPoint.toString()} | ||
${rectContains.toString()}; | ||
`; | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect[]} rects | ||
* @returns {LH.Artifacts.Rect[]} | ||
*/ | ||
function filterOutTinyRects(rects) { | ||
return rects.filter( | ||
rect => rect.width > 1 && rect.height > 1 | ||
); | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect[]} rects | ||
* @returns {LH.Artifacts.Rect[]} | ||
*/ | ||
function filterOutRectsContainedByOthers(rects) { | ||
const rectsToKeep = new Set(rects); | ||
|
||
for (const rect of rects) { | ||
for (const possiblyContainingRect of rects) { | ||
if (rect === possiblyContainingRect) continue; | ||
if (!rectsToKeep.has(possiblyContainingRect)) continue; | ||
if (rectContains(possiblyContainingRect, rect)) { | ||
rectsToKeep.delete(rect); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
return Array.from(rectsToKeep); | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rect | ||
*/ | ||
function getRectCenterPoint(rect) { | ||
return { | ||
x: rect.left + rect.width / 2, | ||
y: rect.top + rect.height / 2, | ||
}; | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rectA | ||
* @param {LH.Artifacts.Rect} rectB | ||
* @returns {boolean} | ||
*/ | ||
function rectsTouchOrOverlap(rectA, rectB) { | ||
// https://stackoverflow.com/questions/2752349/fast-rectangle-to-rectangle-intersection | ||
return ( | ||
rectA.left <= rectB.right && | ||
rectB.left <= rectA.right && | ||
rectA.top <= rectB.bottom && | ||
rectB.top <= rectA.bottom | ||
); | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rectA | ||
* @param {LH.Artifacts.Rect} rectB | ||
*/ | ||
function getBoundingRect(rectA, rectB) { | ||
const left = Math.min(rectA.left, rectB.left); | ||
const right = Math.max(rectA.right, rectB.right); | ||
const top = Math.min(rectA.top, rectB.top); | ||
const bottom = Math.max(rectA.bottom, rectB.bottom); | ||
return addRectWidthAndHeight({ | ||
left, | ||
right, | ||
top, | ||
bottom, | ||
}); | ||
} | ||
|
||
/** | ||
* @param {{left:number, top:number, right:number, bottom: number}} rect | ||
* @return {LH.Artifacts.Rect} | ||
*/ | ||
function addRectWidthAndHeight({left, top, right, bottom}) { | ||
return { | ||
left, | ||
top, | ||
right, | ||
bottom, | ||
width: right - left, | ||
height: bottom - top, | ||
}; | ||
} | ||
|
||
/** | ||
* @param {{x:number, y:number, width:number, height: number}} rect | ||
* @return {LH.Artifacts.Rect} | ||
*/ | ||
function addRectTopAndBottom({x, y, width, height}) { | ||
return { | ||
left: x, | ||
top: y, | ||
right: x + width, | ||
bottom: y + height, | ||
width, | ||
height, | ||
}; | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rect1 | ||
* @param {LH.Artifacts.Rect} rect2 | ||
*/ | ||
function getRectXOverlap(rect1, rect2) { | ||
// https://stackoverflow.com/a/9325084/1290545 | ||
return Math.max( | ||
0, | ||
Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left) | ||
); | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rect1 | ||
* @param {LH.Artifacts.Rect} rect2 | ||
*/ | ||
function getRectYOverlap(rect1, rect2) { | ||
// https://stackoverflow.com/a/9325084/1290545 | ||
return Math.max( | ||
0, | ||
Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top) | ||
); | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rect1 | ||
* @param {LH.Artifacts.Rect} rect2 | ||
*/ | ||
function getRectOverlapArea(rect1, rect2) { | ||
return getRectXOverlap(rect1, rect2) * getRectYOverlap(rect1, rect2); | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rect | ||
* @param {number} centerRectSize | ||
*/ | ||
function getRectAtCenter(rect, centerRectSize) { | ||
return addRectWidthAndHeight({ | ||
left: rect.left + rect.width / 2 - centerRectSize / 2, | ||
top: rect.top + rect.height / 2 - centerRectSize / 2, | ||
right: rect.right - rect.width / 2 + centerRectSize / 2, | ||
bottom: rect.bottom - rect.height / 2 + centerRectSize / 2, | ||
}); | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect} rect | ||
*/ | ||
function getRectArea(rect) { | ||
return rect.width * rect.height; | ||
} | ||
|
||
/** | ||
* @param {LH.Artifacts.Rect[]} rects | ||
*/ | ||
function getLargestRect(rects) { | ||
let largestRect = rects[0]; | ||
for (const rect of rects) { | ||
if (getRectArea(rect) > getRectArea(largestRect)) { | ||
largestRect = rect; | ||
} | ||
} | ||
return largestRect; | ||
} | ||
|
||
/** | ||
* | ||
* @param {LH.Artifacts.Rect[]} rectListA | ||
* @param {LH.Artifacts.Rect[]} rectListB | ||
*/ | ||
function allRectsContainedWithinEachOther(rectListA, rectListB) { | ||
for (const rectA of rectListA) { | ||
for (const rectB of rectListB) { | ||
if (!rectContains(rectA, rectB) && !rectContains(rectB, rectA)) { | ||
return false; | ||
} | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
module.exports = { | ||
rectContainsPoint, | ||
rectContains, | ||
rectContainsString, | ||
addRectWidthAndHeight, | ||
addRectTopAndBottom, | ||
getRectXOverlap, | ||
getRectYOverlap, | ||
getRectOverlapArea, | ||
getRectAtCenter, | ||
getLargestRect, | ||
getRectCenterPoint, | ||
getBoundingRect, | ||
rectsTouchOrOverlap, | ||
allRectsContainedWithinEachOther, | ||
filterOutRectsContainedByOthers, | ||
filterOutTinyRects, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/** | ||
* @license Copyright 2018 Google Inc. All Rights Reserved. | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
*/ | ||
'use strict'; | ||
|
||
const { | ||
filterOutRectsContainedByOthers, | ||
filterOutTinyRects, | ||
rectsTouchOrOverlap, | ||
rectContainsPoint, | ||
getBoundingRect, | ||
getRectCenterPoint, | ||
} = require('./rect-helpers'); | ||
|
||
/** | ||
* Merge client rects together and remove small ones. This may result in a larger overall | ||
* size than that of the individual client rects. | ||
* We use this to simulate a finger tap on those targets later on. | ||
* @param {LH.Artifacts.Rect[]} clientRects | ||
*/ | ||
function getTappableRectsFromClientRects(clientRects) { | ||
// 1x1px rect shouldn't be reason to treat the rect as something the user should tap on. | ||
// Often they're made invisble in some obscure way anyway, and only exist for e.g. accessibiliity. | ||
clientRects = filterOutTinyRects(clientRects); | ||
clientRects = filterOutRectsContainedByOthers(clientRects); | ||
clientRects = mergeTouchingClientRects(clientRects); | ||
return clientRects; | ||
} | ||
|
||
/** | ||
* @param {number} a | ||
* @param {number} b | ||
*/ | ||
function almostEqual(a, b) { | ||
// Sometimes a child will reach out of the parent by | ||
// 1px or 2px, so be somewhat tolerant for merging | ||
return Math.abs(a - b) <= 2; | ||
} | ||
|
||
/** | ||
* Merge touching rects based on what appears as one tappable area to the user. | ||
* @param {LH.Artifacts.Rect[]} clientRects | ||
* @returns {LH.Artifacts.Rect[]} | ||
*/ | ||
function mergeTouchingClientRects(clientRects) { | ||
for (let i = 0; i < clientRects.length; i++) { | ||
for (let j = i + 1; j < clientRects.length; j++) { | ||
const crA = clientRects[i]; | ||
const crB = clientRects[j]; | ||
|
||
/** | ||
* We try to determine whether the rects appear as a single tappable | ||
* area to the user, so that they'd tap in the middle of the merged rect. | ||
* Examples of what we want to merge: | ||
* | ||
* AAABBB | ||
* | ||
* AAA | ||
* AAA | ||
* BBBBB | ||
*/ | ||
const rectsLineUpHorizontally = | ||
almostEqual(crA.top, crB.top) || almostEqual(crA.bottom, crB.bottom); | ||
const rectsLineUpVertically = | ||
almostEqual(crA.left, crB.left) || almostEqual(crA.right, crB.right); | ||
const canMerge = | ||
rectsTouchOrOverlap(crA, crB) && | ||
(rectsLineUpHorizontally || rectsLineUpVertically); | ||
|
||
if (canMerge) { | ||
const replacementClientRect = getBoundingRect(crA, crB); | ||
const mergedRectCenter = getRectCenterPoint(replacementClientRect); | ||
|
||
if ( | ||
!( | ||
rectContainsPoint(crA, mergedRectCenter) || | ||
rectContainsPoint(crB, mergedRectCenter) | ||
) | ||
) { | ||
// Don't merge because the new shape is too different from the | ||
// merged rects, and tapping in the middle wouldn't actually hit | ||
// either rect | ||
continue; | ||
} | ||
|
||
// Replace client rects with merged version | ||
clientRects = clientRects.filter(cr => cr !== crA && cr !== crB); | ||
clientRects.push(replacementClientRect); | ||
|
||
// Start over so we don't have to handle complexity introduced by array mutation. | ||
// Client rect ararys rarely contain more than 5 rects, so starting again doesn't cause perf issues. | ||
return mergeTouchingClientRects(clientRects); | ||
} | ||
} | ||
} | ||
|
||
return clientRects; | ||
} | ||
|
||
module.exports = { | ||
getTappableRectsFromClientRects, | ||
}; |
Oops, something went wrong.