SWKite Class Reference

SketchWaveJS — Kite (Asymmetric Diamond) Shape Class

Overview

SWKite is a SketchWaveJS Core (standalone) base class that represents the general kite quadrilateral — a shape with two pairs of consecutive equal sides: the two upper sides (each of length topSideLength) and the two lower sides (each of length bottomSideLength). It is defined by a center SWPoint, a horizontal half-diagonal (diagX), an upper half-diagonal (diagTop), and a lower half-diagonal (diagBottom).

SWDiamond (rhombus) extends SWKite as the special symmetric case where diagTop === diagBottom, making all four sides equal. SWKite does not extend SWDiamond — the correct geometric hierarchy goes from general to specific: Kite → Rhombus.

Dependencies: p5.js, SWColor, SWPoint, SWGrid, SWSinusoid   |   Extended by: SWDiamond
PropertyDefaultNotes
diagX7.0Half horizontal diagonal (user units)
diagTop10.0Upper half-vertical diagonal — tall top gives classic kite shape
diagBottom4.0Lower half-vertical diagonal — short tail
fillColor#ff8000 (orange)HSB: (30, 100, 100, 80)
strokeColordark orange-brownAuto-darkened from fill color
strokeWeight3Border thickness in pixels
rotation0Degrees CCW

Constructor

new SWKite(center, diagX, diagTop, diagBottom, fillColor, options)
ParameterTypeRequiredDescription
centerSWPointYesCenter point of the kite (user coords)
diagXnumberYesHalf-length of horizontal diagonal (user units)
diagTopnumberYesHalf-length of upper vertical diagonal (user units)
diagBottomnumberYesHalf-length of lower vertical diagonal (user units)
fillColorSWColorYesInterior fill color
optionsObjectNoSee options table below

Options Object

KeyTypeDefaultDescription
strokeColorSWColorblackBorder color
strokeWeightnumber3Border thickness (px)
showCenterbooleanfalseDraw center SWPoint
rotationnumber0Initial rotation in degrees CCW
const center     = new SWPoint(0, 0);
const fillColor  = new SWColor(30, 100, 100, 80, "kiteFill");    // orange, 80% opacity
const borderColor = new SWColor(20, 100, 55, 100, "kiteStroke"); // dark orange-brown

const kite = new SWKite(center, 7, 10, 4, fillColor, {
    strokeColor:  borderColor,
    strokeWeight: 3,
    showCenter:   false,
    rotation:     0
});

Own Properties

Properties stored directly on the SWKite instance. Properties marked inherited come from SWDiamond.

PropertyTypeDescription
center inheritedSWPointCenter point (user coords).
diagX inheritednumberCurrent half horizontal diagonal.
diagTopnumberCurrent upper half-vertical diagonal. May differ from originalDiagTop during breathing.
diagBottomnumberCurrent lower half-vertical diagonal. May differ from originalDiagBottom during breathing.
fillColor inheritedSWColorInterior fill color.
strokeColor inheritedSWColorBorder color.
strokeWeight inheritednumberBorder thickness in pixels.
showCenter inheritedbooleanWhether to draw the center SWPoint.
rotation inheritednumberCurrent rotation in degrees CCW.
originalDiagX inheritednumberStarting diagX — used by breathe() and reset().
originalDiagTopnumberStarting diagTop — used by breathe() and reset().
originalDiagBottomnumberStarting diagBottom — used by breathe() and reset().
originalCenter inheritedSWPointStarting center position — used by reset().

Computed Properties (Getters)

Read-only. New getters specific to SWKite; overriding area and perimeter from SWDiamond.

gettertopSideLength → number

Length of each of the two upper sides: √(diagX² + diagTop²).

Each upper side connects the left/right tip to the top tip of the kite.

getterbottomSideLength → number

Length of each of the two lower sides: √(diagX² + diagBottom²).

Each lower side connects the left/right tip to the bottom tip of the kite.

getterarea → number  (overrides SWDiamond)

Area of the kite: diagX × (diagTop + diagBottom).

Derivation: full horizontal diagonal = 2·diagX; full vertical diagonal = diagTop + diagBottom; area = (d₁ × d₂) / 2 = diagX × (diagTop + diagBottom).

getterperimeter → number  (overrides SWDiamond)

Perimeter = 2 × (topSideLength + bottomSideLength). The kite has two pairs of equal adjacent sides (unlike a rhombus where all four are equal).

getterisRhombus → boolean

