Public
Edited
Feb 22, 2024
1 fork
1 star
Insert cell
Insert cell
{
let canvas = Object.assign(document.createElement("canvas"), {
width: w,
height: h
});
let context = canvas.getContext("webgpu");
let adapter = await navigator.gpu?.requestAdapter();
let device = await adapter?.requestDevice();
let format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });

let [x_buffer, y_buffer] = [x_values, y_values].map((arr) => {
let buffer = device.createBuffer({
size: arr.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
});
new Float32Array(buffer.getMappedRange()).set(arr);
buffer.unmap();
return buffer;
});

let xylayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: { type: "read-only-storage" }
},
{
binding: 1,
visibility: GPUShaderStage.VERTEX,
buffer: { type: "read-only-storage" }
}
]
});

let uniforms = new Float32Array(50);
let u_zoom = uniforms.subarray(0, 16);
let u_window_scale = uniforms.subarray(16, 32);
let u_untransform = uniforms.subarray(32, 48);
let ubuffer = device.createBuffer({
size: uniforms.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
{
let mats = window_transform(scales.x, scales.y, w, h);
u_window_scale.set(mats[0]);
u_untransform.set(mats[1]);
}

let ulayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: { type: "uniform" }
}
]
});

let code = `
@group(0) @binding(0) var<storage, read> x_values : array<f32>;
@group(0) @binding(1) var<storage, read> y_values : array<f32>;
@group(1) @binding(0) var<uniform> uni: Uniforms;
struct Uniforms {
zoom: mat4x4<f32>,
window_scale: mat4x4<f32>,
untransform: mat4x4<f32>,
};

@vertex fn vert(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {
let x = x_values[vertex_index];
let y = y_values[vertex_index];
let t = uni.untransform * uni.zoom * uni.window_scale;
let pos = t * vec4(x, y, 1.0, 1.0);
return pos;
}

@fragment fn frag() -> @location(0) vec4<f32> {
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
`;
let module = device.createShaderModule({ code });
let pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [xylayout, ulayout]
}),
vertex: {
module,
entryPoint: "vert",
buffers: []
},
fragment: {
module,
entryPoint: "frag",
targets: [{ format }]
},
primitive: { topology: "point-list" }
});

let xygroup = device.createBindGroup({
layout: xylayout,
entries: [
{ binding: 0, resource: { buffer: x_buffer } },
{ binding: 1, resource: { buffer: y_buffer } }
]
});
let ugroup = device.createBindGroup({
layout: ulayout,
entries: [{ binding: 0, resource: { buffer: ubuffer } }]
});

function render() {
let encoder = device.createCommandEncoder();
let pass = encoder.beginRenderPass({
colorAttachments: [
{
clearValue: [1, 1, 1, 1],
loadOp: "clear",
storeOp: "store",
view: context.getCurrentTexture().createView()
}
]
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, xygroup);
pass.setBindGroup(1, ugroup);
pass.draw(x_values.length);
pass.end();
device.queue.submit([encoder.finish()]);
}

function zoomed({ k, x, y }) {
let mat = [
[k, 0, 0, 0],
[0, k, 0, 0],
[0, 0, 1, 0],
[x, y, 0, 1]
];
u_zoom.set(mat.flat());
device.queue.writeBuffer(ubuffer, 0, uniforms);
render();
}
d3.select(context.canvas).call(
d3
.zoom()
.scaleExtent([0.1, 1000])
.on("zoom", ({ transform }) => zoomed(transform))
);
zoomed(d3.zoomIdentity);
return context.canvas;
}
Insert cell
x_values = Float32Array.from({ length }).map((_) => (Math.random() - 0.5) * 2)
Insert cell
y_values = Float32Array.from({ length }).map((_) => (Math.random() - 0.5) * 2)
Insert cell
length = 100000
Insert cell
d3 = require("d3")
Insert cell
scales = {
let square_box = d3.min([w, h]);
let scales = {};
let data = { x: x_values, y: y_values };

for (let [name, dim] of [
["x", w],
["y", h]
]) {
const buffer = (dim - square_box) / 2;
scales[name] = d3
.scaleLinear()
.domain(d3.extent(data[name]))
.range([buffer, dim - buffer]);
}

return scales;
}
Insert cell
w = width
Insert cell
h = 300
Insert cell
function window_transform(x_scale, y_scale, width, height) {
// A function that creates the two matrices a webgl shader needs, in addition to the zoom state,
// to stay aligned with canvas and d3 zoom.

// width and height are svg parameters; x and y scales project from the data x and y into the
// the webgl space.

// Given two d3 scales in coordinate space, create two matrices that project from the original
// space into [-1, 1] webgl space.

function gap(array) {
// Return the magnitude of a scale.
return array[1] - array[0];
}

let x_mid = d3.mean(x_scale.domain());
let y_mid = d3.mean(y_scale.domain());

const xmulti = gap(x_scale.range()) / gap(x_scale.domain());
const ymulti = gap(y_scale.range()) / gap(y_scale.domain());

// the xscale and yscale ranges may not be the full width or height.

const aspect_ratio = width / height;

// translates from data space to scaled space.
const m1 = [
[xmulti, 0, 0, 0],
[0, ymulti, 0, 0],
[0, 0, 1, 0],
[
-xmulti * x_mid + d3.mean(x_scale.range()),
-ymulti * y_mid + d3.mean(y_scale.range()),
0,
1
]
];

// translate from scaled space to webgl space.
// The '2' here is because webgl space runs from -1 to 1; the shift at the end is to
// shift from [0, 2] to [-1, 1]
const m2 = [
[2 / width, 0, 0, 0], // First column
[0, -2 / height, 0, 0], // Second column
[0, 0, 1, 0], // Third column (unchanged for z-axis in 2D transformations)
[-1, 1, 0, 1] // Fourth column, with translations adjusted for WebGL space
];

return [m1.flat(), m2.flat()];
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more