Quick Start
On this page

Quick Start

Install

Terminal window
npm install https://github.com/typeshade/typeshade.git
npm install --save-dev tsx

The Git dependency contains TypeScript source. tsx runs the compiler and CLI under Node.js.

Start with a TypeScript file whose first statement is "use typeshade". That directive crosses the boundary into the TypeShade shader language; the example below is a complete first shader, written with a class and stage-decorated functions.

hello.shade.ts
"use typeshade";
class Clip {
@builtin("position") pos: vec4;
}
class Color {
@location(0) color: vec4;
}
@vertex
export function vs(@builtin("vertex_index") i: u32): Clip {
const x = i === 1 ? 0.8 : i === 2 ? 0. : -0.8;
const y = i === 2 ? 0.8 : -0.8;
return { pos: vec4(x, y, 0., 1.) };
}
@fragment
export function fs(): Color {
return { color: vec4(1., 0., 0., 1.) };
}

Live example

sin turns the x coordinate into a wave, and time scrolls it. That is the whole file below, and it runs here: edit a line and the canvas follows on the next keystroke.

stripes.shade.ts
"use typeshade"
const TAU: f32 = 6.2831853
class Uniforms {
time: f32
bands: f32
low: vec3
high: vec3
}
declare const u: uniform<Uniforms>
@fragment
export function main(@location(0) uv: vec2): vec4 {
const phase = (uv.x * u.bands + u.time) * TAU
const wave = sin(phase) * 0.5 + 0.5
return vec4(mix(u.low, u.high, wave), 1.)
}
Sine stripes shader, written in TypeScript and compiled by TypeShade, rendered at build time.
A sine wave across the canvas, mixed between two colours.

bands is how many times the wave repeats between the left edge and the right, and time slides the pattern along. Every column holds one colour, because only uv.x reaches phase.

Filled by the page every frame, with no control: time.

Try the following, with the controls or by editing a line:

  • Drag bands to the bottom of its range for a single wave, then to the top and watch the crests narrow to lines.
  • Change uv.x to uv.y on the phase line. The stripes lie down, because uv runs up the canvas as well as across it.
  • Add speed: f32 to Uniforms, then multiply u.time by u.speed on the phase line. A slider named speed appears under the canvas, and struct Uniforms under Emitted output grows a field: the controls and the WGSL are both read from the module the compiler just built.
Emitted output

The whole module the compiler saw. The page added the vertex entry and the VsOut struct it returns; the rest is the file above.

WGSL
const TAU: f32 = 6.2831853;
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
struct Uniforms {
time: f32,
bands: f32,
low: vec3<f32>,
high: vec3<f32>,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertex
fn fullscreen(@builtin(vertex_index) i: u32) -> VsOut {
let x = select(-1.0, 3.0, (i == 1u));
let y = select(-1.0, 3.0, (i == 2u));
return VsOut(vec4<f32>(x, y, 0.0, 1.0), vec2<f32>(((x * 0.5) + 0.5), ((y * 0.5) + 0.5)));
}
@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let phase = (((uv.x * u.bands) + u.time) * TAU);
let wave = ((sin(phase) * 0.5) + 0.5);
return vec4<f32>(mix(u.low, u.high, wave), 1.0);
}
GLSL ES 3.00 vertex
#version 300 es
precision highp float;
precision highp int;
const float TAU = 6.2831853;
out vec2 uv;
void main() {
uint i = uint(gl_VertexID);
float x = ((i == 1u) ? 3.0 : -1.0);
float y = ((i == 2u) ? 3.0 : -1.0);
gl_Position = vec4(x, y, 0.0, 1.0);
uv = vec2(((x * 0.5) + 0.5), ((y * 0.5) + 0.5));
}
GLSL ES 3.00 fragment
#version 300 es
precision highp float;
precision highp int;
const float TAU = 6.2831853;
layout(std140) uniform Uniforms {
float time;
float bands;
vec3 low;
vec3 high;
} u;
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
float phase = (((uv.x * u.bands) + u.time) * TAU);
float wave = ((sin(phase) * 0.5) + 0.5);
_ret = vec4(mix(u.low, u.high, wave), 1.0);
}
Uniform block
FieldTypeOffset
timef320
bandsf324
lowvec3<f32>16
highvec3<f32>32

A host compiles a file with one call. Save the gradient pass below as gradient.shade.ts, 32 lines with a uniform block, a vertex stage and a fragment stage, and compile it from a host file of 6 lines that calls compile() and prints the WGSL:

gradient.shade.ts
"use typeshade";
class Uniforms {
top: vec4;
bottom: vec4;
mix_bias: f32;
}
declare const u: uniform<Uniforms>;
class VsOut {
@builtin("position") pos: vec4;
@location(0) uv: vec2;
}
@vertex
export function vs_full(@builtin("vertex_index") idx: u32): VsOut {
let pos = vec2(-1., -1.);
if (idx === 1) {
pos = vec2(3., -1.);
} else if (idx === 2) {
pos = vec2(-1., 3.);
}
return { pos: vec4(pos, 0., 1.), uv: vec2((pos.x + 1.) * 0.5, (pos.y + 1.) * 0.5) };
}
@fragment
export function fs_gradient(vo: VsOut): vec4 {
const t = vo.uv.y + u.mix_bias;
const rgb = mix(u.bottom.rgb, u.top.rgb, t);
return vec4(rgb, f32(1.));
}
compile.ts
import { readFileSync } from 'node:fs'
import { compile } from 'typeshade'
const { diagnostics, wgsl } = compile(readFileSync('gradient.shade.ts', 'utf8'))
for (const d of diagnostics) console.error(`${d.line}:${d.character} ${d.message}`)
console.log(wgsl)
Terminal window
npx tsx compile.ts

Running it prints WGSL for both stages. Here is the fragment entry point:

@fragment
fn fs_gradient(vo: VsOut) -> @location(0) vec4<f32> {
let t = (vo.uv.y + u.mix_bias);
let rgb = mix(u.bottom.rgb, u.top.rgb, t);
return vec4<f32>(rgb, 1.0);
}

The GLSL ES 3.00 stage for the same function, and the uniform layout reflect() recovers for it, are on the examples page. The language guide covers the rest of the surface.

The TypeShade / host boundary

TypeShade source is authored with TypeScript syntax, but a file marked with "use typeshade" follows the shader language rules. TypeScript types and syntax form the authoring surface; TypeShade GPU types, resources and shader-stage rules define the shader semantics.

  • "use typeshade" declares the language boundary.
  • GPU entry inputs are explicit parameters such as @builtin(...), not hidden globals.
  • Resource types such as uniform<T> and storage<T> express GPU resource semantics.

Connect it to the host application

A host uses the file one of three ways. It compiles the file and passes the shader source to its own WebGPU or WebGL2 code, or it imports the file and calls what the file exports. An engine that draws its own frames loads the compiled program into the program runtime instead, the third way, which WebGPU and WebGL2 describes.

Compiling the file

This way works today. Compile the file, then let the host application pass the generated WGSL or GLSL ES 3.00 source to WebGPU or WebGL2.

webgpu.ts
const shaderCode = /* WGSL emitted from the TypeShade module */
const shaderModule = device.createShaderModule({ code: shaderCode })
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: {
module: shaderModule,
entryPoint: 'paint',
},
})

TypeShade owns the language semantics and shader emission. The host owns runtime objects and their lifetimes, including the device, pipeline, bind groups, buffers, textures and command encoders.

Importing the file

The Vite plugin supports the Git-installed source package. A host file imports a .shade.ts and calls what it exports, and TypeShade's runtime creates the device and the pipelines those calls need. A helper runs on the CPU. A @compute entry is await entry(bindings, workgroups): it runs on WebGPU or WebGL2 where supported and reads back what it wrote. A full-screen @fragment entry draws into a canvas as entry(canvas, bindings), on WebGPU, then WebGL2, then the CPU.

After installation, add the Vite plugin and the host tsconfig.json settings below. The scripts run Vite and the installed CLI through tsx. The prepare script writes the host views that tsc reads. Add *.shade.typeshade.ts to .gitignore.

Terminal window
npm install --save-dev vite
vite.config.ts
import { defineConfig } from 'vite';
import { typeshade } from 'typeshade/vite';
export default defineConfig({ plugins: [typeshade()] });
tsconfig.json
{
"compilerOptions": {
// ...
"moduleSuffixes": [".typeshade", ""]
},
"exclude": ["src/**/*.shade.ts"]
}
package.json
{
"scripts": {
"dev": "tsx node_modules/vite/bin/vite.js",
"build": "tsx node_modules/vite/bin/vite.js build",
"prepare": "tsx node_modules/typeshade/src/cli/bin.ts sync"
}
}

A host file then draws hello.shade.ts, the file at the top of this page, with one call. A vertex entry cannot be called from host code yet, so the view declares vs as never, and fs fills the whole canvas in red.

app.ts
import { fs } from './hello.shade.ts';
const canvas = document.querySelector('canvas')!;
fs(canvas, {});
Terminal window
npm run dev

WebGPU and WebGL2 describes the ownership boundaries of the compiler, host calls and program runtime.

Status

The repository package is version 0.1.0. The 0.2.0 registry release is not published yet; npm can install the Git source now. The host imports typeshade from the installed package. Commit the lockfile to record the resolved Git revision. Releases lists published versions.

What to learn next

Now connect familiar TypeScript concepts to their TypeShade GPU meaning.

Read the full Language Guide

The VS Code extension shows diagnostics and the WGSL a file compiles to while you type.

Edit this page Report a problem