diff --git a/packages/typegpu-cli/src/create.ts b/packages/typegpu-cli/src/create.ts
index bc7366858c..dba6654ed8 100644
--- a/packages/typegpu-cli/src/create.ts
+++ b/packages/typegpu-cli/src/create.ts
@@ -16,15 +16,19 @@ const GRADIENT_END = [0.216, 0.263, 0.82] as const;
const PROJECT_TEMPLATES = [
{
value: 'vite-simple',
- label: 'Vite (Simple)',
+ label: 'Vite (Bare)',
+ },
+ {
+ value: 'vite-complex',
+ label: 'Vite (Complex - Domain Warping)',
},
{
value: 'vite-react',
- label: 'Vite + React',
+ label: 'Vite + React (Bare)',
},
{
value: 'expo-simple',
- label: 'Expo RN',
+ label: 'Expo RN (Bare)',
},
] as const;
diff --git a/packages/typegpu-cli/templates/template-vite-complex/_gitignore b/packages/typegpu-cli/templates/template-vite-complex/_gitignore
new file mode 100644
index 0000000000..8b7e50214d
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/_gitignore
@@ -0,0 +1,22 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/packages/typegpu-cli/templates/template-vite-complex/_nvmrc b/packages/typegpu-cli/templates/template-vite-complex/_nvmrc
new file mode 100644
index 0000000000..a45fd52cc5
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/_nvmrc
@@ -0,0 +1 @@
+24
diff --git a/packages/typegpu-cli/templates/template-vite-complex/_oxfmtrc.json b/packages/typegpu-cli/templates/template-vite-complex/_oxfmtrc.json
new file mode 100644
index 0000000000..544138be45
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/_oxfmtrc.json
@@ -0,0 +1,3 @@
+{
+ "singleQuote": true
+}
diff --git a/packages/typegpu-cli/templates/template-vite-complex/_package.json b/packages/typegpu-cli/templates/template-vite-complex/_package.json
new file mode 100644
index 0000000000..d8385740d0
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/_package.json
@@ -0,0 +1,36 @@
+{
+ "name": "typegpu-vanilla-vite-complex-project",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "check": "oxlint && oxfmt --check",
+ "fix": "oxlint --fix && oxfmt",
+ "types": "tsc --p ./tsconfig.json --noEmit"
+ },
+ "dependencies": {
+ "@typegpu/color": "^0.11.0",
+ "@typegpu/noise": "^0.11.0",
+ "typegpu": "^0.11.6"
+ },
+ "devDependencies": {
+ "@webgpu/types": "^0.1.70",
+ "eslint-plugin-typegpu": "^0.11.1",
+ "oxfmt": "^0.49.0",
+ "oxlint": "^1.64.0",
+ "typescript": "npm:tsover@6.0.1",
+ "unplugin-typegpu": "^0.11.4",
+ "vite": "^8.0.12"
+ },
+ "overrides": {
+ "typescript": "npm:tsover@6.0.1"
+ },
+ "pnpm": {
+ "overrides": {
+ "typescript": "npm:tsover@6.0.1"
+ }
+ }
+}
diff --git a/packages/typegpu-cli/templates/template-vite-complex/_vscode/settings.json b/packages/typegpu-cli/templates/template-vite-complex/_vscode/settings.json
new file mode 100644
index 0000000000..0dfacd95d2
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/_vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "typescript.preferences.importModuleSpecifier": "relative",
+ "typescript.enablePromptUseWorkspaceTsdk": true
+}
diff --git a/packages/typegpu-cli/templates/template-vite-complex/_zed/settings.json b/packages/typegpu-cli/templates/template-vite-complex/_zed/settings.json
new file mode 100644
index 0000000000..9d7badfbd2
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/_zed/settings.json
@@ -0,0 +1,15 @@
+// Folder-specific settings
+//
+// For a full list of overridable settings, and general information on folder-specific settings,
+// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
+{
+ "lsp": {
+ "vtsls": {
+ "settings": {
+ "typescript": {
+ "tsdk": "node_modules/typescript/lib"
+ }
+ }
+ }
+ }
+}
diff --git a/packages/typegpu-cli/templates/template-vite-complex/index.html b/packages/typegpu-cli/templates/template-vite-complex/index.html
new file mode 100644
index 0000000000..2ae93ce68e
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/index.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+ typegpu-vanilla-vite-complex-project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Type-safe WebGPU
+
+
+
+
+ Vite
+
+
+
Next generation frontend tooling
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/typegpu-cli/templates/template-vite-complex/oxlint.config.ts b/packages/typegpu-cli/templates/template-vite-complex/oxlint.config.ts
new file mode 100644
index 0000000000..7b676ba672
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/oxlint.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from 'oxlint';
+import typegpu from 'eslint-plugin-typegpu';
+
+export default defineConfig({
+ plugins: ['typescript', 'import', 'unicorn', 'oxc'],
+ jsPlugins: ['eslint-plugin-typegpu'],
+ categories: {
+ correctness: 'warn',
+ suspicious: 'warn',
+ },
+ rules: {
+ ...typegpu.configs.recommended.rules,
+ 'typescript/no-non-null-assertion': 'error',
+ 'typescript/no-explicit-any': 'error',
+ 'typescript/no-unsafe-type-assertion': 'off',
+ 'import/no-named-as-default': 'off',
+ },
+ env: {
+ builtin: true,
+ },
+});
diff --git a/packages/typegpu-cli/templates/template-vite-complex/public/favicon.svg b/packages/typegpu-cli/templates/template-vite-complex/public/favicon.svg
new file mode 100644
index 0000000000..8b5e90d1a9
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/public/favicon.svg
@@ -0,0 +1,16 @@
+
diff --git a/packages/typegpu-cli/templates/template-vite-complex/public/icons.svg b/packages/typegpu-cli/templates/template-vite-complex/public/icons.svg
new file mode 100644
index 0000000000..b6755b1121
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/public/icons.svg
@@ -0,0 +1,16 @@
+
diff --git a/packages/typegpu-cli/templates/template-vite-complex/public/typegpu-logo-dark.svg b/packages/typegpu-cli/templates/template-vite-complex/public/typegpu-logo-dark.svg
new file mode 100644
index 0000000000..65c9a02c31
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/public/typegpu-logo-dark.svg
@@ -0,0 +1,59 @@
+
diff --git a/packages/typegpu-cli/templates/template-vite-complex/public/typegpu-logo-light.svg b/packages/typegpu-cli/templates/template-vite-complex/public/typegpu-logo-light.svg
new file mode 100644
index 0000000000..a1b63335fc
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/public/typegpu-logo-light.svg
@@ -0,0 +1,58 @@
+
diff --git a/packages/typegpu-cli/templates/template-vite-complex/public/vite.svg b/packages/typegpu-cli/templates/template-vite-complex/public/vite.svg
new file mode 100644
index 0000000000..5101b674df
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/public/vite.svg
@@ -0,0 +1 @@
+
diff --git a/packages/typegpu-cli/templates/template-vite-complex/src/main.ts b/packages/typegpu-cli/templates/template-vite-complex/src/main.ts
new file mode 100644
index 0000000000..1a92565541
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/src/main.ts
@@ -0,0 +1,148 @@
+import tgpu, { common, d, std } from 'typegpu';
+import { hexToOklab, oklabToRgb } from '@typegpu/color';
+import { perlin2d } from '@typegpu/noise';
+
+// Wiring up the speed slider
+const slider = document.querySelector('#slider') as HTMLInputElement;
+let speed = Number(slider.value);
+slider.addEventListener('input', () => {
+ speed = Number(slider.value);
+});
+
+// Initializing TypeGPU
+const root = await tgpu.init();
+
+const time = root.createUniform(d.f32);
+
+const canvas = document.querySelector('#canvas') as HTMLCanvasElement;
+const context = root.configureContext({ canvas });
+
+function noise(v: d.v2f) {
+ 'use gpu';
+ return perlin2d.sample(v);
+}
+
+const octavesAccessor = tgpu.accessor(d.u32);
+const standardizeNoiseAccessor = tgpu.accessor(d.bool);
+const getMaxValue = tgpu.comptime((octaves: number) => 1 - 2 ** -octaves);
+const rotation = d.mat2x2f(0.8, 0.6, -0.6, 0.8);
+
+function fbm(v: d.v2f) {
+ 'use gpu';
+ let u = d.vec2f(v);
+ let f = d.f32();
+
+ // first octave
+ {
+ let sample = noise(u + time.$);
+ if (standardizeNoiseAccessor.$) sample = sample * 0.5 + 0.5;
+ f += 0.5 * sample;
+ u = rotation * u * 2.01;
+ }
+
+ for (const i of tgpu.unroll(std.range(2, octavesAccessor.$))) {
+ let sample = noise(u);
+ if (standardizeNoiseAccessor.$) sample = sample * 0.5 + 0.5;
+ f += 0.5 ** i * sample;
+ u = rotation * u * (2 + i / 100);
+ }
+
+ // last octave
+ {
+ let sample = noise(u + std.sin(time.$));
+ if (standardizeNoiseAccessor.$) sample = sample * 0.5 + 0.5;
+ f += 0.5 ** d.f32(octavesAccessor.$) * sample;
+ u = rotation * u * 2.01;
+ }
+
+ return f / getMaxValue(octavesAccessor.$);
+}
+
+const fbm4 = tgpu.fn(fbm).with(octavesAccessor, 4).with(standardizeNoiseAccessor, false);
+const fbm6 = tgpu.fn(fbm).with(octavesAccessor, 6).with(standardizeNoiseAccessor, true);
+
+function domainWarp(v: d.v2f) {
+ 'use gpu';
+ return fbm6(v + fbm4(v + fbm4(v)));
+}
+
+function palette(t: number) {
+ 'use gpu';
+ const purple = hexToOklab('#c04bf2');
+ const blue = hexToOklab('#4e65f6');
+ const dark = hexToOklab('#0f092b');
+
+ const factor1 = std.smoothstep(0.25, 0.5, t);
+
+ const factor2 = std.smoothstep(0.5, 0.7, t);
+
+ const mixed = std.mix(std.mix(dark, blue, factor1), purple, factor2);
+
+ return oklabToRgb(mixed);
+}
+
+const VIRTUAL_GRID_SIZE = 4;
+const EPS = 0.04;
+const DIFF_SCALE = 0.4;
+
+const pipeline = root.createRenderPipeline({
+ vertex: common.fullScreenTriangle,
+ fragment: ({ uv }) => {
+ 'use gpu';
+ const centeredUV = (2 * uv - 1) * VIRTUAL_GRID_SIZE;
+
+ const sample = domainWarp(centeredUV);
+ const sampleRight = domainWarp(centeredUV + d.vec2f(EPS, 0));
+ const sampleDown = domainWarp(centeredUV + d.vec2f(0, EPS));
+
+ const dx = d.vec3f(EPS, 0, (sampleRight - sample) * DIFF_SCALE);
+ const dy = d.vec3f(0, EPS, (sampleDown - sample) * DIFF_SCALE);
+
+ const normal = std.normalize(std.cross(dx, dy));
+
+ const lightDir = std.normalize(d.vec3f(0.0, -1.0, 1.0));
+ const diffuse = std.max(0.0, std.dot(normal, lightDir));
+
+ const baseColor = palette(sample);
+
+ const litColor = baseColor * diffuse;
+
+ return d.vec4f(litColor, 1);
+ },
+});
+
+let elapsed = 0;
+let lastTimestamp: number | null = null;
+
+function render(timestamp: number) {
+ if (lastTimestamp !== null) {
+ elapsed += ((timestamp - lastTimestamp) / 1000.0) * speed;
+ }
+ lastTimestamp = timestamp;
+
+ time.write(elapsed);
+ pipeline.withColorAttachment({ view: context }).draw(3);
+
+ requestAnimationFrame(render);
+}
+
+const observer = new ResizeObserver(([entry]) => {
+ if (!entry) {
+ return;
+ }
+ const width =
+ entry.devicePixelContentBoxSize?.[0].inlineSize ||
+ entry.contentBoxSize[0].inlineSize * window.devicePixelRatio;
+ const height =
+ entry.devicePixelContentBoxSize?.[0].blockSize ||
+ entry.contentBoxSize[0].blockSize * window.devicePixelRatio;
+ canvas.width = Math.max(1, Math.min(width, root.device.limits.maxTextureDimension2D));
+ canvas.height = Math.max(1, Math.min(height, root.device.limits.maxTextureDimension2D));
+});
+try {
+ observer.observe(canvas, { box: 'device-pixel-content-box' });
+} catch {
+ observer.observe(canvas, { box: 'content-box' });
+}
+
+requestAnimationFrame(render);
diff --git a/packages/typegpu-cli/templates/template-vite-complex/src/style.css b/packages/typegpu-cli/templates/template-vite-complex/src/style.css
new file mode 100644
index 0000000000..5deb51e380
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/src/style.css
@@ -0,0 +1,315 @@
+:root {
+ --text: #6b6375;
+ --text-h: #08060d;
+ --bg: #fff;
+ --border: #e5e4e7;
+ --accent: #aa3bff;
+ --accent-bg: rgba(170, 59, 255, 0.1);
+ --accent-border: rgba(170, 59, 255, 0.5);
+ --social-bg: rgba(244, 243, 236, 0.5);
+ --shadow: rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --mono: ui-monospace, Consolas, monospace;
+
+ font: 18px/145% var(--sans);
+ letter-spacing: 0.18px;
+ color-scheme: light dark;
+ color: var(--text);
+ background: var(--bg);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ @media (max-width: 1024px) {
+ font-size: 16px;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --text: #9ca3af;
+ --text-h: #f3f4f6;
+ --bg: #16171d;
+ --border: #2e303a;
+ --accent: #c084fc;
+ --accent-bg: rgba(192, 132, 252, 0.15);
+ --accent-border: rgba(192, 132, 252, 0.5);
+ --social-bg: rgba(47, 48, 58, 0.5);
+ --shadow: rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+ }
+
+ .button-icon {
+ filter: invert(1) brightness(2);
+ }
+}
+
+body {
+ margin: 0;
+}
+
+h2 {
+ font-family: var(--heading);
+ font-weight: 500;
+ color: var(--text-h);
+}
+
+h2 {
+ font-size: 24px;
+ line-height: 118%;
+ letter-spacing: -0.24px;
+ margin: 0 0 8px;
+ @media (max-width: 1024px) {
+ font-size: 20px;
+ }
+}
+p {
+ margin: 0;
+}
+
+.slider {
+ appearance: none;
+ width: min(260px, 70vw);
+ height: 24px;
+ background: transparent;
+ border: none;
+ outline: none;
+ cursor: pointer;
+ touch-action: pan-y;
+ margin-bottom: 24px;
+
+ &::-webkit-slider-runnable-track {
+ height: 6px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, #4e65f6, var(--accent));
+ box-shadow: 0 0 12px rgba(170, 59, 255, 0.18);
+ }
+
+ &::-webkit-slider-thumb {
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ margin-top: -6px;
+ border: 2px solid var(--bg);
+ border-radius: 50%;
+ background: var(--accent);
+ cursor: pointer;
+ box-shadow: 0 2px 8px rgba(8, 6, 13, 0.22);
+ transition: transform 0.2s;
+ }
+
+ &::-moz-range-track {
+ height: 6px;
+ background: linear-gradient(90deg, #4e65f6, var(--accent));
+ border-radius: 999px;
+ border: none;
+ box-shadow: 0 0 12px rgba(170, 59, 255, 0.18);
+ }
+
+ &::-moz-range-progress {
+ height: 6px;
+ background: linear-gradient(90deg, #4e65f6, var(--accent));
+ border-radius: 999px;
+ }
+
+ &::-moz-range-thumb {
+ width: 14px;
+ height: 14px;
+ border: 2px solid var(--bg);
+ border-radius: 50%;
+ background: var(--accent);
+ cursor: pointer;
+ box-shadow: 0 2px 8px rgba(8, 6, 13, 0.22);
+ transition: transform 0.2s;
+ }
+
+ &:active::-webkit-slider-thumb {
+ transform: scale(0.92);
+ }
+
+ &:active::-moz-range-thumb {
+ transform: scale(0.92);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 4px;
+ }
+}
+
+#typegpu picture {
+ display: flex;
+ align-items: center;
+ height: 28px;
+ margin: 0 0 8px;
+
+ @media (max-width: 1024px) {
+ height: 24px;
+ justify-content: center;
+ }
+}
+
+.typegpu-logo {
+ display: block;
+ height: 48px;
+ width: auto;
+
+ @media (max-width: 1024px) {
+ height: 32px;
+ margin: 0;
+ }
+}
+
+.title-with-icon {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ @media (max-width: 1024px) {
+ justify-content: center;
+ }
+}
+
+#app {
+ width: 1126px;
+ max-width: 100%;
+ margin: 0 auto;
+ text-align: center;
+ border-inline: 1px solid var(--border);
+ height: 100svh;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+#center {
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+ place-content: center;
+ place-items: center;
+ flex-grow: 1;
+ padding-top: 40px;
+
+ @media (max-width: 1024px) {
+ padding: 32px 20px 24px;
+ gap: 18px;
+ }
+}
+
+#canvas {
+ width: min(55vw, 55svh);
+ height: min(55vw, 55svh);
+ max-width: 100%;
+}
+
+#next-steps {
+ display: flex;
+ border-top: 1px solid var(--border);
+ text-align: left;
+
+ & > div {
+ flex: 1 1 0;
+ min-width: 0;
+ padding: 32px;
+ @media (max-width: 1024px) {
+ padding: 24px 20px;
+ }
+ }
+
+ .icon {
+ width: 22px;
+ height: 22px;
+ }
+
+ @media (max-width: 1024px) {
+ flex-direction: column;
+ text-align: center;
+ }
+}
+
+#typegpu {
+ border-right: 1px solid var(--border);
+
+ @media (max-width: 1024px) {
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+}
+
+#next-steps ul {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin: 32px 0 0;
+
+ a {
+ color: var(--text-h);
+ font-size: 16px;
+ border-radius: 6px;
+ background: var(--social-bg);
+ display: flex;
+ padding: 6px 12px;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+ transition: box-shadow 0.3s;
+
+ &:hover {
+ box-shadow: var(--shadow);
+ }
+ .button-icon {
+ height: 18px;
+ width: 18px;
+ }
+ }
+
+ @media (max-width: 1024px) {
+ margin-top: 20px;
+ justify-content: center;
+
+ li {
+ flex: 1 1 calc(50% - 8px);
+ }
+
+ a {
+ width: 100%;
+ justify-content: center;
+ box-sizing: border-box;
+ }
+ }
+}
+
+#spacer {
+ height: 88px;
+ border-top: 1px solid var(--border);
+ @media (max-width: 1024px) {
+ height: 48px;
+ }
+}
+
+.ticks {
+ position: relative;
+ width: 100%;
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ top: -4.5px;
+ border: 5px solid transparent;
+ }
+
+ &::before {
+ left: 0;
+ border-left-color: var(--border);
+ }
+ &::after {
+ right: 0;
+ border-right-color: var(--border);
+ }
+}
diff --git a/packages/typegpu-cli/templates/template-vite-complex/tsconfig.json b/packages/typegpu-cli/templates/template-vite-complex/tsconfig.json
new file mode 100644
index 0000000000..f1d813d9e3
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "types": ["@webgpu/types", "vite/client"],
+ "skipLibCheck": true,
+
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ "strict": true,
+ "noImplicitReturns": true,
+ "exactOptionalPropertyTypes": true,
+ "erasableSyntaxOnly": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/typegpu-cli/templates/template-vite-complex/vite.config.ts b/packages/typegpu-cli/templates/template-vite-complex/vite.config.ts
new file mode 100644
index 0000000000..7b6199856c
--- /dev/null
+++ b/packages/typegpu-cli/templates/template-vite-complex/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite';
+import typegpu from 'unplugin-typegpu/vite';
+
+export default defineConfig({
+ plugins: [typegpu({})],
+});