@bakery-framework/plugin-dashboard 2.0.0-alpha.9 → 2.0.0-rc.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/README.md +6 -6
- package/package.json +7 -5
- package/src/client/dashboard.ts +3 -10
- package/src/client/parts/logs.ts +19 -13
- package/src/client/parts/metrics.ts +251 -0
- package/src/client/parts/sessions.ts +26 -124
- package/src/client/parts/sparkline-tooltip.ts +158 -0
- package/src/client/parts/sparkline.ts +383 -0
- package/src/client/parts/stats.ts +33 -758
- package/src/client/parts/utils.ts +22 -68
- package/src/components/DBBrowser.tsx +1 -3
- package/src/components/LogsPanel.tsx +0 -2
- package/src/components/SessionsPanel.tsx +0 -3
- package/src/components/StatsPanel.tsx +0 -19
- package/src/components/TopPagesPanel.tsx +0 -1
- package/src/endpoints/logs-socket.ts +72 -0
- package/src/endpoints/sessions.ts +1 -1
- package/src/index.ts +6 -6
- package/src/paths.ts +1 -1
- package/src/setup.ts +104 -18
- package/src/shell.tsx +51 -30
- package/src/test-fixtures.ts +1 -1
- package/src/client/parts/effects.ts +0 -327
package/src/shell.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { getFrameworkVersion } from '@bakery-framework/core'
|
|
2
2
|
import { Bakery } from '@bakery-framework/core/core/bakery'
|
|
3
|
-
import { Try } from '@bakery-framework/core/utils/common'
|
|
4
3
|
import { renderDatabaseBrowser } from './components/DBBrowser'
|
|
5
4
|
import { renderLogsPanel } from './components/LogsPanel'
|
|
6
5
|
import { renderSessionsPanel } from './components/SessionsPanel'
|
|
@@ -13,37 +12,26 @@ import { renderTopPagesPanel } from './components/TopPagesPanel'
|
|
|
13
12
|
* Keeping that contract lets the chrome be replaced without touching the ~3k
|
|
14
13
|
* lines of panel client code.
|
|
15
14
|
*/
|
|
16
|
-
/**
|
|
17
|
-
* A path nothing should claim, used as the control below.
|
|
18
|
-
*/
|
|
19
|
-
const NOT_A_ROUTE = '/__bakery_probe_no_handler_serves_this__'
|
|
20
|
-
|
|
21
15
|
/**
|
|
22
16
|
* Is anything serving `/_db`?
|
|
23
17
|
*
|
|
24
|
-
* Asked
|
|
25
|
-
* import is a package-graph edge, and
|
|
26
|
-
* exactly one of those (this package →
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* **The control probe is what makes this work.** `StaticHandler.canHandle()`
|
|
32
|
-
* returns `true` unconditionally — it is the priority-0 fallback and claims
|
|
33
|
-
* every path — so "does some handler claim `/_db`" is always yes. A handler
|
|
34
|
-
* that claims `/_db` *and declines a path nobody serves* is claiming a
|
|
35
|
-
* namespace rather than catching everything.
|
|
18
|
+
* Asked of the registry's *declarations*, never by importing db-explorer: a
|
|
19
|
+
* plugin-to-plugin import is a package-graph edge, and
|
|
20
|
+
* `tests/conventions.test.ts` allows exactly one of those (this package →
|
|
21
|
+
* analytics). `Handler.namespace` is the declaration: any handler that owns
|
|
22
|
+
* `/_db` as a surface gets the link, including an application serving its own
|
|
23
|
+
* explorer there.
|
|
36
24
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
25
|
+
* This replaces a behavioral probe that called every handler's
|
|
26
|
+
* `canHandle('/_db')` with a control path to exclude the priority-0 catch-all.
|
|
27
|
+
* The probe worked and its `as any` was the tell: the registry could not say
|
|
28
|
+
* what a handler serves, so the question had to be asked by experiment. Now it
|
|
29
|
+
* can: `list()` is typed `typeof Handler[]`, so this reads with no cast, and
|
|
30
|
+
* a second plugin wanting a console entry declares a namespace rather than
|
|
31
|
+
* copying a probe.
|
|
39
32
|
*/
|
|
40
33
|
function explorerIsMounted(): boolean {
|
|
41
|
-
|
|
42
|
-
const claims = (path: string) =>
|
|
43
|
-
Try.return(() => (handler as any).canHandle?.(path) === true, false)
|
|
44
|
-
if (claims('/_db') && !claims(NOT_A_ROUTE)) return true
|
|
45
|
-
}
|
|
46
|
-
return false
|
|
34
|
+
return Bakery.handlers.fetch.list().some(h => h.namespace === '/_db')
|
|
47
35
|
}
|
|
48
36
|
|
|
49
37
|
interface NavEntry {
|
|
@@ -70,7 +58,7 @@ function navSections(explorerMounted: boolean): {
|
|
|
70
58
|
group: 'Data',
|
|
71
59
|
items: [
|
|
72
60
|
// The console does not browse the database any more, so Database is a
|
|
73
|
-
// way *out* of it when the explorer is mounted
|
|
61
|
+
// way *out* of it when the explorer is mounted: a link, not a tab.
|
|
74
62
|
// Without it the entry stays a tab showing the panel that explains
|
|
75
63
|
// where the editor went and how to get it back; an entry that silently
|
|
76
64
|
// navigates to a 404 would be worse than either.
|
|
@@ -107,11 +95,37 @@ function NavItem({ id, label, href }: NavEntry) {
|
|
|
107
95
|
)
|
|
108
96
|
}
|
|
109
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The rendered shell, and the one fact it depends on.
|
|
100
|
+
*
|
|
101
|
+
* Everything in this document is fixed for the life of the process except
|
|
102
|
+
* whether an explorer is mounted at `/_db`, which decides one nav entry, and
|
|
103
|
+
* the framework version, which cannot change at all. Rendering it again per
|
|
104
|
+
* request produced an identical 11.8 KB string every time.
|
|
105
|
+
*
|
|
106
|
+
* Measured at **0.50 ms per render** with a CPU-bound control flat at 27-30 ms
|
|
107
|
+
* - the backlog recorded 12.6 ms, which is 25 times the figure this actually
|
|
108
|
+
* measures. Small, then, and still pure waste on every console page load.
|
|
109
|
+
*
|
|
110
|
+
* Keyed on the mounted flag rather than held unconditionally, so a registry
|
|
111
|
+
* that changes after the first render is followed rather than remembered
|
|
112
|
+
* wrongly. Two entries at most, which is why a plain pair is enough and
|
|
113
|
+
* convention 6 has nothing to say about it.
|
|
114
|
+
*/
|
|
115
|
+
let shellCache: { mounted: boolean; html: string } | null = null
|
|
116
|
+
|
|
117
|
+
/** Test seam (convention 9): one test's shell must not answer for another's. */
|
|
118
|
+
export function __resetShellCache(): void {
|
|
119
|
+
shellCache = null
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
export default function Dashboard() {
|
|
111
123
|
const explorerMounted = explorerIsMounted()
|
|
124
|
+
if (shellCache?.mounted === explorerMounted) return shellCache.html
|
|
125
|
+
|
|
112
126
|
const NAV = navSections(explorerMounted)
|
|
113
127
|
|
|
114
|
-
|
|
128
|
+
const html = (
|
|
115
129
|
<html lang="en">
|
|
116
130
|
<head>
|
|
117
131
|
<meta charSet="UTF-8" />
|
|
@@ -140,7 +154,7 @@ export default function Dashboard() {
|
|
|
140
154
|
<div class="rail-foot">
|
|
141
155
|
<span>Bakery</span>
|
|
142
156
|
{/* Was the literal `v3`, which was never any version of anything
|
|
143
|
-
|
|
157
|
+
: nothing filled the id, and the framework was on 1.x when it
|
|
144
158
|
was written. `getFrameworkVersion()` reads core's own
|
|
145
159
|
manifest, which is the number this label claims to be; the
|
|
146
160
|
app's version is a different question and `BAKERY_VERSION`
|
|
@@ -209,5 +223,12 @@ export default function Dashboard() {
|
|
|
209
223
|
<script src="/_dashboard/dashboard.js"></script>
|
|
210
224
|
</body>
|
|
211
225
|
</html>
|
|
212
|
-
)
|
|
226
|
+
) as unknown as string
|
|
227
|
+
// The same cast `raw()` makes in `core/jsx.ts`: `createElement` is declared
|
|
228
|
+
// to return `string` and actually returns a `SafeHtml`, a String subclass
|
|
229
|
+
// that behaves like one everywhere. Cached as it came rather than through
|
|
230
|
+
// `String()`, so nothing about the value changes by being remembered.
|
|
231
|
+
|
|
232
|
+
shellCache = { mounted: explorerMounted, html }
|
|
233
|
+
return html
|
|
213
234
|
}
|
package/src/test-fixtures.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* A database stand-in that records what it was asked to do and does none of it.
|
|
10
10
|
*
|
|
11
11
|
* The assertion that matters in both suites is that the recorded list stays
|
|
12
|
-
* *empty*
|
|
12
|
+
* *empty* (a rejection that arrives after the truncate is not a rejection), so
|
|
13
13
|
* the stub must reach no real database and must log every method the endpoints
|
|
14
14
|
* can call. `setup.test.ts` (routing and CSRF) and `endpoints/database.test.ts`
|
|
15
15
|
* (the write gate) each kept their own copy, and the copies had already
|
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
export let refreshShimmerCache = () => {}
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Both effects below run on import. Without this guard, importing anything that
|
|
5
|
-
* transitively reaches this file outside a browser throws on the first `window`
|
|
6
|
-
* access — which made every module in `client/parts/` untestable, including the
|
|
7
|
-
* hand-built HTML in the since-retired `database.ts` that produced every XSS
|
|
8
|
-
* found in this repo. A DOM effect with no DOM is a no-op, not an error.
|
|
9
|
-
*/
|
|
10
|
-
const HAS_DOM = typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
11
|
-
|
|
12
|
-
;(function initDashboardShimmer() {
|
|
13
|
-
if (!HAS_DOM) return
|
|
14
|
-
if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches) return
|
|
15
|
-
|
|
16
|
-
let cachedCards: {
|
|
17
|
-
el: HTMLElement
|
|
18
|
-
width: number
|
|
19
|
-
height: number
|
|
20
|
-
pageLeft: number
|
|
21
|
-
pageTop: number
|
|
22
|
-
}[] = []
|
|
23
|
-
const mouse = { x: -9999, y: -9999 }
|
|
24
|
-
let rafId = 0
|
|
25
|
-
|
|
26
|
-
refreshShimmerCache = function updateRects() {
|
|
27
|
-
const cards = Array.from(
|
|
28
|
-
document.querySelectorAll('.glass-effect'),
|
|
29
|
-
) as HTMLElement[]
|
|
30
|
-
|
|
31
|
-
cachedCards = cards.map(el => {
|
|
32
|
-
const rect = el.getBoundingClientRect()
|
|
33
|
-
return {
|
|
34
|
-
el,
|
|
35
|
-
width: rect.width,
|
|
36
|
-
height: rect.height,
|
|
37
|
-
pageLeft: rect.left + window.scrollX,
|
|
38
|
-
pageTop: rect.top + window.scrollY,
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The observer below fires for every subtree mutation on the page, and
|
|
45
|
-
* `refreshShimmerCache` reads `getBoundingClientRect` once per glass card —
|
|
46
|
-
* a forced synchronous layout. The log console appends one node per
|
|
47
|
-
* websocket frame, each in its own task, so a busy server bought one full
|
|
48
|
-
* layout pass per log line. Coalescing into a frame makes that cost
|
|
49
|
-
* proportional to frames, not to mutations.
|
|
50
|
-
*/
|
|
51
|
-
let refreshRafId = 0
|
|
52
|
-
function scheduleRefresh() {
|
|
53
|
-
if (refreshRafId) return
|
|
54
|
-
refreshRafId = requestAnimationFrame(() => {
|
|
55
|
-
refreshRafId = 0
|
|
56
|
-
refreshShimmerCache()
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
window.addEventListener('resize', scheduleRefresh, { passive: true })
|
|
61
|
-
|
|
62
|
-
const observer = new MutationObserver(scheduleRefresh)
|
|
63
|
-
observer.observe(document.body, {
|
|
64
|
-
childList: true,
|
|
65
|
-
subtree: true,
|
|
66
|
-
attributes: true,
|
|
67
|
-
attributeFilter: ['class'],
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
refreshShimmerCache()
|
|
71
|
-
|
|
72
|
-
function updateStyles() {
|
|
73
|
-
const scrollX = window.scrollX
|
|
74
|
-
const scrollY = window.scrollY
|
|
75
|
-
|
|
76
|
-
for (let i = 0; i < cachedCards.length; i++) {
|
|
77
|
-
const c = cachedCards[i]
|
|
78
|
-
const left = c.pageLeft - scrollX
|
|
79
|
-
const top = c.pageTop - scrollY
|
|
80
|
-
|
|
81
|
-
const rx = mouse.x - left
|
|
82
|
-
const ry = mouse.y - top
|
|
83
|
-
|
|
84
|
-
const closestX = Math.max(left, Math.min(mouse.x, left + c.width))
|
|
85
|
-
const closestY = Math.max(top, Math.min(mouse.y, top + c.height))
|
|
86
|
-
const dx = mouse.x - closestX
|
|
87
|
-
const dy = mouse.y - closestY
|
|
88
|
-
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
89
|
-
|
|
90
|
-
if (dist < 200) {
|
|
91
|
-
c.el.style.setProperty('--mouse-x', `${rx}px`)
|
|
92
|
-
c.el.style.setProperty('--mouse-y', `${ry}px`)
|
|
93
|
-
} else {
|
|
94
|
-
c.el.style.removeProperty('--mouse-x')
|
|
95
|
-
c.el.style.removeProperty('--mouse-y')
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
rafId = 0
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
window.addEventListener(
|
|
102
|
-
'pointermove',
|
|
103
|
-
e => {
|
|
104
|
-
mouse.x = e.clientX
|
|
105
|
-
mouse.y = e.clientY
|
|
106
|
-
if (!rafId) rafId = requestAnimationFrame(updateStyles)
|
|
107
|
-
},
|
|
108
|
-
{ passive: true },
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
window.addEventListener(
|
|
112
|
-
'scroll',
|
|
113
|
-
() => {
|
|
114
|
-
if (mouse.x === -9999) return
|
|
115
|
-
if (!rafId) rafId = requestAnimationFrame(updateStyles)
|
|
116
|
-
},
|
|
117
|
-
{ passive: true },
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
window.addEventListener('mouseleave', () => {
|
|
121
|
-
mouse.x = -9999
|
|
122
|
-
mouse.y = -9999
|
|
123
|
-
if (!rafId) rafId = requestAnimationFrame(updateStyles)
|
|
124
|
-
})
|
|
125
|
-
})()
|
|
126
|
-
|
|
127
|
-
;(function initDotPattern() {
|
|
128
|
-
if (!HAS_DOM) return
|
|
129
|
-
|
|
130
|
-
const canvas = document.createElement('canvas')
|
|
131
|
-
canvas.id = 'dot-pattern-canvas'
|
|
132
|
-
canvas.style.cssText = [
|
|
133
|
-
'position: fixed',
|
|
134
|
-
'inset: 0',
|
|
135
|
-
'width: 100%',
|
|
136
|
-
'height: 100%',
|
|
137
|
-
'z-index: -1',
|
|
138
|
-
'pointer-events: none',
|
|
139
|
-
].join(';')
|
|
140
|
-
document.documentElement.prepend(canvas)
|
|
141
|
-
|
|
142
|
-
const ctx = canvas.getContext('2d')!
|
|
143
|
-
const DOT_SPACING = 28
|
|
144
|
-
const DOT_RADIUS = 1.1
|
|
145
|
-
const GLOW_RADIUS = 130
|
|
146
|
-
|
|
147
|
-
interface Dot {
|
|
148
|
-
ox: number
|
|
149
|
-
oy: number
|
|
150
|
-
x: number
|
|
151
|
-
y: number
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let dots: Dot[] = []
|
|
155
|
-
const mouse = { x: -9999, y: -9999 }
|
|
156
|
-
let isAnimating = false
|
|
157
|
-
let isPressed = false
|
|
158
|
-
/**
|
|
159
|
-
* The loop below keeps requesting frames while the pointer is anywhere on
|
|
160
|
-
* the page, which on a hidden tab is pure waste — and this console is the
|
|
161
|
-
* kind of page that lives in a background tab for hours. Ported from the
|
|
162
|
-
* example app's copy of this effect, which had the throttle where this one
|
|
163
|
-
* did not; the two files are separate copies on purpose (a plugin's client
|
|
164
|
-
* bundle and an app's script cannot share a module), so a fix to one is
|
|
165
|
-
* worth checking against the other.
|
|
166
|
-
*/
|
|
167
|
-
let isTabVisible = !document.hidden
|
|
168
|
-
|
|
169
|
-
function initDots() {
|
|
170
|
-
dots = []
|
|
171
|
-
const cols = Math.ceil(canvas.width / DOT_SPACING) + 1
|
|
172
|
-
const rows = Math.ceil(canvas.height / DOT_SPACING) + 1
|
|
173
|
-
|
|
174
|
-
for (let r = 0; r < rows; r++) {
|
|
175
|
-
for (let c = 0; c < cols; c++) {
|
|
176
|
-
const ox = c * DOT_SPACING
|
|
177
|
-
const oy = r * DOT_SPACING
|
|
178
|
-
dots.push({ ox, oy, x: ox, y: oy })
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function resize() {
|
|
184
|
-
canvas.width = window.innerWidth
|
|
185
|
-
canvas.height = window.innerHeight
|
|
186
|
-
initDots()
|
|
187
|
-
if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
|
|
188
|
-
draw()
|
|
189
|
-
} else {
|
|
190
|
-
if (!isAnimating) {
|
|
191
|
-
isAnimating = true
|
|
192
|
-
requestAnimationFrame(draw)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
interface DotTarget {
|
|
198
|
-
x: number
|
|
199
|
-
y: number
|
|
200
|
-
alpha: number
|
|
201
|
-
radius: number
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function computeDotTarget(dot: Dot): DotTarget {
|
|
205
|
-
const dx = mouse.x - dot.ox
|
|
206
|
-
const dy = mouse.y - dot.oy
|
|
207
|
-
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
208
|
-
|
|
209
|
-
let targetX = dot.ox
|
|
210
|
-
let targetY = dot.oy
|
|
211
|
-
let alpha = 0.07
|
|
212
|
-
let radius = DOT_RADIUS
|
|
213
|
-
|
|
214
|
-
if (dist < GLOW_RADIUS) {
|
|
215
|
-
const t = 1 - dist / GLOW_RADIUS
|
|
216
|
-
alpha = 0.07 + t * 0.35
|
|
217
|
-
radius = DOT_RADIUS + t * 0.9
|
|
218
|
-
|
|
219
|
-
if (dist > 0.01) {
|
|
220
|
-
const force = t * (isPressed ? 32 : 18)
|
|
221
|
-
if (isPressed) {
|
|
222
|
-
targetX = dot.ox + (dx / dist) * force
|
|
223
|
-
targetY = dot.oy + (dy / dist) * force
|
|
224
|
-
} else {
|
|
225
|
-
targetX = dot.ox - (dx / dist) * force
|
|
226
|
-
targetY = dot.oy - (dy / dist) * force
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return { x: targetX, y: targetY, alpha, radius }
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function moveDot(dot: Dot, target: DotTarget): boolean {
|
|
235
|
-
const dx = target.x - dot.x
|
|
236
|
-
const dy = target.y - dot.y
|
|
237
|
-
if (Math.abs(dx) > 0.01 || Math.abs(dy) > 0.01) {
|
|
238
|
-
dot.x += dx * 0.15
|
|
239
|
-
dot.y += dy * 0.15
|
|
240
|
-
return true
|
|
241
|
-
}
|
|
242
|
-
dot.x = target.x
|
|
243
|
-
dot.y = target.y
|
|
244
|
-
return false
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function draw() {
|
|
248
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
249
|
-
|
|
250
|
-
let needsMoreFrames = false
|
|
251
|
-
|
|
252
|
-
for (let i = 0; i < dots.length; i++) {
|
|
253
|
-
const dot = dots[i]
|
|
254
|
-
const target = computeDotTarget(dot)
|
|
255
|
-
if (moveDot(dot, target)) needsMoreFrames = true
|
|
256
|
-
|
|
257
|
-
ctx.beginPath()
|
|
258
|
-
ctx.arc(dot.x, dot.y, target.radius, 0, Math.PI * 2)
|
|
259
|
-
ctx.fillStyle = `rgba(255, 255, 255, ${target.alpha})`
|
|
260
|
-
ctx.fill()
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
window.matchMedia('(hover: hover) and (pointer: fine)').matches &&
|
|
265
|
-
isTabVisible
|
|
266
|
-
) {
|
|
267
|
-
if (mouse.x !== -9999 || needsMoreFrames) {
|
|
268
|
-
requestAnimationFrame(draw)
|
|
269
|
-
} else {
|
|
270
|
-
isAnimating = false
|
|
271
|
-
}
|
|
272
|
-
} else {
|
|
273
|
-
// Hidden tab: stop the loop and let visibilitychange restart it, or the
|
|
274
|
-
// flag would stay true and the resume below would never fire.
|
|
275
|
-
isAnimating = false
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
|
|
280
|
-
window.addEventListener('pointermove', e => {
|
|
281
|
-
mouse.x = e.clientX
|
|
282
|
-
mouse.y = e.clientY
|
|
283
|
-
if (!isAnimating) {
|
|
284
|
-
isAnimating = true
|
|
285
|
-
requestAnimationFrame(draw)
|
|
286
|
-
}
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
window.addEventListener('mouseleave', () => {
|
|
290
|
-
mouse.x = -9999
|
|
291
|
-
mouse.y = -9999
|
|
292
|
-
isPressed = false
|
|
293
|
-
if (!isAnimating) {
|
|
294
|
-
isAnimating = true
|
|
295
|
-
requestAnimationFrame(draw)
|
|
296
|
-
}
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
window.addEventListener('pointerdown', () => {
|
|
300
|
-
isPressed = true
|
|
301
|
-
if (!isAnimating) {
|
|
302
|
-
isAnimating = true
|
|
303
|
-
requestAnimationFrame(draw)
|
|
304
|
-
}
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
window.addEventListener('pointerup', () => {
|
|
308
|
-
isPressed = false
|
|
309
|
-
if (!isAnimating) {
|
|
310
|
-
isAnimating = true
|
|
311
|
-
requestAnimationFrame(draw)
|
|
312
|
-
}
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
document.addEventListener('visibilitychange', () => {
|
|
317
|
-
isTabVisible = !document.hidden
|
|
318
|
-
if (isTabVisible && !isAnimating) {
|
|
319
|
-
isAnimating = true
|
|
320
|
-
requestAnimationFrame(draw)
|
|
321
|
-
}
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
window.addEventListener('resize', resize)
|
|
325
|
-
resize()
|
|
326
|
-
draw()
|
|
327
|
-
})()
|