...loading...
SWGrid Class Reference
Overview
The SWGrid class provides a scalable, resizable grid system that transforms between user-defined coordinates and screen (pixel) coordinates. This abstraction allows developers to work in logical, mathematical coordinate systems independent of canvas size or screen resolution.
Key Features:
- User-defined coordinate system mapping to screen pixels
- Automatic coordinate transformation (userToScreen and screenToUser)
- Customizable grid lines with adjustable step sizes
- Optional axes display with origin marker
- Dynamic resizing support without distortion
- Independent x and y scaling
- Configurable colors for grid lines and axes
- Toggle visibility of grid and axes
Understanding Coordinate Systems
Canvas (Screen) Coordinates
By default, p5.js uses a pixel-based coordinate system:
- Origin (0,0): Top-left corner of the canvas
- X-axis: Increases from left to right (0 to width)
- Y-axis: Increases from top to bottom (0 to height)
- Units: Pixels
Canvas Coordinates (400x400 canvas):
(0,0)────────────────────────────────────▶ x (pixels)
│ (400,0)
│
│ Standard p5.js Canvas
│ Origin at top-left
│ Y increases downward
│
│
│
▼ (400,400)
y (pixels)
User-Defined Coordinates
SWGrid allows you to define your own logical coordinate system:
- Origin: Can be anywhere within your defined bounds
- X-axis: Can range from any value to any value (e.g., -10 to 10)
- Y-axis: Can range from any value to any value (e.g., -10 to 10)
- Y-direction: Typically increases upward (mathematical convention)
- Units: Whatever makes sense for your application
User Coordinates (example: -10 to 10 on both axes):
y (user units)
▲
│ (10,10)
10 │─────────────────────────┐
│ │
5 │ User Grid │
│ Origin at │
0 ├─────────(0,0)───────────┤
│ center │
-5 │ │
│ │
-10 │─────────────────────────┘
└─────────┬─────────┬──────▶ x (user units)
-10 0 10
The Transformation
SWGrid handles the mathematical transformation between these two systems:
User to Screen Conversion:
// Given user coordinates (x, y)
// Upper Left user point: UL
// Lower Right user point: LR
screenX = (x - UL.x) × (canvasWidth / (LR.x - UL.x))
screenY = (UL.y - y) × (canvasHeight / (UL.y - LR.y))
// Note: Y-axis is inverted to match mathematical convention
Screen to User Conversion:
// Given screen coordinates (px, py)
userX = px / xScale + UL.x
userY = UL.y - py / yScale
// Where xScale = canvasWidth / (LR.x - UL.x)
// and yScale = canvasHeight / (UL.y - LR.y)
Window Resizing and Distortion
The Problem
When a browser window resizes, the canvas dimensions change. Without proper handling, this causes two major issues:
1. Coordinate Mapping Becomes Invalid
- The scale factors (xScale, yScale) are calculated based on canvas size
- When canvas size changes, these scales are outdated
- Objects appear in wrong positions
- Coordinates no longer map correctly
2. Visual Distortion Occurs
- Circles become ellipses if width/height ratio changes
- Squares become rectangles
- Aspect ratio of drawings changes
- Mathematical relationships are visually distorted
The Solution
SWGrid provides the updateScreenBounds() method to recalculate scale factors when the canvas resizes:
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
myGrid.updateScreenBounds(); // Recalculate scales
}
// Or with fixed aspect ratio:
function windowResized() {
let newW = windowWidth;
let newH = windowWidth; // Keep square
resizeCanvas(newW, newH);
myGrid.updateScreenBounds();
}
Aspect Ratio Considerations
- Keep the canvas aspect ratio constant (e.g., always square)
- Use equal ranges for x and y user coordinates (e.g., both -10 to 10)
- Accept that shapes may stretch if aspect ratios don't match
Maintaining Aspect Ratio
// Force square canvas
function windowResized() {
let size = min(windowWidth, windowHeight);
resizeCanvas(size, size);
grid.updateScreenBounds();
}
// Equal user coordinate ranges
let grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
// Same range: 20 units each axis
});
Allowing Distortion
// Flexible canvas
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
grid.updateScreenBounds();
// Shapes may stretch
}
// Different user coordinate ranges
let grid = new SWGrid({
UL: new SWPoint(-20, 10),
LR: new SWPoint(20, -10)
// Different ranges: 40 vs 20
});
Constructor
new SWGrid({UL, LR, showGrid, showAxes, xStep, yStep, gridColor, axisColor})
Configuration Object Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
UL |
SWPoint | required | Upper left user coordinate (typically has larger y value) |
LR |
SWPoint | required | Lower right user coordinate (typically has smaller y value) |
showGrid |
boolean | true | Whether to display grid lines |
showAxes |
boolean | true | Whether to display x and y axes |
xStep |
number | 1 | Distance between vertical grid lines in user units |
yStep |
number | 1 | Distance between horizontal grid lines in user units |
gridColor |
SWColor | light gray | Color for grid lines |
axisColor |
SWColor | black | Color for axes and origin marker |
Examples:
// Standard mathematical grid: -10 to 10 on both axes
let grid1 = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
// Custom grid with specific styling
let grid2 = new SWGrid({
UL: new SWPoint(-5, 8),
LR: new SWPoint(15, -2),
xStep: 0.5,
yStep: 1,
gridColor: new SWColor(0, 0, 90, 100, "lightGridGray"),
axisColor: new SWColor(240, 100, 50, 100, "darkBlue"),
showGrid: true,
showAxes: true
});
// Minimal grid without grid lines
let grid3 = new SWGrid({
UL: new SWPoint(0, 100),
LR: new SWPoint(100, 0),
showGrid: false,
showAxes: true
});
updateScreenBounds() to initialize scale factors based on the current canvas size.
Properties
| Property | Type | Description |
|---|---|---|
UL |
SWPoint | Upper left user coordinate point |
LR |
SWPoint | Lower right user coordinate point |
showGrid |
boolean | Whether grid lines are visible |
showAxes |
boolean | Whether axes are visible |
xStep |
number | Grid step size for x-axis (user units) |
yStep |
number | Grid step size for y-axis (user units) |
gridColor |
SWColor | Color for grid lines |
axisColor |
SWColor | Color for axes and origin |
screenW |
number | Current canvas width in pixels |
screenH |
number | Current canvas height in pixels |
xScale |
number | Pixels per user unit on x-axis |
yScale |
number | Pixels per user unit on y-axis |
shouldShowOrigin |
boolean | Whether to draw origin marker (set manually) |
xScale, yScale) are recalculated by updateScreenBounds(). Don't modify them directly.
Methods
Coordinate Conversion Methods
userToScreen(x, y)
Converts user coordinates to screen (pixel) coordinates.
Parameters:x(number): User x-coordinatey(number): User y-coordinate
let grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
// Convert user point (5, 3) to screen coordinates
let screenPos = grid.userToScreen(5, 3);
console.log(screenPos); // {x: 300, y: 140} (example)
// Use for drawing
fill(0, 100, 100); // HSB red
circle(screenPos.x, screenPos.y, 20);
screenToUser(px, py)
Converts screen (pixel) coordinates to user coordinates.
Parameters:px(number): Screen x-coordinate (pixels)py(number): Screen y-coordinate (pixels)
// Get user coordinates of mouse position
function draw() {
background(0, 0, 86); // HSB light gray
grid.draw();
let userPos = grid.screenToUser(mouseX, mouseY);
// Display coordinates
fill(0, 0, 0); // HSB black
text(`User: (${userPos.x.toFixed(2)}, ${userPos.y.toFixed(2)})`, 10, 20);
text(`Screen: (${mouseX}, ${mouseY})`, 10, 40);
}
Rendering Methods
draw()
Draws the grid lines and axes on the canvas. Call this in your draw() function.
function draw() {
background(0, 0, 94); // HSB very light gray
grid.draw(); // Draw grid first, then other elements
// Now draw your shapes, points, etc.
// ...
}
showGrid is true. Axes are drawn only if showAxes is true and they fall within the user coordinate bounds.
drawOrigin()
Draws a small circle at the origin (0, 0) if it's within bounds. Automatically called by draw() if shouldShowOrigin is true.
grid.shouldShowOrigin = true;
grid.draw(); // Will include origin marker
Update Methods
updateScreenBounds()
Recalculates screen dimensions and scale factors. Critical: Call this whenever the canvas is resized.
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
grid.updateScreenBounds(); // REQUIRED after resize
}
// Also useful in draw() for dynamic sizing
function draw() {
background(0, 0, 86); // HSB light gray
grid.updateScreenBounds(); // Update every frame if canvas size changes
grid.draw();
}
Configuration Methods
setBounds(UL, LR)
Changes the user coordinate bounds and updates scale factors.
Parameters:UL(SWPoint): New upper left user coordinateLR(SWPoint): New lower right user coordinate
// Zoom in: smaller range
function zoomIn() {
grid.setBounds(
new SWPoint(-5, 5),
new SWPoint(5, -5)
);
}
// Zoom out: larger range
function zoomOut() {
grid.setBounds(
new SWPoint(-20, 20),
new SWPoint(20, -20)
);
}
toggleGrid()
Toggles the visibility of grid lines.
function keyPressed() {
if (key === 'g' || key === 'G') {
grid.toggleGrid();
}
}
toggleAxes()
Toggles the visibility of axes.
function keyPressed() {
if (key === 'a' || key === 'A') {
grid.toggleAxes();
}
}
Utility Methods
toString()
Returns a human-readable summary of the grid's coordinate bounds, step sizes, and current scale. Used internally by demos to display grid info in the page's info panel.
Returns: string// Example output:
// "x:[-10, 10] step:1 y:[-10, 10] step:1 (20.0 px/unit)"
console.log(grid.toString());
// Display in an HTML info panel
document.getElementById('gridScaleSpan').textContent = grid.toString();
Usage Examples
Example 1: Basic Grid Setup
let grid;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
// Create grid from -10 to 10 on both axes
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
}
function draw() {
background(0, 0, 94); // HSB very light gray
grid.draw();
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
grid.updateScreenBounds();
}
Example 2: Plotting Mathematical Functions
let grid;
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10),
xStep: 1,
yStep: 1
});
}
function draw() {
background(0, 0, 94); // HSB very light gray
grid.draw();
// Plot y = sin(x)
stroke(0, 100, 100); // HSB red
strokeWeight(2);
noFill();
beginShape();
for (let x = -10; x <= 10; x += 0.1) {
let y = 5 * Math.sin(x); // Scale for visibility
let screen = grid.userToScreen(x, y);
vertex(screen.x, screen.y);
}
endShape();
}
Example 3: Interactive Coordinate Display
let grid;
function setup() {
createCanvas(500, 500);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({
UL: new SWPoint(-5, 5),
LR: new SWPoint(5, -5),
xStep: 0.5,
yStep: 0.5
});
grid.shouldShowOrigin = true;
}
function draw() {
background(0, 0, 94); // HSB very light gray
grid.draw();
// Get user coordinates of mouse
let userPos = grid.screenToUser(mouseX, mouseY);
// Draw crosshair at mouse position
stroke(0, 100, 100); // HSB red
strokeWeight(1);
let mouseScreen = grid.userToScreen(userPos.x, userPos.y);
line(mouseScreen.x - 10, mouseScreen.y, mouseScreen.x + 10, mouseScreen.y);
line(mouseScreen.x, mouseScreen.y - 10, mouseScreen.x, mouseScreen.y + 10);
// Display coordinates
fill(0, 0, 0); // HSB black
noStroke();
textSize(14);
text(`User: (${userPos.x.toFixed(2)}, ${userPos.y.toFixed(2)})`, 10, 20);
text(`Screen: (${mouseX}, ${mouseY})`, 10, 40);
}
Example 4: Using SWPoint with SWGrid
let grid;
let points = [];
function setup() {
createCanvas(500, 500);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
// Create labeled points in user coordinates
let labels = ["A", "B", "C", "D", "E"];
for (let i = 0; i < 5; i++) {
let x = random(-8, 8);
let y = random(-8, 8);
let color = new SWColor(random(360), 80, 90);
let point = new SWPoint(x, y, undefined, 10, color, labels[i]);
point.showLabel = true;
points.push(point);
}
}
function draw() {
background(0, 0, 94); // HSB very light gray
grid.draw();
// Draw points using grid coordinates
for (let pt of points) {
pt.drawOnGrid(grid);
}
}
Example 5: Dynamic Zoom with Mouse Wheel
let grid;
let zoomLevel = 10;
function setup() {
createCanvas(500, 500);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({
UL: new SWPoint(-zoomLevel, zoomLevel),
LR: new SWPoint(zoomLevel, -zoomLevel)
});
}
function draw() {
background(0, 0, 94); // HSB very light gray
grid.draw();
// Show zoom level
fill(0, 0, 0); // HSB black
noStroke();
text(`Zoom: ±${zoomLevel}`, 10, 20);
}
function mouseWheel(event) {
// Zoom in/out
zoomLevel += event.delta * 0.01;
zoomLevel = constrain(zoomLevel, 1, 50);
// Update grid bounds
grid.setBounds(
new SWPoint(-zoomLevel, zoomLevel),
new SWPoint(zoomLevel, -zoomLevel)
);
return false; // Prevent page scroll
}
Example 6: Aspect Ratio Preservation
let grid;
function setup() {
// Start with square canvas
let size = min(windowWidth, windowHeight);
createCanvas(size, size);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10) // Equal ranges
});
}
function draw() {
background(0, 0, 94); // HSB very light gray
grid.draw();
// Draw a perfect circle in user coordinates
let screenPos = grid.userToScreen(0, 0);
let radius = grid.xScale * 5; // 5 user units
fill(180, 70, 90); // HSB cyan
noStroke();
circle(screenPos.x, screenPos.y, radius * 2);
}
function windowResized() {
// Maintain square aspect ratio
let size = min(windowWidth, windowHeight);
resizeCanvas(size, size);
grid.updateScreenBounds();
// Circle remains circular!
}
Example 7: Keyboard Controls
let grid;
function setup() {
createCanvas(500, 500);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10),
xStep: 1,
yStep: 1
});
}
function draw() {
background(0, 0, 94); // HSB very light gray
grid.draw();
// Instructions
fill(0, 0, 0); // HSB black
noStroke();
textSize(12);
text("Press 'G' to toggle grid", 10, 20);
text("Press 'A' to toggle axes", 10, 40);
text("Press 'O' to toggle origin", 10, 60);
text("Press '+' to increase grid density", 10, 80);
text("Press '-' to decrease grid density", 10, 100);
}
function keyPressed() {
if (key === 'g' || key === 'G') {
grid.toggleGrid();
}
if (key === 'a' || key === 'A') {
grid.toggleAxes();
}
if (key === 'o' || key === 'O') {
grid.shouldShowOrigin = !grid.shouldShowOrigin;
}
if (key === '+' || key === '=') {
grid.xStep = max(0.1, grid.xStep - 0.5);
grid.yStep = max(0.1, grid.yStep - 0.5);
}
if (key === '-' || key === '_') {
grid.xStep = min(5, grid.xStep + 0.5);
grid.yStep = min(5, grid.yStep + 0.5);
}
}
Best Practices
Always Handle Window Resize
function windowResized() {
resizeCanvas(newWidth, newHeight);
grid.updateScreenBounds(); // Don't forget this!
}
Maintain Aspect Ratio for Geometric Accuracy
- Use equal user coordinate ranges (e.g., -10 to 10 on both axes)
- Keep canvas square or force aspect ratio in
windowResized() - If different ranges needed, accept that circles become ellipses
Draw Grid First
function draw() {
background(0, 0, 94); // HSB very light gray
grid.draw(); // Draw grid BEFORE other elements
// ... draw shapes, points, etc.
}
Use User Coordinates for Logic
- Store object positions in user coordinates
- Use
SWPoint.drawOnGrid(grid)for automatic conversion - Convert only when necessary for drawing
Choose Appropriate Step Sizes
- Larger steps (e.g., 5) for coarse grids
- Smaller steps (e.g., 0.1) for fine grids
- Match step size to your application's scale
- Too small steps can clutter the display
Use Meaningful Coordinate Ranges
- For math: -10 to 10 is intuitive
- For data: match your data range
- For physics: use real-world units
- Avoid very large ranges (precision issues)
Integration with SketchWave Ecosystem
Dependencies
- p5.js: Required for all drawing operations
- SWPoint: Required for defining coordinate bounds
- SWColor: Required for grid and axis colors
Loading Order
Ensure scripts are loaded in this order in your HTML:
<!-- p5js CDN-->
<script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.js"></script>
<!-- SketchWave classes -->
<script src="scripts/swColor.js"></script>
<script src="scripts/swPoint.js"></script>
<script src="scripts/swGrid.js"></script>
<!-- Your sketch script -->
<script src="scripts/mySketch.js"></script>
Working with Other SketchWave Classes
// SWPoint has built-in grid support
// Constructor: new SWPoint(x, y, z, strokeWeight, strokeColor, label)
let myPoint = new SWPoint(5, 3, undefined, 8, swRed, "P");
myPoint.showLabel = true;
myPoint.drawOnGrid(grid); // Automatic conversion
// Manual conversion for custom drawing
let screenPos = grid.userToScreen(myPoint.x, myPoint.y);
circle(screenPos.x, screenPos.y, 20);
Common Patterns
// Pattern 1: Physics simulation with real units
let grid = new SWGrid({
UL: new SWPoint(0, 100), // 0-100 meters
LR: new SWPoint(100, 0),
xStep: 10,
yStep: 10
});
// Pattern 2: Data visualization
let grid = new SWGrid({
UL: new SWPoint(0, maxDataValue),
LR: new SWPoint(dataPoints.length, 0),
showGrid: true,
showAxes: true
});
// Pattern 3: Mathematical graphing
let grid = new SWGrid({
UL: new SWPoint(-2*Math.PI, 5),
LR: new SWPoint(2*Math.PI, -5),
xStep: Math.PI/2, // Mark multiples of π/2
yStep: 1
});
Complete Source Code
View the complete, documented source code for the SWGrid class:
Show/Hide Source Code
// File: swGrid.js
// Author: klp
// Purpose: SWGrid class for scalable, resizable grid system in p5.js
// Dependencies: p5.js, SWPoint, SWColor
console.log("[swGrid.js] SWGrid class loaded.");
class SWGrid {
/**
* @param {Object} config - Configuration object
* @param {SWPoint} config.UL - Upper left user coordinate (e.g., new SWPoint(-10, 10))
* @param {SWPoint} config.LR - Lower right user coordinate (e.g., new SWPoint(10, -10))
* @param {boolean} [config.showGrid=true] - Show grid lines
* @param {boolean} [config.showAxes=true] - Show axes
* @param {number} [config.xStep=1] - Grid step for x axis
* @param {number} [config.yStep=1] - Grid step for y axis
* @param {SWColor} [config.gridColor] - Color for grid lines
* @param {SWColor} [config.axisColor] - Color for axes
*/
constructor({UL, LR, showGrid=true, showAxes=true, xStep=1, yStep=1, gridColor, axisColor}) {
this.UL = UL;
this.LR = LR;
this.showGrid = showGrid;
this.showAxes = showAxes;
this.xStep = xStep;
this.yStep = yStep;
this.gridColor = gridColor || new SWColor(0, 0, 85, 100, "gridGray");
this.axisColor = axisColor || new SWColor(0, 0, 0, 100, "axisBlack");
this.updateScreenBounds();
this.shouldShowOrigin = false;
}//end constructor
// Call this in windowResized or after canvas resize
updateScreenBounds() {
this.screenW = width;
this.screenH = height;
this.xScale = this.screenW / (this.LR.x - this.UL.x);
this.yScale = this.screenH / (this.UL.y - this.LR.y);
}//end updateScreenBounds
// Convert user (x, y) to screen (px, py)
userToScreen(x, y) {
const px = (x - this.UL.x) * this.xScale;
const py = (this.UL.y - y) * this.yScale;
return {x: px, y: py};
}//end userToScreen
// Convert screen (px, py) to user (x, y)
screenToUser(px, py) {
const x = px / this.xScale + this.UL.x;
const y = this.UL.y - py / this.yScale;
return {x, y};
}//end screenToUser
// Draw the grid, axes, and origin
draw() {
// Draw grid lines
if (this.showGrid) {
stroke(this.gridColor.col);
strokeWeight(1);
// Vertical grid lines
for (let x = Math.ceil(this.UL.x/this.xStep)*this.xStep; x <= this.LR.x; x += this.xStep) {
const {x: px1, y: py1} = this.userToScreen(x, this.UL.y);
const {x: px2, y: py2} = this.userToScreen(x, this.LR.y);
line(px1, py1, px2, py2);
}
// Horizontal grid lines
for (let y = Math.floor(this.LR.y/this.yStep)*this.yStep; y <= this.UL.y; y += this.yStep) {
const {x: px1, y: py1} = this.userToScreen(this.UL.x, y);
const {x: px2, y: py2} = this.userToScreen(this.LR.x, y);
line(px1, py1, px2, py2);
}
}
// Draw axes
if (this.showAxes) {
stroke(this.axisColor.col);
strokeWeight(2);
// y-axis (x=0)
if (this.UL.x < 0 && this.LR.x > 0) {
const {x: px1, y: py1} = this.userToScreen(0, this.UL.y);
const {x: px2, y: py2} = this.userToScreen(0, this.LR.y);
line(px1, py1, px2, py2);
}
// x-axis (y=0)
if (this.LR.y < 0 && this.UL.y > 0) {
const {x: px1, y: py1} = this.userToScreen(this.UL.x, 0);
const {x: px2, y: py2} = this.userToScreen(this.LR.x, 0);
line(px1, py1, px2, py2);
}
if(this.shouldShowOrigin) {
this.drawOrigin();
}
}
}//end draw
drawOrigin() {
// Draw origin if in bounds
if (this.UL.x < 0 && this.LR.x > 0 && this.LR.y < 0 && this.UL.y > 0) {
const {x: px, y: py} = this.userToScreen(0, 0);
fill(this.axisColor.col);
noStroke();
ellipse(px, py, 6, 6);
}
}//end drawOrigin
// Toggle grid visibility
toggleGrid() {
this.showGrid = !this.showGrid;
}//end toggleGrid
// Toggle axes visibility
toggleAxes() {
this.showAxes = !this.showAxes;
}//end toggleAxes
// Set new user bounds
setBounds(UL, LR) {
this.UL = UL;
this.LR = LR;
this.updateScreenBounds();
}//end setBounds
toString() {
return `x:[${this.UL.x}, ${this.LR.x}] step:${this.xStep} y:[${this.LR.y}, ${this.UL.y}] step:${this.yStep} (${this.xScale.toFixed(1)} px/unit)`;
}//end toString
}//end SWGrid class
// Usage example (in your sketch):
// let grid;
// function setup() {
// createCanvas(400, 400);
// grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
// }
// function draw() {
// background(240);
// grid.updateScreenBounds();
// grid.draw();
// }