fastled 1.2.96__py3-none-any.whl → 1.2.97__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.
fastled/site/build.py CHANGED
@@ -1,449 +1,449 @@
1
- import argparse
2
- import subprocess
3
- from pathlib import Path
4
- from shutil import copytree, rmtree, which
5
-
6
- from fastled.site.examples import EXAMPLES
7
-
8
- CSS_CONTENT = """
9
- /* CSS Reset & Variables */
10
- *, *::before, *::after {
11
- box-sizing: border-box;
12
- margin: 0;
13
- padding: 0;
14
- }
15
-
16
- :root {
17
- --color-background: #121212;
18
- --color-surface: #252525;
19
- --color-surface-transparent: rgba(30, 30, 30, 0.95);
20
- --color-text: #E0E0E0;
21
- --spacing-sm: 5px;
22
- --spacing-md: 10px;
23
- --spacing-lg: 15px;
24
- --transition-speed: 0.3s;
25
- --font-family: 'Roboto Condensed', sans-serif;
26
- --nav-width: 250px;
27
- --border-radius: 5px;
28
- }
29
-
30
- /* Base Styles */
31
- body {
32
- background-color: var(--color-background);
33
- color: var(--color-text);
34
- margin: 0;
35
- padding: 0;
36
- font-family: var(--font-family);
37
- min-height: 100vh;
38
- display: grid;
39
- grid-template-rows: 1fr;
40
- }
41
-
42
- /* Splash Screen */
43
- .splash-screen {
44
- position: fixed;
45
- inset: 0;
46
- background-color: var(--color-background);
47
- display: flex;
48
- justify-content: center;
49
- align-items: center;
50
- z-index: 2000;
51
- transition: opacity var(--transition-speed) ease-out;
52
- }
53
-
54
- .splash-text {
55
- font-size: 14vw;
56
- color: var(--color-text);
57
- font-weight: 300;
58
- font-family: var(--font-family);
59
- opacity: 0;
60
- transition: opacity var(--transition-speed) ease-in;
61
- }
62
-
63
- /* Layout */
64
- .content-wrapper {
65
- position: relative;
66
- width: 100%;
67
- height: 100vh;
68
- overflow-x: hidden;
69
- }
70
-
71
- /* Navigation */
72
- .nav-trigger {
73
- position: fixed;
74
- left: var(--spacing-md);
75
- top: var(--spacing-md);
76
- padding: 15px 30px;
77
- z-index: 1001;
78
- background-color: var(--color-surface);
79
- border-radius: var(--border-radius);
80
- display: flex;
81
- align-items: center;
82
- justify-content: center;
83
- cursor: pointer;
84
- color: var(--color-text);
85
- font-size: 24px;
86
- transition: background-color var(--transition-speed) ease;
87
- }
88
-
89
- .nav-trigger:hover {
90
- background-color: var(--color-surface-transparent);
91
- }
92
-
93
- .nav-trigger .fa-chevron-down {
94
- margin-left: 10px;
95
- transition: transform var(--transition-speed) ease;
96
- }
97
-
98
- .nav-pane.visible + .nav-trigger .fa-chevron-down {
99
- transform: rotate(180deg);
100
- }
101
-
102
- .nav-pane {
103
- position: fixed;
104
- left: var(--spacing-md);
105
- top: 60px;
106
- width: var(--nav-width);
107
- height: auto;
108
- background-color: var(--color-surface-transparent);
109
- border-radius: var(--border-radius);
110
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
111
- transform: translateY(-20px);
112
- opacity: 0;
113
- pointer-events: none;
114
- transition: transform var(--transition-speed) ease,
115
- opacity var(--transition-speed) ease;
116
- }
117
-
118
- .nav-pane.visible {
119
- transform: translateY(0);
120
- opacity: 1;
121
- pointer-events: auto;
122
- }
123
-
124
- /* Main Content */
125
- .main-content {
126
- width: 100%;
127
- height: 100%;
128
- padding: 0;
129
- overflow: hidden;
130
- }
131
-
132
- #example-frame {
133
- width: 100%;
134
- height: 100%;
135
- border: none;
136
- background-color: var(--color-background);
137
- overflow: auto;
138
- }
139
-
140
- /* Example Links */
141
- .example-link {
142
- margin: var(--spacing-sm) var(--spacing-md);
143
- padding: var(--spacing-lg) var(--spacing-md);
144
- border-radius: var(--border-radius);
145
- display: block;
146
- text-decoration: none;
147
- color: var(--color-text);
148
- background-color: var(--color-surface);
149
- transition: background-color var(--transition-speed) ease-in-out,
150
- box-shadow var(--transition-speed) ease-in-out;
151
- position: relative;
152
- padding-right: 35px;
153
- }
154
-
155
- .example-link:hover {
156
- background-color: var(--color-surface-transparent);
157
- box-shadow: var(--shadow-hover, 0 0 10px rgba(255, 255, 255, 0.1));
158
- }
159
-
160
- .example-link:last-child {
161
- margin-bottom: var(--spacing-md);
162
- }
163
-
164
- /* Accessibility */
165
- @media (prefers-reduced-motion: reduce) {
166
- *, *::before, *::after {
167
- animation-duration: 0.01ms !important;
168
- animation-iteration-count: 1 !important;
169
- transition-duration: 0.01ms !important;
170
- scroll-behavior: auto !important;
171
- }
172
- }
173
- """
174
-
175
- INDEX_TEMPLATE = """<!DOCTYPE html>
176
- <html lang="en">
177
- <head>
178
- <meta charset="UTF-8">
179
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
180
- <title>FastLED Examples</title>
181
- <link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300&display=swap" rel="stylesheet">
182
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
183
- <link rel="stylesheet" href="index.css">
184
- </head>
185
- <body>
186
- <div class="splash-screen">
187
- <div class="splash-text">FastLED</div>
188
- </div>
189
- <div class="content-wrapper">
190
- <div class="nav-trigger">Examples <i class="fas fa-chevron-down"></i></div>
191
- <nav class="nav-pane">
192
- {example_links}
193
- </nav>
194
- <main class="main-content">
195
- <iframe id="example-frame" title="Example Content"></iframe>
196
- </main>
197
- </div>
198
- <script>
199
- document.addEventListener('DOMContentLoaded', function() {{
200
- const splashScreen = document.querySelector('.splash-screen');
201
- const splashText = document.querySelector('.splash-text');
202
-
203
- // Ensure splash screen always gets removed
204
- const removeSplashScreen = () => {{
205
- splashScreen.style.opacity = '0';
206
- setTimeout(() => {{
207
- splashScreen.style.display = 'none';
208
- }}, 500);
209
- }};
210
-
211
- // Set a maximum time the splash screen can stay
212
- const maxSplashTime = setTimeout(removeSplashScreen, 2000); // Reduced from 5000ms to 2000ms
213
-
214
- // Try to do nice fade-in/fade-out when possible
215
- try {{
216
- // Add a fallback timer in case font loading fails silently
217
- const fontTimeout = setTimeout(() => {{
218
- splashText.style.opacity = '1';
219
- setTimeout(removeSplashScreen, 1000);
220
- }}, 1000);
221
-
222
- Promise.all([
223
- // Wrap font loading in a timeout promise
224
- Promise.race([
225
- document.fonts.ready,
226
- new Promise((_, reject) => setTimeout(reject, 1500))
227
- ]),
228
- new Promise(resolve => {{
229
- if (document.readyState === 'complete') {{
230
- resolve();
231
- }} else {{
232
- window.addEventListener('load', resolve);
233
- }}
234
- }})
235
- ]).then(() => {{
236
- clearTimeout(maxSplashTime);
237
- clearTimeout(fontTimeout);
238
- splashText.style.opacity = '1';
239
- setTimeout(removeSplashScreen, 1500);
240
- }}).catch(() => {{
241
- // If either promise fails, ensure splash screen is removed
242
- clearTimeout(maxSplashTime);
243
- removeSplashScreen();
244
- }});
245
- }} catch (e) {{
246
- // Final fallback if anything goes wrong
247
- console.warn('Splash screen error:', e);
248
- removeSplashScreen();
249
- }}
250
- const links = document.querySelectorAll('.example-link');
251
- const iframe = document.getElementById('example-frame');
252
- const navPane = document.querySelector('.nav-pane');
253
- const navTrigger = document.querySelector('.nav-trigger');
254
-
255
- // First add checkmarks to all links
256
- links.forEach(link => {{
257
- // Add the checkmark span to each link
258
- const checkmark = document.createElement('i');
259
- checkmark.className = 'fas fa-check';
260
- checkmark.style.display = 'none';
261
- checkmark.style.position = 'absolute';
262
- checkmark.style.right = '10px';
263
- checkmark.style.top = '50%';
264
- checkmark.style.transform = 'translateY(-50%)';
265
- checkmark.style.color = '#E0E0E0';
266
- link.appendChild(checkmark);
267
- }});
268
-
269
- // Now load first example and show its checkmark
270
- if (links.length > 0) {{
271
- // Try to find SdCard example first
272
- let startLink = Array.from(links).find(link => link.textContent === 'SdCard') || links[0];
273
- iframe.src = startLink.getAttribute('href');
274
- startLink.classList.add('active');
275
- startLink.querySelector('.fa-check').style.display = 'inline-block';
276
- }}
277
-
278
- // Add click handlers
279
- links.forEach(link => {{
280
- link.addEventListener('click', function(e) {{
281
- e.preventDefault();
282
- // Hide all checkmarks
283
- links.forEach(l => {{
284
- l.querySelector('.fa-check').style.display = 'none';
285
- l.classList.remove('active');
286
- }});
287
- // Show this checkmark
288
- this.querySelector('.fa-check').style.display = 'inline-block';
289
- this.classList.add('active');
290
- iframe.src = this.getAttribute('href');
291
- hideNav(); // Hide nav after selection
292
- }});
293
- }});
294
-
295
- function showNav() {{
296
- navPane.classList.add('visible');
297
- navPane.style.opacity = '1';
298
- }}
299
-
300
- function hideNav() {{
301
- navPane.style.opacity = '0'; // Start fade out
302
- setTimeout(() => {{
303
- navPane.classList.remove('visible');
304
- }}, 300);
305
- }}
306
-
307
- // Click handlers for nav
308
- navTrigger.addEventListener('click', (e) => {{
309
- e.stopPropagation();
310
- if (navPane.classList.contains('visible')) {{
311
- hideNav();
312
- }} else {{
313
- showNav();
314
- }}
315
- }});
316
-
317
- // Close menu when clicking anywhere in the document
318
- document.addEventListener('click', (e) => {{
319
- if (navPane.classList.contains('visible') &&
320
- !navPane.contains(e.target) &&
321
- !navTrigger.contains(e.target)) {{
322
- hideNav();
323
- }}
324
- }});
325
-
326
- // Close when clicking iframe
327
- iframe.addEventListener('load', () => {{
328
- iframe.contentDocument?.addEventListener('click', () => {{
329
- if (navPane.classList.contains('visible')) {{
330
- hideNav();
331
- }}
332
- }});
333
- }});
334
-
335
- // Initial state
336
- hideNav();
337
- }});
338
- </script>
339
- </body>
340
- </html>
341
- """
342
-
343
-
344
- def _exec(cmd: str) -> None:
345
- subprocess.run(cmd, shell=True, check=True)
346
-
347
-
348
- def build_example(example: str, outputdir: Path) -> None:
349
- if not which("fastled"):
350
- raise FileNotFoundError("fastled executable not found")
351
- src_dir = outputdir / example / "src"
352
- _exec(f"fastled --init={example} {src_dir}")
353
- assert src_dir.exists()
354
- _exec(f"fastled {src_dir / example} --just-compile")
355
- fastled_dir = src_dir / example / "fastled_js"
356
- assert fastled_dir.exists(), f"fastled dir {fastled_dir} not found"
357
- # now copy it to the example dir
358
- example_dir = outputdir / example
359
- copytree(fastled_dir, example_dir, dirs_exist_ok=True)
360
- # now remove the src dir
361
- rmtree(src_dir, ignore_errors=True)
362
- print(f"Built {example} example in {example_dir}")
363
- assert (example_dir / "fastled.wasm").exists()
364
-
365
-
366
- def generate_css(outputdir: Path) -> None:
367
- css_file = outputdir / "index.css"
368
- # with open(css_file, "w") as f:
369
- # f.write(CSS_CONTENT, encoding="utf-8")
370
- css_file.write_text(CSS_CONTENT, encoding="utf-8")
371
-
372
-
373
- def build_index_html(outputdir: Path) -> None:
374
- outputdir = outputdir
375
- assert (
376
- outputdir.exists()
377
- ), f"Output directory {outputdir} not found, you should run build_example first"
378
- index_html = outputdir / "index.html"
379
-
380
- examples = [f for f in outputdir.iterdir() if f.is_dir()]
381
- examples = sorted(examples)
382
-
383
- example_links = "\n".join(
384
- f' <a class="example-link" href="{example.name}/index.html">{example.name}</a>'
385
- for example in examples
386
- )
387
-
388
- with open(index_html, "w") as f:
389
- f.write(INDEX_TEMPLATE.format(example_links=example_links))
390
-
391
-
392
- def parse_args() -> argparse.Namespace:
393
- parser = argparse.ArgumentParser(description="Build FastLED example site")
394
- parser.add_argument(
395
- "--outputdir", type=Path, help="Output directory", required=True
396
- )
397
- parser.add_argument(
398
- "--fast",
399
- action="store_true",
400
- help="Skip regenerating existing examples, only rebuild index.html and CSS",
401
- )
402
- return parser.parse_args()
403
-
404
-
405
- def build(outputdir: Path, fast: bool | None = None, check=False) -> list[Exception]:
406
- outputdir = outputdir
407
- fast = fast or False
408
- errors: list[Exception] = []
409
-
410
- for example in EXAMPLES:
411
- example_dir = outputdir / example
412
- if not fast or not example_dir.exists():
413
- try:
414
- build_example(example=example, outputdir=outputdir)
415
- except Exception as e:
416
- if check:
417
- raise
418
- errors.append(e)
419
-
420
- try:
421
- generate_css(outputdir=outputdir)
422
- except Exception as e:
423
- if check:
424
- raise
425
- errors.append(e)
426
-
427
- try:
428
- build_index_html(outputdir=outputdir)
429
- except Exception as e:
430
- if check:
431
- raise
432
- errors.append(e)
433
-
434
- return errors
435
-
436
-
437
- def main() -> int:
438
- args = parse_args()
439
- outputdir = args.outputdir
440
- fast = args.fast
441
- build(outputdir=outputdir, fast=fast)
442
- return 0
443
-
444
-
445
- if __name__ == "__main__":
446
- import sys
447
-
448
- sys.argv.append("--fast")
449
- main()
1
+ import argparse
2
+ import subprocess
3
+ from pathlib import Path
4
+ from shutil import copytree, rmtree, which
5
+
6
+ from fastled.site.examples import EXAMPLES
7
+
8
+ CSS_CONTENT = """
9
+ /* CSS Reset & Variables */
10
+ *, *::before, *::after {
11
+ box-sizing: border-box;
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ :root {
17
+ --color-background: #121212;
18
+ --color-surface: #252525;
19
+ --color-surface-transparent: rgba(30, 30, 30, 0.95);
20
+ --color-text: #E0E0E0;
21
+ --spacing-sm: 5px;
22
+ --spacing-md: 10px;
23
+ --spacing-lg: 15px;
24
+ --transition-speed: 0.3s;
25
+ --font-family: 'Roboto Condensed', sans-serif;
26
+ --nav-width: 250px;
27
+ --border-radius: 5px;
28
+ }
29
+
30
+ /* Base Styles */
31
+ body {
32
+ background-color: var(--color-background);
33
+ color: var(--color-text);
34
+ margin: 0;
35
+ padding: 0;
36
+ font-family: var(--font-family);
37
+ min-height: 100vh;
38
+ display: grid;
39
+ grid-template-rows: 1fr;
40
+ }
41
+
42
+ /* Splash Screen */
43
+ .splash-screen {
44
+ position: fixed;
45
+ inset: 0;
46
+ background-color: var(--color-background);
47
+ display: flex;
48
+ justify-content: center;
49
+ align-items: center;
50
+ z-index: 2000;
51
+ transition: opacity var(--transition-speed) ease-out;
52
+ }
53
+
54
+ .splash-text {
55
+ font-size: 14vw;
56
+ color: var(--color-text);
57
+ font-weight: 300;
58
+ font-family: var(--font-family);
59
+ opacity: 0;
60
+ transition: opacity var(--transition-speed) ease-in;
61
+ }
62
+
63
+ /* Layout */
64
+ .content-wrapper {
65
+ position: relative;
66
+ width: 100%;
67
+ height: 100vh;
68
+ overflow-x: hidden;
69
+ }
70
+
71
+ /* Navigation */
72
+ .nav-trigger {
73
+ position: fixed;
74
+ left: var(--spacing-md);
75
+ top: var(--spacing-md);
76
+ padding: 15px 30px;
77
+ z-index: 1001;
78
+ background-color: var(--color-surface);
79
+ border-radius: var(--border-radius);
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ cursor: pointer;
84
+ color: var(--color-text);
85
+ font-size: 24px;
86
+ transition: background-color var(--transition-speed) ease;
87
+ }
88
+
89
+ .nav-trigger:hover {
90
+ background-color: var(--color-surface-transparent);
91
+ }
92
+
93
+ .nav-trigger .fa-chevron-down {
94
+ margin-left: 10px;
95
+ transition: transform var(--transition-speed) ease;
96
+ }
97
+
98
+ .nav-pane.visible + .nav-trigger .fa-chevron-down {
99
+ transform: rotate(180deg);
100
+ }
101
+
102
+ .nav-pane {
103
+ position: fixed;
104
+ left: var(--spacing-md);
105
+ top: 60px;
106
+ width: var(--nav-width);
107
+ height: auto;
108
+ background-color: var(--color-surface-transparent);
109
+ border-radius: var(--border-radius);
110
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
111
+ transform: translateY(-20px);
112
+ opacity: 0;
113
+ pointer-events: none;
114
+ transition: transform var(--transition-speed) ease,
115
+ opacity var(--transition-speed) ease;
116
+ }
117
+
118
+ .nav-pane.visible {
119
+ transform: translateY(0);
120
+ opacity: 1;
121
+ pointer-events: auto;
122
+ }
123
+
124
+ /* Main Content */
125
+ .main-content {
126
+ width: 100%;
127
+ height: 100%;
128
+ padding: 0;
129
+ overflow: hidden;
130
+ }
131
+
132
+ #example-frame {
133
+ width: 100%;
134
+ height: 100%;
135
+ border: none;
136
+ background-color: var(--color-background);
137
+ overflow: auto;
138
+ }
139
+
140
+ /* Example Links */
141
+ .example-link {
142
+ margin: var(--spacing-sm) var(--spacing-md);
143
+ padding: var(--spacing-lg) var(--spacing-md);
144
+ border-radius: var(--border-radius);
145
+ display: block;
146
+ text-decoration: none;
147
+ color: var(--color-text);
148
+ background-color: var(--color-surface);
149
+ transition: background-color var(--transition-speed) ease-in-out,
150
+ box-shadow var(--transition-speed) ease-in-out;
151
+ position: relative;
152
+ padding-right: 35px;
153
+ }
154
+
155
+ .example-link:hover {
156
+ background-color: var(--color-surface-transparent);
157
+ box-shadow: var(--shadow-hover, 0 0 10px rgba(255, 255, 255, 0.1));
158
+ }
159
+
160
+ .example-link:last-child {
161
+ margin-bottom: var(--spacing-md);
162
+ }
163
+
164
+ /* Accessibility */
165
+ @media (prefers-reduced-motion: reduce) {
166
+ *, *::before, *::after {
167
+ animation-duration: 0.01ms !important;
168
+ animation-iteration-count: 1 !important;
169
+ transition-duration: 0.01ms !important;
170
+ scroll-behavior: auto !important;
171
+ }
172
+ }
173
+ """
174
+
175
+ INDEX_TEMPLATE = """<!DOCTYPE html>
176
+ <html lang="en">
177
+ <head>
178
+ <meta charset="UTF-8">
179
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
180
+ <title>FastLED Examples</title>
181
+ <link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300&display=swap" rel="stylesheet">
182
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
183
+ <link rel="stylesheet" href="index.css">
184
+ </head>
185
+ <body>
186
+ <div class="splash-screen">
187
+ <div class="splash-text">FastLED</div>
188
+ </div>
189
+ <div class="content-wrapper">
190
+ <div class="nav-trigger">Examples <i class="fas fa-chevron-down"></i></div>
191
+ <nav class="nav-pane">
192
+ {example_links}
193
+ </nav>
194
+ <main class="main-content">
195
+ <iframe id="example-frame" title="Example Content"></iframe>
196
+ </main>
197
+ </div>
198
+ <script>
199
+ document.addEventListener('DOMContentLoaded', function() {{
200
+ const splashScreen = document.querySelector('.splash-screen');
201
+ const splashText = document.querySelector('.splash-text');
202
+
203
+ // Ensure splash screen always gets removed
204
+ const removeSplashScreen = () => {{
205
+ splashScreen.style.opacity = '0';
206
+ setTimeout(() => {{
207
+ splashScreen.style.display = 'none';
208
+ }}, 500);
209
+ }};
210
+
211
+ // Set a maximum time the splash screen can stay
212
+ const maxSplashTime = setTimeout(removeSplashScreen, 2000); // Reduced from 5000ms to 2000ms
213
+
214
+ // Try to do nice fade-in/fade-out when possible
215
+ try {{
216
+ // Add a fallback timer in case font loading fails silently
217
+ const fontTimeout = setTimeout(() => {{
218
+ splashText.style.opacity = '1';
219
+ setTimeout(removeSplashScreen, 1000);
220
+ }}, 1000);
221
+
222
+ Promise.all([
223
+ // Wrap font loading in a timeout promise
224
+ Promise.race([
225
+ document.fonts.ready,
226
+ new Promise((_, reject) => setTimeout(reject, 1500))
227
+ ]),
228
+ new Promise(resolve => {{
229
+ if (document.readyState === 'complete') {{
230
+ resolve();
231
+ }} else {{
232
+ window.addEventListener('load', resolve);
233
+ }}
234
+ }})
235
+ ]).then(() => {{
236
+ clearTimeout(maxSplashTime);
237
+ clearTimeout(fontTimeout);
238
+ splashText.style.opacity = '1';
239
+ setTimeout(removeSplashScreen, 1500);
240
+ }}).catch(() => {{
241
+ // If either promise fails, ensure splash screen is removed
242
+ clearTimeout(maxSplashTime);
243
+ removeSplashScreen();
244
+ }});
245
+ }} catch (e) {{
246
+ // Final fallback if anything goes wrong
247
+ console.warn('Splash screen error:', e);
248
+ removeSplashScreen();
249
+ }}
250
+ const links = document.querySelectorAll('.example-link');
251
+ const iframe = document.getElementById('example-frame');
252
+ const navPane = document.querySelector('.nav-pane');
253
+ const navTrigger = document.querySelector('.nav-trigger');
254
+
255
+ // First add checkmarks to all links
256
+ links.forEach(link => {{
257
+ // Add the checkmark span to each link
258
+ const checkmark = document.createElement('i');
259
+ checkmark.className = 'fas fa-check';
260
+ checkmark.style.display = 'none';
261
+ checkmark.style.position = 'absolute';
262
+ checkmark.style.right = '10px';
263
+ checkmark.style.top = '50%';
264
+ checkmark.style.transform = 'translateY(-50%)';
265
+ checkmark.style.color = '#E0E0E0';
266
+ link.appendChild(checkmark);
267
+ }});
268
+
269
+ // Now load first example and show its checkmark
270
+ if (links.length > 0) {{
271
+ // Try to find SdCard example first
272
+ let startLink = Array.from(links).find(link => link.textContent === 'SdCard') || links[0];
273
+ iframe.src = startLink.getAttribute('href');
274
+ startLink.classList.add('active');
275
+ startLink.querySelector('.fa-check').style.display = 'inline-block';
276
+ }}
277
+
278
+ // Add click handlers
279
+ links.forEach(link => {{
280
+ link.addEventListener('click', function(e) {{
281
+ e.preventDefault();
282
+ // Hide all checkmarks
283
+ links.forEach(l => {{
284
+ l.querySelector('.fa-check').style.display = 'none';
285
+ l.classList.remove('active');
286
+ }});
287
+ // Show this checkmark
288
+ this.querySelector('.fa-check').style.display = 'inline-block';
289
+ this.classList.add('active');
290
+ iframe.src = this.getAttribute('href');
291
+ hideNav(); // Hide nav after selection
292
+ }});
293
+ }});
294
+
295
+ function showNav() {{
296
+ navPane.classList.add('visible');
297
+ navPane.style.opacity = '1';
298
+ }}
299
+
300
+ function hideNav() {{
301
+ navPane.style.opacity = '0'; // Start fade out
302
+ setTimeout(() => {{
303
+ navPane.classList.remove('visible');
304
+ }}, 300);
305
+ }}
306
+
307
+ // Click handlers for nav
308
+ navTrigger.addEventListener('click', (e) => {{
309
+ e.stopPropagation();
310
+ if (navPane.classList.contains('visible')) {{
311
+ hideNav();
312
+ }} else {{
313
+ showNav();
314
+ }}
315
+ }});
316
+
317
+ // Close menu when clicking anywhere in the document
318
+ document.addEventListener('click', (e) => {{
319
+ if (navPane.classList.contains('visible') &&
320
+ !navPane.contains(e.target) &&
321
+ !navTrigger.contains(e.target)) {{
322
+ hideNav();
323
+ }}
324
+ }});
325
+
326
+ // Close when clicking iframe
327
+ iframe.addEventListener('load', () => {{
328
+ iframe.contentDocument?.addEventListener('click', () => {{
329
+ if (navPane.classList.contains('visible')) {{
330
+ hideNav();
331
+ }}
332
+ }});
333
+ }});
334
+
335
+ // Initial state
336
+ hideNav();
337
+ }});
338
+ </script>
339
+ </body>
340
+ </html>
341
+ """
342
+
343
+
344
+ def _exec(cmd: str) -> None:
345
+ subprocess.run(cmd, shell=True, check=True)
346
+
347
+
348
+ def build_example(example: str, outputdir: Path) -> None:
349
+ if not which("fastled"):
350
+ raise FileNotFoundError("fastled executable not found")
351
+ src_dir = outputdir / example / "src"
352
+ _exec(f"fastled --init={example} {src_dir}")
353
+ assert src_dir.exists()
354
+ _exec(f"fastled {src_dir / example} --just-compile")
355
+ fastled_dir = src_dir / example / "fastled_js"
356
+ assert fastled_dir.exists(), f"fastled dir {fastled_dir} not found"
357
+ # now copy it to the example dir
358
+ example_dir = outputdir / example
359
+ copytree(fastled_dir, example_dir, dirs_exist_ok=True)
360
+ # now remove the src dir
361
+ rmtree(src_dir, ignore_errors=True)
362
+ print(f"Built {example} example in {example_dir}")
363
+ assert (example_dir / "fastled.wasm").exists()
364
+
365
+
366
+ def generate_css(outputdir: Path) -> None:
367
+ css_file = outputdir / "index.css"
368
+ # with open(css_file, "w") as f:
369
+ # f.write(CSS_CONTENT, encoding="utf-8")
370
+ css_file.write_text(CSS_CONTENT, encoding="utf-8")
371
+
372
+
373
+ def build_index_html(outputdir: Path) -> None:
374
+ outputdir = outputdir
375
+ assert (
376
+ outputdir.exists()
377
+ ), f"Output directory {outputdir} not found, you should run build_example first"
378
+ index_html = outputdir / "index.html"
379
+
380
+ examples = [f for f in outputdir.iterdir() if f.is_dir()]
381
+ examples = sorted(examples)
382
+
383
+ example_links = "\n".join(
384
+ f' <a class="example-link" href="{example.name}/index.html">{example.name}</a>'
385
+ for example in examples
386
+ )
387
+
388
+ with open(index_html, "w") as f:
389
+ f.write(INDEX_TEMPLATE.format(example_links=example_links))
390
+
391
+
392
+ def parse_args() -> argparse.Namespace:
393
+ parser = argparse.ArgumentParser(description="Build FastLED example site")
394
+ parser.add_argument(
395
+ "--outputdir", type=Path, help="Output directory", required=True
396
+ )
397
+ parser.add_argument(
398
+ "--fast",
399
+ action="store_true",
400
+ help="Skip regenerating existing examples, only rebuild index.html and CSS",
401
+ )
402
+ return parser.parse_args()
403
+
404
+
405
+ def build(outputdir: Path, fast: bool | None = None, check=False) -> list[Exception]:
406
+ outputdir = outputdir
407
+ fast = fast or False
408
+ errors: list[Exception] = []
409
+
410
+ for example in EXAMPLES:
411
+ example_dir = outputdir / example
412
+ if not fast or not example_dir.exists():
413
+ try:
414
+ build_example(example=example, outputdir=outputdir)
415
+ except Exception as e:
416
+ if check:
417
+ raise
418
+ errors.append(e)
419
+
420
+ try:
421
+ generate_css(outputdir=outputdir)
422
+ except Exception as e:
423
+ if check:
424
+ raise
425
+ errors.append(e)
426
+
427
+ try:
428
+ build_index_html(outputdir=outputdir)
429
+ except Exception as e:
430
+ if check:
431
+ raise
432
+ errors.append(e)
433
+
434
+ return errors
435
+
436
+
437
+ def main() -> int:
438
+ args = parse_args()
439
+ outputdir = args.outputdir
440
+ fast = args.fast
441
+ build(outputdir=outputdir, fast=fast)
442
+ return 0
443
+
444
+
445
+ if __name__ == "__main__":
446
+ import sys
447
+
448
+ sys.argv.append("--fast")
449
+ main()