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
3 changes: 2 additions & 1 deletion packages/driver/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"env": {
"browser": true
}
},
"parser": "@typescript-eslint/parser"
}
283 changes: 283 additions & 0 deletions packages/driver/src/dom/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
import _ from 'lodash'
import { isDocument } from './document'

export const detectVisibility = ($el: any) => {
const list = extractTransformInfoFromElements($el)

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

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

type BackfaceVisibility = 'hidden' | 'visible'
type TransformStyle = 'flat' | 'preserve-3d'
type Matrix2D = [
number, number, number,
number, number, number,
]
type Matrix3D = [
number, number, number, number,
number, number, number, number,
number, number, number, number,
number, number, number, number,
]

type Vector3 = [number, number, number]

interface TransformInfo {
backfaceVisibility: BackfaceVisibility
transformStyle: TransformStyle
transform: string
}

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

const $parent = $el.parent()

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

return extractTransformInfoFromElements($parent, list)
}

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

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

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

const numberRegex = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/g
const defaultNormal: Vector3 = [0, 0, 1]
const viewVector: Vector3 = [0, 0, -1]
const identityMatrix3D: Matrix3D = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]

// It became 1e-5 from 1e-10. Because 30deg + 30deg + 30deg is 6.0568e-7 and it caused a false negative.
const TINY_NUMBER = 1e-5

const nextPreserve3d = (i: number, list: TransformInfo[]) => {
return i + 1 < list.length &&
list[i + 1].transformStyle === 'preserve-3d'
}

const finalNormal = (startIndex: number, list: TransformInfo[]) => {
let i = startIndex
let normal = findNormal(parseMatrix3D(list[i].transform))

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

return normal
}

const elIsBackface = (list: TransformInfo[]) => {
// When the direct parent of the target has style, preserve-3d
if (list.length > 1 && list[1].transformStyle === 'preserve-3d') {
// When the target is backface-invisible a2-1-1 ~ a2-1-4
if (list[0].backfaceVisibility === 'hidden') {
let normal = finalNormal(0, list)

if (checkBackface(normal)) {
return true
}
} else {
// When the direct parent of the target is backface-invisible
if (list[1].backfaceVisibility === 'hidden') {
// If it is not none, it is visible. Check a2-3-1
if (list[0].transform === 'none') {
let normal = finalNormal(1, list)

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

// Check 90deg a2-2-3, a2-2-4.
let normal = finalNormal(0, list)

return isElementOrthogonalWithView(normal)
}
} else {
for (let i = 0; i < list.length; i++) {
// Ignore preserve-3d when it is not a direct parent.
// Why? -> https://github.com/cypress-io/cypress/pull/5916
if (i > 0 && list[i].transformStyle === 'preserve-3d') {
continue
}

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

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

return false
}

// This function uses a simplified version of backface culling.
// https://en.wikipedia.org/wiki/Back-face_culling
//
// 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 checkBackface = (normal: Vector3) => {
// Simplified dot product.
// 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 SMALL_NUMBER(10^-10).
if (Math.abs(dot) < TINY_NUMBER) {
dot = 0
}

return dot >= 0
}

const parseMatrix3D = (transform: string): Matrix3D => {
if (transform === 'none') {
return identityMatrix3D
}

if (transform.startsWith('matrix3d')) {
const matrix: Matrix3D = transform.substring(8).match(numberRegex)!.map((n) => {
return parseFloat(n)
}) as Matrix3D

return matrix
}

return toMatrix3d(transform.match(numberRegex)!.map((n) => parseFloat(n)) as Matrix2D)
}

const parseMatrix2D = (transform: string): Matrix2D => {
return transform.match(numberRegex)!.map((n) => parseFloat(n)) as Matrix2D
}

const findNormal = (matrix: Matrix3D, normal: Vector3 = defaultNormal): Vector3 => {
const m = matrix // alias for shorter formula
const v = normal // alias for shorter formula
const computedNormal: Vector3 = [
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 toUnitVector(computedNormal)
}

const toMatrix3d = (m2d: Matrix2D): Matrix3D => {
return [
m2d[0], m2d[1], 0, 0,
m2d[2], m2d[3], 0, 0,
0, 0, 1, 0,
m2d[4], m2d[5], 0, 1,
]
}

const toUnitVector = (v: Vector3): Vector3 => {
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])

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: TransformInfo[]) => {
if (list[1].transformStyle === 'preserve-3d') {
const normal = finalNormal(0, list)

return isElementOrthogonalWithView(normal)
}

return !!_.find(list, (info) => isTransformedToZero(info))
}

const isTransformedToZero = ({ transform }: TransformInfo) => {
if (transform === 'none') {
return false
}

// To understand how this part works,
// you need to understand tranformation matrix first.
// Matrix is hard to explain with only text. So, check these articles.
//
// https://www.useragentman.com/blog/2011/01/07/css3-matrix-transform-for-the-mathematically-challenged/
// https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions
//
if (transform.startsWith('matrix3d')) {
const matrix3d = parseMatrix3D(transform)

if (is3DMatrixScaledTo0(matrix3d)) {
return true
}

const normal = findNormal(matrix3d)

return isElementOrthogonalWithView(normal)
}

const m = parseMatrix2D(transform)

if (is2DMatrixScaledTo0(m)) {
return true
}

return false
}

const is3DMatrixScaledTo0 = (m3d: Matrix3D) => {
const xAxisScaledTo0 = m3d[0] === 0 && m3d[4] === 0 && m3d[8] === 0
const yAxisScaledTo0 = m3d[1] === 0 && m3d[5] === 0 && m3d[9] === 0
const zAxisScaledTo0 = m3d[2] === 0 && m3d[6] === 0 && m3d[10] === 0

if (xAxisScaledTo0 || yAxisScaledTo0 || zAxisScaledTo0) {
return true
}

return false
}

const is2DMatrixScaledTo0 = (m: Matrix2D) => {
const xAxisScaledTo0 = m[0] === 0 && m[2] === 0
const yAxisScaledTo0 = m[1] === 0 && m[3] === 0

if (xAxisScaledTo0 || yAxisScaledTo0) {
return true
}

return false
}

const isElementOrthogonalWithView = (normal: Vector3) => {
// Simplified dot product.
// [0] and [1] are always 0
const dot = viewVector[2] * normal[2]

return Math.abs(dot) < TINY_NUMBER
}
Loading