TypeShade, a shader language you write in TypeScript

TypeShade is a language of its own, with TypeScript's syntax, types and editor. A file that starts with "use typeshade" emits WGSL for WebGPU and GLSL ES 3.00 for WebGL2. 73 of the 98 examples in the repository emit both from one file.

Playground Quick Start Why TypeShade

first.shade.ts
first.shade.ts
"use typeshade"
class Uniforms {
time: f32
mouse: vec2
warp: f32
ink: vec3
paper: vec3
}
declare const u: uniform<Uniforms>
@fragment
export function main(@location(0) uv: vec2): vec4 {
const p = uv * 4.0 - u.mouse * 2.0
const a = sin(p.x + u.time * 0.5) + cos(p.y - u.time * 0.3)
const b = sin(p.y * u.warp + a) * cos(p.x * u.warp - a)
const t = b * 0.5 + 0.5
const s = t * t * (3.0 - 2.0 * t)
return vec4(mix(u.ink, u.paper, s * s * (3.0 - 2.0 * s)), 1.0)
}
Warped bands shader, written in TypeScript and compiled by TypeShade, rendered at build time.

Filled by the page every frame, with no control: time, mouse. mouse runs 0 to 1 over the canvas from the bottom left, the space uv is in.

The file on the left, drawn on the right. Edit a line or move a control and the picture follows.

One file, two targets

The compiler lowers hello.shade.ts to one intermediate representation and emits both targets from it. The file imports nothing, so it is the whole program; the pane beside it holds the WGSL and the GLSL ES 3.00 fragment a host hands to WebGPU or WebGL2, one tab each.

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.) };
}
struct Clip {
@builtin(position) pos: vec4<f32>,
}
struct Color {
@location(0) color: vec4<f32>,
}
@vertex
fn vs(@builtin(vertex_index) i: u32) -> Clip {
let _cse0 = (i == 2u);
let x = select(select(-0.8, 0.0, _cse0), 0.8, (i == 1u));
let y = select(-0.8, 0.8, _cse0);
return Clip(vec4<f32>(x, y, 0.0, 1.0));
}
@fragment
fn fs() -> Color {
return Color(vec4<f32>(1.0, 0.0, 0.0, 1.0));
}
#version 300 es
precision highp float;
precision highp int;
layout(location = 0) out vec4 color;
void main() {
color = vec4(1.0, 0.0, 0.0, 1.0);
}

More shaders

All examples

What carries over from TypeScript

GPU code has three things: resources, value layouts and entry points. TypeShade gives each a place TypeScript already has, and keeps the rest of the language where it fits the GPU. The full rules are in the language guide.

"use typeshade"
The directive is the language boundary

It is the first statement of the file, where a JavaScript directive goes. A file without it does not compile as a shader; with it, everything under it is checked as TypeShade and lowered to the compiler's intermediate representation.

f32, vec3, mat4, sin(x)
GPU types and builtins are globals

f32, vec3, mat4 and the builtins such as sin and vec4(...) need no import, and Math.sin and Math.PI are aliases of the same operations. The checker applies the shader rules to them in the editor, before any code is emitted.

class VsIn { @location(0) uv: vec2 }
A type or a class is a value layout

Plain data is a type alias; a class carries per-field metadata such as @location and @builtin, and its fields lay out the struct both targets receive.

new Circle(center, 0.3).coverage(p)
A class is a TypeScript class

Fields, a constructor and new, methods and static functions, extends with super and abstract, generic classes and functions, and the mixin pattern all compile. A method lowers to a function that takes the struct first, and a generic is compiled once per set of type arguments. The gallery has a file for each of them.

declare const u: uniform<Camera>
declare names a resource the host fills

uniform<T> reads a uniform block, and storage<T, "read_write"> is a storage buffer the shader writes. There is no initializer: the host owns the slot, in the order the file declares them, and reflect() reports its layout.

@fragment export function fs(v: VsOut): vec4
A decorated export is an entry point

@vertex, @fragment and @compute([64, 1, 1]) name the stage; a function without one is a helper. Stage inputs are explicit parameters, a @builtin("vertex_index") or a @location. There is no hidden global.

await scale({ k, xs, ys }, 4)
A host file imports the module and calls it

Through the typeshade/vite plugin, a host file imports a .shade.ts and calls what it exports. A helper runs on the CPU, a @compute entry runs on WebGPU or the WebGL2 compute tier where supported, and a full-screen @fragment entry draws into a canvas as fs(canvas, bindings). The bindings object is typed from the module, so a binding the call leaves out is a type error on the host's line.

Checked against the CPU

The same module runs on the CPU in f64, and the test suite checks the compiler's algebra against it. Every emit is compiled on Tint and linked on WebGL2 on each push. Below, the gradient pass drawn by each backend from the same file.

Gradient pass (source twin) shader, written in TypeScript and compiled by TypeShade, rendered at build time.
The gradient pass, drawn at build time from the emitted WGSL.
Gradient pass (source twin) shader, written in TypeScript and compiled by TypeShade, rendered at build time.
The gradient pass, drawn at build time from the emitted GLSL ES 3.00.

Install

Install the repository with npm through its Git URL. Import the package as typeshade; Quick Start covers installation and the first shader. The 0.2.0 registry release is not published yet.

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.

Playground Quick Start