@gemx-dev/clarity-visualize 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +26 -0
- package/package.json +63 -0
- package/rollup.config.ts +79 -0
- package/src/clarity.ts +3 -0
- package/src/data.ts +103 -0
- package/src/enrich.ts +80 -0
- package/src/global.ts +9 -0
- package/src/heatmap.ts +355 -0
- package/src/index.ts +2 -0
- package/src/interaction.ts +504 -0
- package/src/layout.ts +879 -0
- package/src/styles/blobUnavailable/chineseSimplified.svg +5 -0
- package/src/styles/blobUnavailable/chineseTraditional.svg +5 -0
- package/src/styles/blobUnavailable/dutch.svg +5 -0
- package/src/styles/blobUnavailable/english.svg +5 -0
- package/src/styles/blobUnavailable/french.svg +5 -0
- package/src/styles/blobUnavailable/german.svg +5 -0
- package/src/styles/blobUnavailable/iconOnly.svg +4 -0
- package/src/styles/blobUnavailable/italian.svg +5 -0
- package/src/styles/blobUnavailable/japanese.svg +5 -0
- package/src/styles/blobUnavailable/korean.svg +5 -0
- package/src/styles/blobUnavailable/portuguese.svg +5 -0
- package/src/styles/blobUnavailable/russian.svg +5 -0
- package/src/styles/blobUnavailable/spanish.svg +5 -0
- package/src/styles/blobUnavailable/turkish.svg +5 -0
- package/src/styles/iframeUnavailable/chineseSimplified.svg +5 -0
- package/src/styles/iframeUnavailable/chineseTraditional.svg +5 -0
- package/src/styles/iframeUnavailable/dutch.svg +5 -0
- package/src/styles/iframeUnavailable/english.svg +5 -0
- package/src/styles/iframeUnavailable/french.svg +5 -0
- package/src/styles/iframeUnavailable/german.svg +5 -0
- package/src/styles/iframeUnavailable/iconOnly.svg +4 -0
- package/src/styles/iframeUnavailable/italian.svg +5 -0
- package/src/styles/iframeUnavailable/japanese.svg +5 -0
- package/src/styles/iframeUnavailable/korean.svg +5 -0
- package/src/styles/iframeUnavailable/portuguese.svg +5 -0
- package/src/styles/iframeUnavailable/russian.svg +5 -0
- package/src/styles/iframeUnavailable/spanish.svg +5 -0
- package/src/styles/iframeUnavailable/turkish.svg +5 -0
- package/src/styles/imageMasked/chineseSimplified.svg +5 -0
- package/src/styles/imageMasked/chineseTraditional.svg +5 -0
- package/src/styles/imageMasked/dutch.svg +5 -0
- package/src/styles/imageMasked/english.svg +5 -0
- package/src/styles/imageMasked/french.svg +5 -0
- package/src/styles/imageMasked/german.svg +5 -0
- package/src/styles/imageMasked/iconOnly.svg +4 -0
- package/src/styles/imageMasked/italian.svg +5 -0
- package/src/styles/imageMasked/japanese.svg +5 -0
- package/src/styles/imageMasked/korean.svg +5 -0
- package/src/styles/imageMasked/portuguese.svg +5 -0
- package/src/styles/imageMasked/russian.svg +5 -0
- package/src/styles/imageMasked/spanish.svg +5 -0
- package/src/styles/imageMasked/turkish.svg +5 -0
- package/src/styles/pointer/click.css +31 -0
- package/src/styles/pointer/pointerIcon.svg +18 -0
- package/src/styles/shared.css +6 -0
- package/src/visualizer.ts +297 -0
- package/tsconfig.json +21 -0
- package/tslint.json +33 -0
- package/types/index.d.ts +10 -0
- package/types/string-import.d.ts +9 -0
- package/types/visualize.d.ts +235 -0
package/src/heatmap.ts
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Activity,
|
|
3
|
+
Constant,
|
|
4
|
+
Heatmap,
|
|
5
|
+
Setting,
|
|
6
|
+
ScrollMapInfo,
|
|
7
|
+
PlaybackState,
|
|
8
|
+
} from '@clarity-types/visualize';
|
|
9
|
+
import { Data } from 'clarity-js';
|
|
10
|
+
import { LayoutHelper } from './layout';
|
|
11
|
+
|
|
12
|
+
export class HeatmapHelper {
|
|
13
|
+
static COLORS = ['blue', 'cyan', 'lime', 'yellow', 'red'];
|
|
14
|
+
data: Activity = null;
|
|
15
|
+
scrollData: ScrollMapInfo[] = null;
|
|
16
|
+
max: number = null;
|
|
17
|
+
offscreenRing: HTMLCanvasElement = null;
|
|
18
|
+
gradientPixels: ImageData = null;
|
|
19
|
+
timeout = null;
|
|
20
|
+
observer: ResizeObserver = null;
|
|
21
|
+
state: PlaybackState = null;
|
|
22
|
+
layout: LayoutHelper = null;
|
|
23
|
+
scrollAvgFold: number = null;
|
|
24
|
+
addScrollMakers: boolean = false;
|
|
25
|
+
|
|
26
|
+
constructor(state: PlaybackState, layout: LayoutHelper) {
|
|
27
|
+
this.state = state;
|
|
28
|
+
this.layout = layout;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public reset = (): void => {
|
|
32
|
+
this.data = null;
|
|
33
|
+
this.scrollData = null;
|
|
34
|
+
this.max = null;
|
|
35
|
+
this.offscreenRing = null;
|
|
36
|
+
this.gradientPixels = null;
|
|
37
|
+
this.timeout = null;
|
|
38
|
+
|
|
39
|
+
// Reset resize observer
|
|
40
|
+
if (this.observer) {
|
|
41
|
+
this.observer.disconnect();
|
|
42
|
+
this.observer = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Remove scroll and resize event listeners
|
|
46
|
+
if (this.state && this.state.window) {
|
|
47
|
+
let win = this.state.window;
|
|
48
|
+
win.removeEventListener('scroll', this.redraw, true);
|
|
49
|
+
win.removeEventListener('resize', this.redraw, true);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
public clear = (): void => {
|
|
54
|
+
let doc = this.state.window.document;
|
|
55
|
+
let win = this.state.window;
|
|
56
|
+
let canvas = doc.getElementById(Constant.HeatmapCanvas) as HTMLCanvasElement;
|
|
57
|
+
let de = doc.documentElement;
|
|
58
|
+
if (canvas) {
|
|
59
|
+
canvas.width = de.clientWidth;
|
|
60
|
+
canvas.height = de.clientHeight;
|
|
61
|
+
canvas.style.left = win.pageXOffset + Constant.Pixel;
|
|
62
|
+
canvas.style.top = win.pageYOffset + Constant.Pixel;
|
|
63
|
+
canvas.getContext(Constant.Context).clearRect(0, 0, canvas.width, canvas.height);
|
|
64
|
+
}
|
|
65
|
+
this.reset();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
public scroll = (activity: ScrollMapInfo[], avgFold: number, addMarkers: boolean): void => {
|
|
69
|
+
this.scrollData = this.scrollData || activity;
|
|
70
|
+
this.scrollAvgFold = avgFold != null ? avgFold : this.scrollAvgFold;
|
|
71
|
+
this.addScrollMakers = addMarkers != null ? addMarkers : this.addScrollMakers;
|
|
72
|
+
|
|
73
|
+
let canvas = this.overlay();
|
|
74
|
+
let context = canvas.getContext(Constant.Context);
|
|
75
|
+
let doc = this.state.window.document;
|
|
76
|
+
var body = doc.body;
|
|
77
|
+
var de = doc.documentElement;
|
|
78
|
+
var height = Math.max(
|
|
79
|
+
body.scrollHeight,
|
|
80
|
+
body.offsetHeight,
|
|
81
|
+
de.clientHeight,
|
|
82
|
+
de.scrollHeight,
|
|
83
|
+
de.offsetHeight,
|
|
84
|
+
);
|
|
85
|
+
canvas.height = Math.min(height, Setting.ScrollCanvasMaxHeight);
|
|
86
|
+
canvas.style.top = 0 + Constant.Pixel;
|
|
87
|
+
if (canvas.width > 0 && canvas.height > 0) {
|
|
88
|
+
if (this.scrollData) {
|
|
89
|
+
const grd = context.createLinearGradient(0, 0, 0, canvas.height);
|
|
90
|
+
for (const currentCombination of this.scrollData) {
|
|
91
|
+
const huePercentView =
|
|
92
|
+
1 - currentCombination.cumulativeSum / this.scrollData[0].cumulativeSum;
|
|
93
|
+
const percentView = (currentCombination.scrollReachY / 100) * (height / canvas.height);
|
|
94
|
+
const hue = huePercentView * Setting.MaxHue;
|
|
95
|
+
if (percentView <= 1) {
|
|
96
|
+
grd.addColorStop(percentView, `hsla(${hue}, 100%, 50%, 0.6)`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fill with gradient
|
|
101
|
+
context.fillStyle = grd;
|
|
102
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
103
|
+
if (this.addScrollMakers) {
|
|
104
|
+
this.addInfoMarkers(
|
|
105
|
+
context,
|
|
106
|
+
this.scrollData,
|
|
107
|
+
canvas.width,
|
|
108
|
+
canvas.height,
|
|
109
|
+
this.scrollAvgFold,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
private addInfoMarkers = (
|
|
117
|
+
context: CanvasRenderingContext2D,
|
|
118
|
+
scrollMapInfo: ScrollMapInfo[],
|
|
119
|
+
width: number,
|
|
120
|
+
height: number,
|
|
121
|
+
avgFold: number,
|
|
122
|
+
): void => {
|
|
123
|
+
this.addMarker(context, width, Constant.AverageFold, avgFold, Setting.MarkerMediumWidth);
|
|
124
|
+
const markers = [75, 50, 25];
|
|
125
|
+
for (const marker of markers) {
|
|
126
|
+
const closest = scrollMapInfo.reduce(
|
|
127
|
+
(prev: ScrollMapInfo, curr: ScrollMapInfo): ScrollMapInfo => {
|
|
128
|
+
return Math.abs(curr.percUsers - marker) < Math.abs(prev.percUsers - marker)
|
|
129
|
+
? curr
|
|
130
|
+
: prev;
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
if (
|
|
134
|
+
closest.percUsers >= marker - Setting.MarkerRange &&
|
|
135
|
+
closest.percUsers <= marker + Setting.MarkerRange
|
|
136
|
+
) {
|
|
137
|
+
const markerLine = (closest.scrollReachY / 100) * height;
|
|
138
|
+
this.addMarker(context, width, `${marker}%`, markerLine, Setting.MarkerSmallWidth);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
private addMarker = (
|
|
144
|
+
context: CanvasRenderingContext2D,
|
|
145
|
+
heatmapWidth: number,
|
|
146
|
+
label: string,
|
|
147
|
+
markerY: number,
|
|
148
|
+
markerWidth: number,
|
|
149
|
+
): void => {
|
|
150
|
+
context.beginPath();
|
|
151
|
+
context.moveTo(0, markerY);
|
|
152
|
+
context.lineTo(heatmapWidth, markerY);
|
|
153
|
+
context.setLineDash([2, 2]);
|
|
154
|
+
context.lineWidth = Setting.MarkerLineHeight;
|
|
155
|
+
context.strokeStyle = Setting.MarkerColor;
|
|
156
|
+
context.stroke();
|
|
157
|
+
context.fillStyle = Setting.CanvasTextColor;
|
|
158
|
+
context.fillRect(0, markerY - Setting.MarkerHeight / 2, markerWidth, Setting.MarkerHeight);
|
|
159
|
+
context.fillStyle = Setting.MarkerColor;
|
|
160
|
+
context.font = Setting.CanvasTextFont;
|
|
161
|
+
context.fillText(label, Setting.MarkerPadding, markerY + Setting.MarkerPadding);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
public click = (activity: Activity): void => {
|
|
165
|
+
this.data = this.data || activity;
|
|
166
|
+
let heat = this.transform();
|
|
167
|
+
console.log(`🚀 🐥 ~ HeatmapHelper ~ heat:`, heat);
|
|
168
|
+
let canvas = this.overlay();
|
|
169
|
+
let ctx = canvas.getContext(Constant.Context);
|
|
170
|
+
|
|
171
|
+
if (canvas.width > 0 && canvas.height > 0) {
|
|
172
|
+
// To speed up canvas rendering, we draw ring & gradient on an offscreen canvas, so we can use drawImage API
|
|
173
|
+
// Canvas performance tips: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
|
|
174
|
+
// Pre-render similar primitives or repeating objects on an offscreen canvas
|
|
175
|
+
let ring = this.getRing();
|
|
176
|
+
let gradient = this.getGradient();
|
|
177
|
+
|
|
178
|
+
// Render activity for each (x,y) coordinate in our data
|
|
179
|
+
for (let entry of heat) {
|
|
180
|
+
ctx.globalAlpha = entry.a;
|
|
181
|
+
ctx.drawImage(ring, entry.x - Setting.Radius, entry.y - Setting.Radius);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Add color to the canvas based on alpha value of each pixel
|
|
185
|
+
let pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
186
|
+
for (let i = 0; i < pixels.data.length; i += 4) {
|
|
187
|
+
// For each pixel, we have 4 entries in data array: (r,g,b,a)
|
|
188
|
+
// To pick the right color from gradient pixels, we look at the alpha value of the pixel
|
|
189
|
+
// Alpha value ranges from 0-255
|
|
190
|
+
let alpha = pixels.data[i + 3];
|
|
191
|
+
if (alpha > 0) {
|
|
192
|
+
let offset = (alpha - 1) * 4;
|
|
193
|
+
pixels.data[i] = gradient.data[offset];
|
|
194
|
+
pixels.data[i + 1] = gradient.data[offset + 1];
|
|
195
|
+
pixels.data[i + 2] = gradient.data[offset + 2];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
ctx.putImageData(pixels, 0, 0);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
private overlay = (): HTMLCanvasElement => {
|
|
203
|
+
// Create canvas for visualizing heatmap
|
|
204
|
+
let doc = this.state.window.document;
|
|
205
|
+
let win = this.state.window;
|
|
206
|
+
let de = doc.documentElement;
|
|
207
|
+
let canvas = doc.getElementById(Constant.HeatmapCanvas) as HTMLCanvasElement;
|
|
208
|
+
if (canvas === null) {
|
|
209
|
+
canvas = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
|
|
210
|
+
canvas.id = Constant.HeatmapCanvas;
|
|
211
|
+
canvas.width = 0;
|
|
212
|
+
canvas.height = 0;
|
|
213
|
+
canvas.style.position = Constant.Absolute;
|
|
214
|
+
canvas.style.zIndex = `${Setting.ZIndex}`;
|
|
215
|
+
de.appendChild(canvas);
|
|
216
|
+
win.addEventListener('scroll', this.redraw, true);
|
|
217
|
+
win.addEventListener('resize', this.redraw, true);
|
|
218
|
+
this.observer = this.state.window['ResizeObserver'] ? new ResizeObserver(this.redraw) : null;
|
|
219
|
+
|
|
220
|
+
if (this.observer) {
|
|
221
|
+
this.observer.observe(doc.body);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Ensure canvas is positioned correctly
|
|
226
|
+
canvas.width = de.clientWidth;
|
|
227
|
+
canvas.height = de.clientHeight;
|
|
228
|
+
canvas.style.left = win.pageXOffset + Constant.Pixel;
|
|
229
|
+
canvas.style.top = win.pageYOffset + Constant.Pixel;
|
|
230
|
+
canvas.getContext(Constant.Context).clearRect(0, 0, canvas.width, canvas.height);
|
|
231
|
+
|
|
232
|
+
return canvas;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
private getRing = (): HTMLCanvasElement => {
|
|
236
|
+
if (this.offscreenRing === null) {
|
|
237
|
+
let doc = this.state.window.document;
|
|
238
|
+
this.offscreenRing = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
|
|
239
|
+
this.offscreenRing.width = Setting.Radius * 2;
|
|
240
|
+
this.offscreenRing.height = Setting.Radius * 2;
|
|
241
|
+
let ctx = this.offscreenRing.getContext(Constant.Context);
|
|
242
|
+
ctx.shadowOffsetX = Setting.Radius * 2;
|
|
243
|
+
ctx.shadowBlur = Setting.Radius / 2;
|
|
244
|
+
ctx.shadowColor = Constant.Black;
|
|
245
|
+
ctx.beginPath();
|
|
246
|
+
ctx.arc(-Setting.Radius, Setting.Radius, Setting.Radius / 2, 0, Math.PI * 2, true);
|
|
247
|
+
ctx.closePath();
|
|
248
|
+
ctx.fill();
|
|
249
|
+
}
|
|
250
|
+
return this.offscreenRing;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
private getGradient = (): ImageData => {
|
|
254
|
+
if (this.gradientPixels === null) {
|
|
255
|
+
let doc = this.state.window.document;
|
|
256
|
+
let offscreenGradient = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
|
|
257
|
+
offscreenGradient.width = 1;
|
|
258
|
+
offscreenGradient.height = Setting.Colors;
|
|
259
|
+
let ctx = offscreenGradient.getContext(Constant.Context);
|
|
260
|
+
let gradient = ctx.createLinearGradient(0, 0, 0, Setting.Colors);
|
|
261
|
+
let step = 1 / HeatmapHelper.COLORS.length;
|
|
262
|
+
for (let i = 0; i < HeatmapHelper.COLORS.length; i++) {
|
|
263
|
+
gradient.addColorStop(step * (i + 1), HeatmapHelper.COLORS[i]);
|
|
264
|
+
}
|
|
265
|
+
ctx.fillStyle = gradient;
|
|
266
|
+
ctx.fillRect(0, 0, 1, Setting.Colors);
|
|
267
|
+
this.gradientPixels = ctx.getImageData(0, 0, 1, Setting.Colors);
|
|
268
|
+
}
|
|
269
|
+
return this.gradientPixels;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
private redraw = (event): void => {
|
|
273
|
+
if (this.data) {
|
|
274
|
+
if (this.timeout) {
|
|
275
|
+
clearTimeout(this.timeout);
|
|
276
|
+
}
|
|
277
|
+
this.timeout = setTimeout(this.click, Setting.Interval);
|
|
278
|
+
} else if (this.scrollData) {
|
|
279
|
+
if (event.type != 'scroll') {
|
|
280
|
+
if (this.timeout) {
|
|
281
|
+
clearTimeout(this.timeout);
|
|
282
|
+
}
|
|
283
|
+
this.timeout = setTimeout(this.scroll, Setting.Interval);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
private transform = (): Heatmap[] => {
|
|
289
|
+
let output: Heatmap[] = [];
|
|
290
|
+
let points: { [key: string]: number } = {};
|
|
291
|
+
let localMax = 0;
|
|
292
|
+
let height =
|
|
293
|
+
this.state.window && this.state.window.document
|
|
294
|
+
? this.state.window.document.documentElement.clientHeight
|
|
295
|
+
: 0;
|
|
296
|
+
|
|
297
|
+
for (let element of this.data) {
|
|
298
|
+
let el = this.layout.get(element.hash) as HTMLElement;
|
|
299
|
+
el && console.log(`🚀 🐥 ~ HeatmapHelper ~ el:`, el);
|
|
300
|
+
if (el && typeof el.getBoundingClientRect === 'function') {
|
|
301
|
+
let r = el.getBoundingClientRect();
|
|
302
|
+
let v = this.visible(el, r, height);
|
|
303
|
+
// Process clicks for only visible elements
|
|
304
|
+
if (this.max === null || v) {
|
|
305
|
+
for (let i = 0; i < element.points; i++) {
|
|
306
|
+
let x = Math.round(r.left + (element.x[i] / Data.Setting.ClickPrecision) * r.width);
|
|
307
|
+
let y = Math.round(r.top + (element.y[i] / Data.Setting.ClickPrecision) * r.height);
|
|
308
|
+
let k = `${x}${Constant.Separator}${y}${Constant.Separator}${v ? 1 : 0}`;
|
|
309
|
+
points[k] = k in points ? points[k] + element.clicks[i] : element.clicks[i];
|
|
310
|
+
localMax = Math.max(points[k], localMax);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Set the max value from the firs t
|
|
317
|
+
this.max = this.max ? this.max : localMax;
|
|
318
|
+
|
|
319
|
+
// Once all points are accounted for, convert everything into absolute (x, y)
|
|
320
|
+
for (let coordinates of Object.keys(points)) {
|
|
321
|
+
let parts = coordinates.split(Constant.Separator);
|
|
322
|
+
let alpha = Math.min(points[coordinates] / this.max + Setting.AlphaBoost, 1);
|
|
323
|
+
if (parts[2] === '1') {
|
|
324
|
+
output.push({ x: parseInt(parts[0], 10), y: parseInt(parts[1], 10), a: alpha });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return output;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
private visible = (el: HTMLElement, r: DOMRect, height: number): boolean => {
|
|
332
|
+
let doc: Document | ShadowRoot = this.state.window.document;
|
|
333
|
+
let visibility = r.height > height ? true : false;
|
|
334
|
+
if (visibility === false && r.width > 0 && r.height > 0) {
|
|
335
|
+
while (!visibility && doc) {
|
|
336
|
+
let shadowElement = null;
|
|
337
|
+
let elements = doc.elementsFromPoint(r.left + r.width / 2, r.top + r.height / 2);
|
|
338
|
+
for (let e of elements) {
|
|
339
|
+
// Ignore if top element ends up being the canvas element we added for heatmap visualization
|
|
340
|
+
if (
|
|
341
|
+
e.tagName === Constant.Canvas ||
|
|
342
|
+
(e.id && e.id.indexOf(Constant.ClarityPrefix) === 0)
|
|
343
|
+
) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
visibility = e === el;
|
|
347
|
+
shadowElement = e.shadowRoot && e.shadowRoot != doc ? e.shadowRoot : null;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
doc = shadowElement;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return visibility && r.bottom >= 0 && r.top <= height;
|
|
354
|
+
};
|
|
355
|
+
}
|
package/src/index.ts
ADDED