True when diagTop ≈ diagBottom within 1% tolerance. At that point, the kite degenerates to a rhombus (equivalent to its SWDiamond parent).

Animation Methods

methodbreathe(sinX, sinY, t)  (overrides SWDiamond)
kite.breathe(sinX, sinY, t)

Scales diagX using sinX and both diagTop and diagBottom proportionally using sinY. Preserves the kite's asymmetry ratio during vertical breathing. Pass null to leave that dimension unchanged.

ParamTypeDescription
sinXSWSinusoid | nullSinusoid for diagX scaling. null = hold at originalDiagX.
sinYSWSinusoid | nullSinusoid for diagTop/diagBottom scaling (same multiplier for both). null = hold at originals.
tnumberCurrent time in seconds.
// Uniform breathing (same sinusoid for both)
const sin = new SWSinusoid(0.4, Math.PI, 1.2, 0);
kite.breathe(sin, sin, millis() / 1000);

// Vertical only — scales diagTop and diagBottom together
kite.breathe(null, sin, t);

// Reset (stops breathing, restores originals)
kite.breathe(null, null, 0);
methodreset()  (overrides SWDiamond)
kite.reset()

Restores diagX, diagTop, diagBottom, and center.x/y to their original (pre-animation) values. Also resets diagY (parent reference) to originalDiagTop.

Drawing Methods

methoddraw()  (overrides SWDiamond)
kite.draw()

Draw the kite in pixel space (no grid conversion). Uses quad(top, right, bottom, left) with asymmetric vertical radii ryT = diagTop and ryB = diagBottom.

methoddrawOnGrid(grid)  (overrides SWDiamond)
kite.drawOnGrid(grid)

Draw the kite mapped through a SWGrid. diagX is scaled by grid.xScale; diagTop and diagBottom are each independently scaled by grid.yScale.

kite.drawOnGrid(grid);
methoddrawVerticesOnGrid(grid, dotSize, dotColor)  (overrides SWDiamond)
kite.drawVerticesOnGrid(grid, dotSize = 8, dotColor)

Draw the four kite tip vertices as circular dots on the grid. Each vertex is computed in local space (using asymmetric ryT / ryB) and then rotated to match the kite's current rotation.

ParamTypeDefaultDescription
gridSWGridGrid for coordinate conversion
dotSizenumber8Diameter of each dot (pixels)
dotColorSWColorblueFill color for the dots
methodsetShowCenter(show)  inherited
kite.setShowCenter(show)

Toggle display of the center SWPoint. Equivalent to setting kite.showCenter = show.

Utility Methods

methodgetVerticesUserCoords()
kite.getVerticesUserCoords() → { top, right, bottom, left }

Returns the four kite tip vertices in user coordinates (un-rotated). Note that top is at cy + diagTop and bottom is at cy - diagBottom, unlike the symmetric rhombus.

const v = kite.getVerticesUserCoords();
console.log(v.top);    // { x: cx, y: cy + diagTop }
console.log(v.right);  // { x: cx + diagX, y: cy }
console.log(v.bottom); // { x: cx, y: cy - diagBottom }
console.log(v.left);   // { x: cx - diagX, y: cy }
methodcenterContainsPoint(px, py, grid, tolerance)
kite.centerContainsPoint(px, py, grid, tolerance = 12) → boolean

Returns true if the screen point (px, py) is within tolerance pixels of the kite's center. Used for drag detection in p5's mousePressed().

function mousePressed() {
    if (kite.centerContainsPoint(mouseX, mouseY, grid, 15)) {
        isDraggingCenter = true;
    }
}
methodtoString()
kite.toString() → string

Returns a human-readable summary: SWKite(center: (x, y), diagX: n, diagTop: n, diagBottom: n, area: n, rotation: n°).

SWDiamond Extends SWKite

SWKite is the base class. SWDiamond extends SWKite as the special symmetric case (rhombus). Every SWDiamond is an SWKite, but not every SWKite is an SWDiamond.

📚 Teachable Moment (2026-04-24). We originally built this the wrong way — class SWKite extends SWDiamond. Research confirmed the correct mathematical hierarchy: a rhombus is a special case of a kite (all four sides equal), not the other way around. We refactored to class SWDiamond extends SWKite. This is also the Liskov Substitution Principle: wherever an SWKite is expected, an SWDiamond can always substitute, but you cannot substitute an arbitrary kite where a rhombus is required.
SWDiamond adds / overridesTypeNotes
diagY getter/setterpropertyReturns diagTop; setter sets both diagTop and diagBottom to maintain rhombus constraint
sideLengthgetterAll four sides are equal; alias for topSideLength
aspectRatio, diagonalRatiogetterdiagX / diagY
isSquaregetterTrue when diagX ≈ diagY
toString()methodOverridden to show diagY instead of diagTop/diagBottom

