🌈 SketchWave Gradient Class Reference
Smooth linear gradients with HSB color interpolation
Back to SWGradient Demo 1 Back to SWGradient Demo 2Table of Contents
Overview
SWGradient creates smooth linear gradients at any angle, designed for use with the SketchWaveJS framework and p5.js. Unlike simple CSS gradients, SWGradient uses intelligent HSB color interpolation with hue wraparound for natural color transitions, and supports drawing gradients in screen coordinates, grid coordinates, or within rotated boxes.
Key Features
- HSB Color Interpolation: Blends colors smoothly in HSB space, avoiding muddy browns common in RGB interpolation
- Intelligent Hue Wraparound: Takes the shortest path around the color wheel for natural transitions (e.g., red to violet goes through magenta)
- Flexible Angle System: Rotate gradients from 0° (vertical) to 360° with counterclockwise progression
- Multiple Drawing Modes: Draw on full canvas, full grid, or within custom rotated boxes
- Dual Coordinate Systems: Works seamlessly in screen coordinates or grid (user) coordinates
- Automatic Optimization: Uses bounding box diagonals to determine optimal rendering steps for performance
Design Philosophy
SWGradient is designed to provide high-quality color interpolation while maintaining performance. Key design decisions:
- HSB over RGB: HSB interpolation produces visually pleasing gradients without the dull, muddy colors that occur when interpolating RGB values
- Hue Intelligence: When interpolating hue, the class chooses the shorter path around the color wheel (if hue difference > 180°, it wraps around)
- Per-Pixel Rendering: For grid and box gradients, uses per-pixel rendering for smooth, high-quality results
- Coordinate Flexibility: Supports both screen and grid coordinates, enabling integration with coordinate systems and transformations
How Angle Works
The gradient angle determines the direction of color flow:
- 0°: Vertical gradient (top color at top, bottom color at bottom)
- 90°: Horizontal gradient (top color at left, bottom color at right)
- 180°: Inverted vertical (top color at bottom, bottom color at top)
- 270°: Inverted horizontal (top color at right, bottom color at left)
- Rotation: Angles increase counterclockwise, following standard mathematical convention
Dependencies
SWGradient requires the following SketchWaveJS classes and libraries:
- p5.js: Core drawing and animation library
- SWColor: Color representation with HSB values
- SWGrid: (Optional) Grid coordinate system for
drawOnGrid()anddrawBoxOnGrid() - SWPoint: (Optional) For defining box corners in
drawBoxOnGrid()
Constructor
new SWGradient(topSWColor, bottomSWColor, angleDeg, boxAngleDeg)
Creates a new SWGradient instance with two colors and optional rotation angles.
Parameters
- topSWColor (SWColor) - Color at the start of the gradient (angle=0: top)
- bottomSWColor (SWColor) - Color at the end of the gradient (angle=0: bottom)
- angleDeg (number, optional) - Angle of gradient direction in degrees (default: 0)
- boxAngleDeg (number, optional) - Default rotation angle for boxes in degrees (default: 0)
Understanding the Two-Angle System
The constructor accepts two independent angles that control different aspects:
- angleDeg (gradient direction): Controls which direction colors flow WITHIN the space (0°=vertical, 90°=horizontal)
- boxAngleDeg (box rotation): Controls rotation of the ENTIRE box shape when using
drawBoxOnGrid()
Example: new SWGradient(swRed, swBlue, 0, 45) creates a vertical gradient (colors flow top-to-bottom)
with a default box rotation of 45° (box tilted like a diamond). The gradient stays vertical within the tilted box.
Examples
// Simple vertical gradient (purple to yellow)
let gradient1 = new SWGradient(swPurple, swYellow);
// Equivalent to: new SWGradient(swPurple, swYellow, 0, 0);
// Horizontal gradient (red to blue)
let gradient2 = new SWGradient(swRed, swBlue, 90);
// Diagonal gradient at 45°
let gradient3 = new SWGradient(swGreen, swCyan, 45);
// Vertical gradient in a rotated box (box tilted 30°)
let gradient4 = new SWGradient(swRed, swBlue, 0, 30);
// The gradient flows vertically, but the box is tilted
// Horizontal gradient in a rotated box
let gradient5 = new SWGradient(swPurple, swYellow, 90, -45);
// Colors flow left-to-right, box rotated 45° clockwise
// Custom colors with specific HSB values
let color1 = new SWColor(300, 67, 60, 100, "purple"); // H=300, S=67, B=60
let color2 = new SWColor(60, 100, 100, 100, "yellow"); // H=60, S=100, B=100
let gradient6 = new SWGradient(color1, color2, 135, 20);
Properties
All properties can be read and modified. Changing them will affect subsequent gradient drawing.
| Property | Type | Description |
|---|---|---|
topSWColor |
SWColor | Color at the start of the gradient. At 0°, this appears at the top. Modify directly to change the gradient: gradient.topSWColor = swRed; |
bottomSWColor |
SWColor | Color at the end of the gradient. At 0°, this appears at the bottom. Can be changed dynamically during animation. |
angleDeg |
number | Angle of the gradient direction in degrees. Controls which way colors flow WITHIN the space. 0° = vertical (top to bottom), 90° = horizontal (left to right), angles increase counterclockwise. |
boxAngleDeg |
number | Default rotation angle for boxes in degrees. Controls rotation of the ENTIRE box shape when using drawBoxOnGrid(). 0° = no rotation, positive = counterclockwise, negative = clockwise. This is independent of angleDeg. |
Property Examples
let gradient = new SWGradient(swPurple, swYellow, 0);
// Read properties
console.log(gradient.topSWColor.name); // "purple"
console.log(gradient.bottomSWColor.name); // "yellow"
console.log(gradient.angleDeg); // 0
// Modify properties
gradient.topSWColor = swRed; // Change start color
gradient.bottomSWColor = swBlue; // Change end color
gradient.angleDeg = 45; // Rotate gradient direction to diagonal
gradient.boxAngleDeg = 30; // Set default box rotation
// Animate angle over time
function draw() {
gradient.angleDeg = (frameCount * 0.5) % 360; // Rotate gradient direction continuously
gradient.drawOnGrid(grid);
}
// Animate both angles independently
function draw() {
gradient.angleDeg = (frameCount * 2) % 360; // Fast gradient rotation
gradient.boxAngleDeg = (frameCount * 0.5) % 360; // Slow box rotation
let UL = new SWPoint(-5, 5);
let LR = new SWPoint(5, -5);
gradient.drawBoxOnGrid(grid, UL, LR); // Uses gradient.boxAngleDeg
}
Methods
Drawing Methods
draw(x, y, w, h)
Returns: void
Draws the gradient on the current p5.js canvas at the specified angle in screen coordinates.
Parameters
- x (number, optional) - x position (default: 0)
- y (number, optional) - y position (default: 0)
- w (number, optional) - width of gradient (default: canvas width)
- h (number, optional) - height of gradient (default: canvas height)
Example
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
}
function draw() {
// Fill entire canvas with gradient
let gradient = new SWGradient(swPurple, swYellow, 45);
gradient.draw();
// Draw gradient in specific area
let gradient2 = new SWGradient(swRed, swBlue, 90);
gradient2.draw(50, 50, 300, 200); // x, y, width, height
}
drawOnGrid(grid)
Returns: void
Draws the gradient filling the entire grid area at the specified angle in grid (user) coordinates.
Parameters
- grid (SWGrid) - The grid to use for mapping user to screen coordinates
Example
let grid, gradient;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
// Create grid from (-10, 10) to (10, -10)
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
gradient = new SWGradient(swPurple, swYellow, 0);
}
function draw() {
// Gradient automatically fills entire grid area
gradient.drawOnGrid(grid);
// Optionally draw grid on top
grid.draw();
}
drawBoxOnGrid(grid, UL, LR, boxAngleDeg)
Returns: void
Draws the gradient within a rotated rectangle (box) on the given grid in grid (user) coordinates.
Parameters
- grid (SWGrid) - The grid for user-to-screen mapping
- UL (SWPoint) - Upper left corner in user coordinates (before rotation)
- LR (SWPoint) - Lower right corner in user coordinates (before rotation)
- boxAngleDeg (number, optional) - Box rotation angle in degrees (default:
this.boxAngleDeg)
Two-Angle System Explained
This method uses TWO INDEPENDENT angles:
- gradient.angleDeg (gradient direction): Controls which way colors flow WITHIN the box
- 0° = vertical (top-to-bottom)
- 90° = horizontal (left-to-right)
- Think: "Which way do the colors blend?"
- boxAngleDeg (box rotation): Rotates the ENTIRE box shape in space
- 0° = no rotation (aligned with axes)
- 45° = tilted like a diamond
- Think: "Is the box tilted?"
Example: With angleDeg=0 and boxAngleDeg=45, you get vertical color flow
(top-to-bottom) inside a diamond-shaped (45° rotated) box.
boxAngleDeg parameter defaults to this.boxAngleDeg, giving you three options:
- Set in constructor:
new SWGradient(color1, color2, 0, 45) - Modify as property:
gradient.boxAngleDeg = 30; - Override per-call:
gradient.drawBoxOnGrid(grid, UL, LR, 60)
Examples
// Example 1: Using default box rotation from constructor
let gradient = new SWGradient(swRed, swBlue, 0, 30);
// Vertical gradient, box rotated 30°
let UL = new SWPoint(-5, 5);
let LR = new SWPoint(5, -5);
gradient.drawBoxOnGrid(grid, UL, LR); // Uses 30° from constructor
// Example 2: Modifying box rotation as property
gradient.boxAngleDeg = 45; // Change to 45°
gradient.drawBoxOnGrid(grid, UL, LR); // Now uses 45°
// Example 3: Override for one draw call
gradient.drawBoxOnGrid(grid, UL, LR, 60); // Uses 60° just this once
gradient.drawBoxOnGrid(grid, UL, LR); // Back to 45° (the property value)
// Example 4: Independent angle animation
function draw() {
// Rotate gradient direction
gradient.angleDeg = (frameCount * 2) % 360;
// Rotate box shape independently
gradient.boxAngleDeg = (frameCount * 0.5) % 360;
gradient.drawBoxOnGrid(grid, UL, LR);
// Result: Both the gradient AND the box rotate, but at different speeds!
}
Internal Methods
_interpolateHSB(t)
Returns: p5.Color
Internal method that interpolates between two SWColors using HSB color space with hue wraparound.
Parameters
- t (number) - Interpolation parameter (0 = topColor, 1 = bottomColor)
How Hue Wraparound Works
When interpolating hue values, the method chooses the shorter path around the color wheel:
- Red (0°) to Yellow (60°): Direct interpolation through orange
- Red (0°) to Violet (300°): Wraps around through magenta, not through the full spectrum
- If hue difference > 180°, one hue is adjusted by 360° to take the shorter path
Example (Advanced Usage)
let gradient = new SWGradient(swRed, swBlue, 0);
// Get color at 50% interpolation
let midColor = gradient._interpolateHSB(0.5);
// Get color at 25% interpolation
let quarterColor = gradient._interpolateHSB(0.25);
// Use interpolated color
fill(midColor);
ellipse(200, 200, 100, 100);
Usage Examples
Example 1: Simple Background Gradient
let grid, gradient;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
// Purple to yellow vertical gradient
gradient = new SWGradient(swPurple, swYellow, 0);
}
function draw() {
gradient.drawOnGrid(grid);
grid.draw();
}
Example 2: Interactive Angle Control
let grid, gradient;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
gradient = new SWGradient(swRed, swBlue, 0);
}
function draw() {
// Mouse X position controls gradient angle (0-360°)
gradient.angleDeg = map(mouseX, 0, width, 0, 360);
gradient.drawOnGrid(grid);
grid.draw();
// Display current angle
fill(0);
noStroke();
text(`Angle: ${Math.round(gradient.angleDeg)}°`, 10, 20);
}
Example 3: Animated Rotating Gradient
let grid, gradient;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
gradient = new SWGradient(swGreen, swCyan, 0);
frameRate(30);
}
function draw() {
// Rotate gradient continuously
gradient.angleDeg = (frameCount * 2) % 360;
gradient.drawOnGrid(grid);
grid.draw();
}
Example 4: Two-Angle System Demo
let grid, gradient;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
// Vertical gradient (0°) with box rotated 45° (diamond shape)
gradient = new SWGradient(swRed, swBlue, 0, 45);
}
function draw() {
background(240);
grid.draw();
let UL = new SWPoint(-5, 5);
let LR = new SWPoint(5, -5);
// Colors flow vertically (top-to-bottom) inside a diamond-shaped box
gradient.drawBoxOnGrid(grid, UL, LR);
// Display the two angles
fill(0);
noStroke();
textAlign(LEFT);
text(`Gradient Direction: ${gradient.angleDeg}°`, 10, 20);
text(`Box Rotation: ${gradient.boxAngleDeg}°`, 10, 40);
}
Example 5: Independent Angle Animation
let grid, gradient;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
// Start with both angles at 0°
gradient = new SWGradient(swPurple, swYellow, 0, 0);
frameRate(30);
}
function draw() {
background(240);
grid.draw();
// Rotate gradient direction FAST (2° per frame)
gradient.angleDeg = (frameCount * 2) % 360;
// Rotate box shape SLOWLY (0.5° per frame)
gradient.boxAngleDeg = (frameCount * 0.5) % 360;
let UL = new SWPoint(-5, 5);
let LR = new SWPoint(5, -5);
gradient.drawBoxOnGrid(grid, UL, LR);
// Display both angles
fill(0);
noStroke();
textAlign(LEFT);
text(`Gradient: ${Math.round(gradient.angleDeg)}° | Box: ${Math.round(gradient.boxAngleDeg)}°`, 10, 20);
}
Example 6: Multiple Gradient Boxes with Different Rotations
let grid;
let gradients = [];
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
// Create gradients with different gradient directions and box rotations
gradients.push(new SWGradient(swRed, swYellow, 0, 0)); // Vertical, no rotation
gradients.push(new SWGradient(swBlue, swCyan, 90, 15)); // Horizontal, tilted 15°
gradients.push(new SWGradient(swPurple, swPink, 45, 30)); // Diagonal, tilted 30°
}
function draw() {
background(240);
grid.draw();
// Draw gradient boxes - each uses its own boxAngleDeg property
gradients[0].drawBoxOnGrid(grid,
new SWPoint(-8, 8), new SWPoint(-2, 2));
gradients[1].drawBoxOnGrid(grid,
new SWPoint(2, 8), new SWPoint(8, 2));
gradients[2].drawBoxOnGrid(grid,
new SWPoint(-3, -2), new SWPoint(3, -8));
// Override the third box's rotation for this draw only
// (Uncomment to see the difference)
// gradients[2].drawBoxOnGrid(grid,
// new SWPoint(-3, -2), new SWPoint(3, -8), 60);
}
Example 7: Color Picker Integration
let grid, gradient;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
gradient = new SWGradient(swPurple, swYellow, 0);
// Wire up color pickers (HTML elements)
const topColorPicker = document.getElementById('topColorPicker');
topColorPicker.addEventListener('input', function() {
gradient.topSWColor = SWColor.fromHex(this.value, "topColor");
});
const bottomColorPicker = document.getElementById('bottomColorPicker');
bottomColorPicker.addEventListener('input', function() {
gradient.bottomSWColor = SWColor.fromHex(this.value, "bottomColor");
});
const angleSlider = document.getElementById('angleSlider');
angleSlider.addEventListener('input', function() {
gradient.angleDeg = parseFloat(this.value);
});
}
function draw() {
gradient.drawOnGrid(grid);
grid.draw();
}
Example 8: Heat Map Visualization
let grid, heatGradient;
let dataPoints = [];
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(0, 100),
LR: new SWPoint(100, 0)
});
// Heat map: blue (cold) to red (hot)
let coldColor = new SWColor(240, 100, 100, 100, "cold"); // Blue
let hotColor = new SWColor(0, 100, 100, 100, "hot"); // Red
heatGradient = new SWGradient(coldColor, hotColor, 90);
// Generate random data points
for (let i = 0; i < 50; i++) {
dataPoints.push({
x: random(0, 100),
y: random(0, 100),
value: random(0, 100)
});
}
}
function draw() {
// Draw heat map background
heatGradient.drawOnGrid(grid);
// Draw data points
for (let pt of dataPoints) {
let screenPt = grid.userToScreen(pt.x, pt.y);
fill(0, 0, 0, 50);
noStroke();
ellipse(screenPt.x, screenPt.y, pt.value / 5, pt.value / 5);
}
grid.draw();
}
Best Practices
Color Selection
Performance Optimization
// GOOD: Use noLoop() with redraw() for static gradients
function setup() {
// ... setup code ...
noLoop();
}
function draw() {
gradient.drawOnGrid(grid);
}
// Update only when needed
function changeColor(newColor) {
gradient.topSWColor = newColor;
redraw(); // Redraw once
}
// BAD: Continuous redrawing when not needed
function draw() {
gradient.drawOnGrid(grid); // Redraws every frame unnecessarily
}
Angle Conventions
- Gradient Angle (angleDeg):
- 0° = vertical (top to bottom)
- 90° = horizontal (left to right)
- Controls direction colors flow WITHIN the space
- Box Rotation (boxAngleDeg):
- 0° = no rotation (aligned with axes)
- Positive angles = counterclockwise rotation
- Negative angles = clockwise rotation
- Controls rotation of ENTIRE box shape
- Both angles increase counterclockwise (mathematical standard)
- Use modulo for continuous rotation:
angle = (angle + speed) % 360
Grid vs Screen Coordinates
// Use draw() for pixel-perfect control
gradient.draw(0, 0, 400, 400); // Exact pixel dimensions
// Use drawOnGrid() for coordinate-space backgrounds
gradient.drawOnGrid(grid); // Fills entire coordinate space
// Use drawBoxOnGrid() for positioned gradient elements
gradient.drawBoxOnGrid(grid, UL, LR); // Uses gradient.boxAngleDeg
gradient.drawBoxOnGrid(grid, UL, LR, 45); // Override with 45° rotation
When to Use Property vs Parameter for boxAngleDeg
// PREFER PROPERTY: When all boxes use the same rotation
let gradient = new SWGradient(swRed, swBlue, 0, 30);
gradient.drawBoxOnGrid(grid, UL1, LR1); // Uses 30°
gradient.drawBoxOnGrid(grid, UL2, LR2); // Uses 30°
gradient.drawBoxOnGrid(grid, UL3, LR3); // Uses 30°
// PREFER PARAMETER: When each box needs different rotation
let gradient = new SWGradient(swRed, swBlue, 0); // Default 0°
gradient.drawBoxOnGrid(grid, UL1, LR1, 0); // No rotation
gradient.drawBoxOnGrid(grid, UL2, LR2, 30); // Tilted 30°
gradient.drawBoxOnGrid(grid, UL3, LR3, 60); // Tilted 60°
// HYBRID: Property with occasional override
let gradient = new SWGradient(swRed, swBlue, 0, 45);
gradient.drawBoxOnGrid(grid, UL1, LR1); // Uses 45°
gradient.drawBoxOnGrid(grid, UL2, LR2); // Uses 45°
gradient.drawBoxOnGrid(grid, UL3, LR3, 90); // Special case: 90°
Common Pitfalls
- Wrong Color Mode: Always set
colorMode(HSB, 360, 100, 100, 100)before using SWColor/SWGradient - Missing initializeSWColors(): Call this in
setup()after setting color mode to initialize predefined colors - Parameter Confusion: Remember topSWColor and bottomSWColor are conceptual; their position changes with angle
- Two-Angle Confusion:
angleDegcontrols gradient direction,boxAngleDegcontrols box rotation - they are independent! - Forgetting boxAngleDeg Parameter: If you set
boxAngleDegin constructor or as property, you don't need to pass it todrawBoxOnGrid()
Debugging Tips
// Log gradient properties
console.log("Top color:", gradient.topSWColor.toString());
console.log("Bottom color:", gradient.bottomSWColor.toString());
console.log("Gradient angle:", gradient.angleDeg);
console.log("Box rotation:", gradient.boxAngleDeg);
// Verify HSB values
console.log(`Top: (${gradient.topSWColor.h}, ${gradient.topSWColor.s}, ${gradient.topSWColor.b})`);
// Test interpolation
let midColor = gradient._interpolateHSB(0.5);
console.log("Mid-gradient color:", midColor.toString());
Source Code
// swGradient.js
// SWGradient: Unified gradient class for drawing linear gradients
// Author: klp + GitHub Copilot
// Date: 2026-02-16
//
// Draws smooth linear gradients at any angle:
// - On the full canvas (screen coordinates)
// - On a full grid (user coordinates)
// - Within a custom box on a grid (user coordinates)
//
// Dependencies: SWColor, SWPoint, SWGrid, p5.js
console.log("[swGradient.js] SWGradient class loaded.");
class SWGradient {
/**
* @param {SWColor} topSWColor - Color at the start of the gradient (angle=0: top)
* @param {SWColor} bottomSWColor - Color at the end of the gradient (angle=0: bottom)
* @param {number} [angleDeg=0] - Angle of the gradient direction in degrees (0=vertical, 90=horizontal, positive=CCW)
* @param {number} [boxAngleDeg=0] - Default rotation angle for boxes in degrees (positive=CCW)
*/
constructor(topSWColor, bottomSWColor, angleDeg = 0, boxAngleDeg = 0) {
this.topSWColor = topSWColor;
this.bottomSWColor = bottomSWColor;
this.angleDeg = angleDeg;
this.boxAngleDeg = boxAngleDeg;
}
/**
* Interpolates between two SWColors using HSB color space with hue wraparound
* @param {number} t - Interpolation parameter (0 = topColor, 1 = bottomColor)
* @returns {p5.Color} - Interpolated color
* @private
*/
_interpolateHSB(t) {
// Clamp t to [0, 1]
t = Math.max(0, Math.min(1, t));
// Handle hue wraparound for smooth color transitions
let h1 = this.topSWColor.h;
let h2 = this.bottomSWColor.h;
let dh = h2 - h1;
// If hue difference > 180°, go the "short way" around the color wheel
if (Math.abs(dh) > 180) {
if (dh > 0) {
h1 += 360;
} else {
h2 += 360;
}
}
// Interpolate all channels
let h = (1 - t) * h1 + t * h2;
h = ((h % 360) + 360) % 360; // Normalize to [0, 360)
let s = (1 - t) * this.topSWColor.s + t * this.bottomSWColor.s;
let b = (1 - t) * this.topSWColor.b + t * this.bottomSWColor.b;
let a = (1 - t) * this.topSWColor.a + t * this.bottomSWColor.a;
return color(h, s, b, a);
}
/**
* Draws the gradient on the current p5.js canvas at the specified angle
* @param {number} [x=0] - x position (default 0)
* @param {number} [y=0] - y position (default 0)
* @param {number} [w=width] - width of gradient (default canvas width)
* @param {number} [h=height] - height of gradient (default canvas height)
*/
draw(x = 0, y = 0, w = width, h = height) {
// Four corners of the area
const UL = {x: x, y: y};
const UR = {x: x + w, y: y};
const LL = {x: x, y: y + h};
const LR = {x: x + w, y: y + h};
// Compute gradient direction vector from angle (in radians)
// 0deg = vertical (top to bottom), 90deg = horizontal (left to right), angles increase CCW
const angleRad = (this.angleDeg || 0) * Math.PI / 180;
const dx = Math.sin(angleRad);
const dy = Math.cos(angleRad);
// Project all corners onto the gradient axis to find min/max
const projections = [UL, UR, LL, LR].map(pt => pt.x * dx + pt.y * dy);
const minProj = Math.min(...projections);
const maxProj = Math.max(...projections);
// Use the bounding box diagonal as the number of steps for robust coverage
const bboxDiag = Math.ceil(Math.hypot(UR.x - LL.x, UR.y - LL.y) + Math.hypot(UL.x - LR.x, UL.y - LR.y));
// Center of the area
const cx = (UL.x + UR.x + LL.x + LR.x) / 4;
const cy = (UL.y + UR.y + LL.y + LR.y) / 4;
colorMode(HSB, 360, 100, 100, 100);
for (let i = 0; i <= bboxDiag; i++) {
let t = i / bboxDiag;
let gradCol = this._interpolateHSB(t);
noStroke();
fill(gradCol);
// For each step, draw a strip that covers the bounding box
let proj = minProj + t * (maxProj - minProj);
let centerOnLine = {
x: cx + (proj - (cx * dx + cy * dy)) * dx,
y: cy + (proj - (cx * dx + cy * dy)) * dy
};
push();
translate(centerOnLine.x, centerOnLine.y);
rotate(angleRad + Math.PI / 2); // Rotate rect to be perpendicular to gradient direction
rectMode(CENTER);
rect(0, 0, bboxDiag + 4, 2);
pop();
}
}
/**
* Draws the gradient filling the entire grid area at the specified angle
* @param {SWGrid} grid - The grid to use for mapping user to screen coordinates
*/
drawOnGrid(grid) {
// Get the four corners of the grid in screen coordinates
const UL = grid.userToScreen(grid.UL.x, grid.UL.y);
const UR = grid.userToScreen(grid.LR.x, grid.UL.y);
const LL = grid.userToScreen(grid.UL.x, grid.LR.y);
const LR = grid.userToScreen(grid.LR.x, grid.LR.y);
// Compute gradient direction vector from angle (in radians)
// 0deg = vertical (top to bottom), 90deg = horizontal (left to right), angles increase CCW
const angleRad = (this.angleDeg || 0) * Math.PI / 180;
const dx = Math.sin(angleRad);
const dy = Math.cos(angleRad);
// Project all corners onto the gradient axis to find min/max
const projections = [UL, UR, LL, LR].map(pt => pt.x * dx + pt.y * dy);
const minProj = Math.min(...projections);
const maxProj = Math.max(...projections);
// Get bounding box for grid area
const minX = Math.min(UL.x, UR.x, LL.x, LR.x);
const maxX = Math.max(UL.x, UR.x, LL.x, LR.x);
const minY = Math.min(UL.y, UR.y, LL.y, LR.y);
const maxY = Math.max(UL.y, UR.y, LL.y, LR.y);
colorMode(HSB, 360, 100, 100, 100);
// For each pixel in the bounding box
for (let y = Math.floor(minY); y <= Math.ceil(maxY); y++) {
for (let x = Math.floor(minX); x <= Math.ceil(maxX); x++) {
// Project (x, y) onto gradient axis
let proj = x * dx + y * dy;
let t = (proj - minProj) / (maxProj - minProj);
let gradCol = this._interpolateHSB(t);
stroke(gradCol);
point(x, y);
}
}
}
/**
* Draws the gradient within a rotated rectangle (box) on the given grid
* @param {SWGrid} grid - The grid for user-to-screen mapping
* @param {SWPoint} UL - Upper left corner (user coordinates)
* @param {SWPoint} LR - Lower right corner (user coordinates)
* @param {number} [boxAngleDeg=this.boxAngleDeg] - Rotation angle in degrees (default: uses this.boxAngleDeg property)
*/
drawBoxOnGrid(grid, UL, LR, boxAngleDeg = this.boxAngleDeg) {
// Get the four corners of the box in user coordinates
const UR = new SWPoint(LR.x, UL.y);
const LL = new SWPoint(UL.x, LR.y);
// Convert to screen coordinates
let corners = [UL, UR, LR, LL].map(pt => grid.userToScreen(pt.x, pt.y));
// Center of the box (in screen coordinates)
const cx = (corners[0].x + corners[2].x) / 2;
const cy = (corners[0].y + corners[2].y) / 2;
// Compute box width and height in screen space
const boxW = Math.hypot(corners[1].x - corners[0].x, corners[1].y - corners[0].y);
const boxH = Math.hypot(corners[3].x - corners[0].x, corners[3].y - corners[0].y);
// Rotation angle in radians (screen space)
const theta = (boxAngleDeg || 0) * Math.PI / 180;
// Save current drawing state
push();
translate(cx, cy);
rotate(theta);
// Draw the gradient within the box bounds (per-pixel)
colorMode(HSB, 360, 100, 100, 100);
for (let y = -boxH / 2; y <= boxH / 2; y++) {
for (let x = -boxW / 2; x <= boxW / 2; x++) {
// Project (x, y) onto gradient axis (in box local coords)
// For the box gradient, we use the gradient angle relative to the box
const gradAngleRad = (this.angleDeg || 0) * Math.PI / 180;
const gradDx = Math.sin(gradAngleRad);
const gradDy = Math.cos(gradAngleRad);
// Project point onto gradient axis
let proj = x * gradDx + y * gradDy;
// Find min/max projections for the box corners
const boxCorners = [
{x: -boxW/2, y: -boxH/2},
{x: boxW/2, y: -boxH/2},
{x: -boxW/2, y: boxH/2},
{x: boxW/2, y: boxH/2}
];
const boxProj = boxCorners.map(pt => pt.x * gradDx + pt.y * gradDy);
const minBoxProj = Math.min(...boxProj);
const maxBoxProj = Math.max(...boxProj);
// Calculate t based on projection
let t = (proj - minBoxProj) / (maxBoxProj - minBoxProj);
let gradCol = this._interpolateHSB(t);
stroke(gradCol);
point(x, y);
}
}
pop();
}
}
// Export for module use (if needed)
// export default SWGradient;