Public
Edited
Jan 12
Fork of WebGL Shader
Insert cell
Insert cell
Insert cell
Insert cell
canvas1.html
Insert cell
Insert cell
Insert cell
initCanvas = function({width = 400, height = 400} = {}){
const canvas = document.createElement("canvas");
canvas.style.border = "1px solid black";

const gl = canvas.getContext("webgl");
canvas.height = 400;
if (!gl) throw new Error("WebGL is not supported");
const ratio = !!highres.length && window.devicePixelRatio ? window.devicePixelRatio : 1;
canvas.width = width * ratio;
canvas.height = height * ratio;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
gl.viewport(0, 0, canvas.width, canvas.height); // Match viewport to adjusted resolution
canvas.style.border = "1px solid black";

return {html: canvas, gl, ratio}; // Render the canvas in the Observable cell

}
Insert cell
canvas1 = initCanvas();
Insert cell
Insert cell
Insert cell
vertexShaderSource1 = `
attribute vec2 aPosition; //Accepts vertex positions (2D points) as input.

void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);

//gl_Position is a special variable where the vertex position is stored for rendering.
// Outputs the position of each vertex in normalized device coordinates (NDC),
//where (-1, -1) is the bottom-left corner, (1, 1) is the top-right corner.

//The vec4(aPosition, 0.0, 1.0) converts 2D points into 4D homogeneous coordinates required by WebGL.
}
`;
Insert cell
Insert cell
fragmentShaderSource1 = `
precision mediump float; // Set precision for calculations

void main() {
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0); // Orange color

//gl_FragColor is a special variable where the pixel color is stored.
// Colors are defined as (R, G, B, A) values, where each component ranges from 0.0 (none) to 1.0 (full intensity).
}
`;
Insert cell
Insert cell
compileShaders = function(gl, vertexShaderSource, fragmentShaderSource) {
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error("Shader compilation failed: " + gl.getShaderInfoLog(shader));
}
return shader;
}
function createProgram(gl, vertexShaderSource, fragmentShaderSource) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error("Program linking failed: " + gl.getProgramInfoLog(program));
}
return program;
}
return createProgram(gl, vertexShaderSource, fragmentShaderSource); // Render the WebGL program object
}
Insert cell
Insert cell
{
const gl = canvas1.gl;
const program = compileShaders(gl, vertexShaderSource1, fragmentShaderSource1)
// Define a triangle's vertices
const vertices = new Float32Array([
0.0, 0.5, // Top
-0.5, -0.5, // Bottom left
0.5, -0.5 // Bottom right
]);
// Create and bind a buffer
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// Link buffer data to the shader's aPosition attribute
const aPosition = gl.getAttribLocation(program, "aPosition");
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition);
// Render the triangle
gl.useProgram(program);
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear the canvas to black
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
return "Triangle drawn!"; // Confirmation output
}
Insert cell
Insert cell
canvas2.html
Insert cell
canvas2 = initCanvas({width, height: width})
Insert cell
import { db } from '@observablehq/the-moma-collection-data-exploration'
Insert cell
db
select * from artworks
Insert cell
dataset = {
const X = "Width (cm)";
const Y = "Height (cm)";
const C = "Department";
const D = "DateAcquired";
const data = artworks.filter(f => f[X] && f[Y] && f[C] && f[D]);
const xScale = d3.scaleLog(d3.extent(data, d=>d[X]), [-1,1]);
const yScale = d3.scaleLog(d3.extent(data, d=>d[Y]), [-1,1]);
const cScale = d3.scaleOrdinal()
.domain([...new Set(d3.map(data, d => d[C]))])
.range(d3.schemeCategory10)

function hexToRgb(hex){
const c = d3.color(hex).rgb();
return [c.r/255, c.g/255, c.b/255]
}

const dateParse = d3.utcParse("%Y-%m-%d");

return data.map(m => {
return {
d: (m[D]),
x: xScale(m[X]),
y: yScale(m[Y]),
color: hexToRgb(cScale(m[C])),
}})
}
Insert cell
dateDomain = d3.extent(dataset, d=>d.d)
Insert cell
vertexShaderSource2 = `
attribute vec2 aPosition; // Vertex position
attribute vec3 aColor; // Vertex color
varying vec3 vColor; // Pass color to fragment shader

void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
gl_PointSize = 3.0; // Size of each point
vColor = aColor;
}
`;
Insert cell
fragmentShaderSource2 = `
precision mediump float;
varying vec3 vColor;

void main() {
gl_FragColor = vec4(vColor, 1.0); // Set point color
}
`;
Insert cell
{
const gl = canvas2.gl;
const program = compileShaders(gl, vertexShaderSource2, fragmentShaderSource2);
const vertexBuffer = gl.createBuffer();
const colorBuffer = gl.createBuffer();
const vertices = new Float32Array(dataset.flatMap((d) => [d.x, d.y]));
const colors = new Float32Array(dataset.flatMap((d) => d.color));
// Bind vertex positions
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const aPosition = gl.getAttribLocation(program, "aPosition");
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition);
// Bind vertex colors
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
const aColor = gl.getAttribLocation(program, "aColor");
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aColor);