For full SWDiamond documentation, see the SWDiamond Reference.

Examples

Basic Usage

// In p5.js setup():
colorMode(HSB, 360, 100, 100, 100);

const grid = new SWGrid({ UL: new SWPoint(-12, 10), LR: new SWPoint(12, -10) });

const center = new SWPoint(0, 0);
const fill   = new SWColor(30, 100, 100, 80, "orange"); // HSB orange
const border = new SWColor(20, 100,  55, 100, "brown");  // dark orange-brown

const kite = new SWKite(center, 7, 10, 4, fill, {
    strokeColor:  border,
    strokeWeight: 3
});

// In p5.js draw():
grid.draw();
kite.drawOnGrid(grid);

Breathing (Vertical — preserves asymmetry)

const sinV = new SWSinusoid(0.4, Math.PI, 1.2, -Math.PI / 6);

// In draw():
const t = millis() / 1000;
kite.breathe(null, sinV, t);  // diagTop and diagBottom scale together
kite.drawOnGrid(grid);

isRhombus Detection

// Dynamically check if the kite has become a rhombus
if (kite.isRhombus) {
    console.log("Kite is now a rhombus — diagTop ≈ diagBottom");
}

Spinning Kite

let totalDeg = 0;
let lastTime = 0;

// In draw():
const now   = millis();
const delta = now - lastTime;
lastTime    = now;
totalDeg   += 60 * delta / 1000; // 60°/sec
kite.rotation = totalDeg;
kite.drawOnGrid(grid);

Drag to Reposition

let isDragging = false;

function mousePressed() {
    if (kite.centerContainsPoint(mouseX, mouseY, grid, 15)) isDragging = true;
}

function mouseDragged() {
    if (isDragging) {
        const u = grid.screenToUser(mouseX, mouseY);
        kite.center.x = kite.originalCenter.x = u.x;
        kite.center.y = kite.originalCenter.y = u.y;
    }
}

function mouseReleased() { isDragging = false; }

Show Computed Properties

console.log(`area:              ${kite.area.toFixed(2)}`);
console.log(`perimeter:         ${kite.perimeter.toFixed(2)}`);
console.log(`topSideLength:     ${kite.topSideLength.toFixed(2)}`);
console.log(`bottomSideLength:  ${kite.bottomSideLength.toFixed(2)}`);
console.log(`isRhombus:         ${kite.isRhombus}`);

Tips & Notes

diagTop vs diagBottom. diagTop is the upper half-diagonal (vertex above center); diagBottom is the lower half-diagonal (vertex below center). In the classic kite shape, diagTop > diagBottom. Setting them equal produces a rhombus.
Breathing preserves asymmetry. When using the Vertical breathing mode, sinY multiplies both diagTop and diagBottom by the same scale factor. A kite with diagTop=10, diagBottom=4 will always maintain that 2.5:1 ratio throughout the breathing cycle.
Rotation is visual-only. The push()/translate()/rotate()/pop() pattern means getVerticesUserCoords() returns un-rotated positions.
Script load order. SWKite is a standalone class with no parent dependency. Load it directly before your sketch. If you also use SWDiamond, load SWKite first since SWDiamond now extends SWKite:
<script src="swKite.js"></script>
<!-- only if using SWDiamond: -->
<script src="swDiamond.js"></script>
Orange fill default. The recommended default fill is HSB(30, 100, 100) = #ff8000 (bright orange). The demo auto-darkens this to #6a2c00 for the border.

Source Code

Complete source for swKite.js:

/*
File: swKite.js
Date: 2026-04-24
Author: klp + GitHub Copilot
Workspace: SketchWaveTNT2026-04-21-Stg8
Purpose: SWKite class for SketchWaveJS
Comment(s):

SWKite: A kite shape — the general quadrilateral with two pairs of consecutive
equal sides. Unlike a rhombus (where all four sides are equal), a kite only requires:
  • two upper sides equal: √(diagX² + diagTop²) each
  • two lower sides equal: √(diagX² + diagBottom²) each

A rhombus (SWDiamond) is a special case of a kite where diagTop === diagBottom,
making all four sides equal. SWDiamond correctly extends SWKite — not the reverse.

Defaults: orange fill (HSB 30, 100, 100 = #ff8000), diagX=7, diagTop=10, diagBottom=4.
Breathing scales diagX (sinX) and both vertical halves proportionally (sinY).
Rotation tracks a cumulative angle (degrees, CCW in user/math convention).

Dependencies: p5.js, SWColor, SWPoint, SWGrid, SWSinusoid
*/

