SWRectangle Class Reference
Complete Guide to the SketchWave Rectangle Class
Overview
The SWRectangle class represents a styled rectangle with four corner vertices (vUL, vUR, vLR, vLL), a center point (rCenter), fill color, optional border, and powerful animation capabilities including breathing (independent X/Y scaling) and rotation.
Key Features:
- Geometric Properties: Access area, perimeter, diagonal, aspect ratio, and square detection
- Flexible Rendering: Draw in screen coordinates or grid (user) coordinates
- Advanced Animation: Independent X/Y breathing (directional scaling) with separate sinusoids
- Rotation: Static rotation via constructor, animated rotation about center
- Combined Transforms: Unified
transform()method for seamless breathing + rotation - Styling Options: Customizable fill color, stroke color, stroke weight, vertex/center visibility
- Draggable Center: Optional interactive center point for repositioning
Constructor
new SWRectangle(center, width, height, fillColor, options)
Description: Creates a new rectangle with specified center, dimensions, and styling.
Parameters:
center(SWPoint) - Center point of the rectanglewidth(number) - Width of the rectangle (horizontal dimension)height(number) - Height of the rectangle (vertical dimension)fillColor(SWColor) - Fill color for the rectangleoptions(object, optional) - Configuration options:strokeColor(SWColor) - Border color (default: undefined/no border)strokeWeight(number) - Border thickness (default: 2)showVertices(boolean) - Show corner points (default: false)showCenter(boolean) - Show center point (default: false)rotation(number) - Initial rotation in degrees, CCW positive (default: 0)
Examples:
// Basic rectangle
let center = new SWPoint(0, 0, undefined, 8, swBlack, "C");
let rect = new SWRectangle(center, 10, 6, swYellow);
// Styled rectangle with border
let rect2 = new SWRectangle(
new SWPoint(5, 5),
8,
5,
swCyan, // fill color
{
strokeColor: swBlue,
strokeWeight: 3,
showVertices: true,
showCenter: true
}
);
// Golden rectangle with static rotation
const PHI = (1 + Math.sqrt(5)) / 2; // ≈ 1.618
let goldenRect = new SWRectangle(
new SWPoint(0, 0),
10,
10 / PHI, // ≈ 6.18
new SWColor(214, 50, 97, 50, "lightBlue"),
{
strokeColor: new SWColor(214, 50, 77, 100, "darkBlue"),
strokeWeight: 2,
rotation: 45 // Initial 45° rotation
}
);
Properties
Basic Properties
These properties define the rectangle's structure, appearance, and behavior.
rCenter (SWPoint)
Description: The center point of the rectangle (SWPoint object).
The center serves as the origin for rotation and scaling animations. You can modify its position to move the entire rectangle.
Example:
console.log(`Center: (${rect.rCenter.x}, ${rect.rCenter.y})`);
// Move rectangle by changing center
rect.rCenter.x = 5;
rect.rCenter.y = 10;
// Enable dragging
rect.rCenter.setDraggable(true);
vUL, vUR, vLR, vLL (SWPoint)
Description: The four corner vertices of the rectangle (SWPoint objects).
vUL- Upper-left vertexvUR- Upper-right vertexvLR- Lower-right vertexvLL- Lower-left vertex
Example:
// Access vertex positions
console.log(`UL: (${rect.vUL.x}, ${rect.vUL.y})`);
console.log(`UR: (${rect.vUR.x}, ${rect.vUR.y})`);
console.log(`LR: (${rect.vLR.x}, ${rect.vLR.y})`);
console.log(`LL: (${rect.vLL.x}, ${rect.vLL.y})`);
// Enable pen trails on vertices
rect.vUL.setPen(true);
rect.vUL.setMaxTrailLength(50);
width, height (number)
Description: The width (horizontal) and height (vertical) dimensions of the rectangle.
These values represent the original dimensions. During animation, the visual dimensions may change due to breathing, but these properties remain constant.
Example:
console.log(`Dimensions: ${rect.width} × ${rect.height}`);
fillColor (SWColor)
Description: The fill color of the rectangle.
Example:
// Change fill color
rect.fillColor = new SWColor(180, 70, 80, 100, "customCyan");
strokeColor, strokeWeight (SWColor, number)
Description: The border (stroke) color and thickness.
Example:
// Change border
rect.strokeColor = swRed;
rect.strokeWeight = 4;
// Remove border
rect.strokeColor = undefined;
showVertices, showCenter (boolean)
Description: Control visibility of vertices and center point.
Example:
rect.showVertices = true; // Show corner points
rect.showCenter = true; // Show center point
rect.setShowVertices(true); // Using setter method
rect.setShowCenter(false);
Geometric Properties (Getters)
These properties are computed on-the-fly and cannot be directly assigned.
area (number, getter)
Description: Returns the area of the rectangle (width × height).
Example:
let area = rect.area;
console.log(`Area: ${area.toFixed(2)} square units`);
perimeter (number, getter)
Description: Returns the perimeter of the rectangle (2 × (width + height)).
Example:
let perimeter = rect.perimeter;
console.log(`Perimeter: ${perimeter.toFixed(2)} units`);
diagonal (number, getter)
Description: Returns the length of the rectangle's diagonal (distance from corner to opposite corner).
Example:
let diagonal = rect.diagonal;
console.log(`Diagonal: ${diagonal.toFixed(2)} units`);
aspectRatio (number, getter)
Description: Returns the aspect ratio (width ÷ height).
Example:
let ratio = rect.aspectRatio;
console.log(`Aspect ratio: ${ratio.toFixed(3)}`);
// Check for golden rectangle (φ ≈ 1.618 or 1/φ ≈ 0.618)
const PHI = (1 + Math.sqrt(5)) / 2;
const tolerance = 0.02;
if (Math.abs(ratio - PHI) < tolerance * PHI) {
console.log("Golden rectangle (landscape)!");
} else if (Math.abs(ratio - 1/PHI) < tolerance / PHI) {
console.log("Golden rectangle (portrait)!");
}
Classification Properties (Getters)
isSquare (boolean, getter)
Description: Returns true if the rectangle is a square (width equals height within tolerance).
Example:
if (rect.isSquare) {
console.log("This is a square!");
}
// Less efficient:
for (let i = 0; i < 1000; i++) {
if (rect.area > 50) { ... }
}
// More efficient:
let area = rect.area;
for (let i = 0; i < 1000; i++) {
if (area > 50) { ... }
}
Methods
Rendering Methods
draw()
Description: Draws the rectangle in screen coordinates using p5.js.
Renders the rectangle at its current vertex positions using quad(). If showVertices is true, draws the four corner points. If showCenter is true, draws the center point.
Example:
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
}
function draw() {
background(220);
rect.draw(); // Draws in screen coordinates
}
drawOnGrid(grid)
Description: Draws the rectangle in user (grid) coordinates.
Parameters:
grid(SWGrid) - The grid system for mapping user to screen coordinates
This method maps the rectangle's user-space vertices to screen coordinates using the provided grid. Use this when working with coordinate systems.
Example:
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
}
function draw() {
background(220);
grid.draw();
rect.drawOnGrid(grid); // Draws in grid coordinates
}
Animation Methods
breathe(sinusoidX, sinusoidY, t)
Description: Animates the rectangle with independent X/Y breathing (scaling).
Parameters:
sinusoidX(SWSinusoid|null) - Controls X-axis (horizontal) breathing. Pass null for no X scaling.sinusoidY(SWSinusoid|null) - Controls Y-axis (vertical) breathing. Pass null for no Y scaling.t(number) - Time parameter in seconds
Breathing Modes:
- Uniform:
breathe(sinusoid, sinusoid, t)- Same sinusoid for both axes (traditional breathing) - Horizontal Only:
breathe(sinusoidX, null, t)- Only width varies - Vertical Only:
breathe(null, sinusoidY, t)- Only height varies - Independent X/Y:
breathe(sinusoidX, sinusoidY, t)- Different parameters for each axis
Example:
// Create sinusoids for breathing
const minScale = 0.7, maxScale = 1.3, period = 3.0;
const amp = (maxScale - minScale) / 2; // 0.3
const freq = (2 * Math.PI) / period; // ~2.09
const mid = (minScale + maxScale) / 2; // 1.0
// For uniform breathing (same for X and Y)
let breathe = new SWSinusoid(amp, freq, mid, -Math.PI/6);
function draw() {
background(220);
grid.draw();
// Uniform breathing
rect.breathe(breathe, breathe, frameCount * 0.02);
rect.drawOnGrid(grid);
}
// For independent X/Y breathing
let breatheX = new SWSinusoid(0.3, 2 * Math.PI / 2.0, 1.0, -Math.PI/6); // 2 sec period
let breatheY = new SWSinusoid(0.2, 2 * Math.PI / 4.0, 1.0, -Math.PI/6); // 4 sec period
function draw() {
background(220);
grid.draw();
// Independent X/Y breathing
rect.breathe(breatheX, breatheY, frameCount * 0.02);
rect.drawOnGrid(grid);
}
// For horizontal only breathing
function draw() {
background(220);
grid.draw();
// Horizontal only
rect.breathe(breatheX, null, frameCount * 0.02);
rect.drawOnGrid(grid);
}
originalUL, originalUR, etc. This ensures smooth, predictable scaling regardless of previous animations.
rotateAboutCenter(degPerSec, t)
Description: Animates continuous rotation about the rectangle's center.
Parameters:
degPerSec(number) - Angular velocity in degrees per second (CCW positive, CW negative)t(number) - Time in seconds
Rotates the rectangle smoothly over time. The rotation always starts from the original vertex positions.
Example:
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
let center = new SWPoint(0, 0, undefined, 8, swBlack, "C");
rect = new SWRectangle(center, 8, 5, swYellow, {
strokeColor: swOrange,
strokeWeight: 2
});
}
function draw() {
background(220);
grid.draw();
// Rotate at 30°/sec (CCW)
rect.rotateAboutCenter(30, frameCount * 0.02);
rect.drawOnGrid(grid);
}
rotateAboutCenterBy(angleDeg)
Description: Rotates the rectangle by a specific angle (one-time rotation, not animated).
Parameters:
angleDeg(number) - Angle in degrees (CCW positive)
Example:
// Rotate rectangle by 45 degrees once
rect.rotateAboutCenterBy(45);
transform(options)
Description: Unified method for applying breathing (X/Y scaling) and/or rotation simultaneously.
Parameters (all optional):
sinusoidX(SWSinusoid|null) - X-axis breathing sinusoid (null = no X scaling)sinusoidY(SWSinusoid|null) - Y-axis breathing sinusoid (null = no Y scaling)t(number) - Time in secondsdegPerSec(number|null) - Angular velocity for rotation (null = no rotation)
This method combines breathing and rotation from the original vertex positions, preventing interference between animations. The order of operations is: (1) Apply independent X/Y scaling, (2) Apply rotation about center.
Examples:
// Example 1: Uniform breathing + rotation
let breathe = new SWSinusoid(0.3, 2 * Math.PI / 3.0, 1.0, -Math.PI/6);
function draw() {
background(220);
grid.draw();
rect.transform({
sinusoidX: breathe,
sinusoidY: breathe,
t: frameCount * 0.02,
degPerSec: 30
});
rect.drawOnGrid(grid);
}
// Example 2: Independent X/Y breathing + rotation
let breatheX = new SWSinusoid(0.3, 2 * Math.PI / 2.0, 1.0, -Math.PI/6);
let breatheY = new SWSinusoid(0.2, 2 * Math.PI / 4.0, 1.0, -Math.PI/6);
function draw() {
background(220);
grid.draw();
rect.transform({
sinusoidX: breatheX,
sinusoidY: breatheY,
t: frameCount * 0.02,
degPerSec: 45
});
rect.drawOnGrid(grid);
}
// Example 3: Rotation only
function draw() {
background(220);
grid.draw();
rect.transform({
degPerSec: 30,
t: frameCount * 0.02
});
rect.drawOnGrid(grid);
}
// Example 4: Horizontal breathing only
function draw() {
background(220);
grid.draw();
rect.transform({
sinusoidX: breatheX,
sinusoidY: null,
t: frameCount * 0.02
});
rect.drawOnGrid(grid);
}
transform() when combining breathing and rotation for smoother, interference-free animations. The method handles both effects from the original vertex positions.
Transformation Methods
horizShiftBy(xInc)
Description: Shifts the rectangle horizontally by the specified amount.
Parameters:
xInc(number) - Amount to shift in the x direction (positive = right, negative = left)
Example:
// Move rectangle 5 units to the right
rect.horizShiftBy(5);
// Move rectangle 3 units to the left
rect.horizShiftBy(-3);
vertShiftBy(yInc)
Description: Shifts the rectangle vertically by the specified amount.
Parameters:
yInc(number) - Amount to shift in the y direction (positive = up, negative = down)
Example:
// Move rectangle 3 units up
rect.vertShiftBy(3);
// Move rectangle 2 units down
rect.vertShiftBy(-2);
reset()
Description: Resets all vertices to their original positions (stored when the rectangle was created or after rotation was applied).
This is useful for stopping animations and returning the rectangle to its initial state.
Example:
// After animation, reset to original configuration
rect.reset();
Utility Methods
setShowVertices(show)
Description: Sets whether to show the four corner vertices.
Parameters:
show(boolean) - True to show vertices, false to hide (default: true)
Example:
rect.setShowVertices(true); // Show vertices
rect.setShowVertices(false); // Hide vertices
setShowCenter(show)
Description: Sets whether to show the center point.
Parameters:
show(boolean) - True to show center, false to hide (default: true)
Example:
rect.setShowCenter(true); // Show center
rect.setShowCenter(false); // Hide center
toString()
Description: Returns a string representation of the rectangle.
Example:
console.log(rect.toString());
// Output: "SWRectangle(center: (x, y), width: W, height: H, area: A)"
Usage Examples
Example 1: Basic Rectangle Display
let grid;
let rect;
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({
UL: new SWPoint(-10, 10),
LR: new SWPoint(10, -10)
});
let center = new SWPoint(0, 0, undefined, 8, swBlack, "C");
rect = new SWRectangle(center, 8, 5, swYellow, {
strokeColor: swOrange,
strokeWeight: 2,
showVertices: true,
showCenter: true
});
}
function draw() {
background(220);
grid.draw();
rect.drawOnGrid(grid);
// Display properties
fill(0);
noStroke();
text(`Area: ${rect.area.toFixed(2)}`, 10, 20);
text(`Perimeter: ${rect.perimeter.toFixed(2)}`, 10, 40);
text(`Aspect Ratio: ${rect.aspectRatio.toFixed(3)}`, 10, 60);
}
Example 2: Animated Uniform Breathing
let grid;
let rect;
let breathe;
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
let center = new SWPoint(0, 0, undefined, 8, swBlack, "C");
rect = new SWRectangle(center, 6, 6, swCyan, {
strokeColor: swBlue,
strokeWeight: 2
});
// Create breathing sinusoid (0.7× to 1.3×, 3 second period)
const minScale = 0.7, maxScale = 1.3, period = 3.0;
const amp = (maxScale - minScale) / 2;
const freq = (2 * Math.PI) / period;
const mid = (minScale + maxScale) / 2;
breathe = new SWSinusoid(amp, freq, mid, -Math.PI/6);
}
function draw() {
background(220);
grid.draw();
// Apply uniform breathing
rect.breathe(breathe, breathe, frameCount * 0.02);
rect.drawOnGrid(grid);
}
Example 3: Golden Rectangle with Rotation
let grid;
let goldenRect;
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
// Create golden rectangle (φ ≈ 1.618)
const PHI = (1 + Math.sqrt(5)) / 2;
let center = new SWPoint(0, 0, undefined, 8, swBlack, "C");
goldenRect = new SWRectangle(
center,
10,
10 / PHI,
new SWColor(214, 50, 97, 50, "lightBlue"),
{
strokeColor: new SWColor(214, 50, 77, 100, "darkBlue"),
strokeWeight: 2
}
);
}
function draw() {
background(220);
grid.draw();
// Rotate at 30°/sec
goldenRect.rotateAboutCenter(30, frameCount * 0.02);
goldenRect.drawOnGrid(grid);
// Display aspect ratio
fill(0);
noStroke();
const ratio = goldenRect.aspectRatio;
text(`Aspect Ratio: ${ratio.toFixed(3)} (φ ≈ ${((1 + Math.sqrt(5)) / 2).toFixed(3)})`, 10, 20);
}
Example 4: Independent X/Y Breathing + Rotation
let grid;
let rect;
let breatheX, breatheY;
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
let center = new SWPoint(0, 0, undefined, 8, swBlack, "C");
rect = new SWRectangle(center, 8, 5, swMagenta, {
strokeColor: swRed,
strokeWeight: 2
});
// Independent X and Y breathing sinusoids
// X: 0.7-1.3× over 2 seconds
breatheX = new SWSinusoid(0.3, 2 * Math.PI / 2.0, 1.0, -Math.PI/6);
// Y: 0.8-1.2× over 4 seconds
breatheY = new SWSinusoid(0.2, 2 * Math.PI / 4.0, 1.0, -Math.PI/6);
}
function draw() {
background(220);
grid.draw();
// Combined independent breathing + rotation
rect.transform({
sinusoidX: breatheX,
sinusoidY: breatheY,
t: frameCount * 0.02,
degPerSec: 45
});
rect.drawOnGrid(grid);
}
Example 5: Interactive Draggable Rectangle
let grid;
let rect;
let isDragging = false;
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
let center = new SWPoint(0, 0, undefined, 12, swRed, "C");
rect = new SWRectangle(center, 6, 4, swYellow, {
strokeColor: swOrange,
strokeWeight: 2,
showCenter: true
});
// Enable dragging
rect.rCenter.setDraggable(true);
}
function draw() {
background(220);
grid.draw();
// Handle dragging
if (isDragging && rect.rCenter.draggable) {
let userPos = grid.screenToUser(mouseX, mouseY);
// Calculate shift needed
let dx = userPos.x - rect.rCenter.x;
let dy = userPos.y - rect.rCenter.y;
// Move entire rectangle
rect.horizShiftBy(dx);
rect.vertShiftBy(dy);
}
rect.drawOnGrid(grid);
// Display position
fill(0);
noStroke();
text(`Center: (${rect.rCenter.x.toFixed(2)}, ${rect.rCenter.y.toFixed(2)})`, 10, 20);
}
function mousePressed() {
if (mouseX < 0 || mouseX > width || mouseY < 0 || mouseY > height) return;
// Check if clicked on center
if (rect && rect.rCenter) {
let screenPos = grid.userToScreen(rect.rCenter.x, rect.rCenter.y);
let d = dist(mouseX, mouseY, screenPos.x, screenPos.y);
if (d < rect.rCenter.strokeWeight * 2) {
isDragging = true;
}
}
}
function mouseReleased() {
isDragging = false;
}
Example 6: Directional Breathing Modes
let grid;
let rect;
let breatheX, breatheY;
let mode = 'uniform'; // 'uniform', 'horizontal', 'vertical', 'independent'
function setup() {
createCanvas(600, 600);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
let center = new SWPoint(0, 0, undefined, 8, swBlack, "C");
rect = new SWRectangle(center, 8, 5, swCyan, {
strokeColor: swBlue,
strokeWeight: 2
});
// Create breathing sinusoids
breatheX = new SWSinusoid(0.3, 2 * Math.PI / 2.0, 1.0, -Math.PI/6);
breatheY = new SWSinusoid(0.2, 2 * Math.PI / 4.0, 1.0, -Math.PI/6);
}
function draw() {
background(220);
grid.draw();
let t = frameCount * 0.02;
// Apply breathing based on mode
switch (mode) {
case 'uniform':
rect.breathe(breatheX, breatheX, t);
break;
case 'horizontal':
rect.breathe(breatheX, null, t);
break;
case 'vertical':
rect.breathe(null, breatheY, t);
break;
case 'independent':
rect.breathe(breatheX, breatheY, t);
break;
}
rect.drawOnGrid(grid);
// Display mode
fill(0);
noStroke();
text(`Mode: ${mode} (press 1-4 to change)`, 10, 20);
text(`1: Uniform | 2: Horizontal | 3: Vertical | 4: Independent`, 10, 40);
}
function keyPressed() {
if (key === '1') mode = 'uniform';
if (key === '2') mode = 'horizontal';
if (key === '3') mode = 'vertical';
if (key === '4') mode = 'independent';
}
Tips & Best Practices
breathe() or transform() from the draw() loop, not from setup(). These methods update vertex positions each frame based on the time parameter.
-Math.PI/6 in your SWSinusoid to start at the original size (scale factor 1.0), then grow to max and shrink to min. This provides the smoothest "breathing" effect.
transform() instead of calling breathe() and rotateAboutCenter() separately. The transform() method applies both from the original vertex positions, avoiding interference.
- Horizontal only: Creates a left-right squeeze/stretch
- Vertical only: Creates a top-bottom squeeze/stretch
- Independent X/Y with different periods: Creates wave-like, undulating motion
- Independent X/Y with opposite phases: Creates interesting shape transformations
rect.vUL.x = 5) will reshape the rectangle but won't update the width and height properties. For most use cases, use horizShiftBy() and vertShiftBy() to move the entire rectangle.
toString() to log rectangle state:
console.log(rect.toString());
console.log(`Area: ${rect.area.toFixed(2)}`);
console.log(`Aspect ratio: ${rect.aspectRatio.toFixed(3)}`);
- Static rotation (constructor option): Sets the initial orientation of the rectangle. This becomes the "original" position for animations.
- Animated rotation (
rotateAboutCenter()ortransform()): Continuous rotation animation that starts from the original (possibly statically rotated) position.
Source Code
Below is the complete source code for the SWRectangle class:
// swRectangle.js
// SWRectangle: A rectangle with 4 SWPoint vertices, center, fill color,
// optional border, and animation features
// Author: klp + GitHub Copilot
// Date: 2026-02-16
//
// Dependencies: SWPoint, SWColor, SWGrid, SWSinusoid
console.log("[swRectangle.js] SWRectangle class loaded.");
class SWRectangle {
constructor(center, width, height, fillColor, options = {}) {
this.rCenter = center;
this.width = width;
this.height = height;
this.fillColor = fillColor;
this.strokeColor = options.strokeColor || undefined;
this.strokeWeight = options.strokeWeight || 2;
this.showVertices = options.showVertices !== undefined ? options.showVertices : false;
this.showCenter = options.showCenter !== undefined ? options.showCenter : false;
this.rotation = options.rotation || 0;
// Set center point styling
const centerStrokeWeight = this.strokeWeight > 0 ? this.strokeWeight : 4;
const centerColor = (this.strokeColor && this.strokeColor.col) ?
this.strokeColor :
(typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"));
this.rCenter.strokeWeight = centerStrokeWeight;
this.rCenter.strokeColor = centerColor;
// Create corner vertices
const hw = this.width / 2;
const hh = this.height / 2;
const vertexColor = this.strokeColor ||
(typeof swBlue !== 'undefined' ? swBlue : new SWColor(240, 100, 100, 100, "blue"));
const vertexWeight = centerStrokeWeight;
// Create corners: UL, UR, LR, LL
this.vUL = new SWPoint(this.rCenter.x - hw, this.rCenter.y + hh,
undefined, vertexWeight, vertexColor, "UL");
this.vUR = new SWPoint(this.rCenter.x + hw, this.rCenter.y + hh,
undefined, vertexWeight, vertexColor, "UR");
this.vLR = new SWPoint(this.rCenter.x + hw, this.rCenter.y - hh,
undefined, vertexWeight, vertexColor, "LR");
this.vLL = new SWPoint(this.rCenter.x - hw, this.rCenter.y - hh,
undefined, vertexWeight, vertexColor, "LL");
// Apply initial rotation if specified
if (this.rotation !== 0) {
this._rotateVertices(this.rotation);
}
// Store original positions for animation
this.originalCenter = new SWPoint(this.rCenter.x, this.rCenter.y,
undefined, centerStrokeWeight, centerColor);
this.originalUL = new SWPoint(this.vUL.x, this.vUL.y,
undefined, vertexWeight, vertexColor);
this.originalUR = new SWPoint(this.vUR.x, this.vUR.y,
undefined, vertexWeight, vertexColor);
this.originalLR = new SWPoint(this.vLR.x, this.vLR.y,
undefined, vertexWeight, vertexColor);
this.originalLL = new SWPoint(this.vLL.x, this.vLL.y,
undefined, vertexWeight, vertexColor);
}
_rotateVertices(angleDeg) {
const angleRad = angleDeg * Math.PI / 180;
const cx = this.rCenter.x;
const cy = this.rCenter.y;
const cosA = Math.cos(angleRad);
const sinA = Math.sin(angleRad);
[this.vUL, this.vUR, this.vLR, this.vLL].forEach(v => {
const dx = v.x - cx;
const dy = v.y - cy;
v.x = cx + dx * cosA - dy * sinA;
v.y = cy + dx * sinA + dy * cosA;
});
}
get area() {
return this.width * this.height;
}
get perimeter() {
return 2 * (this.width + this.height);
}
get diagonal() {
return Math.sqrt(this.width * this.width + this.height * this.height);
}
get isSquare() {
const tolerance = 0.01;
return Math.abs(this.width - this.height) < tolerance * Math.max(this.width, this.height);
}
get aspectRatio() {
return this.width / this.height;
}
draw() {
if (this.fillColor && this.fillColor.col) {
fill(this.fillColor.col);
} else {
noFill();
}
if (this.strokeColor && this.strokeColor.col) {
stroke(this.strokeColor.col);
strokeWeight(this.strokeWeight);
} else {
noStroke();
}
quad(
this.vUL.x, this.vUL.y,
this.vUR.x, this.vUR.y,
this.vLR.x, this.vLR.y,
this.vLL.x, this.vLL.y
);
noStroke();
noFill();
if (this.showVertices) {
this.vUL.draw();
this.vUR.draw();
this.vLR.draw();
this.vLL.draw();
}
if (this.showCenter) {
this.rCenter.draw();
}
}
drawOnGrid(grid) {
const ul = grid.userToScreen(this.vUL.x, this.vUL.y);
const ur = grid.userToScreen(this.vUR.x, this.vUR.y);
const lr = grid.userToScreen(this.vLR.x, this.vLR.y);
const ll = grid.userToScreen(this.vLL.x, this.vLL.y);
if (this.fillColor && this.fillColor.col) {
fill(this.fillColor.col);
} else {
noFill();
}
if (this.strokeColor && this.strokeColor.col) {
stroke(this.strokeColor.col);
strokeWeight(this.strokeWeight);
} else {
noStroke();
}
quad(ul.x, ul.y, ur.x, ur.y, lr.x, lr.y, ll.x, ll.y);
noStroke();
noFill();
if (this.showVertices) {
this.vUL.drawOnGrid(grid);
this.vUR.drawOnGrid(grid);
this.vLR.drawOnGrid(grid);
this.vLL.drawOnGrid(grid);
}
if (this.showCenter) {
this.rCenter.drawOnGrid(grid);
}
}
rotateAboutCenterBy(angleDeg) {
this._rotateVertices(angleDeg);
}
rotateAboutCenter(degPerSec, t) {
const angleDeg = degPerSec * t;
const angleRad = angleDeg * Math.PI / 180;
const cx = this.originalCenter.x;
const cy = this.originalCenter.y;
const cosA = Math.cos(angleRad);
const sinA = Math.sin(angleRad);
[
[this.vUL, this.originalUL],
[this.vUR, this.originalUR],
[this.vLR, this.originalLR],
[this.vLL, this.originalLL]
].forEach(([v, orig]) => {
const dx = orig.x - cx;
const dy = orig.y - cy;
v.x = cx + dx * cosA - dy * sinA;
v.y = cy + dx * sinA + dy * cosA;
});
}
breathe(sinusoidX, sinusoidY, t) {
const cx = this.originalCenter.x;
const cy = this.originalCenter.y;
const minScale = 0.1;
let scaleX = sinusoidX ? sinusoidX.getValue(t) : 1;
if (scaleX < minScale) scaleX = minScale;
let scaleY = sinusoidY ? sinusoidY.getValue(t) : 1;
if (scaleY < minScale) scaleY = minScale;
[
[this.vUL, this.originalUL],
[this.vUR, this.originalUR],
[this.vLR, this.originalLR],
[this.vLL, this.originalLL]
].forEach(([v, orig]) => {
const dx = orig.x - cx;
const dy = orig.y - cy;
v.x = cx + dx * scaleX;
v.y = cy + dy * scaleY;
});
}
transform({sinusoidX = null, sinusoidY = null, t = 0, degPerSec = null} = {}) {
const cx = this.originalCenter.x;
const cy = this.originalCenter.y;
let scaleX = 1;
if (sinusoidX) {
scaleX = sinusoidX.getValue(t);
if (scaleX < 0.1) scaleX = 0.1;
}
let scaleY = 1;
if (sinusoidY) {
scaleY = sinusoidY.getValue(t);
if (scaleY < 0.1) scaleY = 0.1;
}
let angleRad = 0;
if (degPerSec !== null) {
const angleDeg = degPerSec * t;
angleRad = angleDeg * Math.PI / 180;
}
const cosA = Math.cos(angleRad);
const sinA = Math.sin(angleRad);
[
[this.vUL, this.originalUL],
[this.vUR, this.originalUR],
[this.vLR, this.originalLR],
[this.vLL, this.originalLL]
].forEach(([v, orig]) => {
const dx = orig.x - cx;
const dy = orig.y - cy;
const scaledX = dx * scaleX;
const scaledY = dy * scaleY;
v.x = cx + scaledX * cosA - scaledY * sinA;
v.y = cy + scaledX * sinA + scaledY * cosA;
});
}
reset() {
this.vUL.x = this.originalUL.x;
this.vUL.y = this.originalUL.y;
this.vUR.x = this.originalUR.x;
this.vUR.y = this.originalUR.y;
this.vLR.x = this.originalLR.x;
this.vLR.y = this.originalLR.y;
this.vLL.x = this.originalLL.x;
this.vLL.y = this.originalLL.y;
}
setShowVertices(show = true) {
this.showVertices = show;
}
setShowCenter(show = true) {
this.showCenter = show;
}
horizShiftBy(xInc) {
this.rCenter.x += xInc;
this.vUL.x += xInc;
this.vUR.x += xInc;
this.vLR.x += xInc;
this.vLL.x += xInc;
this.originalCenter.x += xInc;
this.originalUL.x += xInc;
this.originalUR.x += xInc;
this.originalLR.x += xInc;
this.originalLL.x += xInc;
}
vertShiftBy(yInc) {
this.rCenter.y += yInc;
this.vUL.y += yInc;
this.vUR.y += yInc;
this.vLR.y += yInc;
this.vLL.y += yInc;
this.originalCenter.y += yInc;
this.originalUL.y += yInc;
this.originalUR.y += yInc;
this.originalLR.y += yInc;
this.originalLL.y += yInc;
}
toString() {
return `SWRectangle(center: (${this.rCenter.x}, ${this.rCenter.y}), ` +
`width: ${this.width}, height: ${this.height}, area: ${this.area})`;
}
}
// Export for module use (if needed)
// export default SWRectangle;