gl.useProgram(program);
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear the canvas to black
gl.clear(gl.COLOR_BUFFER_BIT);
// Render all points
gl.drawArrays(gl.POINTS, 0, dataset.length);
return "Scatterplot rendered!"; // Confirmation output
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
canvas3.html
Insert cell
canvas3 = initCanvas({width, height: width});
Insert cell
vertexShaderSource3 = `
attribute vec2 aPosition;
uniform vec2 u_scale; // Scale factors
uniform vec2 u_translation; // Translation offset
attribute vec3 aColor; // Vertex color
varying vec3 vColor; // Pass color to fragment shader
uniform float uPointSize; // pass point size
void main() {
vec2 position = aPosition * u_scale + u_translation;
// Output the final transformed position
gl_Position = vec4(position, 0.0, 1.0);
// Uniform point size (optional)
gl_PointSize = uPointSize;

vColor = aColor;
}
`
Insert cell
fragmentShaderSource3 = `
precision mediump float;
varying vec3 vColor;
uniform float uAlpha; // Alpha value

void main() {
gl_FragColor = vec4(vColor, uAlpha);
}
`;
Insert cell
app = {
const gl = canvas3.gl;
const ratio = canvas3.ratio;
const program = compileShaders(gl, vertexShaderSource3, fragmentShaderSource3);

const sliderDates = slider.map(d => d3.timeDay.offset(dateDomain[0], d))
const data = dataset.filter(f => sliderDates[0] < f.d && f.d < sliderDates[1]);

console.log("eval")
// Flatten dataset into a Float32Array for WebGL
const vertices = new Float32Array(data.flatMap((d) => [d.x, d.y]));
const colors = new Float32Array(data.flatMap((d) => d.color));
// Create a buffer and bind data
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// Link the buffer to the vertex shader's aPosition attribute
const aPosition = gl.getAttribLocation(program, "aPosition");
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition);

// Bind vertex colors
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
const aColor = gl.getAttribLocation(program, "aColor");
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aColor);
// Uniform for transformation
const uTranslation = gl.getUniformLocation(program, "u_translation");
const uScale = gl.getUniformLocation(program, "u_scale");
const uPointSize = gl.getUniformLocation(program, "uPointSize");
const uAlpha = gl.getUniformLocation(program, "uAlpha");

const blendingMethods = {
standard: {srcFactor: gl.SRC_ALPHA, destFactor: gl.ONE_MINUS_SRC_ALPHA},
additive: {srcFactor: gl.SRC_ALPHA, destFactor: gl.ONE},
multiplicative: {srcFactor: gl.DST_ALPHA, destFactor: gl.ZERO},
screen: {srcFactor: gl.ONE, destFactor: gl.ONE_MINUS_SRC_ALPHA},
}
const bm = blendingMethods[blendingMethod || "screen"];
gl.enable(gl.BLEND);
gl.blendFunc(bm.srcFactor, bm.destFactor);

const aScale = d3.scaleLinear([1,20], [0.1, 1]).clamp(true);
const sScale = d3.scaleLinear([1,10], [1.5, 3]).clamp(true);
// Transformation state
const state = {
zoom: 2,
panX: 0,
panY: 0,
isDragging: false,
lastX: 0,
lastY: 0,
}

// Render function
function render() {
gl.useProgram(program);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

const visible = sliderDates[0]
gl.uniform2fv(uTranslation, [state.panX, state.panY]);
gl.uniform2fv(uScale, [state.zoom, state.zoom]);
gl.uniform1f(uAlpha, aScale(state.zoom));
gl.uniform1f(uPointSize, sScale(state.zoom) * ratio);
gl.drawArrays(gl.POINTS, 0, data.length);
}

function setState(prop, value){
state[prop] = value;
return state[prop];
}

render(); // Initial render
return {render, setState, state};

}

Insert cell
{
const ratio = canvas3.ratio;
// Mouse wheel for zoom
canvas3.html.addEventListener("wheel", (event) => {
app.setState("zoom", app.state.zoom * (event.deltaY < 0 ? 1.1 : 0.9));
app.render();
event.preventDefault();
});
//drag start
canvas3.html.addEventListener("mousedown", (event) => {
app.setState("isDragging", true);
app.setState("lastX", event.clientX);
app.setState("lastY", event.clientY);
});

//drag
canvas3.html.addEventListener("mousemove", (event) => {
if (app.state.isDragging) {
const dx = (event.clientX - app.state.lastX) / (canvas3.html.width/(2*ratio));
const dy = (event.clientY - app.state.lastY) / (canvas3.html.height/(2*ratio));

app.setState("panX", app.state.panX + dx);
app.setState("panY", app.state.panY - dy);
app.setState("lastX", event.clientX);
app.setState("lastY", event.clientY);
app.render();
}
});

//drag end
canvas3.html.addEventListener("mouseup", () => {
app.setState("isDragging", false);
});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof zoom = Inputs.range([10, 100], {value: 25, transform: Math.log, label: "Zoom"})
Insert cell
shader({width, height: 200, iTime: true, visibility, inputs: {size: viewof zoom}})`

mat2 rotate2d(float a) {
return mat2(cos(a), -sin(a), sin(a), cos(a));
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 p = (fragCoord.xy - iResolution.xy / 2.0) * rotate2d(iTime / 10.0);
float k = float(mod(p.x, size * 2.0) < size == mod(p.y, size * 2.0) < size);
fragColor = vec4(0.0, 0.105, 0.255, k + 0.75);
}

`
Insert cell
import {shader} from "@mbostock/shader"
Insert cell
import { toc } from "@climatelab/toc@45"
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