console.log("[swKite.js] SWKite class loaded.");

class SWKite {
    /**
     * @param {SWPoint} center       - Center point (SWPoint instance)
     * @param {number}  diagX        - Half-length of horizontal diagonal (user units)
     * @param {number}  diagTop      - Half-length of upper vertical diagonal (user units)
     * @param {number}  diagBottom   - Half-length of lower vertical diagonal (user units)
     * @param {SWColor} fillColor    - Fill color (SWColor instance)
     * @param {Object}  [options]
     *   strokeColor  : SWColor  — border color (optional)
     *   strokeWeight : number   — border thickness (default 3)
     *   showCenter   : boolean  — show center SWPoint (default false)
     *   rotation     : number   — initial rotation in degrees CCW (default 0)
     */
    constructor(center, diagX, diagTop, diagBottom, fillColor, options = {}) {
        // Pass diagTop as diagY to parent; parent handles center, diagX, colors, rotation
        super(center, diagX, diagTop, fillColor, options);
        this.diagTop    = diagTop;
        this.diagBottom = diagBottom;
        this.originalDiagTop    = diagTop;
        this.originalDiagBottom = diagBottom;
    }//end constructor

    // ── Geometric getters ──────────────────────────────────────────────────

    /**
     * Side length of the two upper sides: √(diagX² + diagTop²)
     */
    get topSideLength() {
        return Math.sqrt(this.diagX * this.diagX + this.diagTop * this.diagTop);
    }//end topSideLength

    /**
     * Side length of the two lower sides: √(diagX² + diagBottom²)
     */
    get bottomSideLength() {
        return Math.sqrt(this.diagX * this.diagX + this.diagBottom * this.diagBottom);
    }//end bottomSideLength

    /**
     * Area of a kite = (full horizontal diagonal × full vertical diagonal) / 2
     *                = (2 × diagX) × (diagTop + diagBottom) / 2
     *                = diagX × (diagTop + diagBottom)
     */
    get area() {
        return this.diagX * (this.diagTop + this.diagBottom);
    }//end area

    /**
     * Perimeter = 2 × (topSideLength + bottomSideLength)
     */
    get perimeter() {
        return 2 * (this.topSideLength + this.bottomSideLength);
    }//end perimeter

    /**
     * isRhombus — true when diagTop ≈ diagBottom (within 1% tolerance).
     * A kite becomes a rhombus when both vertical half-diagonals are equal.
     */
    get isRhombus() {
        const denom = Math.max(this.diagTop, this.diagBottom);
        if (denom === 0) return true;
        return Math.abs(this.diagTop - this.diagBottom) / denom < 0.01;
    }//end isRhombus

    // ── Drawing ────────────────────────────────────────────────────────────

    /**
     * Draw the kite in pixel space (no grid conversion).
     * Coordinates are already in screen pixels.
     */
    draw() {
        const rx  = this.diagX;
        const ryT = this.diagTop;
        const ryB = this.diagBottom;
        push();
        translate(this.center.x, this.center.y);
        rotate(-this.rotation * Math.PI / 180);

        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(top, right, bottom, left) — kite vertices
        quad(0, -ryT,  rx, 0,  0, ryB,  -rx, 0);
        pop();

        if (this.showCenter) {
            this.center.draw();
        }
    }//end draw

