Skip to content

Commit 4099ed8

Browse files
sainthkhchrisbreiding
authored andcommitted
Check backface visibility when the parents of the target elemen… (#5916)
* Moved and added tests to separate file. It's created for visual tests. * For future reference. Committed for future reference. * Fixed. * Preparation for merge. * Moved transform code to transform.ts. * Typed transform.ts * Named test cases. * Added a new test case and refactored functions. * Fix transform + overflow error. Co-authored-by: Chris Breiding <chrisbreiding@users.noreply.github.com>
1 parent 596c2bd commit 4099ed8

File tree

6 files changed

+645
-186
lines changed

6 files changed

+645
-186
lines changed

packages/driver/.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"env": {
33
"browser": true
4-
}
4+
},
5+
"parser": "@typescript-eslint/parser"
56
}
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import _ from 'lodash'
2+
import { isDocument } from './document'
3+
4+
export const detectVisibility = ($el: any) => {
5+
const list = extractTransformInfoFromElements($el)
6+
7+
if (existsInvisibleBackface(list)) {
8+
return elIsBackface(list) ? 'backface' : 'visible'
9+
}
10+
11+
return elIsTransformedToZero(list) ? 'transformed' : 'visible'
12+
}
13+
14+
type BackfaceVisibility = 'hidden' | 'visible'
15+
type TransformStyle = 'flat' | 'preserve-3d'
16+
type Matrix2D = [
17+
number, number, number,
18+
number, number, number,
19+
]
20+
type Matrix3D = [
21+
number, number, number, number,
22+
number, number, number, number,
23+
number, number, number, number,
24+
number, number, number, number,
25+
]
26+
27+
type Vector3 = [number, number, number]
28+
29+
interface TransformInfo {
30+
backfaceVisibility: BackfaceVisibility
31+
transformStyle: TransformStyle
32+
transform: string
33+
}
34+
35+
const extractTransformInfoFromElements = ($el: any, list: TransformInfo[] = []): TransformInfo[] => {
36+
list.push(extractTransformInfo($el))
37+
38+
const $parent = $el.parent()
39+
40+
if (!$parent.length || isDocument($parent)) {
41+
return list
42+
}
43+
44+
return extractTransformInfoFromElements($parent, list)
45+
}
46+
47+
const extractTransformInfo = ($el): TransformInfo => {
48+
const el = $el[0]
49+
const style = getComputedStyle(el)
50+
51+
return {
52+
backfaceVisibility: style.getPropertyValue('backface-visibility') as BackfaceVisibility,
53+
transformStyle: style.getPropertyValue('transform-style') as TransformStyle,
54+
transform: style.getPropertyValue('transform'),
55+
}
56+
}
57+
58+
const existsInvisibleBackface = (list: TransformInfo[]) => {
59+
return !!_.find(list, { backfaceVisibility: 'hidden' })
60+
}
61+
62+
const numberRegex = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/g
63+
const defaultNormal: Vector3 = [0, 0, 1]
64+
const viewVector: Vector3 = [0, 0, -1]
65+
const identityMatrix3D: Matrix3D = [
66+
1, 0, 0, 0,
67+
0, 1, 0, 0,
68+
0, 0, 1, 0,
69+
0, 0, 0, 1,
70+
]
71+
72+
// It became 1e-5 from 1e-10. Because 30deg + 30deg + 30deg is 6.0568e-7 and it caused a false negative.
73+
const TINY_NUMBER = 1e-5
74+
75+
const nextPreserve3d = (i: number, list: TransformInfo[]) => {
76+
return i + 1 < list.length &&
77+
list[i + 1].transformStyle === 'preserve-3d'
78+
}
79+
80+
const finalNormal = (startIndex: number, list: TransformInfo[]) => {
81+
let i = startIndex
82+
let normal = findNormal(parseMatrix3D(list[i].transform))
83+
84+
while (nextPreserve3d(i, list)) {
85+
i++
86+
normal = findNormal(parseMatrix3D(list[i].transform), normal)
87+
}
88+
89+
return normal
90+
}
91+
92+
const elIsBackface = (list: TransformInfo[]) => {
93+
// When the direct parent of the target has style, preserve-3d
94+
if (list.length > 1 && list[1].transformStyle === 'preserve-3d') {
95+
// When the target is backface-invisible a2-1-1 ~ a2-1-4
96+
if (list[0].backfaceVisibility === 'hidden') {
97+
let normal = finalNormal(0, list)
98+
99+
if (checkBackface(normal)) {
100+
return true
101+
}
102+
} else {
103+
// When the direct parent of the target is backface-invisible
104+
if (list[1].backfaceVisibility === 'hidden') {
105+
// If it is not none, it is visible. Check a2-3-1
106+
if (list[0].transform === 'none') {
107+
let normal = finalNormal(1, list)
108+
109+
if (checkBackface(normal)) {
110+
return true
111+
}
112+
}
113+
}
114+
115+
// Check 90deg a2-2-3, a2-2-4.
116+
let normal = finalNormal(0, list)
117+
118+
return isElementOrthogonalWithView(normal)
119+
}
120+
} else {
121+
for (let i = 0; i < list.length; i++) {
122+
// Ignore preserve-3d when it is not a direct parent.
123+
// Why? -> https://github.com/cypress-io/cypress/pull/5916
124+
if (i > 0 && list[i].transformStyle === 'preserve-3d') {
125+
continue
126+
}
127+
128+
if (list[i].backfaceVisibility === 'hidden' && list[i].transform.startsWith('matrix3d')) {
129+
let normal = findNormal(parseMatrix3D(list[i].transform))
130+
131+
if (checkBackface(normal)) {
132+
return true
133+
}
134+
}
135+
}
136+
}
137+
138+
return false
139+
}
140+
141+
// This function uses a simplified version of backface culling.
142+
// https://en.wikipedia.org/wiki/Back-face_culling
143+
//
144+
// We defined view vector, (0, 0, -1), - eye to screen.
145+
// and default normal vector of an element, (0, 0, 1)
146+
// When dot product of them are >= 0, item is visible.
147+
const checkBackface = (normal: Vector3) => {
148+
// Simplified dot product.
149+
// viewVector[0] and viewVector[1] are always 0. So, they're ignored.
150+
let dot = viewVector[2] * normal[2]
151+
152+
// Because of the floating point number rounding error,
153+
// cos(90deg) isn't 0. It's 6.12323e-17.
154+
// And it sometimes causes errors when dot product value is something like -6.12323e-17.
155+
// So, we're setting the dot product result to 0 when its absolute value is less than SMALL_NUMBER(10^-10).
156+
if (Math.abs(dot) < TINY_NUMBER) {
157+
dot = 0
158+
}
159+
160+
return dot >= 0
161+
}
162+
163+
const parseMatrix3D = (transform: string): Matrix3D => {
164+
if (transform === 'none') {
165+
return identityMatrix3D
166+
}
167+
168+
if (transform.startsWith('matrix3d')) {
169+
const matrix: Matrix3D = transform.substring(8).match(numberRegex)!.map((n) => {
170+
return parseFloat(n)
171+
}) as Matrix3D
172+
173+
return matrix
174+
}
175+
176+
return toMatrix3d(transform.match(numberRegex)!.map((n) => parseFloat(n)) as Matrix2D)
177+
}
178+
179+
const parseMatrix2D = (transform: string): Matrix2D => {
180+
return transform.match(numberRegex)!.map((n) => parseFloat(n)) as Matrix2D
181+
}
182+
183+
const findNormal = (matrix: Matrix3D, normal: Vector3 = defaultNormal): Vector3 => {
184+
const m = matrix // alias for shorter formula
185+
const v = normal // alias for shorter formula
186+
const computedNormal: Vector3 = [
187+
m[0] * v[0] + m[4] * v[1] + m[8] * v[2],
188+
m[1] * v[0] + m[5] * v[1] + m[9] * v[2],
189+
m[2] * v[0] + m[6] * v[1] + m[10] * v[2],
190+
]
191+
192+
return toUnitVector(computedNormal)
193+
}
194+
195+
const toMatrix3d = (m2d: Matrix2D): Matrix3D => {
196+
return [
197+
m2d[0], m2d[1], 0, 0,
198+
m2d[2], m2d[3], 0, 0,
199+
0, 0, 1, 0,
200+
m2d[4], m2d[5], 0, 1,
201+
]
202+
}
203+
204+
const toUnitVector = (v: Vector3): Vector3 => {
205+
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
206+
207+
return [v[0] / length, v[1] / length, v[2] / length]
208+
}
209+
210+
// This function checks 2 things that can happen: scale and rotate to 0 in width or height.
211+
const elIsTransformedToZero = (list: TransformInfo[]) => {
212+
if (list[1].transformStyle === 'preserve-3d') {
213+
const normal = finalNormal(0, list)
214+
215+
return isElementOrthogonalWithView(normal)
216+
}
217+
218+
return !!_.find(list, (info) => isTransformedToZero(info))
219+
}
220+
221+
const isTransformedToZero = ({ transform }: TransformInfo) => {
222+
if (transform === 'none') {
223+
return false
224+
}
225+
226+
// To understand how this part works,
227+
// you need to understand tranformation matrix first.
228+
// Matrix is hard to explain with only text. So, check these articles.
229+
//
230+
// https://www.useragentman.com/blog/2011/01/07/css3-matrix-transform-for-the-mathematically-challenged/
231+
// https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions
232+
//
233+
if (transform.startsWith('matrix3d')) {
234+
const matrix3d = parseMatrix3D(transform)
235+
236+
if (is3DMatrixScaledTo0(matrix3d)) {
237+
return true
238+
}
239+
240+
const normal = findNormal(matrix3d)
241+
242+
return isElementOrthogonalWithView(normal)
243+
}
244+
245+
const m = parseMatrix2D(transform)
246+
247+
if (is2DMatrixScaledTo0(m)) {
248+
return true
249+
}
250+
251+
return false
252+
}
253+
254+
const is3DMatrixScaledTo0 = (m3d: Matrix3D) => {
255+
const xAxisScaledTo0 = m3d[0] === 0 && m3d[4] === 0 && m3d[8] === 0
256+
const yAxisScaledTo0 = m3d[1] === 0 && m3d[5] === 0 && m3d[9] === 0
257+
const zAxisScaledTo0 = m3d[2] === 0 && m3d[6] === 0 && m3d[10] === 0
258+
259+
if (xAxisScaledTo0 || yAxisScaledTo0 || zAxisScaledTo0) {
260+
return true
261+
}
262+
263+
return false
264+
}
265+
266+
const is2DMatrixScaledTo0 = (m: Matrix2D) => {
267+
const xAxisScaledTo0 = m[0] === 0 && m[2] === 0
268+
const yAxisScaledTo0 = m[1] === 0 && m[3] === 0
269+
270+
if (xAxisScaledTo0 || yAxisScaledTo0) {
271+
return true
272+
}
273+
274+
return false
275+
}
276+
277+
const isElementOrthogonalWithView = (normal: Vector3) => {
278+
// Simplified dot product.
279+
// [0] and [1] are always 0
280+
const dot = viewVector[2] * normal[2]
281+
282+
return Math.abs(dot) < TINY_NUMBER
283+
}

0 commit comments

Comments
 (0)