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.
Files changed (143) hide show
  1. figrecipe/__init__.py +74 -76
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/_panel.py +67 -0
  4. figrecipe/_api/_save.py +100 -4
  5. figrecipe/_cli/__init__.py +7 -0
  6. figrecipe/_cli/_compose.py +87 -0
  7. figrecipe/_cli/_convert.py +117 -0
  8. figrecipe/_cli/_crop.py +82 -0
  9. figrecipe/_cli/_edit.py +70 -0
  10. figrecipe/_cli/_extract.py +128 -0
  11. figrecipe/_cli/_fonts.py +47 -0
  12. figrecipe/_cli/_info.py +67 -0
  13. figrecipe/_cli/_main.py +58 -0
  14. figrecipe/_cli/_reproduce.py +79 -0
  15. figrecipe/_cli/_style.py +77 -0
  16. figrecipe/_cli/_validate.py +66 -0
  17. figrecipe/_cli/_version.py +50 -0
  18. figrecipe/_composition/__init__.py +32 -0
  19. figrecipe/_composition/_alignment.py +452 -0
  20. figrecipe/_composition/_compose.py +179 -0
  21. figrecipe/_composition/_import_axes.py +127 -0
  22. figrecipe/_composition/_visibility.py +125 -0
  23. figrecipe/_dev/__init__.py +2 -0
  24. figrecipe/_dev/browser/__init__.py +69 -0
  25. figrecipe/_dev/browser/_audio.py +240 -0
  26. figrecipe/_dev/browser/_caption.py +356 -0
  27. figrecipe/_dev/browser/_click_effect.py +146 -0
  28. figrecipe/_dev/browser/_cursor.py +196 -0
  29. figrecipe/_dev/browser/_highlight.py +105 -0
  30. figrecipe/_dev/browser/_narration.py +237 -0
  31. figrecipe/_dev/browser/_recorder.py +446 -0
  32. figrecipe/_dev/browser/_utils.py +178 -0
  33. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  34. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  35. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  36. figrecipe/_editor/__init__.py +36 -36
  37. figrecipe/_editor/_bbox/_extract.py +155 -9
  38. figrecipe/_editor/_bbox/_extract_text.py +124 -0
  39. figrecipe/_editor/_call_overrides.py +183 -0
  40. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  41. figrecipe/_editor/_figure_layout.py +211 -0
  42. figrecipe/_editor/_flask_app.py +157 -16
  43. figrecipe/_editor/_helpers.py +17 -8
  44. figrecipe/_editor/_hitmap/_detect.py +89 -32
  45. figrecipe/_editor/_hitmap_main.py +4 -4
  46. figrecipe/_editor/_overrides.py +4 -1
  47. figrecipe/_editor/_plot_types_registry.py +190 -0
  48. figrecipe/_editor/_render_overrides.py +38 -11
  49. figrecipe/_editor/_renderer.py +46 -1
  50. figrecipe/_editor/_routes_annotation.py +114 -0
  51. figrecipe/_editor/_routes_axis.py +35 -6
  52. figrecipe/_editor/_routes_captions.py +130 -0
  53. figrecipe/_editor/_routes_composition.py +270 -0
  54. figrecipe/_editor/_routes_core.py +15 -173
  55. figrecipe/_editor/_routes_datatable.py +364 -0
  56. figrecipe/_editor/_routes_element.py +37 -19
  57. figrecipe/_editor/_routes_files.py +443 -0
  58. figrecipe/_editor/_routes_image.py +200 -0
  59. figrecipe/_editor/_routes_snapshot.py +94 -0
  60. figrecipe/_editor/_routes_style.py +28 -8
  61. figrecipe/_editor/_templates/__init__.py +40 -2
  62. figrecipe/_editor/_templates/_html.py +97 -103
  63. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  64. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  65. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  66. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  67. figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
  68. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  69. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  70. figrecipe/_editor/_templates/_scripts/_api.py +1 -1
  71. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  72. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  73. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  74. figrecipe/_editor/_templates/_scripts/_core.py +94 -37
  75. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  76. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  77. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  78. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  79. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  80. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  81. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  82. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  83. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  84. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  85. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  86. figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
  87. figrecipe/_editor/_templates/_scripts/_files.py +274 -40
  88. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  89. figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
  90. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  91. figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
  92. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  93. figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
  94. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  95. figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
  96. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  97. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  98. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  99. figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
  100. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  101. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  102. figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
  103. figrecipe/_editor/_templates/_styles/__init__.py +9 -0
  104. figrecipe/_editor/_templates/_styles/_base.py +47 -0
  105. figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
  106. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  107. figrecipe/_editor/_templates/_styles/_controls.py +168 -3
  108. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  109. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  110. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  111. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  112. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  113. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  114. figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
  115. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  116. figrecipe/_editor/_templates/_styles/_forms.py +98 -0
  117. figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
  118. figrecipe/_editor/_templates/_styles/_modals.py +29 -0
  119. figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
  120. figrecipe/_editor/_templates/_styles/_preview.py +213 -8
  121. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  122. figrecipe/_editor/static/audio/click.mp3 +0 -0
  123. figrecipe/_editor/static/click.mp3 +0 -0
  124. figrecipe/_editor/static/icons/favicon.ico +0 -0
  125. figrecipe/_integrations/__init__.py +17 -0
  126. figrecipe/_integrations/_scitex_stats.py +298 -0
  127. figrecipe/_params/_DECORATION_METHODS.py +2 -0
  128. figrecipe/_recorder.py +28 -3
  129. figrecipe/_reproducer/_core.py +60 -49
  130. figrecipe/_utils/__init__.py +3 -0
  131. figrecipe/_utils/_bundle.py +205 -0
  132. figrecipe/_wrappers/_axes.py +150 -2
  133. figrecipe/_wrappers/_caption_generator.py +218 -0
  134. figrecipe/_wrappers/_figure.py +26 -1
  135. figrecipe/_wrappers/_stat_annotation.py +274 -0
  136. figrecipe/styles/_style_applier.py +10 -2
  137. figrecipe/styles/presets/SCITEX.yaml +11 -4
  138. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
  139. figrecipe-0.9.0.dist-info/RECORD +277 -0
  140. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  141. figrecipe-0.7.4.dist-info/RECORD +0 -188
  142. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  143. {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