diff --git a/src/components/SampleCategory.module.css b/src/components/SampleCategory.module.css
new file mode 100644
index 00000000..abc8e616
--- /dev/null
+++ b/src/components/SampleCategory.module.css
@@ -0,0 +1,8 @@
+.sampleCategory {
+ display: flex;
+}
+
+
+li.selected a {
+ color: #ff0000;
+}
\ No newline at end of file
diff --git a/src/components/SampleCategory.tsx b/src/components/SampleCategory.tsx
new file mode 100644
index 00000000..428b8006
--- /dev/null
+++ b/src/components/SampleCategory.tsx
@@ -0,0 +1,84 @@
+import styles from './SampleCategory.module.css';
+
+import { NextRouter } from 'next/router';
+import Link from 'next/link';
+import { PageCategory } from '../pages/samples/[slug]';
+
+type PageType = {
+ [key: string]: React.ComponentType & { render: { preload: () => void } };
+};
+
+type PageComponentType = {
+ [key: string]: React.ComponentType;
+};
+
+interface SampleCategoryProps {
+ category: PageCategory;
+ router: NextRouter;
+ onClickPageLink: () => void;
+}
+
+export const SampleCategory = ({
+ category,
+ onClickPageLink,
+ router,
+}: SampleCategoryProps) => {
+ const { title, pages, sampleNames } = category;
+ return (
+
+
+
+ {title}
+
+
+ {sampleNames.map((slug) => {
+ return (
+
onClickPageLink()}
+ />
+ );
+ })}
+
+ );
+};
+
+interface SampleLinkProps {
+ router: NextRouter;
+ slug: string;
+ pages: PageComponentType;
+ onClick: () => void;
+}
+
+export const SampleLink = ({
+ router,
+ slug,
+ pages,
+ onClick,
+}: SampleLinkProps) => {
+ const className =
+ router.pathname === `/samples/[slug]` && router.query['slug'] === slug
+ ? styles.selected
+ : undefined;
+
+ return (
+ {
+ (pages as PageType)[slug].render.preload();
+ }}
+ >
+ onClick()}>
+ {slug}
+
+
+ );
+};
diff --git a/src/pages/MainLayout.module.css b/src/pages/MainLayout.module.css
index c60673fb..f91ac2a9 100644
--- a/src/pages/MainLayout.module.css
+++ b/src/pages/MainLayout.module.css
@@ -23,15 +23,15 @@
margin-block-end: 16px;
}
+.exampleList h3 {
+ color: rgb(43, 126, 171);
+}
+
.exampleList li {
list-style: none;
padding: 0.3em 0;
}
-.exampleList li.selected a {
- color: #ff0000;
-}
-
.expand {
display: none;
float: right;
@@ -45,7 +45,6 @@
.panel .panelContents {
display: block;
transition: max-height 0s;
- overflow: none;
max-height: 100vh;
}
@@ -67,12 +66,12 @@
.panel .panelContents {
display: block;
transition: max-height 0.3s ease-out;
- overflow: hidden;
max-height: 0px;
}
.panel[data-expanded='false'] .panelContents {
max-height: 0vh;
+ overflow: hidden;
}
.panel[data-expanded='true'] .panelContents {
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index d01f075c..a44ec918 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -7,20 +7,16 @@ import { useMemo, memo, useState } from 'react';
import './styles.css';
import styles from './MainLayout.module.css';
-import { pages } from './samples/[slug]';
+import { pageCategories } from './samples/[slug]';
+import { SampleCategory } from '../components/SampleCategory';
const title = 'WebGPU Samples';
-type PageType = {
- [key: string]: React.ComponentType & { render: { preload: () => void } };
-};
-
const MainLayout: React.FunctionComponent = ({
Component,
pageProps,
}) => {
const router = useRouter();
- const samplesNames = Object.keys(pages);
const [listExpanded, setListExpanded] = useState(false);
const ComponentMemo = useMemo(() => {
@@ -66,36 +62,26 @@ const MainLayout: React.FunctionComponent = ({
Github
-
- {samplesNames.map((slug) => {
- const className =
- router.pathname === `/samples/[slug]` &&
- router.query['slug'] === slug
- ? styles.selected
- : undefined;
- return (
- - {
- (pages as PageType)[slug].render.preload();
- }}
- >
- {
- setListExpanded(false);
- }}
- >
- {slug}
-
-
- );
- })}
-
+ {pageCategories.map((category) => {
+ return (
+
+ setListExpanded(false)}
+ />
+
+ );
+ })}
- Other Pages
-
+ Other Pages
+
-
import('../../sample/helloTriangle/main')),
helloTriangleMSAA: dynamic(
() => import('../../sample/helloTriangleMSAA/main')
),
- resizeCanvas: dynamic(() => import('../../sample/resizeCanvas/main')),
rotatingCube: dynamic(() => import('../../sample/rotatingCube/main')),
twoCubes: dynamic(() => import('../../sample/twoCubes/main')),
texturedCube: dynamic(() => import('../../sample/texturedCube/main')),
instancedCube: dynamic(() => import('../../sample/instancedCube/main')),
fractalCube: dynamic(() => import('../../sample/fractalCube/main')),
- cameras: dynamic(() => import('../../sample/cameras/main')),
cubemap: dynamic(() => import('../../sample/cubemap/main')),
- computeBoids: dynamic(() => import('../../sample/computeBoids/main')),
- animometer: dynamic(() => import('../../sample/animometer/main')),
- videoUploading: dynamic(() => import('../../sample/videoUploading/main')),
- videoUploadingWebCodecs: dynamic(
- () => import('../../sample/videoUploadingWebCodecs/main')
- ),
+};
+
+// Samples that demonstrate functionality specific to WebGPU, or demonstrate the particularities
+// of how WebGPU implements a particular feature within its api. For instance, while many of the
+// sampler parameters in the 'samplerParameters' sample have direct analogues in other graphics api,
+// the primary purpose of 'sampleParameters' is to demonstrate their specific nomenclature and
+// functionality within the context of the WebGPU API.
+const webGPUFeaturesPages: PageComponentType = {
samplerParameters: dynamic(
() => import('../../sample/samplerParameters/main')
),
- imageBlur: dynamic(() => import('../../sample/imageBlur/main')),
- shadowMapping: dynamic(() => import('../../sample/shadowMapping/main')),
reversedZ: dynamic(() => import('../../sample/reversedZ/main')),
+ renderBundles: dynamic(() => import('../../sample/renderBundles/main')),
+};
+
+// A selection of samples demonstrating various graphics techniques, utilizing various features
+// of the WebGPU API, and often executing render and compute pipelines in tandem to achieve their
+// visual results. The techniques demonstrated may even be independent of WebGPU (e.g. 'cameras')
+const graphicsDemoPages: PageComponentType = {
+ cameras: dynamic(() => import('../../sample/cameras/main')),
+ normalMap: dynamic(() => import('../../sample/normalMap/main')),
+ shadowMapping: dynamic(() => import('../../sample/shadowMapping/main')),
deferredRendering: dynamic(
() => import('../../sample/deferredRendering/main')
),
particles: dynamic(() => import('../../sample/particles/main')),
+ imageBlur: dynamic(() => import('../../sample/imageBlur/main')),
cornell: dynamic(() => import('../../sample/cornell/main')),
- gameOfLife: dynamic(() => import('../../sample/gameOfLife/main')),
- renderBundles: dynamic(() => import('../../sample/renderBundles/main')),
- worker: dynamic(() => import('../../sample/worker/main')),
'A-buffer': dynamic(() => import('../../sample/a-buffer/main')),
- bitonicSort: dynamic(() => import('../../sample/bitonicSort/main')),
- normalMap: dynamic(() => import('../../sample/normalMap/main')),
skinnedMesh: dynamic(() => import('../../sample/skinnedMesh/main')),
};
+// Samples that demonstrate the GPGPU functionality of WebGPU. These samples generally provide some
+// user-facing representation (e.g. image, text, or audio) of the result of compute operations.
+// Any rendering code is primarily for visualization, not key to the unique part of the sample;
+// rendering could also be done using canvas2D without detracting from the sample's usefulness.
+const gpuComputeDemoPages: PageComponentType = {
+ computeBoids: dynamic(() => import('../../sample/computeBoids/main')),
+ gameOfLife: dynamic(() => import('../../sample/gameOfLife/main')),
+ bitonicSort: dynamic(() => import('../../sample/bitonicSort/main')),
+};
+
+// Samples that demonstrate how to integrate WebGPU and/or WebGPU render operations with other
+// functionalities provided by the web platform.
+const webPlatformPages: PageComponentType = {
+ resizeCanvas: dynamic(() => import('../../sample/resizeCanvas/main')),
+ videoUploading: dynamic(() => import('../../sample/videoUploading/main')),
+ videoUploadingWebCodecs: dynamic(
+ () => import('../../sample/videoUploadingWebCodecs/main')
+ ),
+ worker: dynamic(() => import('../../sample/worker/main')),
+};
+
+// Samples whose primary purpose is to benchmark WebGPU performance.
+const benchmarkPages: PageComponentType = {
+ animometer: dynamic(() => import('../../sample/animometer/main')),
+};
+
+const pages: PageComponentType = {
+ ...graphicsBasicsPages,
+ ...webGPUFeaturesPages,
+ ...graphicsDemoPages,
+ ...gpuComputeDemoPages,
+ ...webPlatformPages,
+ ...benchmarkPages,
+};
+
+export interface PageCategory {
+ title: string;
+ pages: PageComponentType;
+ sampleNames: string[];
+}
+
+const createPageCategory = (
+ title: string,
+ pages: PageComponentType
+): PageCategory => {
+ return {
+ title,
+ pages,
+ sampleNames: Object.keys(pages),
+ };
+};
+
+export const pageCategories: PageCategory[] = [
+ createPageCategory('Basic Graphics', graphicsBasicsPages),
+ createPageCategory('WebGPU Features', webGPUFeaturesPages),
+ createPageCategory('GPGPU Demos', gpuComputeDemoPages),
+ createPageCategory('Graphics Techniques', graphicsDemoPages),
+ createPageCategory('Web Platform Integration', webPlatformPages),
+ createPageCategory('Benchmarks', benchmarkPages),
+];
+
function Page({ slug }: Props): JSX.Element {
const PageComponent = pages[slug];
return ;
}
export const getStaticPaths: GetStaticPaths = async () => {
+ const paths = Object.keys(pages).map((p) => ({
+ params: { slug: p },
+ }));
return {
- paths: Object.keys(pages).map((p) => {
- return { params: { slug: p } };
- }),
+ paths,
fallback: false,
};
};
diff --git a/src/pages/styles.css b/src/pages/styles.css
index 7a54fbff..7a697034 100644
--- a/src/pages/styles.css
+++ b/src/pages/styles.css
@@ -24,6 +24,11 @@ a:hover {
text-decoration: underline;
}
+h3 {
+ margin-bottom: 5px;
+ margin-top: 5px;
+}
+
main {
position: relative;
flex: 1;