diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs index 7c6487dcd8..9b1495f6aa 100644 --- a/apps/typegpu-docs/astro.config.mjs +++ b/apps/typegpu-docs/astro.config.mjs @@ -39,6 +39,10 @@ export default defineConfig({ markdown: { remarkPlugins: [remarkMath], rehypePlugins: [rehypeMathJax], + shikiConfig: { + theme: 'one-dark-pro', + wrap: true, + }, }, vite: { define: { diff --git a/apps/typegpu-docs/src/assets/Group.svg b/apps/typegpu-docs/src/assets/Group.svg new file mode 100644 index 0000000000..94412e98af --- /dev/null +++ b/apps/typegpu-docs/src/assets/Group.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/typegpu-docs/src/assets/externalopen.svg b/apps/typegpu-docs/src/assets/externalopen.svg new file mode 100644 index 0000000000..f7589a04f8 --- /dev/null +++ b/apps/typegpu-docs/src/assets/externalopen.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/typegpu-docs/src/assets/github-icon-dark.svg b/apps/typegpu-docs/src/assets/github-icon-dark.svg new file mode 100644 index 0000000000..bfad70a2d0 --- /dev/null +++ b/apps/typegpu-docs/src/assets/github-icon-dark.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/apps/typegpu-docs/src/assets/hero/as_a_foundation.svg b/apps/typegpu-docs/src/assets/hero/as_a_foundation.svg new file mode 100644 index 0000000000..4d652e9892 --- /dev/null +++ b/apps/typegpu-docs/src/assets/hero/as_a_foundation.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/typegpu-docs/src/assets/hero/as_a_puzzle_piece.svg b/apps/typegpu-docs/src/assets/hero/as_a_puzzle_piece.svg new file mode 100644 index 0000000000..eef4d648ac --- /dev/null +++ b/apps/typegpu-docs/src/assets/hero/as_a_puzzle_piece.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/typegpu-docs/src/assets/hero/caustics_bg.webp b/apps/typegpu-docs/src/assets/hero/caustics_bg.webp new file mode 100644 index 0000000000..7f12239152 Binary files /dev/null and b/apps/typegpu-docs/src/assets/hero/caustics_bg.webp differ diff --git a/apps/typegpu-docs/src/assets/hero/fish_bg.webp b/apps/typegpu-docs/src/assets/hero/fish_bg.webp new file mode 100644 index 0000000000..273077ec8d Binary files /dev/null and b/apps/typegpu-docs/src/assets/hero/fish_bg.webp differ diff --git a/apps/typegpu-docs/src/assets/hero/for_libraries.svg b/apps/typegpu-docs/src/assets/hero/for_libraries.svg new file mode 100644 index 0000000000..e7868ca23c --- /dev/null +++ b/apps/typegpu-docs/src/assets/hero/for_libraries.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/typegpu-docs/src/assets/hero/jellySlider_bg.jpg b/apps/typegpu-docs/src/assets/hero/jellySlider_bg.jpg new file mode 100644 index 0000000000..3db4a7edc2 Binary files /dev/null and b/apps/typegpu-docs/src/assets/hero/jellySlider_bg.jpg differ diff --git a/apps/typegpu-docs/src/assets/hero/rave_bg.webp b/apps/typegpu-docs/src/assets/hero/rave_bg.webp new file mode 100644 index 0000000000..9b223e0f23 Binary files /dev/null and b/apps/typegpu-docs/src/assets/hero/rave_bg.webp differ diff --git a/apps/typegpu-docs/src/assets/hero/rayMarch_bg.webp b/apps/typegpu-docs/src/assets/hero/rayMarch_bg.webp new file mode 100644 index 0000000000..cf68cfba35 Binary files /dev/null and b/apps/typegpu-docs/src/assets/hero/rayMarch_bg.webp differ diff --git a/apps/typegpu-docs/src/assets/hero/reflection_bg.jpg b/apps/typegpu-docs/src/assets/hero/reflection_bg.jpg new file mode 100644 index 0000000000..a423650794 Binary files /dev/null and b/apps/typegpu-docs/src/assets/hero/reflection_bg.jpg differ diff --git a/apps/typegpu-docs/src/assets/moon.svg b/apps/typegpu-docs/src/assets/moon.svg new file mode 100644 index 0000000000..a091833730 --- /dev/null +++ b/apps/typegpu-docs/src/assets/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/typegpu-docs/src/assets/sun.svg b/apps/typegpu-docs/src/assets/sun.svg new file mode 100644 index 0000000000..ebdbb6e9a8 --- /dev/null +++ b/apps/typegpu-docs/src/assets/sun.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/typegpu-docs/src/assets/youtube.svg b/apps/typegpu-docs/src/assets/youtube.svg new file mode 100644 index 0000000000..f4e653d625 --- /dev/null +++ b/apps/typegpu-docs/src/assets/youtube.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/typegpu-docs/src/components/Backlog.astro b/apps/typegpu-docs/src/components/Backlog.astro new file mode 100644 index 0000000000..35aa5dd0ec --- /dev/null +++ b/apps/typegpu-docs/src/components/Backlog.astro @@ -0,0 +1,148 @@ +--- +import { Image } from "astro:assets"; +import as_a_foundation from "../assets/hero/as_a_foundation.svg"; +import as_a_puzzle_piece from "../assets/hero/as_a_puzzle_piece.svg"; +import for_libraries from "../assets/hero/for_libraries.svg"; +--- + +
+ +
+
+

Backlog

+
+ For libraries icon +
+
+ +
+
+

Imperative code

+

+ Perform multiple background tasks at the same time and improve the + every... +

+
+
+

Need something?

+

+ Let us know about your improvement ideas, maybe they will land here! +

+
+
+
+ + +
+
+

In progress

+
+ As a puzzle piece icon +
+
+ +
+
+

Pipelines

+

+ Perform multiple background tasks at the same time and improve the + every... +

+
+ Planned for late 2025 +
+
+
+
+ + +
+
+

Shipped

+
+ As a foundation icon +
+
+
+
+

Data structures & buffers

+

+ Perform multiple background tasks at the same time and improve the + every... +

+
+ May 2024 + v0.1 +
+
+ +
+

Bind groups

+

+ Perform multiple background tasks at the same time and improve the + every... +

+
+ Oct 2024 + v0.2 +
+
+ +
+

Linker

+

+ Perform multiple background tasks at the same time and improve the + every... +

+
+ Feb 2025 + v0.3 +
+
+ +
+

Functions

+

+ Perform multiple background tasks at the same time and improve the + every... +

+
+ Feb 2025 + v0.3 +
+
+
+
+
diff --git a/apps/typegpu-docs/src/components/CodeButton.astro b/apps/typegpu-docs/src/components/CodeButton.astro new file mode 100644 index 0000000000..a4c217403d --- /dev/null +++ b/apps/typegpu-docs/src/components/CodeButton.astro @@ -0,0 +1,29 @@ +--- +interface Props { + title: string; + message: string; + icon: ((_props: astroHTML.JSX.SVGAttributes) => any) & ImageMetadata; +} + +const { title, message, icon } = Astro.props as Props; +--- + +
+
+
+ {icon && Icon} +
+ +

+ {title} +

+ +

+ {message} +

+
+
diff --git a/apps/typegpu-docs/src/components/CodePen.astro b/apps/typegpu-docs/src/components/CodePen.astro new file mode 100644 index 0000000000..26c24f3f66 --- /dev/null +++ b/apps/typegpu-docs/src/components/CodePen.astro @@ -0,0 +1,56 @@ +--- +import { Code } from "astro/components"; + +type SupportedLanguages = + | "typescript" + | "javascript" + | "jsx" + | "tsx" + | "html" + | "css" + | "json" + | "yaml" + | "markdown" + | "bash" + | "shell"; + +interface Props { + code: string; + language?: SupportedLanguages; +} + +const { code, language = "typescript" } = Astro.props as Props; + +const formattedCode = code.trim(); +--- + +
+
+ +
+
+ + diff --git a/apps/typegpu-docs/src/components/CodeShowcase.astro b/apps/typegpu-docs/src/components/CodeShowcase.astro new file mode 100644 index 0000000000..1b177eb3de --- /dev/null +++ b/apps/typegpu-docs/src/components/CodeShowcase.astro @@ -0,0 +1,167 @@ +--- +import CodeButton from "./CodeButton.astro"; +import CodePen from "./CodePen.astro"; +import as_a_foundation from "../assets/hero/as_a_foundation.svg"; +import as_a_puzzle_piece from "../assets/hero/as_a_puzzle_piece.svg"; +import for_libraries from "../assets/hero/for_libraries.svg"; + +const sectionId = "interactive-code-section"; +--- + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ { + 'use gpu'; + return d.vec2f(a - r, a + r); +}; + +// +// #1) Can be called in JS +// +const range = neighborhood(1.1, 0.5); +// ^? d.v2f + +// +// #2) Used to generate WGSL +// +const main = () => { + 'use gpu'; + return neighborhood(1.1, 0.5); +}; + +const wgsl = tgpu.resolve([main]); +// ^? string + +// +// #3) Executed on the GPU (generates WGSL underneath) +// +root['~unstable'] + .createGuardedComputePipeline(main) + .dispatchThreads();`} + language="typescript" + /> + + d.arrayOf(d.arrayOf(d.f32, height), width); + +// ...then infer types from them +type HeightMap = ReturnType; + +export async function generateHeightMap( + root: TgpuRoot, + opts: { width: number; height: number }, +): Promise & StorageFlag> { + const buffer = root + .createBuffer(HeightMap(opts.width, opts.height)) + .$usage('storage'); + + const rawBuffer = root.unwrap(buffer); // => GPUBuffer + + // Here we can do anything we would usually do with a + // WebGPU buffer, like populating it in a compute shader. + // 'rawBuffer' is the WebGPU resource that is backing the + // typed 'buffer' object, meaning any changes to it will + // be visible in both. + + return buffer; +}`} + language="typescript" + /> + >> & StorageFlag + +// ERROR: Argument of type 'TgpuBuffer>>' is +// not assignable to parameter of type 'TgpuBuffer>>' +plot.array1d(root, terrainBuffer); + +// SUCCESS! +plot.array2d(root, terrainBuffer);}`} + language="typescript" + /> +
+
+ + + + diff --git a/apps/typegpu-docs/src/components/GetStartedButton.astro b/apps/typegpu-docs/src/components/GetStartedButton.astro index ac722ec1d0..66feb3e52f 100644 --- a/apps/typegpu-docs/src/components/GetStartedButton.astro +++ b/apps/typegpu-docs/src/components/GetStartedButton.astro @@ -1,16 +1,33 @@ +--- +interface Props { + title?: string; + href?: string; + class?: string; +} + +const { + title = "Get started", + href = "/TypeGPU/getting-started", + class: className, +} = Astro.props; +--- + - Get started + {href} +> + {title} + fill="none" + > + {title} + + + diff --git a/apps/typegpu-docs/src/components/HoverExampleIsland.tsx b/apps/typegpu-docs/src/components/HoverExampleIsland.tsx new file mode 100644 index 0000000000..942a31b087 --- /dev/null +++ b/apps/typegpu-docs/src/components/HoverExampleIsland.tsx @@ -0,0 +1,185 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { + PointerEvent as ReactPointerEvent, + TouchEvent as ReactTouchEvent, +} from 'react'; +import { useAtom } from 'jotai'; +import { activeExampleAtom } from '../utils/examples/activeExampleAtom.ts'; +import { executeExample } from '../utils/examples/exampleRunner.ts'; +import { isGPUSupported } from '../utils/isGPUSupported.ts'; + +type Props = { + exampleKey: string; + html: string; +}; + +export default function HoverExampleIsland({ exampleKey, html }: Props) { + const rootRef = useRef(null); + const containerRef = useRef(null); + const cleanupRef = useRef<(() => void) | undefined>(undefined); + const twoFingerActiveRef = useRef(false); + + const [activeExample, setActiveExample] = useAtom(activeExampleAtom); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + + const isActive = activeExample === exampleKey; + + const reset = useCallback(() => { + cleanupRef.current?.(); + cleanupRef.current = undefined; + if (containerRef.current) { + containerRef.current.innerHTML = ''; + } + }, []); + + const activate = () => setActiveExample(exampleKey); + const deactivate = () => + setActiveExample((prev) => (prev === exampleKey ? null : prev)); + + // pointer events + const handlePointerEnter = (e: ReactPointerEvent) => + e.pointerType !== 'touch' && activate(); + const handlePointerLeave = (e: ReactPointerEvent) => + e.pointerType !== 'touch' && deactivate(); + const handleTouchStart = (e: ReactTouchEvent) => { + if (e.touches.length >= 2) { + e.preventDefault(); + twoFingerActiveRef.current = true; + } + }; + const handleTouchMove = (e: ReactTouchEvent) => { + if (twoFingerActiveRef.current) e.preventDefault(); + }; + const handleTouchEnd = (e: ReactTouchEvent) => { + if (e.touches.length === 0 && twoFingerActiveRef.current) { + twoFingerActiveRef.current = false; + setActiveExample((prev) => (prev === exampleKey ? null : exampleKey)); + } + }; + const handleTouchCancel = () => { + twoFingerActiveRef.current = false; + }; + + // intersection observer + useEffect(() => { + const element = rootRef.current; + if (!element) return; + + const observer = new IntersectionObserver(([entry]) => { + if (!entry.isIntersecting) { + twoFingerActiveRef.current = false; + deactivate(); + } + }); + + observer.observe(element); + return () => observer.disconnect(); + }); + + // Sync data-active attribute on overlay + useEffect(() => { + const overlay = containerRef.current?.closest('[data-hover-overlay]'); + if (!overlay) return; + + overlay.toggleAttribute('data-active', isActive); + return () => overlay.removeAttribute('data-active'); + }, [isActive]); + + // examples runner + useEffect(() => { + if (!isActive) { + reset(); + setIsLoading(false); + return; + } + + let cancelled = false; + + (async () => { + try { + setIsLoading(true); + setError(undefined); + + if (!isGPUSupported) { + throw new Error('WebGPU is not enabled/supported in this browser.'); + } + + if (!containerRef.current) return; + + containerRef.current.innerHTML = html; + hideHelpElement(containerRef.current); + resizeCanvases(containerRef.current); + + const tsPath = `../pages/landing-examples/${exampleKey}/index.ts`; + const tsImport = () => + import(/* @vite-ignore */ `${tsPath}?t=${Date.now()}`); + + const { dispose } = await executeExample(tsImport); + + if (cancelled) { + dispose(); + return; + } + + cleanupRef.current = dispose; + } catch (err) { + console.error(err); + setError( + err instanceof Error ? err.message : 'Failed to load example.', + ); + reset(); + } finally { + if (!cancelled) setIsLoading(false); + } + })(); + + return () => { + cancelled = true; + reset(); + }; + }, [exampleKey, html, isActive, reset]); + + return ( +
+ {error + ?

{error}

+ : ( +
+ {isLoading && ( + + Loading… + + )} +
+
+ )} +
+ ); +} + +function hideHelpElement(container: HTMLElement) { + const help = container.querySelector('#help'); + if (help) help.style.display = 'none'; +} + +function resizeCanvases(container: HTMLElement) { + const dpr = window.devicePixelRatio || 1; + + for (const canvas of container.querySelectorAll('canvas')) { + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width * dpr; + canvas.height = rect.height * dpr * 2; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + } +} diff --git a/apps/typegpu-docs/src/components/TgpuExamples.tsx b/apps/typegpu-docs/src/components/TgpuExamples.tsx new file mode 100644 index 0000000000..dd887935bc --- /dev/null +++ b/apps/typegpu-docs/src/components/TgpuExamples.tsx @@ -0,0 +1,107 @@ +import ExternalOpenSvg from '../assets/externalopen.svg'; +import CausticsGif from '../assets/hero/caustics_bg.webp'; +import RayMarchingGif from '../assets/hero/rayMarch_bg.webp'; +import JellySliderGif from '../assets/hero/jellySlider_bg.jpg'; +import FishGif from '../assets/hero/fish_bg.webp'; +import ReflectionGif from '../assets/hero/reflection_bg.jpg'; +import VaporRaveGif from '../assets/hero/rave_bg.webp'; + +import HoverExampleIsland from './HoverExampleIsland.tsx'; + +// biome-ignore lint/correctness/useImportExtensions: Vite ?raw query is valid, extension is .html +import fishHtml from '../pages/landing-examples/3d-fish/index.html?raw'; +// biome-ignore lint/correctness/useImportExtensions: Vite ?raw query is valid, extension is .html +import causticsHtml from '../pages/landing-examples/caustics/index.html?raw'; +// biome-ignore lint/correctness/useImportExtensions: Vite ?raw query is valid, extension is .html +import reflectionHtml from '../pages/landing-examples/cubemap-reflection/index.html?raw'; +// biome-ignore lint/correctness/useImportExtensions: Vite ?raw query is valid, extension is .html +import jellyHtml from '../pages/landing-examples/jelly-slider/index.html?raw'; +// biome-ignore lint/correctness/useImportExtensions: Vite ?raw query is valid, extension is .html +import rayMarchingHtml from '../pages/landing-examples/ray-marching/index.html?raw'; +// biome-ignore lint/correctness/useImportExtensions: Vite ?raw query is valid, extension is .html +import vaporRaveHtml from '../pages/landing-examples/vaporrave/index.html?raw'; + +const galleryItems = [ + { + asset: ReflectionGif, + title: 'Reflection', + key: 'cubemap-reflection', + html: reflectionHtml, + }, + { + asset: JellySliderGif, + title: 'Jelly Slider', + key: 'jelly-slider', + html: jellyHtml, + }, + { + asset: VaporRaveGif, + title: 'VaporRave', + key: 'vaporrave', + html: vaporRaveHtml, + }, + { + asset: RayMarchingGif, + title: 'Ray Marching', + key: 'ray-marching', + html: rayMarchingHtml, + }, + { asset: FishGif, title: '3D Fish', key: '3d-fish', html: fishHtml }, + { + asset: CausticsGif, + title: 'Caustics', + key: 'caustics', + html: causticsHtml, + }, +] as const; + +export default function TgpuExamples() { + return ( + <> +
+ Hint: tap the example with two fingers to preview +
+ +
+ {galleryItems.map((item) => ( +
+ + + {item.title} + + Open example + + + {item.title} + +
+
+ +
+
+
+ ))} +
+ + ); +} diff --git a/apps/typegpu-docs/src/components/ThemeSwitcher.astro b/apps/typegpu-docs/src/components/ThemeSwitcher.astro new file mode 100644 index 0000000000..51f7618716 --- /dev/null +++ b/apps/typegpu-docs/src/components/ThemeSwitcher.astro @@ -0,0 +1,64 @@ +--- +import { Image } from "astro:assets"; +import LightModeIconSvg from "../assets/sun.svg"; +import DarkModeIconSvg from "../assets/moon.svg"; +--- + +
+ + + +
+ + diff --git a/apps/typegpu-docs/src/components/Videos.astro b/apps/typegpu-docs/src/components/Videos.astro new file mode 100644 index 0000000000..d8cdd2dbd2 --- /dev/null +++ b/apps/typegpu-docs/src/components/Videos.astro @@ -0,0 +1,67 @@ +--- +import YoutubeSvg from "../assets/youtube.svg"; + +const videos = [ + { + title: "Iwo Plaza – Your GPU is a JavaScript runtime* (TypeGPU deep-dive)", + src: "https://www.youtube.com/embed/pBRLqJaG4kk?si=-xaSG_MET9-Q1ZfK", + className: "md:col-span-2", + }, + { + title: "TypeGPU: enhance your WebGPU project with type-safe API", + src: "https://www.youtube.com/embed/71TO3OIgStM?si=y96EGOPGKv0lL1D7", + }, + { + title: "Simplifying Compute Shaders with TypeGPU: GPU Worklets & More", + src: "https://www.youtube.com/embed/QrHZXiXxu9A?si=CSaZznbx0hf3PlQj", + }, + { + title: "Iwo Plaza – The Road to Type-Safety on the GPU | RNCK #15", + src: "https://www.youtube.com/embed/DysmnC1D2gQ?si=0pR77V9xatZdX2Co", + }, + { + title: "Live-Coding a Liquid Glass Effect with TypeGPU", + src: "https://www.youtube.com/embed/5PREvbHirEY?si=N2Cs3xn9IG8_Wh9g", + }, +]; +--- + +
+ { + videos.map((video) => ( +
+ + Watch on YouTube + {video.title} + +