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.