Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check backface visibility when the parents of the target element has css style transform-style: preserve-3d. #5916

Merged
merged 11 commits into from
Jan 7, 2020
224 changes: 181 additions & 43 deletions packages/driver/src/dom/visibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,7 @@ const isHidden = (el, name = 'isHidden()') => {
// when an element is scaled to 0 in one axis
// it is not visible to users.
// So, it is hidden.
if (elIsHiddenByTransform($el)) {
return true
}

if (elIsBackface($el)) {
if (elIsHiddenByTransform($el) !== 'visible') {
return true
}

Expand Down Expand Up @@ -123,52 +119,196 @@ const elHasVisibilityHidden = ($el) => {
return $el.css('visibility') === 'hidden'
}

const numberRegex = /-?[0-9]+(?:\.[0-9]+)?/g
// This is a simplified version of backface culling.
const elIsHiddenByTransform = ($el) => {
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
const list = extractTransformInfoFromElements($el)

if (existsInvisibleBackface(list)) {
return elIsBackface(list) ? 'backface' : 'visible'
}

return elIsTransformedToZero(list) ? 'transformed' : 'visible'
}

const extractTransformInfoFromElements = ($el, list = []) => {
list.push(extractTransformInfo($el))

const $parent = $el.parent()

if (!$parent.length || $document.isDocument($parent)) {
return list
}

return extractTransformInfoFromElements($parent, list)
}

const extractTransformInfo = ($el) => {
const el = $el[0]
const style = getComputedStyle(el)

return {
el,
backfaceVisibility: style.getPropertyValue('backface-visibility'),
transformStyle: style.getPropertyValue('transform-style'),
transform: style.getPropertyValue('transform'),
}
}

const existsInvisibleBackface = (list) => {
return !!_.find(list, { backfaceVisibility: 'hidden' })
}

const numberRegex = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/g
const defaultNormal = [0, 0, 1]
// This function uses a simplified version of backface culling.
// https://en.wikipedia.org/wiki/Back-face_culling
//
// We defined view normal vector, (0, 0, -1), - eye to screen.
// and default normal vector, (0, 0, 1)
// We defined view vector, (0, 0, -1), - eye to screen.
// and default normal vector of an element, (0, 0, 1)
// When dot product of them are >= 0, item is visible.
const elIsBackface = ($el) => {
const el = $el[0]
const style = getComputedStyle(el)
const backface = style.getPropertyValue('backface-visibility')
const backfaceInvisible = backface === 'hidden'
const transform = style.getPropertyValue('transform')
const elIsBackface = (list) => {
const nextPreserve3d = (i) => {
return i + 1 < list.length &&
list[i + 1].transformStyle === 'preserve-3d'
}

if (!backfaceInvisible || !transform.startsWith('matrix3d')) {
return false
let i = 0

const finalNormal = (startIndex) => {
i = startIndex
let normal = findNormal(list[i].transform)

while (nextPreserve3d(i)) {
i++
normal = findNormal(list[i].transform, normal)
}

return normal
}

const m3d = transform.substring(8).match(numberRegex)
const defaultNormal = [0, 0, -1]
const elNormal = findNormal(m3d)
const skipToNextFlat = () => {
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
while (nextPreserve3d(i)) {
i++
}

i++
}

//
if (1 < list.length & list[1].transformStyle === 'preserve-3d') {
if (list[0].backfaceVisibility === 'hidden') {
let normal = finalNormal(0)

if (checkBackface(normal)) {
return true
}

i++
} else {
if (list[1].backfaceVisibility === 'visible') {
const { width, height } = list[0].el.getBoundingClientRect()

if (width === 0 || height === 0) {
return true
}

skipToNextFlat()
} else {
if (list[0].transform !== 'none') {
skipToNextFlat()
} else {
i++

let normal = finalNormal(i)

if (checkBackface(normal)) {
return true
}

i++
}
}
}
} else {
for (; i < list.length; i++) {
if (i > 0 && list[i].transformStyle === 'preserve-3d') {
continue
}

if (list[i].backfaceVisibility === 'hidden' && list[i].transform.startsWith('matrix3d')) {
let normal = findNormal(list[i].transform)

if (checkBackface(normal)) {
return true
}
}
}
}

return false
}

const checkBackface = (normal) => {
const viewVector = [0, 0, -1]

// Simplified dot product.
// [0] and [1] are always 0
const dot = defaultNormal[2] * elNormal[2]
// viewVector[0] and viewVector[1] are always 0. So, they're ignored.
let dot = viewVector[2] * normal[2]

// Because of the floating point number rounding error,
// cos(90deg) isn't 0. It's 6.12323e-17.
// And it sometimes causes errors when dot product value is something like -6.12323e-17.
// So, we're setting the dot product result to 0 when its absolute value is less than 1e-10(10^-10).
if (Math.abs(dot) < 1e-10) {
dot = 0
}

return dot >= 0
}

const findNormal = (m) => {
const length = Math.sqrt(+m[8] * +m[8] + +m[9] * +m[9] + +m[10] * +m[10])
const findNormal = (matrix, normal = defaultNormal) => {
if (matrix === 'none') {
return normal
}

let m

if (matrix.startsWith('matrix3d')) {
m = matrix.substring(8).match(numberRegex)
} else {
m = toMatrix3d(matrix.match(numberRegex))
}

const v = normal // alias for shorter formula
const computedNormal = [
m[0] * v[0] + m[4] * v[1] + m[8] * v[2],
m[1] * v[0] + m[5] * v[1] + m[9] * v[2],
m[2] * v[0] + m[6] * v[1] + m[10] * v[2],
]

return [+m[8] / length, +m[9] / length, +m[10] / length]
return toUnitVector(computedNormal)
}

const elHasVisibilityCollapse = ($el) => {
return $el.css('visibility') === 'collapse'
const toMatrix3d = (m2d) => {
return [
m2d[0], m2d[1], 0, 0,
m2d[2], m2d[3], 0, 0,
0, 0, 1, 0,
m2d[4], m2d[5], 0, 1,
]
}

// This function checks 2 things that can happen: scale and rotate
const elIsHiddenByTransform = ($el) => {
// We need to see the final calculation of the element.
const el = $el[0]
const toUnitVector = (v) => {
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])

const style = window.getComputedStyle(el)
const transform = style.getPropertyValue('transform')
return [v[0] / length, v[1] / length, v[2] / length]
}

// This function checks 2 things that can happen: scale and rotate to 0 in width or height.
const elIsTransformedToZero = (list) => {
return !!_.find(list, (info) => isTransformedToZero(info))
}

const isTransformedToZero = ({ transform, el }) => {
// Test scaleZ(0)
// width or height of getBoundingClientRect aren't 0 when scaleZ(0).
// But it is invisible.
Expand Down Expand Up @@ -203,6 +343,10 @@ const elIsHiddenByTransform = ($el) => {
return false
}

const elHasVisibilityCollapse = ($el) => {
return $el.css('visibility') === 'collapse'
}

const elHasDisplayNone = ($el) => {
return $el.css('display') === 'none'
}
Expand Down Expand Up @@ -359,14 +503,6 @@ const elIsHiddenByAncestors = function ($el, $origEl = $el) {
return !elDescendentsHavePositionFixedOrAbsolute($parent, $origEl)
}

if (elIsHiddenByTransform($parent)) {
return true
}

if (elIsBackface($parent)) {
return true
}

// continue to recursively walk up the chain until we reach body or html
return elIsHiddenByAncestors($parent, $origEl)
}
Expand Down Expand Up @@ -483,11 +619,13 @@ const getReasonIsHidden = function ($el) {
return `This element '${node}' is not visible because it has an effective width and height of: '${width} x ${height}' pixels.`
}

if (elIsHiddenByTransform($el)) {
const transformResult = elIsHiddenByTransform($el)

if (transformResult === 'transformed') {
return `This element '${node}' is not visible because it is hidden by transform.`
}

if (elIsBackface($el)) {
if (transformResult === 'backface') {
return `This element '${node}' is not visible because it is rotated and its backface is hidden.`
}

Expand Down
Loading