diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..1b0a1c0
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,15 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(cd ../Crafter/Crafter.Graphics && echo \"=== C++ files declaring wgpu* imports ===\"; grep -rln 'import_name\\(\"wgpu' implementations/ interfaces/; echo; echo \"=== sample: how a wgpu import + public wrapper is declared \\(canvas size + a string-taking one\\) ===\"; grep -rn -B2 -A2 -E 'import_name\\\\\\(\"wgpu\\(GetCanvasWidth|SurfaceWidth|LoadCustomShader|Init\\)\"' implementations/ interfaces/ | head -50)",
+ "Read(//home/jorijn/repos/Crafter/Crafter.Graphics/**)",
+ "Read(//home/jorijn/repos/Crafter/Crafter.Graphics/interfaces/**)",
+ "Read(//home/jorijn/repos/Crafter/Crafter.Graphics/additional/**)",
+ "Bash(crafter-build --local)"
+ ],
+ "additionalDirectories": [
+ "/home/jorijn/repos/Crafter/Crafter.Graphics/additional",
+ "/home/jorijn/repos/Crafter/Crafter.Graphics/interfaces"
+ ]
+ }
+}
diff --git a/README.md b/README.md
index 800d0a7..4af1c79 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,7 @@
This is the source code for catcrafts.net, a website built entirely in C++ using the Crafter.Graphics library.
```bash
-crafter-build build executable
-./run.sh
+crafter-build -r
```
This will compile the project and serve it locally on `http:://localhost:8080`.
diff --git a/implementations/Catcrafts-Blog.cpp b/implementations/Catcrafts-Blog.cpp
index 2b5433b..ec49e65 100644
--- a/implementations/Catcrafts-Blog.cpp
+++ b/implementations/Catcrafts-Blog.cpp
@@ -10,11 +10,34 @@ export module Catcrafts:Blog_impl;
import :Blog;
import :Root;
import :Views;
+import :Demo;
import Crafter.Graphics;
import std;
using namespace Crafter;
+namespace Catcrafts {
+ // Post that hosts the live ray-traced WebGPU demo. RenderBlogPost
+ // injects the mount container into this post and mounts the render
+ // canvas into it once the DOM is in place.
+ constexpr std::string_view kDemoPostSlug = "hello-world-2";
+
+ // Markup for the embedded demo: a fixed-height host box the render
+ // canvas is reparented into (see Catcrafts:Demo / WebGPU::SetCanvasMount)
+ // plus a caption. The id must match kDemoMountId.
+ constexpr std::string_view kDemoCardHtml = R"(
+
+
+
+ Live above: a scene ray-traced in real time — four coloured
+ point lights, one soft shadow per light — running through the
+ Crafter.Graphics WebGPU wavefront tracer, driven from this page's
+ C++ compiled to WebAssembly. Not a video, not an iframe: the same
+ WASM module that rendered this text is tracing those pixels.
+
+
)";
+}
+
export namespace Catcrafts {
// Persistent storage for the per-post card click handlers. HtmlElementPtr
// unregisters its listeners on destruction, so the elements have to
@@ -63,6 +86,7 @@ export namespace Catcrafts {
void RenderBlogPost(const std::string_view slug) {
for(const BlogPost& post : posts) {
if(post.slug == slug) {
+ const bool isDemo = post.slug == kDemoPostSlug;
MainContent().SetInnerHTML(std::format(R"(
)", post.name, post.date, post.content));
+ {}
+
)", post.name, post.date, post.content, isDemo ? kDemoCardHtml : std::string_view{}));
+
+ // The #webgpu-demo container now exists in the DOM, so the
+ // render canvas can be reparented into it and tracing can
+ // start. RenderRoot() already called UnmountDemo() for us.
+ if(isDemo) MountDemo();
return;
}
}
diff --git a/implementations/Catcrafts-Demo.cpp b/implementations/Catcrafts-Demo.cpp
new file mode 100644
index 0000000..73a1355
--- /dev/null
+++ b/implementations/Catcrafts-Demo.cpp
@@ -0,0 +1,214 @@
+/*
+catcrafts.net
+Copyright (C) 2026 Catcrafts
+
+The source code of this website is made available for viewing purposes only.
+No permission is granted to copy, modify, distribute, or create derivative works.
+
+Ray-traced WebGPU demo embedded in the blog. The scene (a checkered floor
+with five pillars lit by four coloured point lights, each pillar casting
+four separable coloured shadows) and its four WGSL stages are adapted from
+Crafter.Graphics' RTMultiShadow example — the wavefront software ray tracer
+that is the WebGPU/DOM RT path. Differences from the example:
+
+ * No input: the camera auto-orbits the scene, driven by a frame counter,
+ so the demo just plays. The embedded canvas is offset from the viewport
+ origin, which the mouse-coordinate bridge doesn't account for, so an
+ interactive camera would be misaligned anyway.
+ * The whole StartInit()/FinishInit() init lives in SetupDemo(); the render
+ pass is only attached to the window while the demo is mounted into
+ #webgpu-demo, so the rest of the site stays a plain DOM document.
+*/
+
+export module Catcrafts:Demo_impl;
+import :Demo;
+import Crafter.Graphics;
+import Crafter.Math;
+import Crafter.Event;
+import std;
+
+using namespace Crafter;
+namespace fs = std::filesystem;
+
+namespace {
+ // Matches LIGHT_COUNT in shaders/closesthit.wgsl and drives the
+ // per-pixel shadow-ray budget (one shadow ray per light).
+ constexpr std::uint32_t kLightCount = 4;
+
+ // Mirrors `struct Camera` in shaders/raygen.wgsl byte-for-byte.
+ struct CameraGPU {
+ float origin[3]; float pad0;
+ float right[3]; float tanHalf;
+ float up[3]; float aspect;
+ float forward[3]; float pad1;
+ };
+ static_assert(sizeof(CameraGPU) == 64);
+
+ // Axis-aligned box: 8 corners between mn and mx.
+ std::array, 8> BoxVerts(float mnx, float mny, float mnz,
+ float mxx, float mxy, float mxz) {
+ return {{
+ {mnx, mny, mnz}, {mxx, mny, mnz}, {mxx, mxy, mnz}, {mnx, mxy, mnz},
+ {mnx, mny, mxz}, {mxx, mny, mxz}, {mxx, mxy, mxz}, {mnx, mxy, mxz},
+ }};
+ }
+ // Mesh::Build takes mutable spans, so this can't be constexpr.
+ std::array kBoxIndices {{
+ 0,1,2, 0,2,3, 5,4,7, 5,7,6, 4,0,3, 4,3,7,
+ 1,5,6, 1,6,2, 4,5,1, 4,1,0, 3,2,6, 3,6,7,
+ }};
+
+ // Cross-module demo state. Raw pointers into function-local statics in
+ // SetupDemo() (valid for the program's lifetime); the site builds with
+ // -fno-c++-static-destructors so none of it is torn down at exit.
+ Window* gWindow = nullptr;
+ RTPass* gRtPass = nullptr;
+ bool gActive = false;
+ std::uint32_t gFrame = 0;
+}
+
+namespace Catcrafts {
+
+ void SetupDemo(Window& window) {
+ gWindow = &window;
+
+ auto cmd = window.StartInit();
+
+ static DescriptorHeapWebGPU heap;
+ heap.Initialize(/*images*/ 1, /*buffers*/ 2, /*samplers*/ 1);
+
+ static std::array shaders {{
+ WebGPUShader(fs::path("raygen.wgsl"), "raygen_main", WebGPURTStage::Raygen),
+ WebGPUShader(fs::path("miss.wgsl"), "miss_main", WebGPURTStage::Miss),
+ WebGPUShader(fs::path("closesthit.wgsl"), "closesthit_main", WebGPURTStage::ClosestHit),
+ WebGPUShader(fs::path("resolve.wgsl"), "resolve_main", WebGPURTStage::Resolve),
+ }};
+ static ShaderBindingTableWebGPU sbt;
+ sbt.Init(shaders);
+
+ static std::array raygenGroups {{ { .type = RTShaderGroupType::General, .generalShader = 0 } }};
+ static std::array missGroups {{ { .type = RTShaderGroupType::General, .generalShader = 1 } }};
+ static std::array hitGroups {{ { .type = RTShaderGroupType::TrianglesHitGroup, .closestHitShader = 2 } }};
+
+ // One user binding: the camera storage buffer at @group(3).
+ static std::array bindings {{
+ { .group = 3, .binding = 0, .kind = UICustomBindingKind::Buffer, .pushOffset = 0 },
+ }};
+
+ static PipelineRTWebGPU pipeline;
+ pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, sbt, bindings);
+
+ // Meshes: a large ground slab and a pillar (origin at its base).
+ static auto groundVerts = BoxVerts(-30.0f, -1.0f, -30.0f, 30.0f, 0.0f, 30.0f);
+ static auto pillarVerts = BoxVerts(-0.8f, 0.0f, -0.8f, 0.8f, 6.0f, 0.8f);
+ static Mesh ground, pillar;
+ ground.Build(groundVerts, kBoxIndices, cmd);
+ pillar.Build(pillarVerts, kBoxIndices, cmd);
+
+ static WebGPUBuffer cameraBuf;
+ cameraBuf.Create(1);
+ static std::array userHandles { cameraBuf.handle };
+
+ // Instances: ground (customIndex 0) + five pillars.
+ struct Placement { float x, z; };
+ static constexpr std::array kPillars {{
+ { 0.0f, 0.0f }, { 5.0f, 5.0f }, { -5.0f, 5.0f }, { 5.0f, -5.0f }, { -5.0f, -5.0f },
+ }};
+ static std::vector renderers;
+ renderers.reserve(1 + kPillars.size());
+ auto addInstance = [&](std::uint64_t blasAddr, float x, float z) {
+ renderers.emplace_back();
+ RenderingElement3D& r = renderers.back();
+ auto& tx = r.instance.transform.matrix;
+ tx[0][0] = 1; tx[0][1] = 0; tx[0][2] = 0; tx[0][3] = x;
+ tx[1][0] = 0; tx[1][1] = 1; tx[1][2] = 0; tx[1][3] = 0;
+ tx[2][0] = 0; tx[2][1] = 0; tx[2][2] = 1; tx[2][3] = z;
+ r.instance.instanceCustomIndex = static_cast(renderers.size() - 1);
+ r.instance.mask = 0xFF;
+ r.instance.instanceShaderBindingTableRecordOffset = 0;
+ r.instance.flags = kRTGeometryInstanceForceOpaque;
+ r.instance.accelerationStructureReference = blasAddr;
+ RenderingElement3D::Add(&r);
+ };
+ addInstance(ground.blasAddr, 0.0f, 0.0f);
+ for (const auto& p : kPillars) addInstance(pillar.blasAddr, p.x, p.z);
+ RenderingElement3D::BuildTLAS(cmd, 0);
+
+ window.descriptorHeap = &heap;
+ window.FinishInit();
+
+ static RTPass rtPass(&pipeline);
+ rtPass.handlesPtr = userHandles.data();
+ rtPass.handlesCount = static_cast(userHandles.size());
+ rtPass.maxDepth = 2; // primary + shadow
+ rtPass.raysPerPixel = kLightCount; // one shadow ray per light per pixel
+ gRtPass = &rtPass;
+
+ // Auto-orbit camera. Only does work (and touches the GPU buffer)
+ // while the demo is mounted; otherwise the tick is a cheap no-op.
+ // Capture nothing: cameraBuf is a static local (usable directly),
+ // and the window is reached through the stable gWindow pointer —
+ // capturing the reference parameter into this static lambda would
+ // dangle once SetupDemo() returns.
+ static EventListener camTick(&window.onBeforeUpdate, []() {
+ if (!gActive || gWindow == nullptr || gWindow->height == 0) return;
+
+ const float t = static_cast(gFrame) * 0.006f;
+ ++gFrame;
+
+ const float radius = 24.0f;
+ const float px = std::cos(t) * radius;
+ const float pz = std::sin(t) * radius;
+ const float py = 12.0f;
+
+ // forward = normalize(centre - eye), centre ≈ (0, 2, 0)
+ float fx = -px, fy = 2.0f - py, fz = -pz;
+ const float fl = std::sqrt(fx*fx + fy*fy + fz*fz);
+ fx /= fl; fy /= fl; fz /= fl;
+
+ // right = normalize(cross(forward, worldUp)), worldUp = (0,1,0)
+ float rx = fy*0.0f - fz*1.0f;
+ float ry = fz*0.0f - fx*0.0f;
+ float rz = fx*1.0f - fy*0.0f;
+ const float rl = std::sqrt(rx*rx + ry*ry + rz*rz);
+ rx /= rl; ry /= rl; rz /= rl;
+
+ // up = cross(right, forward)
+ const float ux = ry*fz - rz*fy;
+ const float uy = rz*fx - rx*fz;
+ const float uz = rx*fy - ry*fx;
+
+ CameraGPU& g = cameraBuf.value[0];
+ g.origin[0]=px; g.origin[1]=py; g.origin[2]=pz; g.pad0=0;
+ g.right[0]=rx; g.right[1]=ry; g.right[2]=rz;
+ g.up[0]=ux; g.up[1]=uy; g.up[2]=uz;
+ g.forward[0]=fx; g.forward[1]=fy; g.forward[2]=fz;
+ g.aspect = static_cast(gWindow->width) / static_cast(gWindow->height);
+ g.tanHalf = std::tan(70.0f * 3.14159265f / 360.0f);
+ g.pad1 = 0;
+ cameraBuf.FlushDevice();
+ });
+
+ // Start as a plain DOM page: canvas detached + hidden, no RT pass.
+ WebGPU::SetCanvasMount("");
+ }
+
+ void MountDemo() {
+ if (!gWindow || !gRtPass) return;
+ if (!gActive) {
+ gWindow->passes.push_back(gRtPass);
+ gActive = true;
+ }
+ WebGPU::SetCanvasMount(kDemoMountId);
+ }
+
+ void UnmountDemo() {
+ if (!gWindow || !gRtPass) return;
+ if (gActive) {
+ auto& p = gWindow->passes;
+ std::erase(p, gRtPass);
+ gActive = false;
+ }
+ WebGPU::SetCanvasMount("");
+ }
+}
diff --git a/implementations/Catcrafts-Root.cpp b/implementations/Catcrafts-Root.cpp
index 9f6c699..91fe7a7 100644
--- a/implementations/Catcrafts-Root.cpp
+++ b/implementations/Catcrafts-Root.cpp
@@ -10,6 +10,7 @@ export module Catcrafts:Root_impl;
import :Root;
import :Views;
import :Blog;
+import :Demo;
import Crafter.Graphics;
import std;
@@ -17,6 +18,11 @@ using namespace Crafter;
namespace Catcrafts {
void RenderRoot(const std::string_view route) {
+ // Every route change replaces 's innerHTML, which would
+ // orphan the demo canvas if it's currently mounted inside a post.
+ // Detach + hide it first; the post renderer re-mounts if needed.
+ UnmountDemo();
+
std::string currentRoute = std::string(route);
if(currentRoute == "/blog" || currentRoute == "/") {
diff --git a/implementations/main.cpp b/implementations/main.cpp
index 5f510a2..fce783e 100644
--- a/implementations/main.cpp
+++ b/implementations/main.cpp
@@ -18,7 +18,14 @@ int main() {
// plus the rAF driver; even without subscribing to its events we need
// it so StartSync() can keep the wasm module alive past main() (the
// bridge stashes a raw pointer, so the storage has to outlive main).
- static Window window(0, 0, "Catcrafts");
+ // The dimensions are a hint only — the JS bridge sizes the render
+ // surface to the canvas (full viewport, or the mount element).
+ static Window window(1280, 720, "Catcrafts");
+
+ // Build the ray-tracing pipeline + scene now (runs StartInit/FinishInit).
+ // The render canvas starts hidden; the demo only traces once a route
+ // mounts it into #webgpu-demo (see Catcrafts:Demo).
+ SetupDemo(window);
InitializePage();
@@ -34,6 +41,7 @@ int main() {
RenderRoot(Router::GetPath());
+ window.Render();
window.StartUpdate();
window.StartSync();
return 0;
diff --git a/interfaces/Catcrafts-Blog.cppm b/interfaces/Catcrafts-Blog.cppm
index 6a1bbd9..7cca3ee 100644
--- a/interfaces/Catcrafts-Blog.cppm
+++ b/interfaces/Catcrafts-Blog.cppm
@@ -19,10 +19,23 @@ export namespace Catcrafts {
std::string content;
};
std::vector posts {
+ {
+ "Hello World! 2?",
+ "hello-world-2",
+ "2026-07-18",
+ R"(So this blog has been mega dead but today i bring something exciting that was already released months ago but never deployed xd.
+
+ Crafter.CppDOM is dead, long live Crafter.Graphics!
+
+ This website is now updated to use the new Crafter.Graphics library, which allow for C++ manpiulation of the DOM, and as a new feature WebGPU!
+
+ And what better way to flex WebGPU than ray tracing? Here's a little scene traced live in your browser, shadows and all, straight from the same C++ compiled to WASM that rendered this very post:
+ )"
+ },
{
"In WASM, Exit doesn't mean done.",
"in-wasm-exit-doesnt-mean-done",
- "2026-11-14",
+ "2025-11-14",
R"(So if anyone looked at the source code for this website pefore this post you would have seen that everything was allocated with new, for example this very blog was defined as std::vector<BlogPost> posts = new std::vector<BlogPost>{...};
Reason for this was that everything became corrupted when callbacking from JS, not knowing sure as to why this was the fastest solution, but after debugging the problem became clear.
@@ -40,7 +53,7 @@ export namespace Catcrafts {
{
"Hello World!",
"hello-world",
- "2026-11-12",
+ "2025-11-12",
R"(Welcome to catcrafts.net!
Here we believe optimization is everything and C++ is a gift from god.
This blog will mostly be dedicated to random tidbits i come across while working on my Crafter series of libraries.
diff --git a/interfaces/Catcrafts-Demo.cppm b/interfaces/Catcrafts-Demo.cppm
new file mode 100644
index 0000000..cfd9325
--- /dev/null
+++ b/interfaces/Catcrafts-Demo.cppm
@@ -0,0 +1,36 @@
+/*
+catcrafts.net
+Copyright (C) 2026 Catcrafts
+
+The source code of this website is made available for viewing purposes only.
+No permission is granted to copy, modify, distribute, or create derivative works.
+*/
+
+export module Catcrafts:Demo;
+import Crafter.Graphics;
+import std;
+using namespace Crafter;
+
+export namespace Catcrafts {
+ // DOM element id that the ray-traced WebGPU demo renders into. The
+ // blog post embeds a with this id; MountDemo() reparents the
+ // Crafter.Graphics render canvas into it via WebGPU::SetCanvasMount.
+ inline constexpr std::string_view kDemoMountId = "webgpu-demo";
+
+ // Build the ray-tracing pipeline + scene once and start the render
+ // loop plumbing. Runs the window's StartInit()/FinishInit(), so call
+ // it from main() right after the Window is constructed and before the
+ // page chrome / routes are built. The canvas starts detached + hidden;
+ // nothing is traced until MountDemo() is called.
+ void SetupDemo(Window& window);
+
+ // Reparent the render canvas into #webgpu-demo and begin tracing. Call
+ // only after the element exists in the DOM (i.e. right after the post's
+ // innerHTML has been set). Idempotent.
+ void MountDemo();
+
+ // Stop tracing and detach + hide the canvas, leaving a plain DOM page.
+ // Call before any route re-render that would replace #webgpu-demo.
+ // Safe to call when the demo is not mounted.
+ void UnmountDemo();
+}
diff --git a/interfaces/Catcrafts.cppm b/interfaces/Catcrafts.cppm
index bfe722d..7049fe8 100644
--- a/interfaces/Catcrafts.cppm
+++ b/interfaces/Catcrafts.cppm
@@ -9,4 +9,5 @@ No permission is granted to copy, modify, distribute, or create derivative works
export module Catcrafts;
export import :Views;
export import :Blog;
-export import :Root;
\ No newline at end of file
+export import :Root;
+export import :Demo;
\ No newline at end of file
diff --git a/project.cpp b/project.cpp
index 1460a2f..b116e8f 100644
--- a/project.cpp
+++ b/project.cpp
@@ -46,17 +46,19 @@ extern "C" Configuration CrafterBuildProject(std::span a
ApplyStandardArgs(cfg, args);
cfg.dependencies = { graphics };
- std::array ifaces = {
+ std::array ifaces = {
"interfaces/Catcrafts",
"interfaces/Catcrafts-Views",
"interfaces/Catcrafts-Blog",
"interfaces/Catcrafts-Root",
+ "interfaces/Catcrafts-Demo",
};
- std::array impls = {
+ std::array impls = {
"implementations/main",
"implementations/Catcrafts-Blog",
"implementations/Catcrafts-Root",
"implementations/Catcrafts-Views",
+ "implementations/Catcrafts-Demo",
};
cfg.GetInterfacesAndImplementations(ifaces, impls);
@@ -64,6 +66,12 @@ extern "C" Configuration CrafterBuildProject(std::span a
cfg.files.emplace_back(fs::path("robots.txt"));
cfg.files.emplace_back(fs::path("sitemap.xml"));
cfg.files.emplace_back(fs::path("favicon.svg"));
+ // WGSL for the ray-traced WebGPU demo embedded in the blog (see
+ // interfaces/Catcrafts-Demo.cppm). Fetched at runtime by WebGPUShader.
+ cfg.files.emplace_back(fs::path("shaders/raygen.wgsl"));
+ cfg.files.emplace_back(fs::path("shaders/miss.wgsl"));
+ cfg.files.emplace_back(fs::path("shaders/closesthit.wgsl"));
+ cfg.files.emplace_back(fs::path("shaders/resolve.wgsl"));
// Loaded as a