/
transform.ts
299 lines (237 loc) · 8.27 KB
/
transform.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import _ from 'lodash'
import { getParent } from './elements'
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[] => {
const info = extractTransformInfo($el)
if (info) {
list.push(info)
}
const $parent = getParent($el)
if (!$parent.length || isDocument($parent)) {
return list
}
return extractTransformInfoFromElements($parent, list)
}
const extractTransformInfo = ($el): TransformInfo | null => {
const el = $el[0]
const style = getComputedStyle(el)
const backfaceVisibility = style.getPropertyValue('backface-visibility') as BackfaceVisibility
// When an element is not in the DOM tree, getComputedStyle() returns empty string.
// In an edge case from frameworks like `vue-fragment`
// `parentNode` is modified and out of the DOM tree.
// @see https://github.com/cypress-io/cypress/pull/6787
// @see https://github.com/cypress-io/cypress/issues/6745
if (backfaceVisibility === '') {
return null
}
return {
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.some((info) => info.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
}