@usman404/crowjs 1.1.2 β†’ 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRIBUTING.md CHANGED
@@ -1 +1,67 @@
1
+ # Contributing to CrowJS πŸ¦β€β¬›
1
2
 
3
+ First off, thank you for considering contributing to CrowJS! It’s people like you who make the p5.js ecosystem better for everyone.
4
+
5
+ To maintain code quality and stability, we follow a strict contribution workflow. Please read the guidelines below before getting started.
6
+
7
+ ---
8
+
9
+ ## πŸ—οΈ Getting Started
10
+
11
+ ### 1. Understand the Structure
12
+ CrowJS is a canvas-native library. Before writing code, please:
13
+ * **Read the Documentation:** Visit [crow-js.vercel.app](https://crow-js.vercel.app/) to understand the component lifecycle and event system.
14
+ * **Explore the Source:** Look at `Core/Root.js` to see how the main engine works and `UIComponents/` to see how existing components (like Buttons or Sliders) are implemented.
15
+
16
+ ### 2. Local Setup
17
+ 1. Fork the repository.
18
+ 2. Clone your fork locally:
19
+ ```bash
20
+ git clone [https://github.com/YOUR_USERNAME/CrowJS.git](https://github.com/YOUR_USERNAME/CrowJS.git)
21
+ ```
22
+ 3. Run `npm install` to set up the development environment (including VitePress for docs).
23
+
24
+ ---
25
+
26
+ ## 🚦 Contribution Workflow
27
+
28
+ **Note:** Direct pushes to the `main` branch are disabled. All changes must come through a Pull Request (PR).
29
+
30
+ ### Step 1: Create a Branch
31
+ Always create a descriptive branch for your work:
32
+ ```bash
33
+ git checkout -b feature/your-feature-name
34
+ # OR
35
+ git checkout -b fix/issue-description
36
+ ```
37
+
38
+ ### Step 2: Make Your Changes
39
+ * Follow the existing code style (clean, documented, and modular).
40
+ * If you add a new component, ensure it inherits from the appropriate base class.
41
+ * Update the documentation in the `docs/` folder if your changes affect the API.
42
+
43
+ ### Step 3: Open a Pull Request
44
+ 1. Push your branch to your fork.
45
+ 2. Open a Pull Request against the CrowJS `main` branch.
46
+ 3. **Requirement:** Per our repository settings, at least **one approving review** is required before a PR can be merged.
47
+ 4. Ensure all conversations and feedback in the PR are resolved.
48
+
49
+ ---
50
+
51
+ ## πŸ“ Coding Guidelines
52
+ * **No DOM Interference:** Remember, CrowJS is canvas-native. Avoid using `document.createElement` or CSS overlays unless absolutely necessary.
53
+ * **Performance:** p5.js sketches run at 60fps. Ensure your component's `draw` (or `show`) methods are highly optimized.
54
+ * **Compatibility:** Ensure your code works with the latest version of p5.js.
55
+
56
+ ## πŸ› Reporting Bugs
57
+ If you find a bug, please open an **Issue** with:
58
+ * A clear description of the bug.
59
+ * A minimal `sketch.js` example demonstrating the issue.
60
+ * Your browser and OS version.
61
+
62
+ ## πŸ’‘ Feature Requests
63
+ Have an idea for a new component (like a Color Picker or Graph)? Open an issue with the "Feature Request" label to discuss it before starting work!
64
+
65
+ ---
66
+
67
+ Created with ❀️ by [Usman Ali](https://github.com/UsmanAli404)
@@ -134,12 +134,25 @@ export class GridFrame extends Frame{
134
134
  {
135
135
  //check if grid is configured or not
136
136
  if(this.grid===null){
137
+ //preserve any user-set weights before auto-configuring
138
+ let savedRowWeights = [...this.rowWeights];
139
+ let savedColWeights = [...this.colWeights];
140
+
137
141
  this.gridConfig(this.rows, this.cols);
138
- console.log(`grid configured automatically [${this.rows}x${this.cols}]`);
139
142
 
140
- // console.log("element can't be added: gird not configured,");
141
- // console.log("call .gridConfig(rows, cols) first to configure grid before adding any elements!");
142
- return;
143
+ //restore user-set weights
144
+ for(let i=0; i<savedRowWeights.length && i<this.rows; i++){
145
+ if(savedRowWeights[i] != null){
146
+ this.rowConfig(i, savedRowWeights[i]);
147
+ }
148
+ }
149
+ for(let i=0; i<savedColWeights.length && i<this.cols; i++){
150
+ if(savedColWeights[i] != null){
151
+ this.colConfig(i, savedColWeights[i]);
152
+ }
153
+ }
154
+
155
+ console.log(`grid configured automatically [${this.rows}x${this.cols}]`);
143
156
  }
144
157
 
145
158
  if(element===null){
@@ -326,6 +339,18 @@ export class GridFrame extends Frame{
326
339
  gridConfig(rows, cols){
327
340
  this.rows = rows;
328
341
  this.cols = cols;
342
+
343
+ //clear stale children
344
+ for(let i=0; i<this.children.length; i++){
345
+ this.children[i].parent = null;
346
+ }
347
+ this.children = [];
348
+
349
+ //reset weights before reconfiguring
350
+ this.rowWeights = [];
351
+ this.colWeights = [];
352
+ this.totalRowWeight = 0;
353
+ this.totalColWeight = 0;
329
354
 
330
355
  for(let i=0; i<this.rows; i++){
331
356
  this.rowConfig(i, 1);
@@ -299,6 +299,17 @@ export class ScrollFrame extends Frame{
299
299
  this.totalWeight -= this.preferences[index][0];
300
300
  this.preferences = this.preferences.filter((_, i) => i!==index);
301
301
  this.removeChild(element);
302
+
303
+ //recalculate scroll tracking indices
304
+ if(this.enableVScroll){
305
+ this.recalcTopper();
306
+ this.recalcDeepest();
307
+ }
308
+ if(this.enableHScroll){
309
+ this.recalcLeftist();
310
+ this.recalcRightist();
311
+ }
312
+
302
313
  this.redraw();
303
314
 
304
315
  console.log(`element (id: ${element.id}) successfully removed from ${this.constructor.name} (id: ${this.id})!`);
@@ -315,14 +326,15 @@ export class ScrollFrame extends Frame{
315
326
  return;
316
327
  }
317
328
 
318
- if(weight<1){
319
- console.log("weight to set must be >= 1");
329
+ if(weight<=0){
330
+ console.log("weight can't be non-positive");
320
331
  return;
321
332
  }
322
333
 
323
334
  this.totalWeight -= this.preferences[index][0];
324
335
  this.totalWeight += weight;
325
336
  this.preferences[index][0] = weight;
337
+ this.redraw();
326
338
  }
327
339
 
328
340
  /**
@@ -356,6 +368,7 @@ export class ScrollFrame extends Frame{
356
368
  }
357
369
 
358
370
  this.preferences[index][1] = padL;
371
+ this.redraw();
359
372
  }
360
373
 
361
374
  /**
@@ -389,6 +402,7 @@ export class ScrollFrame extends Frame{
389
402
  }
390
403
 
391
404
  this.preferences[index][2] = padR;
405
+ this.redraw();
392
406
  }
393
407
 
394
408
  /**
@@ -422,6 +436,7 @@ export class ScrollFrame extends Frame{
422
436
  }
423
437
 
424
438
  this.preferences[index][3] = padT;
439
+ this.redraw();
425
440
  }
426
441
 
427
442
  /**
@@ -455,6 +470,7 @@ export class ScrollFrame extends Frame{
455
470
  }
456
471
 
457
472
  this.preferences[index][4] = padB;
473
+ this.redraw();
458
474
  }
459
475
 
460
476
  /**
@@ -556,6 +572,63 @@ export class ScrollFrame extends Frame{
556
572
  this.rightist = i;
557
573
  }
558
574
  }
575
+
576
+ /**
577
+ * Recalculates the topmost child index by scanning all children
578
+ */
579
+ recalcTopper(){
580
+ if(this.children.length===0){ this.topper=-1; return; }
581
+ this.topper=0;
582
+ for(let i=1; i<this.children.length; i++){
583
+ if(this.children[i].y + this.preferences[i][3] <
584
+ this.children[this.topper].y + this.preferences[this.topper][3]){
585
+ this.topper=i;
586
+ }
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Recalculates the bottommost child index by scanning all children
592
+ */
593
+ recalcDeepest(){
594
+ if(this.children.length===0){ this.deepest=-1; return; }
595
+ this.deepest=0;
596
+ for(let i=1; i<this.children.length; i++){
597
+ if(this.children[i].y + this.children[i].height - this.preferences[i][4] >
598
+ this.children[this.deepest].y + this.children[this.deepest].height - this.preferences[this.deepest][4]){
599
+ this.deepest=i;
600
+ }
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Recalculates the leftmost child index by scanning all children
606
+ */
607
+ recalcLeftist(){
608
+ if(this.children.length===0){ this.leftist=-1; return; }
609
+ this.leftist=0;
610
+ for(let i=1; i<this.children.length; i++){
611
+ if(this.children[i].x + this.preferences[i][1] <
612
+ this.children[this.leftist].x + this.preferences[this.leftist][1]){
613
+ this.leftist=i;
614
+ }
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Recalculates the rightmost child index by scanning all children
620
+ */
621
+ recalcRightist(){
622
+ if(this.children.length===0){ this.rightist=-1; return; }
623
+ this.rightist=0;
624
+ for(let i=1; i<this.children.length; i++){
625
+ if(this.children[i].x + this.children[i].width - this.preferences[i][2] >
626
+ this.children[this.rightist].x + this.children[this.rightist].width - this.preferences[this.rightist][2]){
627
+ this.rightist=i;
628
+ }
629
+ }
630
+ }
631
+
559
632
  /**
560
633
  * Scrolls content downward
561
634
  */
package/README.md CHANGED
@@ -1,130 +1,132 @@
1
- # CrowJS πŸ¦β€β¬›
2
- **CrowJS** is a lightweight and extensible GUI library built on top of [p5.js](https://p5js.org/), designed for creative coders and simulation builders who need intuitive, canvas-native UI components like buttons, sliders, input fields, and more β€” all rendered directly inside the p5.js canvas.
1
+ <div align="center">
2
+ <img src="/docs/public/crowjs_favicon.png" alt="CrowJS Logo" width="270" />
3
+
4
+ # CrowJS
5
+
6
+ **A lightweight, canvas-native GUI library built for p5.js.**
7
+
8
+ [![NPM Version](https://img.shields.io/npm/v/@usman404/crowjs?style=for-the-badge&logo=npm&color=CB3837)](https://www.npmjs.com/package/@usman404/crowjs)
9
+ [![Read the Docs](https://img.shields.io/badge/πŸ“–_Read_The_Docs-181717?style=for-the-badge&logo=vercel&logoColor=white)](https://crow-js.vercel.app/)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
11
+
12
+ <br/>
13
+
14
+ *Designed for creative coders and simulation builders who need intuitive UI components like buttons, sliders, and input fields rendered directly inside the p5.js canvas.*
15
+ </div>
3
16
 
4
- ![Logo](/crowjs-01-01.png)
17
+ ---
5
18
 
6
- ## Features
7
- - Minimal and canvas-native UI for p5.js sketches
8
- - Frames, nested layouts, scrollable & grid containers
9
- - Customizable UI components: buttons, sliders, toggles, input fields, etc.
10
- - Event system inspired by the web (click, hover, focus, blur)
11
- - Modular design with support for custom components
12
- - Fully drawn inside p5.js (no HTML overlays or DOM interference)
13
- - Ideal for creative coding, simulations, visual experiments, and tools
19
+ ## 🌟 Why CrowJS?
20
+ While p5.js is amazing for generative art and simulations, there's a noticeable gap when it comes to in-sketch UI. CrowJS fills that gap by giving you a **canvas-first GUI framework** that blends seamlessly with your visuals without relying on clunky HTML overlays or DOM interference.
14
21
 
15
- ## Getting Started
22
+ ## ✨ Features
23
+ - **Minimal & Canvas-Native:** UI drawn directly inside your p5.js sketches.
24
+ - **Layout Systems:** Frames, nested layouts, scrollable areas, and grid containers.
25
+ - **Customizable Components:** Buttons, sliders, toggles, input fields, and more.
26
+ - **Web-Style Event System:** Supports `click`, `hover`, `focus`, and `blur`.
27
+ - **Modular Design:** Easily create and integrate your own custom components.
28
+ - **Perfect For:** Creative coding, visual experiments, simulations, and internal tools.
16
29
 
17
- ### 1. Install
30
+ ---
18
31
 
19
- To use **CrowJS**, begin by downloading this repository and placing the `CrowJS` folder inside your project's source code.
20
- Once included, open the `index.html` file in your browser; it already contains the required setup for p5.js and for loading CrowJS modules.
32
+ ## πŸš€ Getting Started
21
33
 
22
- All GUI development should be done inside **`sketch.js`**, which serves as the main entry point for both your p5.js sketch and all CrowJS components.
34
+ ### 1. Installation
23
35
 
24
- ---
36
+ **Via NPM (Recommended)**
37
+ ```bash
38
+ npm install @usman404/crowjs
39
+ ```
25
40
 
26
- ### ⚠ Important Notes
27
- - **Do not rename** the `sketch.js` file. CrowJS depends on this filename for consistent module loading.
28
- - **Do not alter** the structure or script tags inside `index.html`. Modifying them may prevent CrowJS from running correctly.
29
- - A **CDN-based installation** method will be added in the future to make setup even easier.
41
+ **Via Manual Download**
42
+ Download this repository and place the `CrowJS` folder inside your project's source code. Open the `index.html` file in your browser (it already contains the required setup for p5.js and module loading).
30
43
 
31
- ---
44
+ > [!WARNING]
45
+ > - **Do not rename** the `sketch.js` file. CrowJS depends on this filename for consistent module loading.
46
+ > - **Do not alter** the structure or script tags inside `index.html`. Modifying them may prevent CrowJS from running correctly.
32
47
 
33
48
  ### 2. Basic Example
49
+ All GUI development should be done inside **`sketch.js`**, which serves as the main entry point for both your p5.js sketch and your CrowJS components.
50
+
34
51
  ```javascript
35
52
  import { Root } from "./Core/Root.js";
36
- import { Label } from "./UIComponents/Label.js";
37
-
53
+ import { Button } from "./UIComponents/Button.js";
38
54
 
39
55
  /** @type {Root} */
40
56
  let root;
41
- let clickTimes=1;
42
-
43
- window.setup = function(){
44
- createCanvas(windowWidth, windowHeight);
45
- root = new Root();
57
+ let clickTimes = 0;
46
58
 
47
- let btnWidth = 200;
48
- let btnHeight = 100;
59
+ function setup() {
60
+ root = new Root();
49
61
 
50
- let btn = new Label((windowWidth/2)-(btnWidth/2), (windowHeight/2)-(btnHeight/2), 200, 100, "Hello! πŸ‘‹",
51
- {cornerRadius: 20,
52
- backgroundColor: "rgba(0, 0, 0, 1)",
53
- textColor: "rgba(255, 255, 255, 1)",
54
- });
62
+ const button = new Button(0, 0, 200, 100, "Click Me! πŸ¦β€β¬›", {
63
+ cornerRadius: 10,
64
+ });
55
65
 
56
- btn.addEventListener("click", (event)=>{
57
- clickTimes+=1;
58
- event.target.setText(`You clicked ${clickTimes} \ntimes!`);
59
- });
66
+ button.addEventListener('click', (event) => {
67
+ clickTimes += 1;
68
+ event.target.setText(`You clicked ${clickTimes} times!`);
69
+ });
60
70
 
61
- root.add(btn);
71
+ root.add(button);
62
72
  }
63
73
 
64
- window.draw = function () {
65
- background('rgba(255,255,255,255)');
74
+ function draw() {
66
75
  root.show();
76
+
77
+ // Event Listeners
67
78
  root.mouseEnterEventListeners(mouseX, mouseY);
68
79
  root.hoverEventListeners(mouseX, mouseY);
69
80
  root.mouseLeaveEventListeners(mouseX, mouseY);
70
- root.keyDownEventListeners(mouseX, mouseY);
71
- }
72
-
73
- window.mouseDragged = function (){
74
- root.mouseDraggedEventListeners(mouseX, mouseY);
75
81
  }
76
82
 
77
- window.mouseClicked = function(){
83
+ window.mouseClicked = function() {
78
84
  root.mouseClickedEventListeners(mouseX, mouseY);
79
85
  }
80
86
 
81
- window.mousePressed = function(){
87
+ window.mousePressed = function() {
82
88
  root.mousePressedEventListeners(mouseX, mouseY);
83
89
  }
84
90
 
85
- window.mouseReleased = function(){
91
+ window.mouseReleased = function() {
86
92
  root.mouseReleasedEventListeners(mouseX, mouseY);
87
93
  }
94
+ ```
88
95
 
89
- window.mouseWheel = function(event){
90
- root.mouseWheelEventListeners(mouseX, mouseY, event);
91
- }
96
+ ---
92
97
 
93
- window.keyPressed = function(event){
94
- if (keyCode === UP_ARROW || keyCode === DOWN_ARROW || keyCode === LEFT_ARROW || keyCode === RIGHT_ARROW) {
95
- event.preventDefault();
96
- }
98
+ ## 🧩 Components Overview
99
+
100
+ - Label(x, y, width, height, text, {options})
101
+ - Button
102
+ - ScrollFrame
103
+ - GridFrame
104
+
105
+ *More components coming soon!*
97
106
 
98
- root.keyPressedEventListeners(mouseX, mouseY);
99
- }
100
- ```
101
-
102
- ## Components Overview
103
- - `Label(x, y, width, height, text, {options})`
104
- - `ScrollFrame`, `GridFrame`, `Label`, etc.
107
+ ---
105
108
 
106
- More components and documentation coming soon.
109
+ ## πŸ“š Documentation & Tutorials
110
+ A full **video series** and our documentation site are actively being updated!
107
111
 
108
- ## Documentation & Tutorials
109
- A full **video series** and documentation site are in the works!
110
- Stay tuned for:
112
+ Check out the official docs at **[crow-js.vercel.app](https://crow-js.vercel.app/)** for:
111
113
  - Getting started tutorials
112
- - Custom component creation
114
+ - Custom component creation guides
113
115
  - Internals of CrowJS for contributors
114
116
 
115
- ## Why CrowJS?
116
- While p5.js is amazing for generative art and simulations, there's a noticeable gap when it comes to in-sketch UI. CrowJS fills that gap by giving you a canvas-first GUI framework that blends seamlessly with your visuals.
117
+ ---
117
118
 
118
- ## Contributing
119
- CrowJS is open-source! Contributions, bug reports, and ideas are welcome.
120
- See the `CONTRIBUTING.md` file for details.
119
+ ## 🀝 Contributing
120
+ CrowJS is open-source! Contributions, bug reports, and ideas are highly welcome.
121
+ Please see the `CONTRIBUTING.md` file for details on how to get started.
121
122
 
122
- ## License
123
- This project is licensed under the MIT License.
124
- See the `LICENSE` file for details.
123
+ ## πŸ”— Links
124
+ - **Creator's Sketches:** [Usman's p5.js Editor](https://editor.p5js.org/Usman_Ali/sketches/)
125
+ - **Powered By:** [p5.js Official Site](https://p5js.org)
125
126
 
126
- ## Links
127
- - My sketches: [https://editor.p5js.org/Usman_Ali/sketches/](p5js.org/Usman_Ali/sketches/)
128
- - p5.js: [https://p5js.org](https://p5js.org)
127
+ ---
129
128
 
130
- Created with ❀️ by **Usman**
129
+ <div align="center">
130
+ Created with ❀️ by <b>Usman</b> <br/>
131
+ <i>Licensed under the MIT License.</i>
132
+ </div>
@@ -0,0 +1,296 @@
1
+ import { TextComponent } from './TextComponent.js';
2
+ import { Component } from '../Core/Component.js';
3
+
4
+ export class Checkbox extends TextComponent {
5
+ /**
6
+ * Creates a checkbox component with a toggleable checked state and label text.
7
+ * @param {number} x - The x-coordinate
8
+ * @param {number} y - The y-coordinate
9
+ * @param {number} width - The width
10
+ * @param {number} height - The height
11
+ * @param {string} label - The text to display next to the checkbox
12
+ * @param {Object} options - Configuration options
13
+ * @param {string|null} options.id - Component ID
14
+ * @param {Component|null} options.parent - Parent component
15
+ * @param {boolean} options.checked - Initial checked state
16
+ * @param {p5.Color} options.backgroundColor - Background color
17
+ * @param {p5.Color} options.textColor - Text color
18
+ * @param {p5.Color} options.boxColor - Checkbox box background color (unchecked)
19
+ * @param {p5.Color} options.boxBorderColor - Checkbox box border color
20
+ * @param {number} options.boxBorderWidth - Checkbox box border width
21
+ * @param {p5.Color} options.checkedColor - Background color of the box when checked
22
+ * @param {p5.Color} options.checkmarkColor - Checkmark color
23
+ * @param {number} options.boxSize - Size of the checkbox box in pixels
24
+ * @param {number} options.boxGap - Gap between checkbox box and label text
25
+ * @param {number} options.boxCornerRadius - Corner radius of the checkbox box
26
+ * @param {p5.Color} options.hoverBoxBorderColor - Box border color on hover
27
+ * @param {boolean} options.enabled - Whether the checkbox is interactive
28
+ * @param {boolean} options.borderFlag - Whether to show component border
29
+ * @param {p5.Color} options.borderColor - Component border color
30
+ * @param {number} options.borderWidth - Component border width
31
+ * @param {number} options.cornerRadius - Component corner radius
32
+ * @param {boolean} options.enableShadow - Enable shadow rendering
33
+ * @param {string} options.shadowColor - Shadow color (CSS color string)
34
+ * @param {number} options.shadowBlur - Shadow blur radius
35
+ * @param {number} options.shadowOffsetX - Shadow offset on X axis
36
+ * @param {number} options.shadowOffsetY - Shadow offset on Y axis
37
+ * @param {string} options.HTextAlign - Horizontal text alignment
38
+ * @param {string} options.VTextAlign - Vertical text alignment
39
+ * @param {number} options.pad - General padding
40
+ * @param {number} options.padx - Horizontal padding
41
+ * @param {number} options.pady - Vertical padding
42
+ * @param {number} options.padl - Left padding
43
+ * @param {number} options.padr - Right padding
44
+ * @param {number} options.padt - Top padding
45
+ * @param {number} options.padb - Bottom padding
46
+ * @param {boolean} options.wrap - Whether to wrap text
47
+ * @param {string} options.wrapMode - Wrap mode: "word" or "char"
48
+ * @param {string} options.noWrapMode - No-wrap mode: "ellipsis" or "font-size"
49
+ * @param {string} options.ellipsisMode - Ellipsis mode: "leading", "center", or "trailing"
50
+ * @param {number} options.margin - General margin for all sides
51
+ * @param {number} options.marginx - Horizontal margin (left and right)
52
+ * @param {number} options.marginy - Vertical margin (top and bottom)
53
+ * @param {number} options.marginl - Left margin
54
+ * @param {number} options.marginr - Right margin
55
+ * @param {number} options.margint - Top margin
56
+ * @param {number} options.marginb - Bottom margin
57
+ */
58
+ constructor(x, y, width, height, label, {
59
+ id = null,
60
+ parent = null,
61
+ checked = false,
62
+ backgroundColor = color('#1e1e2e'),
63
+ textColor = color('#e0e0e0'),
64
+ boxColor = color('#2a2a3d'),
65
+ boxBorderColor = color('#3a3a4d'),
66
+ boxBorderWidth = 2,
67
+ checkedColor = color('#4a9eff'),
68
+ checkmarkColor = color('#ffffff'),
69
+ boxSize = 18,
70
+ boxGap = 8,
71
+ boxCornerRadius = 4,
72
+ hoverBoxBorderColor = color('#5a5a7a'),
73
+ enabled = true,
74
+ borderFlag = false,
75
+ borderColor = color('#3a3a4d'),
76
+ borderWidth = 1,
77
+ cornerRadius = 8,
78
+ enableShadow = false,
79
+ shadowColor = 'rgba(0,0,0,0.5)',
80
+ shadowBlur = 12,
81
+ shadowOffsetX = 0,
82
+ shadowOffsetY = 4,
83
+ HTextAlign = 'left',
84
+ VTextAlign = 'center',
85
+ pad = 5,
86
+ padx = null,
87
+ pady = null,
88
+ padl = null,
89
+ padr = null,
90
+ padt = null,
91
+ padb = null,
92
+ wrap = false,
93
+ wrapMode = 'word',
94
+ noWrapMode = 'font-size',
95
+ ellipsisMode = 'trailing',
96
+ margin = 0,
97
+ marginx = null,
98
+ marginy = null,
99
+ marginl = null,
100
+ marginr = null,
101
+ margint = null,
102
+ marginb = null,
103
+ minWidth = 0,
104
+ minHeight = 0,
105
+ showDebugOverlay = false,
106
+ } = {}) {
107
+ super(x, y, width, height, label, {
108
+ id,
109
+ parent,
110
+ backgroundColor,
111
+ textColor,
112
+ borderFlag,
113
+ borderColor,
114
+ borderWidth,
115
+ cornerRadius,
116
+ enableShadow,
117
+ shadowColor,
118
+ shadowBlur,
119
+ shadowOffsetX,
120
+ shadowOffsetY,
121
+ HTextAlign,
122
+ VTextAlign,
123
+ pad,
124
+ padx,
125
+ pady,
126
+ padl,
127
+ padr,
128
+ padt,
129
+ padb,
130
+ wrap,
131
+ wrapMode,
132
+ noWrapMode,
133
+ ellipsisMode,
134
+ icon: null,
135
+ iconSize: 20,
136
+ iconPosition: 'left',
137
+ iconGap: 6,
138
+ iconTintColor: null,
139
+ iconOpacity: 255,
140
+ margin,
141
+ marginx,
142
+ marginy,
143
+ marginl,
144
+ marginr,
145
+ margint,
146
+ marginb,
147
+ minWidth,
148
+ minHeight,
149
+ showDebugOverlay,
150
+ });
151
+
152
+ this.checked = checked;
153
+ this.boxColor = boxColor;
154
+ this.boxBorderColor = boxBorderColor;
155
+ this.boxBorderWidth = boxBorderWidth;
156
+ this.checkedColor = checkedColor;
157
+ this.checkmarkColor = checkmarkColor;
158
+ this.boxSize = boxSize;
159
+ this.boxGap = boxGap;
160
+ this.boxCornerRadius = boxCornerRadius;
161
+ this.hoverBoxBorderColor = hoverBoxBorderColor;
162
+ this.enabled = enabled;
163
+ this.isHovered = false;
164
+
165
+ // Absorb box space into left padding so that all TextComponent layout
166
+ // machinery (renderText, updateLabelSize, getContentWidth, debug overlay)
167
+ // works correctly without any overrides.
168
+ this.padl += this.boxSize + this.boxGap;
169
+
170
+ // Recalculate label size with the updated padl
171
+ if (!this.wrap && this.noWrapMode === 'font-size') {
172
+ this.updateLabelSize();
173
+ }
174
+
175
+ this.addEventListener('mouseEnter', () => {
176
+ if (!this.enabled) return;
177
+ this.isHovered = true;
178
+ });
179
+
180
+ this.addEventListener('hover', (event) => {
181
+ if (!this.enabled) return;
182
+ cursor('pointer');
183
+ event.stopPropagation();
184
+ });
185
+
186
+ this.addEventListener('mouseLeave', () => {
187
+ this.isHovered = false;
188
+ });
189
+
190
+ this.addEventListener('click', () => {
191
+ if (!this.enabled) return;
192
+ this.checked = !this.checked;
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Returns whether the checkbox is currently checked
198
+ * @returns {boolean}
199
+ */
200
+ isChecked() {
201
+ return this.checked;
202
+ }
203
+
204
+ /**
205
+ * Sets the checked state of the checkbox
206
+ * @param {boolean} checked - Checked state
207
+ */
208
+ setChecked(checked) {
209
+ this.checked = checked;
210
+ }
211
+
212
+ /**
213
+ * Toggles the checked state
214
+ */
215
+ toggle() {
216
+ this.checked = !this.checked;
217
+ }
218
+
219
+ /**
220
+ * Enables or disables the checkbox
221
+ * @param {boolean} enabled - True to enable, false to disable
222
+ */
223
+ setEnabled(enabled) {
224
+ this.enabled = enabled;
225
+ if (!enabled) {
226
+ this.isHovered = false;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Renders the checkbox with label
232
+ */
233
+ show() {
234
+ if (this.enableShadow) {
235
+ this.drawShadow();
236
+ }
237
+
238
+ push();
239
+ beginClip();
240
+ rect(this.x, this.y, this.width, this.height, this.cornerRadius);
241
+ endClip();
242
+ translate(this.x, this.y);
243
+
244
+ // Background
245
+ fill(this.backgroundColor);
246
+ rect(0, 0, this.width, this.height, this.cornerRadius);
247
+
248
+ // Checkbox box position: sits in the gap between original padl and expanded padl
249
+ const originalPadl = this.padl - this.boxSize - this.boxGap;
250
+ const boxX = originalPadl;
251
+ const boxY = (this.height - this.boxSize) / 2;
252
+
253
+ // Draw checkbox box
254
+ if (this.checked) {
255
+ fill(this.checkedColor);
256
+ } else {
257
+ fill(this.boxColor);
258
+ }
259
+
260
+ if (this.isHovered && this.enabled) {
261
+ stroke(this.hoverBoxBorderColor);
262
+ } else {
263
+ stroke(this.boxBorderColor);
264
+ }
265
+ strokeWeight(this.boxBorderWidth);
266
+ rect(boxX, boxY, this.boxSize, this.boxSize, this.boxCornerRadius);
267
+
268
+ // Draw checkmark when checked
269
+ if (this.checked) {
270
+ stroke(this.checkmarkColor);
271
+ strokeWeight(2.5);
272
+ noFill();
273
+ const cx = boxX + this.boxSize * 0.5;
274
+ const cy = boxY + this.boxSize * 0.5;
275
+ const s = this.boxSize * 0.3;
276
+ // Checkmark path: two connected lines
277
+ line(cx - s * 0.6, cy, cx - s * 0.1, cy + s * 0.5);
278
+ line(cx - s * 0.1, cy + s * 0.5, cx + s * 0.7, cy - s * 0.5);
279
+ }
280
+
281
+ // Draw label text β€” renderText uses this.padl which already includes box space
282
+ noStroke();
283
+ fill(this.enabled ? this.textColor : color(120, 120, 140));
284
+ this.renderText();
285
+
286
+ // Border
287
+ if (this.borderFlag) {
288
+ noFill();
289
+ stroke(this.borderColor);
290
+ strokeWeight(this.borderWidth);
291
+ rect(0, 0, this.width, this.height, this.cornerRadius);
292
+ }
293
+
294
+ pop();
295
+ }
296
+ }
@@ -0,0 +1,360 @@
1
+ import { TextComponent } from './TextComponent.js';
2
+ import { Component } from '../Core/Component.js';
3
+
4
+ /**
5
+ * Static registry of radio button groups.
6
+ * Maps group name β†’ Set of RadioButton instances.
7
+ * @type {Map<string, Set<RadioButton>>}
8
+ */
9
+ const _radioGroups = new Map();
10
+
11
+ export class RadioButton extends TextComponent {
12
+ /**
13
+ * Creates a radio button component with group-exclusive selection and label text.
14
+ * Only one radio button per `group` can be selected at a time.
15
+ *
16
+ * @param {number} x - The x-coordinate
17
+ * @param {number} y - The y-coordinate
18
+ * @param {number} width - The width
19
+ * @param {number} height - The height
20
+ * @param {string} label - The text to display next to the radio button
21
+ * @param {Object} options - Configuration options
22
+ * @param {string|null} options.id - Component ID
23
+ * @param {Component|null} options.parent - Parent component
24
+ * @param {string} options.group - The group name this radio button belongs to
25
+ * @param {boolean} options.selected - Initial selected state
26
+ * @param {string|*} options.value - The value this radio button represents
27
+ * @param {p5.Color} options.backgroundColor - Background color
28
+ * @param {p5.Color} options.textColor - Text color
29
+ * @param {p5.Color} options.circleColor - Outer circle background color (unselected)
30
+ * @param {p5.Color} options.circleBorderColor - Outer circle border color
31
+ * @param {number} options.circleBorderWidth - Outer circle border width
32
+ * @param {p5.Color} options.selectedColor - Inner dot color when selected
33
+ * @param {p5.Color} options.selectedCircleBorderColor - Outer circle border color when selected
34
+ * @param {number} options.circleSize - Diameter of the outer circle in pixels
35
+ * @param {number} options.dotSize - Diameter of the inner dot in pixels
36
+ * @param {number} options.circleGap - Gap between circle and label text
37
+ * @param {p5.Color} options.hoverCircleBorderColor - Circle border color on hover
38
+ * @param {boolean} options.enabled - Whether the radio button is interactive
39
+ * @param {boolean} options.borderFlag - Whether to show component border
40
+ * @param {p5.Color} options.borderColor - Component border color
41
+ * @param {number} options.borderWidth - Component border width
42
+ * @param {number} options.cornerRadius - Component corner radius
43
+ * @param {boolean} options.enableShadow - Enable shadow rendering
44
+ * @param {string} options.shadowColor - Shadow color (CSS color string)
45
+ * @param {number} options.shadowBlur - Shadow blur radius
46
+ * @param {number} options.shadowOffsetX - Shadow offset on X axis
47
+ * @param {number} options.shadowOffsetY - Shadow offset on Y axis
48
+ * @param {string} options.HTextAlign - Horizontal text alignment
49
+ * @param {string} options.VTextAlign - Vertical text alignment
50
+ * @param {number} options.pad - General padding
51
+ * @param {number} options.padx - Horizontal padding
52
+ * @param {number} options.pady - Vertical padding
53
+ * @param {number} options.padl - Left padding
54
+ * @param {number} options.padr - Right padding
55
+ * @param {number} options.padt - Top padding
56
+ * @param {number} options.padb - Bottom padding
57
+ * @param {boolean} options.wrap - Whether to wrap text
58
+ * @param {string} options.wrapMode - Wrap mode: "word" or "char"
59
+ * @param {string} options.noWrapMode - No-wrap mode: "ellipsis" or "font-size"
60
+ * @param {string} options.ellipsisMode - Ellipsis mode: "leading", "center", or "trailing"
61
+ * @param {number} options.margin - General margin for all sides
62
+ * @param {number} options.marginx - Horizontal margin (left and right)
63
+ * @param {number} options.marginy - Vertical margin (top and bottom)
64
+ * @param {number} options.marginl - Left margin
65
+ * @param {number} options.marginr - Right margin
66
+ * @param {number} options.margint - Top margin
67
+ * @param {number} options.marginb - Bottom margin
68
+ */
69
+ constructor(x, y, width, height, label, {
70
+ id = null,
71
+ parent = null,
72
+ group = 'default',
73
+ selected = false,
74
+ value = null,
75
+ backgroundColor = color('#1e1e2e'),
76
+ textColor = color('#e0e0e0'),
77
+ circleColor = color('#2a2a3d'),
78
+ circleBorderColor = color('#3a3a4d'),
79
+ circleBorderWidth = 2,
80
+ selectedColor = color('#4a9eff'),
81
+ selectedCircleBorderColor = color('#4a9eff'),
82
+ circleSize = 18,
83
+ dotSize = 10,
84
+ circleGap = 8,
85
+ hoverCircleBorderColor = color('#5a5a7a'),
86
+ enabled = true,
87
+ borderFlag = false,
88
+ borderColor = color('#3a3a4d'),
89
+ borderWidth = 1,
90
+ cornerRadius = 8,
91
+ enableShadow = false,
92
+ shadowColor = 'rgba(0,0,0,0.5)',
93
+ shadowBlur = 12,
94
+ shadowOffsetX = 0,
95
+ shadowOffsetY = 4,
96
+ HTextAlign = 'left',
97
+ VTextAlign = 'center',
98
+ pad = 5,
99
+ padx = null,
100
+ pady = null,
101
+ padl = null,
102
+ padr = null,
103
+ padt = null,
104
+ padb = null,
105
+ wrap = false,
106
+ wrapMode = 'word',
107
+ noWrapMode = 'font-size',
108
+ ellipsisMode = 'trailing',
109
+ margin = 0,
110
+ marginx = null,
111
+ marginy = null,
112
+ marginl = null,
113
+ marginr = null,
114
+ margint = null,
115
+ marginb = null,
116
+ minWidth = 0,
117
+ minHeight = 0,
118
+ showDebugOverlay = false,
119
+ } = {}) {
120
+ super(x, y, width, height, label, {
121
+ id,
122
+ parent,
123
+ backgroundColor,
124
+ textColor,
125
+ borderFlag,
126
+ borderColor,
127
+ borderWidth,
128
+ cornerRadius,
129
+ enableShadow,
130
+ shadowColor,
131
+ shadowBlur,
132
+ shadowOffsetX,
133
+ shadowOffsetY,
134
+ HTextAlign,
135
+ VTextAlign,
136
+ pad,
137
+ padx,
138
+ pady,
139
+ padl,
140
+ padr,
141
+ padt,
142
+ padb,
143
+ wrap,
144
+ wrapMode,
145
+ noWrapMode,
146
+ ellipsisMode,
147
+ icon: null,
148
+ iconSize: 20,
149
+ iconPosition: 'left',
150
+ iconGap: 6,
151
+ iconTintColor: null,
152
+ iconOpacity: 255,
153
+ margin,
154
+ marginx,
155
+ marginy,
156
+ marginl,
157
+ marginr,
158
+ margint,
159
+ marginb,
160
+ minWidth,
161
+ minHeight,
162
+ showDebugOverlay,
163
+ });
164
+
165
+ this.group = group;
166
+ this.selected = false;
167
+ this.value = value ?? label;
168
+ this.circleColor = circleColor;
169
+ this.circleBorderColor = circleBorderColor;
170
+ this.circleBorderWidth = circleBorderWidth;
171
+ this.selectedColor = selectedColor;
172
+ this.selectedCircleBorderColor = selectedCircleBorderColor;
173
+ this.circleSize = circleSize;
174
+ this.dotSize = dotSize;
175
+ this.circleGap = circleGap;
176
+ this.hoverCircleBorderColor = hoverCircleBorderColor;
177
+ this.enabled = enabled;
178
+ this.isHovered = false;
179
+
180
+ // Absorb circle space into left padding so that all TextComponent layout
181
+ // machinery (renderText, updateLabelSize, getContentWidth, debug overlay)
182
+ // works correctly without any overrides.
183
+ this.padl += this.circleSize + this.circleGap;
184
+
185
+ // Recalculate label size with the updated padl
186
+ if (!this.wrap && this.noWrapMode === 'font-size') {
187
+ this.updateLabelSize();
188
+ }
189
+
190
+ // Register in group
191
+ if (!_radioGroups.has(this.group)) {
192
+ _radioGroups.set(this.group, new Set());
193
+ }
194
+ _radioGroups.get(this.group).add(this);
195
+
196
+ // If initially selected, deselect others in the group
197
+ if (selected) {
198
+ this.select();
199
+ }
200
+
201
+ this.addEventListener('mouseEnter', () => {
202
+ if (!this.enabled) return;
203
+ this.isHovered = true;
204
+ });
205
+
206
+ this.addEventListener('hover', (event) => {
207
+ if (!this.enabled) return;
208
+ cursor('pointer');
209
+ event.stopPropagation();
210
+ });
211
+
212
+ this.addEventListener('mouseLeave', () => {
213
+ this.isHovered = false;
214
+ });
215
+
216
+ this.addEventListener('click', () => {
217
+ if (!this.enabled) return;
218
+ this.select();
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Selects this radio button and deselects all others in the same group.
224
+ */
225
+ select() {
226
+ const group = _radioGroups.get(this.group);
227
+ if (group) {
228
+ for (const rb of group) {
229
+ if (rb !== this) {
230
+ rb.selected = false;
231
+ }
232
+ }
233
+ }
234
+ this.selected = true;
235
+ }
236
+
237
+ /**
238
+ * Deselects this radio button.
239
+ */
240
+ deselect() {
241
+ this.selected = false;
242
+ }
243
+
244
+ /**
245
+ * Returns whether this radio button is currently selected.
246
+ * @returns {boolean}
247
+ */
248
+ isSelected() {
249
+ return this.selected;
250
+ }
251
+
252
+ /**
253
+ * Returns the value of the currently selected radio button in this group.
254
+ * @returns {*|null} The value, or null if none selected.
255
+ */
256
+ getSelectedValue() {
257
+ const group = _radioGroups.get(this.group);
258
+ if (group) {
259
+ for (const rb of group) {
260
+ if (rb.selected) return rb.value;
261
+ }
262
+ }
263
+ return null;
264
+ }
265
+
266
+ /**
267
+ * Returns all RadioButton instances in the same group.
268
+ * @returns {RadioButton[]}
269
+ */
270
+ getGroupMembers() {
271
+ const group = _radioGroups.get(this.group);
272
+ return group ? [...group] : [];
273
+ }
274
+
275
+ /**
276
+ * Enables or disables the radio button.
277
+ * @param {boolean} enabled - True to enable, false to disable
278
+ */
279
+ setEnabled(enabled) {
280
+ this.enabled = enabled;
281
+ if (!enabled) {
282
+ this.isHovered = false;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Removes this radio button from its group registry.
288
+ * Call this when the radio button is permanently removed from the UI.
289
+ */
290
+ destroy() {
291
+ const group = _radioGroups.get(this.group);
292
+ if (group) {
293
+ group.delete(this);
294
+ if (group.size === 0) {
295
+ _radioGroups.delete(this.group);
296
+ }
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Renders the radio button with label.
302
+ */
303
+ show() {
304
+ if (this.enableShadow) {
305
+ this.drawShadow();
306
+ }
307
+
308
+ push();
309
+ beginClip();
310
+ rect(this.x, this.y, this.width, this.height, this.cornerRadius);
311
+ endClip();
312
+ translate(this.x, this.y);
313
+
314
+ // Background
315
+ fill(this.backgroundColor);
316
+ rect(0, 0, this.width, this.height, this.cornerRadius);
317
+
318
+ // Radio circle position: sits in the gap between original padl and expanded padl
319
+ const originalPadl = this.padl - this.circleSize - this.circleGap;
320
+ const cx = originalPadl + this.circleSize / 2;
321
+ const cy = this.height / 2;
322
+
323
+ // Draw outer circle
324
+ if (this.selected) {
325
+ fill(this.circleColor);
326
+ stroke(this.selectedCircleBorderColor);
327
+ } else {
328
+ fill(this.circleColor);
329
+ if (this.isHovered && this.enabled) {
330
+ stroke(this.hoverCircleBorderColor);
331
+ } else {
332
+ stroke(this.circleBorderColor);
333
+ }
334
+ }
335
+ strokeWeight(this.circleBorderWidth);
336
+ ellipse(cx, cy, this.circleSize, this.circleSize);
337
+
338
+ // Draw inner dot when selected
339
+ if (this.selected) {
340
+ noStroke();
341
+ fill(this.selectedColor);
342
+ ellipse(cx, cy, this.dotSize, this.dotSize);
343
+ }
344
+
345
+ // Draw label text β€” renderText uses this.padl which already includes circle space
346
+ noStroke();
347
+ fill(this.enabled ? this.textColor : color(120, 120, 140));
348
+ this.renderText();
349
+
350
+ // Border
351
+ if (this.borderFlag) {
352
+ noFill();
353
+ stroke(this.borderColor);
354
+ strokeWeight(this.borderWidth);
355
+ rect(0, 0, this.width, this.height, this.cornerRadius);
356
+ }
357
+
358
+ pop();
359
+ }
360
+ }
@@ -0,0 +1,3 @@
1
+ - npm run docs:dev to run dev server and test docs site
2
+ - bump npm versions:
3
+
package/index.js CHANGED
@@ -16,6 +16,8 @@ const ScrollFrame = require('./Frames/ScrollFrame');
16
16
  const Input = require('./UIComponents/Input');
17
17
  const Label = require('./UIComponents/Label');
18
18
  const Button = require('./UIComponents/Button');
19
+ const Checkbox = require('./UIComponents/Checkbox');
20
+ const RadioButton = require('./UIComponents/RadioButton');
19
21
  const TextField = require('./UIComponents/TextField');
20
22
  const UIComponent = require('./UIComponents/UIComponent');
21
23
  const Icon = require('./UIComponents/Icon');
@@ -39,6 +41,8 @@ module.exports = {
39
41
  Input,
40
42
  Label,
41
43
  Button,
44
+ Checkbox,
45
+ RadioButton,
42
46
  TextField,
43
47
  UIComponent,
44
48
  Icon
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@usman404/crowjs",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "lightweight and extensible GUI library built on top of p5.js",
5
5
  "keywords": [
6
- "CROWJS"
6
+ "p5js",
7
+ "gui",
8
+ "canvas",
9
+ "creative-coding",
10
+ "crowjs"
7
11
  ],
8
12
  "homepage": "https://crow-js.vercel.app/",
9
13
  "bugs": {
@@ -11,11 +15,11 @@
11
15
  },
12
16
  "repository": {
13
17
  "type": "git",
14
- "url": "https://github.com/UsmanAli404/CrowJS"
18
+ "url": "git+https://github.com/UsmanAli404/CrowJS.git"
15
19
  },
16
20
  "license": "MIT",
17
21
  "author": "Usman Ali",
18
- "type": "commonjs",
22
+ "type": "module",
19
23
  "main": "index.js",
20
24
  "scripts": {
21
25
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -27,4 +31,4 @@
27
31
  "@iconify/vue": "^4.1.2",
28
32
  "vitepress": "^1.6.4"
29
33
  }
30
- }
34
+ }
package/problems.txt CHANGED
@@ -1 +1,4 @@
1
- - work on default look of all items.
1
+ - work on default look of all items.
2
+ - make a about me page
3
+ - add more components
4
+ -
package/sketch.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Root } from "./Core/Root.js";
2
2
  import { Label } from "./UIComponents/Label.js";
3
-
3
+ import { Checkbox } from "./UIComponents/Checkbox.js";
4
+ import { RadioButton } from "./UIComponents/RadioButton.js";
5
+ import { GridFrame } from "./Frames/GridFrame.js";
4
6
 
5
7
  /** @type {Root} */
6
8
  let root;
@@ -10,21 +12,23 @@ window.setup = function(){
10
12
  createCanvas(windowWidth, windowHeight);
11
13
  root = new Root();
12
14
 
13
- let btnWidth = 200;
14
- let btnHeight = 100;
15
+ const frame = new GridFrame(20, 20, 500, 50, {
16
+ rows: 1,
17
+ cols: 3,
18
+ backgroundColor: "#1e1e2e",
19
+ });
20
+
21
+ const rb1 = new RadioButton(0, 0, 0, 0, "Low", { group: "quality", value: "low", selected: true });
22
+ const rb2 = new RadioButton(0, 0, 0, 0, "Medium", { group: "quality", value: "medium" });
23
+ const rb3 = new RadioButton(0, 0, 0, 0, "High", { group: "quality", value: "high" });
15
24
 
16
- let btn = new Label((windowWidth/2)-(btnWidth/2), (windowHeight/2)-(btnHeight/2), 200, 100, "Hello! πŸ‘‹",
17
- {cornerRadius: 20,
18
- backgroundColor: "rgba(0, 0, 0, 1)",
19
- textColor: "rgba(255, 255, 255, 1)",
20
- });
25
+ frame.colConfig(1, 2);
21
26
 
22
- btn.addEventListener("click", (event)=>{
23
- clickTimes+=1;
24
- event.target.setText(`You clicked ${clickTimes} \ntimes!`);
25
- });
27
+ frame.add(rb1, 0, 0);
28
+ frame.add(rb2, 0, 1);
29
+ frame.add(rb3, 0, 2);
26
30
 
27
- root.add(btn);
31
+ root.add(frame);
28
32
  }
29
33
 
30
34
  window.draw = function () {