PageSourceSearch

https://ludicon.com/

html ludicon.com collected 2026-10-01 14:42:13 UTC 33,226 bytes, 800 lines download raw bytes

1<!doctype html>
2<html lang="en">
3  <head>
4    <meta charset="utf-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1" />
6    <title>Ludicon</title>
7    <link rel="canonical" href="https://ludicon.com/" />
8
9    <meta
10      name="description"
11      content="Ludicon builds best-in-class texture and mesh processing tools. Focused on Spark and spark.js for real-time GPU compression."
12    />
13    <meta property="og:title" content="Ludicon" />
14    <meta
15      property="og:description"
16      content="Texture & mesh processing tools. Spark and spark.js for real-time GPU compression."
17    />
18    <meta property="og:image" content="https://ludicon.com/assets/ludicon-card.jpg" />
19    <meta name="twitter:card" content="summary_large_image" />
20    <meta name="twitter:title" content="Ludicon" />
21    <meta
22      name="twitter:description"
23      content="Texture & mesh processing tools. Spark and spark.js for real-time GPU compression."
24    />
25    <meta name="twitter:image" content="https://ludicon.com/assets/ludicon-card.jpg" />
26
27    <!-- Google tag (gtag.js) -->
vendor: 68 bytes, lines 27-28
27
28    <script async src="https://www.googletagmanager.com/gtag/js?id=
28G-N5MLWTT7JM
vendor: 16 bytes, lines 28-29
28"></script>
29    
29<script>
30      
vendor: 162 bytes, lines 30-35
30window.dataLayer = window.dataLayer || [];
31      function gtag() {
32        dataLayer.push(arguments);
33      }
34      gtag("js", new Date());
35      gtag("config", "
35G-N5MLWTT7JM
vendor: 8 bytes, lines 35-36
35");
36    
36</script>
36
37    
37<script
38      src="https://cdn.counter.dev/script.js"
39      data-id="d35a48c2-0e1d-4b08-972d-bd0016273314"
40      data-utcoffset="-7"
41    ></script>
41
42
43    <link rel="preconnect" href="https://fonts.googleapis.com" />
44    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
45    <link
46      href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap"
47      rel="stylesheet"
48    />
49    <link rel="stylesheet" href="styles.css" />
50    
50<script src="nav.js" defer></script>
50
51  </head>
52  <body>
53    <svg class="sr-only" aria-hidden="true">
54      <symbol id="octicon-mark-github" viewBox="0 0 24 24">
55        <path
56          d="M12 .5C5.65.5.5 5.65.5 12c0 5.1 3.29 9.42 7.86 10.95.58.1.79-.25.79-.56v-2.02c-3.2.7-3.88-1.37-3.88-1.37-.53-1.34-1.3-1.7-1.3-1.7-1.06-.72.08-.71.08-.71 1.17.08 1.79 1.2 1.79 1.2 1.04 1.78 2.73 1.27 3.4.97.1-.75.41-1.27.75-1.56-2.55-.29-5.23-1.28-5.23-5.7 0-1.26.45-2.29 1.19-3.1-.12-.3-.52-1.52.11-3.16 0 0 .98-.31 3.2 1.18.93-.26 1.93-.39 2.93-.39 1 0 2 .13 2.93.39 2.22-1.49 3.2-1.18 3.2-1.18.63 1.64.23 2.86.11 3.16.74.81 1.19 1.84 1.19 3.1 0 4.43-2.69 5.4-5.25 5.68.42.37.8 1.1.8 2.22v3.29c0 .31.21.67.8.56A10.52 10.52 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5z"
57        ></path>
58      </symbol>
59      <symbol id="octicon-star" viewBox="0 0 16 16">
60        <path
61          d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"
62        ></path>
63      </symbol>
64      <symbol id="octicon-repo-forked" viewBox="0 0 16 16">
65        <path
66          d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"
67        ></path>
68      </symbol>
69    </svg>
70
71    <div class="nav-wrap">
72      <nav class="nav">
73        <a href="https://ludicon.com/" class="brand">
74          <img
75            class="logo"
76            src="spark/assets/image-20230303125815841.png"
77            alt="Ludicon logo"
78          />Ludicon
79        </a>
80        <button class="nav-toggle" type="button" aria-expanded="false" aria-controls="nav-links">
81          <span aria-hidden="true"></span>
82          Menu
83        </button>
84        <div class="nav-links" id="nav-links">
85          <a href="#products">Products</a>
86          <a href="#clients">Clients</a>
87          <a href="#blog">Updates</a>
88          <a href="#open-source">Open Source</a>
89          <a href="#about">About</a>
90        </div>
91      </nav>
92    </div>
93
94    <div class="page">
95      <section class="hero" aria-labelledby="hero-title">
96        <div>
97          <h1 class="hero-title" id="hero-title">Real-Time Texture Compression</h1>
98          <p class="hero-lead">
99            Advanced texture compression from decades of real-time graphics experience.
100          </p>
101        </div>
102        <div class="hero-panel">
103          <h3>We solve hard problems in real-time graphics</h3>
104          <ul class="hero-list">
105            <li>Production-ready GPU texture codecs</li>
106            <li>Custom compression technology</li>
107            <li>Platform-specific optimizations</li>
108            <li>Deep graphics expertise</li>
109          </ul>
110        </div>
111        <div class="hero-actions">
112          <a class="button primary" href="#about">Get in Touch</a>
113        </div>
114      </section>
115
116      <section class="section" id="products">
117        <h2 class="section-title">Products</h2>
118        <div class="product-grid">
119          <a class="card tile" href="spark/">
120            <div class="card-body">
121              <h3 class="product-title">
122                <em>Spark</em
123                ><img src="spark/assets/image-20230303125815841.png" alt="spark logo" />
124              </h3>
125              <p class="product-subtitle">Real-Time GPU Texture Codecs</p>
126              <div class="pill-row">
127                <span class="pill">Direct3D</span>
128                <span class="pill">Vulkan</span>
129                <span class="pill">Metal</span>
130                <span class="pill">PlayStation</span>
131                <span class="pill">WebGPU</span>
132                <span class="pill">WebGL</span>
133              </div>
134            </div>
135          </a>
136          <a class="card tile" href="sparkjs/">
137            <div class="card-body">
138              <h3 class="product-title">
139                <em>spark.js</em
140                ><img src="spark/assets/image-20230303125815841.png" alt="spark logo" />
141              </h3>
142              <p class="product-subtitle">Real-Time GPU Texture Codecs for the Web</p>
143              <div class="pill-row">
144                <span class="pill">WebGPU</span>
145                <span class="pill">WebGL</span>
146              </div>
147            </div>
148          </a>
149          <!--a class="card tile" href="flare/">
150            <div class="card-body">
151              <h3 class="product-title">
152                <em>Flare</em><img src="spark/assets/flare.png" alt="flare logo" />
153              </h3>
154              <p class="product-subtitle">Coming soon</p>
155              <div class="pill-row"></div>
156            </div>
157          </a>
158          <a class="card tile" href="flare/">
159            <div class="card-body">
160              <h3 class="product-title">
161                <em>Blaze</em><img src="spark/assets/blaze2.png" alt="blaze logo" />
162              </h3>
163              <p class="product-subtitle">Coming soon</p>
164              <div class="pill-row"></div>
165            </div>
166          </a-->
167        </div>
168      </section>
169
170      <section class="section" id="clients">
171        <h2 class="section-title">Clients</h2>
172        <p class="section-lead">
173          Industry-leading studios trust Ludicon for their texture compression needs.
174        </p>
175        <div class="client-grid">
176          <a class="client-tile tile" href="https://2k.com" target="_blank" rel="noreferrer">
177            <img class="logo-square" src="spark/assets/clients/2k-logo.png" alt="2K" />
178          </a>
179          <a class="client-tile tile" href="https://netflix.com" target="_blank" rel="noreferrer">
180            <img src="spark/assets/clients/Netflix-logo2.png" alt="Netflix" />
181          </a>
182          <a class="client-tile tile" href="https://www.maxon.net" target="_blank" rel="noreferrer">
183            <img src="spark/assets/clients/Maxon.svg" alt="Maxon" />
184          </a>
185          <a
186            class="client-tile tile"
187            href="https://infiniteflight.com"
188            target="_blank"
189            rel="noreferrer"
190          >
191            <img src="spark/assets/clients/InfiniteFlight-logo.svg" alt="Infinite Flight" />
192          </a>
193          <a class="client-tile tile" href="https://hypehype.com" target="_blank" rel="noreferrer">
194            <img src="spark/assets/clients/hypehype-logo.svg" alt="HypeHype" />
195          </a>
196          <a
197            class="client-tile tile invert-light"
198            href="https://wolfire.com"
199            target="_blank"
200            rel="noreferrer"
201          >
202            <img src="spark/assets/clients/wolfire-games-logo.png" alt="Wolfire Games" />
203          </a>
204        </div>
205      </section>
206
207      <section class="section" id="blog">
208        <h2 class="section-title">Updates</h2>
209        <p class="section-lead">Technical updates, release notes, and write-ups from our blog.</p>
210        <ul class="blog-list" id="spark-posts"></ul>
211        <span class="blog-more" id="spark-posts-more">Loading posts…</span>
212      </section>
213
214      <section class="section" id="open-source">
215        <h2 class="section-title">Open Source</h2>
216        <p class="section-lead">Selected open-source projects we’ve created over the years.</p>
217        <div class="oss-grid" id="oss-grid">
218          <a
219            class="card tile oss-card"
220            href="https://github.com/Ludicon/ic-metrics"
221            target="_blank"
222            rel="noreferrer"
223            data-repo="Ludicon/ic-metrics"
224          >
225            <div class="card-body">
226              <h3 class="oss-name">
227                <span class="icon" aria-hidden="true">
228                  <svg viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="GitHub">
229                    <use href="#octicon-mark-github"></use>
230                  </svg> </span
231                >ic-metrics
232              </h3>
233              <p class="oss-desc" data-oss-desc>Loading description…</p>
234              <div class="oss-meta">
235                <span
236                  ><span class="icon" aria-hidden="true">
237                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="stars">
238                      <use href="#octicon-star"></use>
239                    </svg> </span
240                  ><span data-oss-stars>—</span></span
241                >
242                <span
243                  ><span class="icon" aria-hidden="true">
244                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="forks">
245                      <use href="#octicon-repo-forked"></use>
246                    </svg> </span
247                  ><span data-oss-forks>—</span></span
248                >
249              </div>
250            </div>
251          </a>
252          <a
253            class="card tile oss-card"
254            href="https://github.com/Ludicon/spark-unity"
255            target="_blank"
256            rel="noreferrer"
257            data-repo="Ludicon/spark-unity"
258          >
259            <div class="card-body">
260              <h3 class="oss-name">
261                <span class="icon" aria-hidden="true">
262                  <svg viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="GitHub">
263                    <use href="#octicon-mark-github"></use>
264                  </svg> </span
265                >Spark for Unity
266              </h3>
267              <p class="oss-desc" data-oss-desc>Loading description…</p>
268              <div class="oss-meta">
269                <span
270                  ><span class="icon" aria-hidden="true">
271                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="stars">
272                      <use href="#octicon-star"></use>
273                    </svg> </span
274                  ><span data-oss-stars>—</span></span
275                >
276                <span
277                  ><span class="icon" aria-hidden="true">
278                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="forks">
279                      <use href="#octicon-repo-forked"></use>
280                    </svg> </span
281                  ><span data-oss-forks>—</span></span
282                >
283              </div>
284            </div>
285          </a>
286          <a
287            class="card tile oss-card"
288            href="https://github.com/Ludicon/spark.js"
289            target="_blank"
290            rel="noreferrer"
291            data-repo="Ludicon/spark.js"
292          >
293            <div class="card-body">
294              <h3 class="oss-name">
295                <span class="icon" aria-hidden="true">
296                  <svg viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="GitHub">
297                    <use href="#octicon-mark-github"></use>
298                  </svg> </span
299                >spark.js
300              </h3>
301              <p class="oss-desc" data-oss-desc>Loading description…</p>
302              <div class="oss-meta">
303                <span
304                  ><span class="icon" aria-hidden="true">
305                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="stars">
306                      <use href="#octicon-star"></use>
307                    </svg> </span
308                  ><span data-oss-stars>—</span></span
309                >
310                <span
311                  ><span class="icon" aria-hidden="true">
312                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="forks">
313                      <use href="#octicon-repo-forked"></use>
314                    </svg> </span
315                  ><span data-oss-forks>—</span></span
316                >
317              </div>
318            </div>
319          </a>
320          <a
321            class="card tile oss-card"
322            href="https://github.com/Ludicon/spark-gltf-demo"
323            target="_blank"
324            rel="noreferrer"
325            data-repo="Ludicon/spark-gltf-demo"
326          >
327            <div class="card-body">
328              <h3 class="oss-name">
329                <span class="icon" aria-hidden="true">
330                  <svg viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="GitHub">
331                    <use href="#octicon-mark-github"></use>
332                  </svg> </span
333                >spark glTF demo
334              </h3>
335              <p class="oss-desc" data-oss-desc>Loading description…</p>
336              <div class="oss-meta">
337                <span
338                  ><span class="icon" aria-hidden="true">
339                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="stars">
340                      <use href="#octicon-star"></use>
341                    </svg> </span
342                  ><span data-oss-stars>—</span></span
343                >
344                <span
345                  ><span class="icon" aria-hidden="true">
346                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="forks">
347                      <use href="#octicon-repo-forked"></use>
348                    </svg> </span
349                  ><span data-oss-forks>—</span></span
350                >
351              </div>
352            </div>
353          </a>
354          <a
355            class="card tile oss-card"
356            href="https://github.com/Ludicon/gltf-tex"
357            target="_blank"
358            rel="noreferrer"
359            data-repo="Ludicon/gltf-tex"
360          >
361            <div class="card-body">
362              <h3 class="oss-name">
363                <span class="icon" aria-hidden="true">
364                  <svg viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="GitHub">
365                    <use href="#octicon-mark-github"></use>
366                  </svg> </span
367                >gltf-tex
368              </h3>
369              <p class="oss-desc" data-oss-desc>Loading description…</p>
370              <div class="oss-meta">
371                <span
372                  ><span class="icon" aria-hidden="true">
373                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="stars">
374                      <use href="#octicon-star"></use>
375                    </svg> </span
376                  ><span data-oss-stars>—</span></span
377                >
378                <span
379                  ><span class="icon" aria-hidden="true">
380                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="forks">
381                      <use href="#octicon-repo-forked"></use>
382                    </svg> </span
383                  ><span data-oss-forks>—</span></span
384                >
385              </div>
386            </div>
387          </a>
388          <a
389            class="card tile oss-card"
390            href="https://github.com/castano/icbc"
391            target="_blank"
392            rel="noreferrer"
393            data-repo="castano/icbc"
394          >
395            <div class="card-body">
396              <h3 class="oss-name">
397                <span class="icon" aria-hidden="true">
398                  <svg viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="GitHub">
399                    <use href="#octicon-mark-github"></use>
400                  </svg> </span
401                >icbc
402              </h3>
403              <p class="oss-desc" data-oss-desc>Loading description…</p>
404              <div class="oss-meta">
405                <span
406                  ><span class="icon" aria-hidden="true">
407                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="stars">
408                      <use href="#octicon-star"></use>
409                    </svg> </span
410                  ><span data-oss-stars>—</span></span
411                >
412                <span
413                  ><span class="icon" aria-hidden="true">
414                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="forks">
415                      <use href="#octicon-repo-forked"></use>
416                    </svg> </span
417                  ><span data-oss-forks>—</span></span
418                >
419              </div>
420            </div>
421          </a>
422          <a
423            class="card tile oss-card"
424            href="https://github.com/castano/nvidia-texture-tools"
425            target="_blank"
426            rel="noreferrer"
427            data-repo="castano/nvidia-texture-tools"
428          >
429            <div class="card-body">
430              <h3 class="oss-name">
431                <span class="icon" aria-hidden="true">
432                  <svg viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="GitHub">
433                    <use href="#octicon-mark-github"></use>
434                  </svg> </span
435                >
435nvidia-texture-tools
436              </h3>
437              <p class="oss-desc" data-oss-desc>Loading description…</p>
438              <div class="oss-meta">
439                <span
440                  ><span class="icon" aria-hidden="true">
441                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="stars">
442                      <use href="#octicon-star"></use>
443                    </svg> </span
444                  ><span data-oss-stars>—</span></span
445                >
446                <span
447                  ><span class="icon" aria-hidden="true">
448                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="forks">
449                      <use href="#octicon-repo-forked"></use>
450                    </svg> </span
451                  ><span data-oss-forks>—</span></span
452                >
453              </div>
454            </div>
455          </a>
456          <a
457            class="card tile oss-card"
458            href="https://github.com/Thekla/thekla_atlas"
459            target="_blank"
460            rel="noreferrer"
461            data-repo="Thekla/thekla_atlas"
462          >
463            <div class="card-body">
464              <h3 class="oss-name">
465                <span class="icon" aria-hidden="true">
466                  <svg viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="GitHub">
467                    <use href="#octicon-mark-github"></use>
468                  </svg> </span
469                >thekla_atlas
470              </h3>
471              <p class="oss-desc" data-oss-desc>Loading description…</p>
472              <div class="oss-meta">
473                <span
474                  ><span class="icon" aria-hidden="true">
475                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="stars">
476                      <use href="#octicon-star"></use>
477                    </svg> </span
478                  ><span data-oss-stars>—</span></span
479                >
480                <span
481                  ><span class="icon" aria-hidden="true">
482                    <svg viewBox="0 0 16 16" fill="currentColor" role="img" aria-label="forks">
483                      <use href="#octicon-repo-forked"></use>
484                    </svg> </span
485                  ><span data-oss-forks>—</span></span
486                >
487              </div>
488            </div>
489          </a>
490        </div>
491      </section>
492
493      <section class="section" id="about">
494        <h2 class="section-title">About Ludicon</h2>
495        <p class="section-lead">
496          A boutique graphics technology company specializing in real-time texture compression.
497        </p>
498        <div class="split">
499          <div class="info-box">
500            <h4>Studio leadership</h4>
501            <p>
502              Ludicon LLC is founded, privately owned, and operated by Ignacio Castaño since 2022.
503            </p>
504            <a class="button" href="castano/">Meet Ignacio</a>
505          </div>
506          <div class="info-box">
507            <div
508              id="mlb2-mainpage"
509              class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-mainpage"
510            >
511              <div class="ml-form-embedWrapper embedForm">
512                <div class="ml-form-embedBody ml-form-embedBodyDefault row-form">
513                  <h4>Stay updated</h4>
514                  <p>Subscribe to our newsletter to receive product updates and announcements.</p>
515                  <form
516                    class="ml-block-form newsletter-form"
517                    action="https://assets.mailerlite.com/jsonp/502550/forms/92994003937527367/subscribe"
518                    data-code=""
519                    method="post"
520                    target="_blank"
521                  >
522                    <div class="ml-form-formContent">
523                      <div class="ml-form-fieldRow ml-last-item">
524                        <div
525                          class="ml-field-group ml-field-email ml-validate-email ml-validate-required"
526                        >
527                          <input
528                            type="email"
529                            class="form-control"
530                            data-inputmask=""
531                            name="fields[email]"
532                            placeholder="Email"
533                            autocomplete="email"
534                            aria-label="email"
535                            aria-required="true"
536                          />
537                        </div>
538                      </div>
539                    </div>
540                    <input type="hidden" name="ml-submit" value="1" />
541                    <div class="ml-form-embedSubmit">
542                      <button type="submit" class="button">Subscribe</button>
543                    </div>
544                    <input type="hidden" name="anticsrf" value="true" />
545                  </form>
546                </div>
547                <div class="ml-form-successBody row-success" style="display: none">
548                  <div class="ml-form-successContent">
549                    <h4>Thank you!</h4>
550                    <p>You have successfully joined our subscriber list.</p>
551                  </div>
552                </div>
553              </div>
554            </div>
555          </div>
556          <div class="info-box">
557            <h4>Let's talk</h4>
558            <p>
559              Have a texture or mesh processing challenge? We can evaluate, integrate, and customize
560              our solutions to your pipeline.
561            </p>
562            <a class="button" href="mailto:[email protected]">Contact Us</a>
563          </div>
564        </div>
565      </section>
566
567      <footer>
568        <div>© 2026 Ludicon LLC. All rights reserved.</div>
569        <div class="footer-links">
570          <a href="privacy/">Privacy</a>
571        </div>
572      </footer>
573    </div>
574
575    
575<script>
576      (function () {
577        const cards = document.querySelectorAll("[data-repo]");
578        if (!cards.length) return;
579
580        const cacheKey = (repo) => `ludicon_oss_${repo}`;
581        const cacheTtlMs = 6 * 60 * 60 * 1000;
582
583        const readCache = (repo) => {
584          try {
585            const raw = localStorage.getItem(cacheKey(repo));
586            if (!raw) return null;
587            const cached = JSON.parse(raw);
588            if (!cached || !cached.ts) return null;
589            if (Date.now() - cached.ts > cacheTtlMs) return null;
590            return cached;
591          } catch {
592            return null;
593          }
594        };
595
596        const writeCache = (repo, data) => {
597          try {
598            localStorage.setItem(cacheKey(repo), JSON.stringify({ ts: Date.now(), ...data }));
599          } catch {}
600        };
601
602        const fetchRepo = async (repo) => {
603          const res = await fetch(`https://api.github.com/repos/${repo}`);
604          if (!res.ok) throw new Error("GitHub API error");
605          return res.json();
606        };
607
608        cards.forEach(async (card) => {
609          const repo = card.getAttribute("data-repo");
610          try {
611            const cached = readCache(repo);
612            const data = cached?.data || (await fetchRepo(repo));
613            if (!cached?.data) {
614              writeCache(repo, { data });
615            }
616            const descEl = card.querySelector("[data-oss-desc]");
617            const starEl = card.querySelector("[data-oss-stars]");
618            const forkEl = card.querySelector("[data-oss-forks]");
619            if (descEl) descEl.textContent = data.description || "—";
620            if (starEl) starEl.textContent = (data.stargazers_count ?? 0).toLocaleString();
621            if (forkEl) forkEl.textContent = (data.forks_count ?? 0).toLocaleString();
622          } catch (err) {
623            const descEl = card.querySelector("[data-oss-desc]");
624            if (descEl) descEl.textContent = "Description unavailable.";
625          }
626        });
627
628        const postList = document.getElementById("spark-posts");
629        let loadMoreBtn = document.getElementById("spark-posts-more");
630        if (!postList || !loadMoreBtn) return;
631
632        const blogBase = "https://www.ludicon.com/castano/blog";
633        const apiBase = `${blogBase}/wp-json/wp/v2`;
634        const formatDate = (iso) => {
635          const date = new Date(iso);
636          return date.toLocaleDateString(undefined, {
637            year: "numeric",
638            month: "short",
639            day: "numeric",
640          });
641        };
642
643        let sparkCategoryId = null;
644        let currentOffset = 0;
645        const initialSize = 3;
646        const moreSize = 3;
647        let hasMore = true;
648        let totalPosts = 0;
649
650        const renderPosts = (posts, replace = false) => {
651          if (replace) postList.innerHTML = "";
652          posts.forEach((post) => {
653            const li = document.createElement("li");
654            li.className = "blog-item";
655
656            const imageContainerHtml = post.featured_image
657              ? `<div class="blog-image-container"><img class="blog-image" src="${post.featured_image}" alt="" /></div>`
658              : `<div class="blog-image-container"></div>`;
659
660            li.innerHTML = `
661              <a href="${post.link}" target="_blank" rel="noreferrer" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; height: 100%;">
662                ${imageContainerHtml}
663                <div class="blog-content">
664                  <div class="blog-title">${post.title.rendered}</div>
665                  <span class="blog-date">${formatDate(post.date)}</span>
666                </div>
667              </a>
668            `;
669            postList.appendChild(li);
670          });
671        };
672
673        const setLoadMoreVisibility = () => {
674          // Convert to link if not already
675          if (loadMoreBtn.tagName !== "A") {
676            const newLink = document.createElement("a");
677            newLink.className = "blog-more";
678            newLink.id = "spark-posts-more";
679            newLink.href = "#";
680            newLink.textContent = "Load more";
681            loadMoreBtn.replaceWith(newLink);
682            loadMoreBtn = newLink;
683            // Re-attach click listener
684            loadMoreBtn.addEventListener("click", handleLoadMore);
685          } else {
686            loadMoreBtn.textContent = "Load more";
687          }
688          loadMoreBtn.style.display = hasMore ? "inline-flex" : "none";
689        };
690
691        const setLoadingState = () => {
692          // Convert to span for loading state
693          if (loadMoreBtn.tagName === "A") {
694            const newSpan = document.createElement("span");
695            newSpan.className = "blog-more";
696            newSpan.id = "spark-posts-more";
697            newSpan.textContent = "Loading posts…";
698            newSpan.style.display = "inline-flex";
699            loadMoreBtn.replaceWith(newSpan);
700            loadMoreBtn = newSpan;
701          } else {
702            loadMoreBtn.textContent = "Loading posts…";
703          }
704        };
705
706        const fetchPosts = async (limit, offset) => {
707          const postRes = await fetch(
708            `${apiBase}/posts?categories=${sparkCategoryId}&per_page=${limit}&offset=${offset}&orderby=date&order=desc&_embed=wp:featuredmedia`,
709          );
710          if (!postRes.ok) throw new Error("Post fetch failed");
711          const posts = await postRes.json();
712
713          // Extract featured image URLs from embedded media
714          const postsWithImages = posts.map((post) => {
715            const featuredImage =
716              post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.medium_large
717                ?.source_url ||
718              post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.large?.source_url ||
719              post._embedded?.["wp:featuredmedia"]?.[0]?.source_url ||
720              null;
721
722            return {
723              ...post,
724              featured_image: featuredImage,
725            };
726          });
727
728          totalPosts = Number(postRes.headers.get("X-WP-Total") || posts.length);
729          hasMore = offset + limit < totalPosts;
730          setLoadMoreVisibility();
731          return postsWithImages;
732        };
733
734        const loadSparkPosts = async () => {
735          try {
736            const catRes = await fetch(`${apiBase}/categories?slug=spark`);
737            if (!catRes.ok) throw new Error("Category fetch failed");
738            const cats = await catRes.json();
739            sparkCategoryId = cats?.[0]?.id;
740            if (!sparkCategoryId) throw new Error("Category not found");
741            currentOffset = 0;
742            const posts = await fetchPosts(initialSize, currentOffset);
743            if (!posts.length) throw new Error("No posts found");
744            renderPosts(posts, true);
745            setLoadMoreVisibility();
746          } catch (err) {
747            postList.innerHTML = `
748            <li class="blog-item">
749              <a class="blog-title" href="${blogBase}/category/spark/" target="_blank" rel="noreferrer">View Spark posts on the blog</a>
750              <span class="blog-date">—</span>
751            </li>
752          `;
753            loadMoreBtn.style.display = "none";
754          }
755        };
756
757        const handleLoadMore = async (event) => {
758          event.preventDefault();
759          if (!sparkCategoryId || !hasMore) return;
760          setLoadingState();
761          try {
762            currentOffset += moreSize;
763            const posts = await fetchPosts(moreSize, currentOffset);
764            if (posts.length) {
765              renderPosts(posts, false);
766            } else {
767              hasMore = false;
768              setLoadMoreVisibility();
769            }
770          } catch (err) {
771            console.error("Load more failed:", err);
772          }
773        };
774
775        loadMoreBtn.addEventListener("click", handleLoadMore);
776
777        loadSparkPosts();
778      })();
779    </script>
779
780
781    
781<script>
782      function ml_webform_success_mainpage() {
783        var form = document.querySelector(".ml-subscribe-form-mainpage .row-form");
784        var success = document.querySelector(".ml-subscribe-form-mainpage .row-success");
785        if (form && success) {
786          form.style.display = "none";
787          success.style.display = "block";
788        }
789      }
790    </script>
790
791
792    
792<script
793      src="https://groot.mailerlite.com/js/w/webforms.min.js?v2d8fb22bb5b3677f161552cd9e774127"
794      type="text/javascript"
795    ></script>
795
796    
796<script>
797      fetch("https://assets.mailerlite.com/jsonp/502550/forms/92994003937527367/takel");
798    </script>
798
799  </body>
800</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.