    /**
     * Draw the kite mapped through a SWGrid.
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const { x: cx, y: cy } = grid.userToScreen(this.center.x, this.center.y);
        const rx  = this.diagX      * grid.xScale;
        const ryT = this.diagTop    * grid.yScale;
        const ryB = this.diagBottom * grid.yScale;

        push();
        translate(cx, cy);
        rotate(-this.rotation * Math.PI / 180);

        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(top, right, bottom, left) — kite vertices
        quad(0, -ryT,  rx, 0,  0, ryB,  -rx, 0);
        pop();

        if (this.showCenter) {
            this.center.drawOnGrid(grid);
        }
    }//end drawOnGrid

    /**
     * Draw the four kite tip vertices as dots on the grid.
     * Applies the same rotation as drawOnGrid() so dots follow the shape.
     * @param {SWGrid}  grid
     * @param {number}  [dotSize=8]   - Diameter of each dot in pixels
     * @param {SWColor} [dotColor]    - Color for the dots
     */
    drawVerticesOnGrid(grid, dotSize = 8, dotColor) {
        const { x: cx, y: cy } = grid.userToScreen(this.center.x, this.center.y);
        const rx  = this.diagX      * grid.xScale;
        const ryT = this.diagTop    * grid.yScale;
        const ryB = this.diagBottom * grid.yScale;

        // Four tip positions relative to center (screen space, un-rotated)
        const localVerts = [
            {x:  0,  y: -ryT},  // top
            {x:  rx, y:  0  },  // right
            {x:  0,  y:  ryB},  // bottom
            {x: -rx, y:  0  }   // left
        ];

        // Apply the same rotation as drawOnGrid()
        const angle = -this.rotation * Math.PI / 180;
        const cosA  = Math.cos(angle);
        const sinA  = Math.sin(angle);

        push();
        noStroke();
        if (dotColor && dotColor.col) {
            fill(dotColor.col);
        } else {
            fill(0, 75, 90, 100); // blue fallback
        }
        for (const v of localVerts) {
            const sx = cx + (v.x * cosA - v.y * sinA);
            const sy = cy + (v.x * sinA + v.y * cosA);
            circle(sx, sy, dotSize);
        }
        pop();
    }//end drawVerticesOnGrid

    // ── Animation ─────────────────────────────────────────────────────────

    /**
     * Apply sinusoidal breathing to diagX and/or the vertical diagonals.
     * sinX scales diagX; sinY scales both diagTop and diagBottom proportionally,
     * preserving the kite's asymmetry ratio.
     * Pass null to leave that dimension unchanged (holds at original value).
     * @param {SWSinusoid|null} sinX - Sinusoid for diagX scaling
     * @param {SWSinusoid|null} sinY - Sinusoid for diagTop/diagBottom scaling
     * @param {number} t             - Current time in seconds
     */
    breathe(sinX, sinY, t) {
        if (sinX !== null && sinX !== undefined) {
            this.diagX = this.originalDiagX * sinX.getValue(t);
        } else {
            this.diagX = this.originalDiagX;
        }
        if (sinY !== null && sinY !== undefined) {
            const scale = sinY.getValue(t);
            this.diagTop    = this.originalDiagTop    * scale;
            this.diagBottom = this.originalDiagBottom * scale;
        } else {
            this.diagTop    = this.originalDiagTop;
            this.diagBottom = this.originalDiagBottom;
        }
        // Keep parent's diagY in sync for any parent code that references it
        this.diagY = this.diagTop;
    }//end breathe

    /**
     * Reset all diagonals and center to their original (pre-animation) values.
     */
    reset() {
        this.diagX      = this.originalDiagX;
        this.diagTop    = this.originalDiagTop;
        this.diagBottom = this.originalDiagBottom;
        this.diagY      = this.originalDiagTop;
        this.center.x   = this.originalCenter.x;
        this.center.y   = this.originalCenter.y;
    }//end reset

    // ── Vertex helpers ────────────────────────────────────────────────────

    /**
     * Returns the four kite tip vertices in user coordinates (un-rotated).
     * @returns {{ top: {x,y}, right: {x,y}, bottom: {x,y}, left: {x,y} }}
     */
    getVerticesUserCoords() {
        return {
            top:    { x: this.center.x,              y: this.center.y + this.diagTop    },
            right:  { x: this.center.x + this.diagX, y: this.center.y                  },
            bottom: { x: this.center.x,              y: this.center.y - this.diagBottom },
            left:   { x: this.center.x - this.diagX, y: this.center.y                  }
        };
    }//end getVerticesUserCoords

    // ── Hit detection ─────────────────────────────────────────────────────
    // centerContainsPoint() is inherited from SWDiamond unchanged.

    /**
     * String representation
     */
    toString() {
        return `SWKite(center: (${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}), ` +
               `diagX: ${this.diagX.toFixed(2)}, diagTop: ${this.diagTop.toFixed(2)}, ` +
               `diagBottom: ${this.diagBottom.toFixed(2)}, ` +
               `area: ${this.area.toFixed(2)}, rotation: ${this.rotation.toFixed(1)}°)`;
    }//end toString
}//end class SWKite

// export default SWKite;