@usman404/crowjs 1.0.4 → 1.1.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.
@@ -0,0 +1,374 @@
1
+ import { UIComponent } from './UIComponent.js';
2
+
3
+ export class Icon extends UIComponent {
4
+ /**
5
+ * Creates an image/icon component with efficient cached rendering.
6
+ *
7
+ * The image is drawn to an off-screen p5.Graphics buffer and only
8
+ * re-rendered when the source, size, tint, or fit mode changes.
9
+ * On every `show()` call, only the cached buffer is blitted to
10
+ * the canvas – making repeated frames essentially free.
11
+ *
12
+ * @param {number} x - The x-coordinate
13
+ * @param {number} y - The y-coordinate
14
+ * @param {number} width - The width
15
+ * @param {number} height - The height
16
+ * @param {p5.Image|p5.Graphics} img - The image to display (loaded via loadImage or createGraphics)
17
+ * @param {Object} options - Configuration options
18
+ * @param {string|null} options.id - Component ID
19
+ * @param {Component|null} options.parent - Parent component
20
+ * @param {p5.Color} options.backgroundColor - Background color (null = transparent)
21
+ * @param {boolean} options.borderFlag - Whether to show border
22
+ * @param {p5.Color} options.borderColor - Border color
23
+ * @param {number} options.borderWidth - Border width
24
+ * @param {number} options.cornerRadius - Corner radius for rounded corners
25
+ * @param {boolean} options.enableShadow - Enable shadow rendering
26
+ * @param {string} options.shadowColor - Shadow color (CSS color string)
27
+ * @param {number} options.shadowBlur - Shadow blur radius
28
+ * @param {number} options.shadowOffsetX - Shadow offset on X axis
29
+ * @param {number} options.shadowOffsetY - Shadow offset on Y axis
30
+ * @param {string} options.fitMode - How the image fits inside the component:
31
+ * "contain" – scale to fit entirely, preserving aspect ratio (default)
32
+ * "cover" – scale to cover the area, cropping overflow
33
+ * "fill" – stretch to fill (ignores aspect ratio)
34
+ * "none" – render at original size, centered
35
+ * @param {p5.Color|null} options.tintColor - Optional tint applied to the image
36
+ * @param {number} options.opacity - Image opacity 0-255 (default 255)
37
+ * @param {number} options.margin - General margin for all sides
38
+ * @param {number} options.marginx - Horizontal margin
39
+ * @param {number} options.marginy - Vertical margin
40
+ * @param {number} options.marginl - Left margin
41
+ * @param {number} options.marginr - Right margin
42
+ * @param {number} options.margint - Top margin
43
+ * @param {number} options.marginb - Bottom margin
44
+ */
45
+ constructor(x, y, width, height, img, {
46
+ id = null,
47
+ parent = null,
48
+ backgroundColor = null,
49
+ borderFlag = false,
50
+ borderColor = null,
51
+ borderWidth = 1,
52
+ cornerRadius = 0,
53
+ enableShadow = false,
54
+ shadowColor = 'rgba(0,0,0,0.5)',
55
+ shadowBlur = 12,
56
+ shadowOffsetX = 0,
57
+ shadowOffsetY = 4,
58
+ fitMode = 'contain',
59
+ tintColor = null,
60
+ opacity = 255,
61
+ margin = 0,
62
+ marginx = null,
63
+ marginy = null,
64
+ marginl = null,
65
+ marginr = null,
66
+ margint = null,
67
+ marginb = null,
68
+ minWidth = 0,
69
+ minHeight = 0,
70
+ showDebugOverlay = false,
71
+ } = {}) {
72
+ super(
73
+ x, y, width, height,
74
+ backgroundColor ?? color(0, 0, 0, 0),
75
+ borderFlag,
76
+ borderColor ?? color('#3a3a4d'),
77
+ borderWidth,
78
+ cornerRadius,
79
+ enableShadow,
80
+ shadowColor,
81
+ shadowBlur,
82
+ shadowOffsetX,
83
+ shadowOffsetY,
84
+ {
85
+ parent,
86
+ type: 'Icon',
87
+ id,
88
+ margin,
89
+ marginx,
90
+ marginy,
91
+ marginl,
92
+ marginr,
93
+ margint,
94
+ marginb,
95
+ minWidth,
96
+ minHeight,
97
+ showDebugOverlay,
98
+ }
99
+ );
100
+
101
+ /** @type {p5.Image|p5.Graphics} The source image */
102
+ this.img = img;
103
+
104
+ /** @type {string} Fit mode: "contain" | "cover" | "fill" | "none" */
105
+ this.fitMode = fitMode;
106
+
107
+ /** @type {p5.Color|null} Optional tint color */
108
+ this.tintColor = tintColor;
109
+
110
+ /** @type {number} Opacity 0-255 */
111
+ this.opacity = opacity;
112
+
113
+ // ---- Cached off-screen buffer ----
114
+ /** @private */
115
+ this._cache = null;
116
+ /** @private – tracks parameters that were used to build the cache */
117
+ this._cacheKey = null;
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Public setters – each one invalidates the cache so the next show()
122
+ // re-renders automatically.
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /**
126
+ * Replace the displayed image
127
+ * @param {p5.Image|p5.Graphics} img
128
+ */
129
+ setImage(img) {
130
+ this.img = img;
131
+ this._invalidateCache();
132
+ }
133
+
134
+ /**
135
+ * Change the fit mode
136
+ * @param {"contain"|"cover"|"fill"|"none"} mode
137
+ */
138
+ setFitMode(mode) {
139
+ this.fitMode = mode;
140
+ this._invalidateCache();
141
+ }
142
+
143
+ /**
144
+ * Change the tint color
145
+ * @param {p5.Color|null} c
146
+ */
147
+ setTintColor(c) {
148
+ this.tintColor = c;
149
+ this._invalidateCache();
150
+ }
151
+
152
+ /**
153
+ * Change opacity (0-255)
154
+ * @param {number} o
155
+ */
156
+ setOpacity(o) {
157
+ this.opacity = o;
158
+ this._invalidateCache();
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Inherited stubs
163
+ // ---------------------------------------------------------------------------
164
+ updateWidth() {}
165
+ updateHeight() {}
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Rendering
169
+ // ---------------------------------------------------------------------------
170
+
171
+ /**
172
+ * Renders the icon/image component.
173
+ *
174
+ * On each call only the pre-rendered buffer is drawn to the main canvas.
175
+ * The buffer is rebuilt only when the image, size, tint, fit mode, or
176
+ * opacity changes.
177
+ */
178
+ show() {
179
+ if (!this.img) return;
180
+
181
+ // Shadow (drawn directly – it's cheap)
182
+ if (this.enableShadow) {
183
+ this.drawShadow();
184
+ }
185
+
186
+ // Rebuild cache when stale
187
+ const key = this._buildCacheKey();
188
+ if (this._cacheKey !== key) {
189
+ this._rebuildCache();
190
+ this._cacheKey = key;
191
+ }
192
+
193
+ // Blit cached buffer
194
+ push();
195
+ imageMode(CORNER);
196
+ image(this._cache, this.x, this.y);
197
+ pop();
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Cache helpers (private)
202
+ // ---------------------------------------------------------------------------
203
+
204
+ /**
205
+ * Computes a lightweight string key that captures every parameter
206
+ * influencing the cached render. Comparing this string tells us
207
+ * whether the cache is still valid.
208
+ * @private
209
+ * @returns {string}
210
+ */
211
+ _buildCacheKey() {
212
+ // For image identity we use a combination of dimensions + a frame-stable id.
213
+ // p5.Image objects don't carry a unique id, but we can use width×height
214
+ // plus the object reference (via a WeakRef-free approach: just store the
215
+ // reference and compare in _invalidateCache).
216
+ const imgId = this.img ? `${this.img.width}x${this.img.height}` : 'null';
217
+ return [
218
+ imgId,
219
+ this.width,
220
+ this.height,
221
+ this.fitMode,
222
+ this.tintColor ? this.tintColor.toString() : 'none',
223
+ this.opacity,
224
+ this.cornerRadius,
225
+ this.backgroundColor ? this.backgroundColor.toString() : 'none',
226
+ this.borderFlag,
227
+ this.borderColor ? this.borderColor.toString() : 'none',
228
+ this.borderWidth,
229
+ ].join('|');
230
+ }
231
+
232
+ /**
233
+ * Forces the cache to be rebuilt on the next show().
234
+ * @private
235
+ */
236
+ _invalidateCache() {
237
+ this._cacheKey = null;
238
+ }
239
+
240
+ /**
241
+ * Builds (or rebuilds) the off-screen p5.Graphics buffer.
242
+ * @private
243
+ */
244
+ _rebuildCache() {
245
+ const w = Math.ceil(this.width);
246
+ const h = Math.ceil(this.height);
247
+ if (w <= 0 || h <= 0) return;
248
+
249
+ // Reuse or create buffer
250
+ if (!this._cache || this._cache.width !== w || this._cache.height !== h) {
251
+ if (this._cache) this._cache.remove(); // free previous buffer
252
+ this._cache = createGraphics(w, h);
253
+ } else {
254
+ this._cache.clear();
255
+ }
256
+
257
+ const pg = this._cache;
258
+
259
+ // Background
260
+ if (this.backgroundColor) {
261
+ pg.noStroke();
262
+ pg.fill(this.backgroundColor);
263
+ pg.rect(0, 0, w, h, this.cornerRadius);
264
+ }
265
+
266
+ // Clip to rounded rect (if cornerRadius > 0)
267
+ if (this.cornerRadius > 0) {
268
+ pg.drawingContext.save();
269
+ this._clipRoundedRect(pg.drawingContext, 0, 0, w, h, this.cornerRadius);
270
+ pg.drawingContext.clip();
271
+ }
272
+
273
+ // Compute destination rect based on fitMode
274
+ const { dx, dy, dw, dh, sx, sy, sw, sh } = this._computeFit(w, h);
275
+
276
+ // Tint + opacity
277
+ if (this.tintColor) {
278
+ pg.tint(this.tintColor, this.opacity);
279
+ } else if (this.opacity < 255) {
280
+ pg.tint(255, this.opacity);
281
+ }
282
+
283
+ pg.imageMode(CORNER);
284
+ // Use the 9-argument form to support cover-mode cropping
285
+ pg.image(this.img, dx, dy, dw, dh, sx, sy, sw, sh);
286
+
287
+ // Remove tint
288
+ pg.noTint();
289
+
290
+ // Restore clip
291
+ if (this.cornerRadius > 0) {
292
+ pg.drawingContext.restore();
293
+ }
294
+
295
+ // Border
296
+ if (this.borderFlag) {
297
+ pg.noFill();
298
+ pg.stroke(this.borderColor);
299
+ pg.strokeWeight(this.borderWidth);
300
+ pg.rect(0, 0, w, h, this.cornerRadius);
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Traces a rounded-rectangle path on a Canvas2D context.
306
+ * @private
307
+ */
308
+ _clipRoundedRect(ctx, x, y, w, h, r) {
309
+ r = Math.min(r, w / 2, h / 2);
310
+ ctx.beginPath();
311
+ ctx.moveTo(x + r, y);
312
+ ctx.lineTo(x + w - r, y);
313
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
314
+ ctx.lineTo(x + w, y + h - r);
315
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
316
+ ctx.lineTo(x + r, y + h);
317
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
318
+ ctx.lineTo(x, y + r);
319
+ ctx.quadraticCurveTo(x, y, x + r, y);
320
+ ctx.closePath();
321
+ }
322
+
323
+ /**
324
+ * Computes source and destination rects based on the current fitMode.
325
+ * @private
326
+ * @param {number} cw - Container width
327
+ * @param {number} ch - Container height
328
+ * @returns {{dx:number, dy:number, dw:number, dh:number, sx:number, sy:number, sw:number, sh:number}}
329
+ */
330
+ _computeFit(cw, ch) {
331
+ const iw = this.img.width;
332
+ const ih = this.img.height;
333
+
334
+ switch (this.fitMode) {
335
+ case 'fill':
336
+ return { dx: 0, dy: 0, dw: cw, dh: ch, sx: 0, sy: 0, sw: iw, sh: ih };
337
+
338
+ case 'contain': {
339
+ const scale = Math.min(cw / iw, ch / ih);
340
+ const dw = iw * scale;
341
+ const dh = ih * scale;
342
+ return {
343
+ dx: (cw - dw) / 2,
344
+ dy: (ch - dh) / 2,
345
+ dw, dh,
346
+ sx: 0, sy: 0, sw: iw, sh: ih,
347
+ };
348
+ }
349
+
350
+ case 'cover': {
351
+ const scale = Math.max(cw / iw, ch / ih);
352
+ const sw = cw / scale;
353
+ const sh = ch / scale;
354
+ return {
355
+ dx: 0, dy: 0, dw: cw, dh: ch,
356
+ sx: (iw - sw) / 2,
357
+ sy: (ih - sh) / 2,
358
+ sw, sh,
359
+ };
360
+ }
361
+
362
+ case 'none':
363
+ default: {
364
+ // Centered at original size
365
+ return {
366
+ dx: (cw - iw) / 2,
367
+ dy: (ch - ih) / 2,
368
+ dw: iw, dh: ih,
369
+ sx: 0, sy: 0, sw: iw, sh: ih,
370
+ };
371
+ }
372
+ }
373
+ }
374
+ }
@@ -15,22 +15,29 @@ export class Input extends UIComponent{
15
15
  * @param {number} borderWidth - Border width
16
16
  * @param {number} cornerRadius - Corner radius
17
17
  * @param {boolean} enableShadow - Enable shadow
18
- * @param {string} shadowColor - Shadow color
19
- * @param {number} shadowIntensity - Shadow opacity
20
- * @param {number} shadowSpread - Shadow spread
21
- * @param {number} shadowDetail - Shadow layers
18
+ * @param {string} shadowColor - Shadow color (CSS color string)
19
+ * @param {number} shadowBlur - Shadow blur radius
20
+ * @param {number} shadowOffsetX - Shadow offset on X axis
21
+ * @param {number} shadowOffsetY - Shadow offset on Y axis
22
22
  * @param {Object} options - Additional options
23
23
  * @param {Component|null} options.parent - Parent component
24
24
  * @param {string} options.type - Component type
25
25
  * @param {string|null} options.id - Component ID
26
+ * @param {number} options.margin - General margin for all sides
27
+ * @param {number} options.marginx - Horizontal margin (left and right)
28
+ * @param {number} options.marginy - Vertical margin (top and bottom)
29
+ * @param {number} options.marginl - Left margin
30
+ * @param {number} options.marginr - Right margin
31
+ * @param {number} options.margint - Top margin
32
+ * @param {number} options.marginb - Bottom margin
26
33
  */
27
34
  constructor(x, y, width, height, backgroundColor, borderFlag, borderColor, borderWidth,
28
- cornerRadius, enableShadow, shadowColor, shadowIntensity, shadowSpread, shadowDetail,
29
- {parent=null, type="", id=null} = {}
35
+ cornerRadius, enableShadow, shadowColor, shadowBlur, shadowOffsetX, shadowOffsetY,
36
+ {parent=null, type="", id=null, margin=0, marginx=null, marginy=null, marginl=null, marginr=null, margint=null, marginb=null, minWidth=0, minHeight=0, showDebugOverlay=false} = {}
30
37
  ){
31
38
  super(x, y, width, height, backgroundColor, borderFlag, borderColor,
32
- borderWidth, cornerRadius, enableShadow, shadowColor, shadowIntensity,
33
- shadowSpread, shadowDetail, {parent: parent, type: type, id: id});
39
+ borderWidth, cornerRadius, enableShadow, shadowColor, shadowBlur,
40
+ shadowOffsetX, shadowOffsetY, {parent: parent, type: type, id: id, margin: margin, marginx: marginx, marginy: marginy, marginl: marginl, marginr: marginr, margint: margint, marginb: marginb, minWidth: minWidth, minHeight: minHeight, showDebugOverlay: showDebugOverlay});
34
41
  this.isFocused = false;
35
42
  // this.addEventListener("focus", (event)=>this.onFocus());
36
43
  // this.addEventListener("blur", (event)=>this.onBlur());