ShaderGraph is an interactive WebGL plotting toolkit built with p5.js. It gives you a draggable and zoomable 2D coordinate grid, then lets you draw equations, highlight regions, render contour lines, open multiple canvases, animate variables, and plug in custom shaders for more advanced visuals such as Mandelbrot-style rendering.
The project is useful for math visualization, simulation sketches, interactive equation demos, and shader experiments.
- Interactive 2D coordinate grid with mouse drag panning and scroll-wheel zooming.
- Automatic major and minor grid spacing using readable
1, 2, 5, 10, ...unit steps. - Coordinate conversion helpers for moving between grid units, canvas pixels, and p5.js WebGL positions.
- Implicit 2D equation drawing through
FormulaDrawer. - Positive and negative region overlays for equations of the form
f(x, y) = 0. - Contour plotting for scalar fields through
ScalarFieldDrawer. - Animated and changeable variables through shader uniforms.
- Multiple independent canvases on one page.
- Custom shader loading for specialized visuals.
- Example pages for formulas, contours, multiple canvases, and custom shaders.
ShaderGraph/
|-- .gitattributes
|-- .gitignore
|-- LICENSE
|-- README.md
|-- Examples/
| |-- README.md
| |-- Example.html
| |-- formulaEx.html
| |-- scalarEx.html
| `-- multCanvasEx.html
|-- Simulator/
| |-- Coordinate-Handler/
| |-- formula-drawer/
| |-- scalar-field-drawer/
| |-- mandlebrotShader/
| |-- styles/
| `-- assets/
|-- jsconfig.json
|-- package-lock.json
`-- package.json
Install the development dependency first if you want VS Code hover information and p5.js autocomplete:
npm installThe HTML files use absolute paths such as /Simulator/..., so run the project from a local static server instead of opening the HTML files directly.
From the project root:
python -m http.server 8000Then open:
http://localhost:8000/Examples/formulaEx.html
You can also use another static server, such as VS Code Live Server or npx serve ..
The examples load p5.js from a CDN, so you need an internet connection unless you replace the CDN script tags with local p5.js files.
This repository does not commit node_modules/. The dependency files are:
package.jsonpackage-lock.json
After cloning or pulling the project, run:
npm installThat recreates node_modules/ locally and installs @types/p5, which is used by VS Code for hover information and autocomplete. The browser examples still load the p5.js runtime from the CDN script tags in each HTML file.
The project also includes:
.gitignoreto keep generated dependencies, editor folders, logs, and OS metadata out of Git..gitattributesto keep text files normalized with LF line endings and mark the bundled font as binary.jsconfig.jsonto connect JavaScript files with the p5 type definitions.
If VS Code does not show p5 hover information after npm install, reload the VS Code window or run TypeScript: Restart TS Server.
Click inside a canvas to activate it.
Drag with the mouse to pan around the coordinate plane.
Use the mouse wheel to zoom in or out. The zoom keeps the mouse position anchored so navigation feels natural.
When using multiple canvases, each canvas has its own CoordinateHandler, so each one can be moved and zoomed independently.
Every sketch follows the same general pattern:
- Create a p5.js sketch.
- Create a
CoordinateHandler. - Load the grid shader and any drawers in
preload. - Create a WebGL canvas in
setup. - Update the coordinate handler and draw your objects in
draw.
/** @param {import("p5")} env */
const sketch = (env) => {
const coorHandler = new CoordinateHandler(env);
const formula = new FormulaDrawer(env, `return y - x * x;`);
env.preload = () => {
coorHandler.load();
formula.compileShader();
};
env.setup = () => {
const cnv = env.createCanvas(env.windowWidth, env.windowHeight, env.WEBGL);
cnv.parent("canvas-container");
coorHandler.setup(cnv);
env.pixelDensity(1);
};
env.mouseWheel = (event) => {
coorHandler.handleZoom(event);
};
env.draw = () => {
env.clear();
env.background(0.18 * 255, 0.18 * 255, 0.18 * 255);
coorHandler.update();
coorHandler.drawGrid();
formula.drawOn(coorHandler);
coorHandler.drawUnitNumbers();
};
};
new p5(sketch);Use FormulaDrawer to draw implicit equations. The drawer expects a GLSL-style function body that returns a float. The curve is drawn where the value crosses zero.
For example, this draws the parabola y = x^2:
const formula = new FormulaDrawer(env, `return y - x * x;`);This draws a circle with radius 2:
const circle = new FormulaDrawer(env, `return x * x + y * y - 4.;`);The variables x and y are available inside the formula. Because the formula is inserted into a shader, use GLSL-style numeric syntax where needed, such as 4. instead of just 4.
FormulaDrawer can color the positive side, negative side, and intersection line separately:
const circle = new FormulaDrawer(env, `return x * x + y * y - 4.;`, {
colorIntersect: [1.0, 0.9, 0.2, 1.0],
colorPlus: [1.0, 0.2, 0.2, 0.25],
colorMinus: [0.2, 0.4, 1.0, 0.25],
});Color values are RGBA arrays from 0.0 to 1.0.
colorIntersectcontrols the equation line.colorPluscontrols the region wheref(x, y) > 0.colorMinuscontrols the region wheref(x, y) < 0.
Use ScalarFieldDrawer to draw contour lines for a scalar field, which is the same idea as plotting level curves of a 3D surface z = f(x, y).
const scalarField = new ScalarFieldDrawer(env, `return sin(x) * cos(y);`, {
color: [1.0, 0.6, 0.1, 1.0],
resolution: 5,
});Higher resolution values create more contour lines per grid unit.
Both FormulaDrawer and ScalarFieldDrawer can receive extra variables through addVar. These variables become shader uniforms and can be updated every frame.
const circle = new FormulaDrawer(
env,
`return x * x + y * y - pow(radius, 2.);`,
{
addVar: ["radius"],
colorIntersect: [1.0, 0.9, 0.2, 1.0],
colorPlus: [0, 0, 0, 0],
colorMinus: [0, 0, 0, 0],
},
);Then update it in draw:
circle.update([Math.sin(env.frameCount / 50) + 2]);
circle.drawOn(coorHandler);The order of values passed to update must match the order of variable names in addVar.
For multiple variables, list each uniform name in addVar, then pass the values in the same order:
const ellipse = new FormulaDrawer(
env,
`return pow(x / radiusX, 2.) + pow(y / radiusY, 2.) - 1.;`,
{
addVar: ["radiusX", "radiusY"],
colorIntersect: [0.2, 1.0, 0.8, 1.0],
colorPlus: [0, 0, 0, 0],
colorMinus: [0, 0, 0, 0],
},
);Then update both values together:
ellipse.update([
2 + Math.sin(env.frameCount / 60),
1 + 0.5 * Math.cos(env.frameCount / 80),
]);
ellipse.drawOn(coorHandler);CoordinateHandler.load() can receive a custom shader. This is useful when you want to replace the default grid rendering with a specialized shader effect.
env.preload = () => {
const shader = env.loadShader(
"/Simulator/Coordinate-Handler/default.vert",
"/Simulator/mandlebrotShader/mandlebrot.frag",
);
coorHandler.load(shader);
};The Mandelbrot example uses this pattern.
See Examples/README.md for a guided explanation of the included examples.
Useful entry points:
Examples/Example.html- basic coordinate handling and custom shader loading.Examples/formulaEx.html- implicit equations, highlighted regions, and animated variables.Examples/scalarEx.html- contour plots from scalar fields.Examples/multCanvasEx.html- multiple independent canvases on one page.
Manages the visible coordinate system. It handles panning, zooming, grid drawing, axis labels, coordinate bounds, and coordinate conversion.
Common methods:
load(gridShader, font)loads shader and font resources.setup(canvas, centreX, centreY, sideLength, sideLengthRatio, majorThick, minorThick, gridColor)initializes the coordinate system.update()handles activation, dragging, and shader uniforms.handleZoom(event)handles mouse wheel zoom.drawGrid()draws the grid shader.drawUnitNumbers()draws coordinate labels.getMinMaxVal()returns[minx, maxx, miny, maxy].
Draws an implicit 2D formula where f(x, y) = 0.
Common options:
colorIntersectfor the equation line.colorPlusfor the positive region.colorMinusfor the negative region.addVarfor custom animated variables.funcModefor line rendering mode.
Draws contour lines for scalar fields.
Common options:
colorfor contour color.addVarfor custom animated variables.resolutionfor contour density.
If the canvas is blank, check the browser console first. Shader syntax errors usually appear there.
If assets fail to load, make sure you are running the project from a local server at the project root.
If an asset works on Windows but fails after publishing, check path casing. Hosts such as GitHub Pages are case-sensitive, so /Simulator/assets/Inconsolata.otf and /Simulator/assets/inconsolata.otf are different paths.
If a formula does not draw, confirm that it returns a float and follows GLSL-style syntax.
If zooming does not work, click inside the canvas first to activate it.
If performance drops, reduce shader loop counts, lower the number of canvases, simplify formulas, or keep env.pixelDensity(1).
This project is licensed under the MIT License. See LICENSE.
The included Inconsolata font uses the SIL Open Font License, included in Simulator/assets/SIL Open Font License.txt.
See THIRD_PARTY_NOTICES.md for third-party asset and dependency notices.