{
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = util.ctx(512, 512);
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({
code: `
struct Vertex {
@location(0) position: vec2f,
@location(1) color: vec4f,
@location(2) offset: vec2f,
@location(3) scale: vec2f,
};
struct VertexOut {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
@vertex
fn vs(vertex: Vertex) -> VertexOut {
var vsOut: VertexOut;
vsOut.position = vec4f(vertex.position * vertex.scale + vertex.offset, 0.0, 1.0);
vsOut.color = vertex.color;
return vsOut;
}
@fragment
fn fs(vsOut: VertexOut) -> @location(0) vec4f {
return vsOut.color;
}
`
})
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
{
arrayStride: 2 * 4,
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x2' },
],
},
{
arrayStride: 6 * 4,
stepMode: 'instance',
attributes: [
{ shaderLocation: 1, offset: 0, format: 'float32x4' },
{ shaderLocation: 2, offset: 0 + 4 * 4, format: 'float32x2' },
],
},
{
arrayStride: 2 * 4,
stepMode: 'instance',
attributes: [
{ shaderLocation: 3, offset: 0, format: 'float32x2' },
]
}
]
},
fragment: {
module,
entryPoint: 'fs',
targets: [{ format }]
}
})
const xExtent = [Math.min(...data.map(d => d.len)), Math.max(...data.map(d => d.len))];
const yExtent = [Math.min(...data.map(d => d.depth)), Math.max(...data.map(d => d.depth))];
const kNumObjects = data.length;
const dynamicData = [];
const staticUnitSize =
4 * 4 +
2 * 4;
const dynamicUnitSize =
2 * 4;
const staticBufferSize = staticUnitSize * kNumObjects;
const dynamicBufferSize = dynamicUnitSize * kNumObjects;
const staticBuffer = device.createBuffer({
size: staticBufferSize,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
const dynamicBuffer = device.createBuffer({
size: dynamicBufferSize,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
const kColorOffset = 0;
const kOffsetOffset = 4;
const kScaleOffset = 0;
const staticValues = new Float32Array(staticBufferSize / 4);
for (let i = 0; i < kNumObjects; i++) {
const d = data[i];
const c = species.indexOf(data[i].species);
const staticOffset = i * (staticUnitSize / 4);
staticValues.set(
[+(c === 0), +(c === 1), +(c === 2), 1],
staticOffset + kColorOffset
);
staticValues.set(
[
-1 + ((d.len - xExtent[0]) / (xExtent[1] - xExtent[0])) * (1 - (-1)),
-1 + ((d.depth - yExtent[0]) / (yExtent[1] - yExtent[0])) * (1 - (-1))
],
staticOffset + kOffsetOffset
);
dynamicData.push({ scale: 1 });
}
device.queue.writeBuffer(staticBuffer, 0, staticValues);
const dynamicValues = new Float32Array(dynamicBufferSize / 4);
const { vertices, count: vertexCount } = util.geom.circle({ r: 0.01, innerR: 0 });
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);
const renderPassDescriptor = {
colorAttachments: [
{
clearValue: [0, 0, 0, 1],
loadOp: 'clear',
storeOp: 'store',
view: undefined,
},
],
};
function render() {
renderPassDescriptor.colorAttachments[0].view = context.getCurrentTexture().createView();
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setVertexBuffer(1, staticBuffer);
pass.setVertexBuffer(2, dynamicBuffer);
const aspect = context.canvas.width / context.canvas.height;
dynamicData.forEach(({ scale }, i) => {
const offset = i * (dynamicUnitSize / 4);
dynamicValues.set([scale / aspect, scale], offset + kScaleOffset);
});
device.queue.writeBuffer(dynamicBuffer, 0, dynamicValues);
pass.draw(vertexCount, kNumObjects);
pass.end();
device.queue.submit([encoder.finish()]);
}
render()
return htl.html`
<figure>
${context.canvas}
<figcaption>
x = penguin culmen length (mm); y = penguin culment depth (mm)
<br><br>
${legend}
<br>
</figcaption>
</figure>
`;
}