figrecipe 0.7.4__py3-none-any.whl → 0.9.0__py3-none-any.whl
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.
- figrecipe/__init__.py +74 -76
- figrecipe/__main__.py +12 -0
- figrecipe/_api/_panel.py +67 -0
- figrecipe/_api/_save.py +100 -4
- figrecipe/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +2 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_editor/__init__.py +36 -36
- figrecipe/_editor/_bbox/_extract.py +155 -9
- figrecipe/_editor/_bbox/_extract_text.py +124 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +157 -16
- figrecipe/_editor/_helpers.py +17 -8
- figrecipe/_editor/_hitmap/_detect.py +89 -32
- figrecipe/_editor/_hitmap_main.py +4 -4
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_render_overrides.py +38 -11
- figrecipe/_editor/_renderer.py +46 -1
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +35 -6
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +15 -173
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +37 -19
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +28 -8
- figrecipe/_editor/_templates/__init__.py +40 -2
- figrecipe/_editor/_templates/_html.py +97 -103
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +1 -1
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +94 -37
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
- figrecipe/_editor/_templates/_scripts/_files.py +274 -40
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
- figrecipe/_editor/_templates/_styles/__init__.py +9 -0
- figrecipe/_editor/_templates/_styles/_base.py +47 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +168 -3
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +98 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
- figrecipe/_editor/_templates/_styles/_modals.py +29 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
- figrecipe/_editor/_templates/_styles/_preview.py +213 -8
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +2 -0
- figrecipe/_recorder.py +28 -3
- figrecipe/_reproducer/_core.py +60 -49
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +150 -2
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +26 -1
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/styles/_style_applier.py +10 -2
- figrecipe/styles/presets/SCITEX.yaml +11 -4
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe-0.7.4.dist-info/RECORD +0 -188
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Caption overlay for demo recordings.
|
|
4
|
+
|
|
5
|
+
Shows text captions/banners during demo recordings to explain
|
|
6
|
+
what is happening in the demo.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# JavaScript template for showing caption
|
|
10
|
+
SHOW_CAPTION_JS = """
|
|
11
|
+
(text) => {
|
|
12
|
+
// Remove existing caption if any
|
|
13
|
+
const existing = document.getElementById('demo-caption');
|
|
14
|
+
if (existing) existing.remove();
|
|
15
|
+
|
|
16
|
+
// Add CSS if not exists
|
|
17
|
+
if (!document.getElementById('demo-caption-style')) {
|
|
18
|
+
const style = document.createElement('style');
|
|
19
|
+
style.id = 'demo-caption-style';
|
|
20
|
+
style.textContent = `
|
|
21
|
+
@keyframes demo-caption-fade-in {
|
|
22
|
+
0% { opacity: 0; transform: translateY(20px); }
|
|
23
|
+
100% { opacity: 1; transform: translateY(0); }
|
|
24
|
+
}
|
|
25
|
+
@keyframes demo-caption-fade-out {
|
|
26
|
+
0% { opacity: 1; transform: translateY(0); }
|
|
27
|
+
100% { opacity: 0; transform: translateY(-20px); }
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
document.head.appendChild(style);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create caption element
|
|
34
|
+
const caption = document.createElement('div');
|
|
35
|
+
caption.id = 'demo-caption';
|
|
36
|
+
caption.textContent = text;
|
|
37
|
+
caption.style.cssText = `
|
|
38
|
+
position: fixed;
|
|
39
|
+
bottom: 50px;
|
|
40
|
+
left: 50%;
|
|
41
|
+
transform: translateX(-50%);
|
|
42
|
+
background: rgba(0, 0, 0, 0.85);
|
|
43
|
+
color: white;
|
|
44
|
+
padding: 18px 40px;
|
|
45
|
+
border-radius: 10px;
|
|
46
|
+
font-size: 24px;
|
|
47
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
50
|
+
z-index: 2147483645;
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
animation: demo-caption-fade-in 0.3s ease-out forwards;
|
|
53
|
+
max-width: 80%;
|
|
54
|
+
text-align: center;
|
|
55
|
+
`;
|
|
56
|
+
document.body.appendChild(caption);
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
HIDE_CAPTION_JS = """
|
|
63
|
+
() => {
|
|
64
|
+
const caption = document.getElementById('demo-caption');
|
|
65
|
+
if (caption) {
|
|
66
|
+
caption.style.animation = 'demo-caption-fade-out 0.3s ease-out forwards';
|
|
67
|
+
setTimeout(() => caption.remove(), 300);
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# JavaScript for title screen with blur overlay
|
|
74
|
+
TITLE_SCREEN_JS = """
|
|
75
|
+
async (args) => {
|
|
76
|
+
const { title, subtitle = '', timestamp = '', duration = 2000 } = args;
|
|
77
|
+
|
|
78
|
+
// Add CSS animations if not exists
|
|
79
|
+
if (!document.getElementById('demo-title-style')) {
|
|
80
|
+
const style = document.createElement('style');
|
|
81
|
+
style.id = 'demo-title-style';
|
|
82
|
+
style.textContent = `
|
|
83
|
+
@keyframes demo-title-fade-in {
|
|
84
|
+
0% { opacity: 0; }
|
|
85
|
+
100% { opacity: 1; }
|
|
86
|
+
}
|
|
87
|
+
@keyframes demo-title-fade-out {
|
|
88
|
+
0% { opacity: 1; }
|
|
89
|
+
100% { opacity: 0; }
|
|
90
|
+
}
|
|
91
|
+
@keyframes demo-title-text-in {
|
|
92
|
+
0% { opacity: 0; transform: scale(0.9); }
|
|
93
|
+
100% { opacity: 1; transform: scale(1); }
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
document.head.appendChild(style);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create blur overlay
|
|
100
|
+
const overlay = document.createElement('div');
|
|
101
|
+
overlay.id = 'demo-title-overlay';
|
|
102
|
+
overlay.style.cssText = `
|
|
103
|
+
position: fixed;
|
|
104
|
+
top: 0;
|
|
105
|
+
left: 0;
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: 100%;
|
|
108
|
+
background: rgba(0, 0, 0, 0.7);
|
|
109
|
+
backdrop-filter: blur(8px);
|
|
110
|
+
-webkit-backdrop-filter: blur(8px);
|
|
111
|
+
z-index: 2147483646;
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
animation: demo-title-fade-in 0.5s ease-out forwards;
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
// Create title text
|
|
120
|
+
const titleEl = document.createElement('div');
|
|
121
|
+
titleEl.style.cssText = `
|
|
122
|
+
color: white;
|
|
123
|
+
font-size: 48px;
|
|
124
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
125
|
+
font-weight: 700;
|
|
126
|
+
text-align: center;
|
|
127
|
+
animation: demo-title-text-in 0.6s ease-out 0.2s both;
|
|
128
|
+
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
129
|
+
`;
|
|
130
|
+
titleEl.textContent = title;
|
|
131
|
+
overlay.appendChild(titleEl);
|
|
132
|
+
|
|
133
|
+
// Create subtitle if provided
|
|
134
|
+
if (subtitle) {
|
|
135
|
+
const subtitleEl = document.createElement('div');
|
|
136
|
+
subtitleEl.style.cssText = `
|
|
137
|
+
color: rgba(255, 255, 255, 0.8);
|
|
138
|
+
font-size: 24px;
|
|
139
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
140
|
+
font-weight: 400;
|
|
141
|
+
margin-top: 16px;
|
|
142
|
+
text-align: center;
|
|
143
|
+
animation: demo-title-text-in 0.6s ease-out 0.4s both;
|
|
144
|
+
`;
|
|
145
|
+
subtitleEl.textContent = subtitle;
|
|
146
|
+
overlay.appendChild(subtitleEl);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Create timestamp if provided
|
|
150
|
+
if (timestamp) {
|
|
151
|
+
const timestampEl = document.createElement('div');
|
|
152
|
+
timestampEl.style.cssText = `
|
|
153
|
+
color: rgba(255, 255, 255, 0.5);
|
|
154
|
+
font-size: 14px;
|
|
155
|
+
font-family: monospace;
|
|
156
|
+
margin-top: 24px;
|
|
157
|
+
text-align: center;
|
|
158
|
+
animation: demo-title-text-in 0.6s ease-out 0.5s both;
|
|
159
|
+
`;
|
|
160
|
+
timestampEl.textContent = timestamp;
|
|
161
|
+
overlay.appendChild(timestampEl);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
document.body.appendChild(overlay);
|
|
165
|
+
|
|
166
|
+
// Wait and fade out
|
|
167
|
+
await new Promise(r => setTimeout(r, duration));
|
|
168
|
+
|
|
169
|
+
overlay.style.animation = 'demo-title-fade-out 0.5s ease-out forwards';
|
|
170
|
+
await new Promise(r => setTimeout(r, 500));
|
|
171
|
+
overlay.remove();
|
|
172
|
+
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def show_caption(page, text: str) -> bool:
|
|
179
|
+
"""Show caption text overlay on page.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
page : playwright.async_api.Page
|
|
184
|
+
Playwright page object.
|
|
185
|
+
text : str
|
|
186
|
+
Caption text to display.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
bool
|
|
191
|
+
True if successful.
|
|
192
|
+
"""
|
|
193
|
+
return await page.evaluate(SHOW_CAPTION_JS, text)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def hide_caption(page) -> bool:
|
|
197
|
+
"""Hide caption overlay from page.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
page : playwright.async_api.Page
|
|
202
|
+
Playwright page object.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
bool
|
|
207
|
+
True if successful.
|
|
208
|
+
"""
|
|
209
|
+
return await page.evaluate(HIDE_CAPTION_JS)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def show_title_screen(
|
|
213
|
+
page,
|
|
214
|
+
title: str,
|
|
215
|
+
subtitle: str = "",
|
|
216
|
+
timestamp: str = "",
|
|
217
|
+
duration_ms: int = 2000,
|
|
218
|
+
) -> bool:
|
|
219
|
+
"""Show title screen with blur overlay and fade effect.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
page : playwright.async_api.Page
|
|
224
|
+
Playwright page object.
|
|
225
|
+
title : str
|
|
226
|
+
Main title text.
|
|
227
|
+
subtitle : str, optional
|
|
228
|
+
Subtitle text (default: "").
|
|
229
|
+
timestamp : str, optional
|
|
230
|
+
Timestamp to display (default: "").
|
|
231
|
+
duration_ms : int, optional
|
|
232
|
+
Duration to show title in milliseconds (default: 2000).
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
bool
|
|
237
|
+
True if successful.
|
|
238
|
+
"""
|
|
239
|
+
args = {
|
|
240
|
+
"title": title,
|
|
241
|
+
"subtitle": subtitle,
|
|
242
|
+
"timestamp": timestamp,
|
|
243
|
+
"duration": duration_ms,
|
|
244
|
+
}
|
|
245
|
+
return await page.evaluate(TITLE_SCREEN_JS, args)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# JavaScript for closing branding screen
|
|
249
|
+
CLOSING_SCREEN_JS = """
|
|
250
|
+
async (args) => {
|
|
251
|
+
const { duration = 2500 } = args;
|
|
252
|
+
|
|
253
|
+
// Add CSS animations
|
|
254
|
+
if (!document.getElementById('demo-closing-style')) {
|
|
255
|
+
const style = document.createElement('style');
|
|
256
|
+
style.id = 'demo-closing-style';
|
|
257
|
+
style.textContent = `
|
|
258
|
+
@keyframes demo-closing-fade-in {
|
|
259
|
+
0% { opacity: 0; }
|
|
260
|
+
100% { opacity: 1; }
|
|
261
|
+
}
|
|
262
|
+
@keyframes demo-closing-fade-out {
|
|
263
|
+
0% { opacity: 1; }
|
|
264
|
+
100% { opacity: 0; }
|
|
265
|
+
}
|
|
266
|
+
`;
|
|
267
|
+
document.head.appendChild(style);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Create overlay
|
|
271
|
+
const overlay = document.createElement('div');
|
|
272
|
+
overlay.id = 'demo-closing-overlay';
|
|
273
|
+
overlay.style.cssText = `
|
|
274
|
+
position: fixed;
|
|
275
|
+
top: 0;
|
|
276
|
+
left: 0;
|
|
277
|
+
width: 100%;
|
|
278
|
+
height: 100%;
|
|
279
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
280
|
+
z-index: 2147483646;
|
|
281
|
+
display: flex;
|
|
282
|
+
flex-direction: column;
|
|
283
|
+
align-items: center;
|
|
284
|
+
justify-content: center;
|
|
285
|
+
animation: demo-closing-fade-in 0.5s ease-out forwards;
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
// FigRecipe title
|
|
289
|
+
const title = document.createElement('div');
|
|
290
|
+
title.style.cssText = `
|
|
291
|
+
color: white;
|
|
292
|
+
font-size: 56px;
|
|
293
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
294
|
+
font-weight: 700;
|
|
295
|
+
margin-bottom: 16px;
|
|
296
|
+
`;
|
|
297
|
+
title.textContent = 'FigRecipe';
|
|
298
|
+
overlay.appendChild(title);
|
|
299
|
+
|
|
300
|
+
// Part of SciTeX
|
|
301
|
+
const scitex = document.createElement('div');
|
|
302
|
+
scitex.style.cssText = `
|
|
303
|
+
color: rgba(255, 255, 255, 0.7);
|
|
304
|
+
font-size: 20px;
|
|
305
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
306
|
+
margin-bottom: 24px;
|
|
307
|
+
`;
|
|
308
|
+
scitex.innerHTML = 'Part of SciTeX™';
|
|
309
|
+
overlay.appendChild(scitex);
|
|
310
|
+
|
|
311
|
+
// URL
|
|
312
|
+
const url = document.createElement('div');
|
|
313
|
+
url.style.cssText = `
|
|
314
|
+
color: #4da6ff;
|
|
315
|
+
font-size: 18px;
|
|
316
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
317
|
+
`;
|
|
318
|
+
url.textContent = 'https://scitex.ai';
|
|
319
|
+
overlay.appendChild(url);
|
|
320
|
+
|
|
321
|
+
document.body.appendChild(overlay);
|
|
322
|
+
|
|
323
|
+
// Wait and fade out
|
|
324
|
+
await new Promise(r => setTimeout(r, duration));
|
|
325
|
+
|
|
326
|
+
overlay.style.animation = 'demo-closing-fade-out 0.5s ease-out forwards';
|
|
327
|
+
await new Promise(r => setTimeout(r, 500));
|
|
328
|
+
overlay.remove();
|
|
329
|
+
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
async def show_closing_screen(page, duration_ms: int = 2500) -> bool:
|
|
336
|
+
"""Show closing branding screen with FigRecipe and SciTeX.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
page : playwright.async_api.Page
|
|
341
|
+
Playwright page object.
|
|
342
|
+
duration_ms : int, optional
|
|
343
|
+
Duration to show screen in milliseconds (default: 2500).
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
bool
|
|
348
|
+
True if successful.
|
|
349
|
+
"""
|
|
350
|
+
args = {"duration": duration_ms}
|
|
351
|
+
return await page.evaluate(CLOSING_SCREEN_JS, args)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
__all__ = ["show_caption", "hide_caption", "show_title_screen", "show_closing_screen"]
|
|
355
|
+
|
|
356
|
+
# EOF
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Click effect visualization for demo recordings.
|
|
4
|
+
|
|
5
|
+
Shows ripple/pulse animation at click locations to make
|
|
6
|
+
mouse clicks visible in recordings.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# JavaScript to inject click effect handler
|
|
10
|
+
CLICK_EFFECT_JS = """
|
|
11
|
+
() => {
|
|
12
|
+
// Add CSS animation if not exists
|
|
13
|
+
if (!document.getElementById('demo-click-style')) {
|
|
14
|
+
const style = document.createElement('style');
|
|
15
|
+
style.id = 'demo-click-style';
|
|
16
|
+
style.textContent = `
|
|
17
|
+
@keyframes demo-click-ripple {
|
|
18
|
+
0% {
|
|
19
|
+
transform: translate(-50%, -50%) scale(0);
|
|
20
|
+
opacity: 1;
|
|
21
|
+
}
|
|
22
|
+
100% {
|
|
23
|
+
transform: translate(-50%, -50%) scale(1);
|
|
24
|
+
opacity: 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
.demo-click-ripple {
|
|
28
|
+
position: fixed;
|
|
29
|
+
width: 60px;
|
|
30
|
+
height: 60px;
|
|
31
|
+
border: 4px solid #FF4444;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
pointer-events: none;
|
|
34
|
+
z-index: 2147483646;
|
|
35
|
+
animation: demo-click-ripple 0.6s ease-out forwards;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
document.head.appendChild(style);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Initialize click sound audio element
|
|
42
|
+
if (!window._demoClickAudio) {
|
|
43
|
+
window._demoClickAudio = new Audio('/static/audio/click.mp3');
|
|
44
|
+
window._demoClickAudio.volume = 0.15;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Function to play click sound
|
|
48
|
+
window._playClickSound = () => {
|
|
49
|
+
if (window._demoClickAudio) {
|
|
50
|
+
window._demoClickAudio.currentTime = 0;
|
|
51
|
+
window._demoClickAudio.play().catch(() => {});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Remove existing handler if any
|
|
56
|
+
if (window._demoClickHandler) {
|
|
57
|
+
document.removeEventListener('mousedown', window._demoClickHandler);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add click handler
|
|
61
|
+
window._demoClickHandler = (e) => {
|
|
62
|
+
// Play click sound
|
|
63
|
+
if (window._playClickSound) window._playClickSound();
|
|
64
|
+
|
|
65
|
+
const ripple = document.createElement('div');
|
|
66
|
+
ripple.className = 'demo-click-ripple';
|
|
67
|
+
ripple.style.left = e.clientX + 'px';
|
|
68
|
+
ripple.style.top = e.clientY + 'px';
|
|
69
|
+
document.body.appendChild(ripple);
|
|
70
|
+
|
|
71
|
+
// Also pulse the cursor if it exists
|
|
72
|
+
const cursor = document.getElementById('demo-cursor');
|
|
73
|
+
if (cursor) {
|
|
74
|
+
cursor.style.transform = 'translate(-50%, -50%) scale(1.3)';
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
cursor.style.transform = 'translate(-50%, -50%) scale(1)';
|
|
77
|
+
}, 150);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Remove ripple after animation
|
|
81
|
+
setTimeout(() => ripple.remove(), 600);
|
|
82
|
+
};
|
|
83
|
+
document.addEventListener('mousedown', window._demoClickHandler);
|
|
84
|
+
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
REMOVE_CLICK_EFFECT_JS = """
|
|
90
|
+
() => {
|
|
91
|
+
// Remove handler
|
|
92
|
+
if (window._demoClickHandler) {
|
|
93
|
+
document.removeEventListener('mousedown', window._demoClickHandler);
|
|
94
|
+
delete window._demoClickHandler;
|
|
95
|
+
}
|
|
96
|
+
// Remove audio element
|
|
97
|
+
if (window._demoClickAudio) {
|
|
98
|
+
window._demoClickAudio.pause();
|
|
99
|
+
delete window._demoClickAudio;
|
|
100
|
+
}
|
|
101
|
+
delete window._playClickSound;
|
|
102
|
+
// Remove style
|
|
103
|
+
const style = document.getElementById('demo-click-style');
|
|
104
|
+
if (style) style.remove();
|
|
105
|
+
// Remove any remaining ripples
|
|
106
|
+
document.querySelectorAll('.demo-click-ripple').forEach(el => el.remove());
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def inject_click_effect(page) -> bool:
|
|
113
|
+
"""Inject click ripple effect handler into page.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
page : playwright.async_api.Page
|
|
118
|
+
Playwright page object.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
bool
|
|
123
|
+
True if successful.
|
|
124
|
+
"""
|
|
125
|
+
return await page.evaluate(CLICK_EFFECT_JS)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def remove_click_effect(page) -> bool:
|
|
129
|
+
"""Remove click effect handler from page.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
page : playwright.async_api.Page
|
|
134
|
+
Playwright page object.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
bool
|
|
139
|
+
True if successful.
|
|
140
|
+
"""
|
|
141
|
+
return await page.evaluate(REMOVE_CLICK_EFFECT_JS)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
__all__ = ["inject_click_effect", "remove_click_effect"]
|
|
145
|
+
|
|
146
|
+
# EOF
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Mouse cursor visualization for demo recordings.
|
|
4
|
+
|
|
5
|
+
Injects a visible cursor element that follows mouse movements,
|
|
6
|
+
making demos more intuitive when the system cursor isn't captured.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# JavaScript to inject cursor visualization
|
|
10
|
+
CURSOR_JS = """
|
|
11
|
+
() => {
|
|
12
|
+
// Remove existing cursor if any
|
|
13
|
+
const existing = document.getElementById('demo-cursor');
|
|
14
|
+
if (existing) existing.remove();
|
|
15
|
+
|
|
16
|
+
// Create cursor element
|
|
17
|
+
const cursor = document.createElement('div');
|
|
18
|
+
cursor.id = 'demo-cursor';
|
|
19
|
+
cursor.style.cssText = `
|
|
20
|
+
position: fixed;
|
|
21
|
+
width: 24px;
|
|
22
|
+
height: 24px;
|
|
23
|
+
border: 3px solid #FF4444;
|
|
24
|
+
border-radius: 50%;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
z-index: 2147483647;
|
|
27
|
+
box-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
|
|
28
|
+
transform: translate(-50%, -50%);
|
|
29
|
+
transition: transform 0.05s ease-out;
|
|
30
|
+
`;
|
|
31
|
+
document.body.appendChild(cursor);
|
|
32
|
+
|
|
33
|
+
// Create inner dot
|
|
34
|
+
const dot = document.createElement('div');
|
|
35
|
+
dot.style.cssText = `
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 50%;
|
|
38
|
+
left: 50%;
|
|
39
|
+
width: 6px;
|
|
40
|
+
height: 6px;
|
|
41
|
+
background: #FF4444;
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
transform: translate(-50%, -50%);
|
|
44
|
+
`;
|
|
45
|
+
cursor.appendChild(dot);
|
|
46
|
+
|
|
47
|
+
// Track mouse movement
|
|
48
|
+
window._demoCursorHandler = (e) => {
|
|
49
|
+
cursor.style.left = e.clientX + 'px';
|
|
50
|
+
cursor.style.top = e.clientY + 'px';
|
|
51
|
+
};
|
|
52
|
+
document.addEventListener('mousemove', window._demoCursorHandler);
|
|
53
|
+
|
|
54
|
+
// Initial position at center
|
|
55
|
+
cursor.style.left = window.innerWidth / 2 + 'px';
|
|
56
|
+
cursor.style.top = window.innerHeight / 2 + 'px';
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
REMOVE_CURSOR_JS = """
|
|
63
|
+
() => {
|
|
64
|
+
const cursor = document.getElementById('demo-cursor');
|
|
65
|
+
if (cursor) cursor.remove();
|
|
66
|
+
if (window._demoCursorHandler) {
|
|
67
|
+
document.removeEventListener('mousemove', window._demoCursorHandler);
|
|
68
|
+
delete window._demoCursorHandler;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# JavaScript to animate cursor to a position
|
|
75
|
+
MOVE_CURSOR_JS = """
|
|
76
|
+
async (args) => {
|
|
77
|
+
const { x, y, duration = 500 } = args;
|
|
78
|
+
const cursor = document.getElementById('demo-cursor');
|
|
79
|
+
if (!cursor) return false;
|
|
80
|
+
|
|
81
|
+
// Get current position
|
|
82
|
+
const startX = parseFloat(cursor.style.left) || window.innerWidth / 2;
|
|
83
|
+
const startY = parseFloat(cursor.style.top) || window.innerHeight / 2;
|
|
84
|
+
|
|
85
|
+
// Animate to target
|
|
86
|
+
const startTime = performance.now();
|
|
87
|
+
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
function animate(currentTime) {
|
|
90
|
+
const elapsed = currentTime - startTime;
|
|
91
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
92
|
+
|
|
93
|
+
// Ease-out cubic for natural motion
|
|
94
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
95
|
+
|
|
96
|
+
const currentX = startX + (x - startX) * eased;
|
|
97
|
+
const currentY = startY + (y - startY) * eased;
|
|
98
|
+
|
|
99
|
+
cursor.style.left = currentX + 'px';
|
|
100
|
+
cursor.style.top = currentY + 'px';
|
|
101
|
+
|
|
102
|
+
if (progress < 1) {
|
|
103
|
+
requestAnimationFrame(animate);
|
|
104
|
+
} else {
|
|
105
|
+
resolve(true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
requestAnimationFrame(animate);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def inject_cursor(page) -> bool:
|
|
115
|
+
"""Inject visible cursor element into page.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
page : playwright.async_api.Page
|
|
120
|
+
Playwright page object.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
bool
|
|
125
|
+
True if successful.
|
|
126
|
+
"""
|
|
127
|
+
return await page.evaluate(CURSOR_JS)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def remove_cursor(page) -> bool:
|
|
131
|
+
"""Remove cursor visualization from page.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
page : playwright.async_api.Page
|
|
136
|
+
Playwright page object.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
bool
|
|
141
|
+
True if successful.
|
|
142
|
+
"""
|
|
143
|
+
return await page.evaluate(REMOVE_CURSOR_JS)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def move_cursor_to(page, x: float, y: float, duration_ms: int = 500) -> bool:
|
|
147
|
+
"""Animate cursor to a specific position.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
page : playwright.async_api.Page
|
|
152
|
+
Playwright page object.
|
|
153
|
+
x : float
|
|
154
|
+
Target X coordinate.
|
|
155
|
+
y : float
|
|
156
|
+
Target Y coordinate.
|
|
157
|
+
duration_ms : int, optional
|
|
158
|
+
Animation duration in milliseconds (default: 500).
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
bool
|
|
163
|
+
True if successful.
|
|
164
|
+
"""
|
|
165
|
+
args = {"x": x, "y": y, "duration": duration_ms}
|
|
166
|
+
return await page.evaluate(MOVE_CURSOR_JS, args)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def move_cursor_to_element(page, locator, duration_ms: int = 500) -> bool:
|
|
170
|
+
"""Animate cursor to an element's center.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
page : playwright.async_api.Page
|
|
175
|
+
Playwright page object.
|
|
176
|
+
locator : playwright.async_api.Locator
|
|
177
|
+
Playwright locator for target element.
|
|
178
|
+
duration_ms : int, optional
|
|
179
|
+
Animation duration in milliseconds (default: 500).
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
bool
|
|
184
|
+
True if successful.
|
|
185
|
+
"""
|
|
186
|
+
box = await locator.bounding_box()
|
|
187
|
+
if not box:
|
|
188
|
+
return False
|
|
189
|
+
x = box["x"] + box["width"] / 2
|
|
190
|
+
y = box["y"] + box["height"] / 2
|
|
191
|
+
return await move_cursor_to(page, x, y, duration_ms)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
__all__ = ["inject_cursor", "remove_cursor", "move_cursor_to", "move_cursor_to_element"]
|
|
195
|
+
|
|
196
|
+
# EOF
|