@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.
- package/Core/Component.js +207 -1
- package/Core/GUIEvent/KeyboardEvent.js +2 -1
- package/Core/Root.js +40 -30
- package/Frames/DummyFrame.js +14 -12
- package/Frames/Frame.js +117 -57
- package/Frames/FrameComponent.js +9 -2
- package/Frames/GridFrame.js +131 -27
- package/Frames/ScrollFrame.js +154 -40
- package/README.md +8 -8
- package/UIComponents/Button.js +281 -0
- package/UIComponents/Icon.js +374 -0
- package/UIComponents/Input.js +15 -8
- package/UIComponents/Label.js +102 -158
- package/UIComponents/TextComponent.js +838 -0
- package/UIComponents/TextField.js +170 -39
- package/UIComponents/UIComponent.js +68 -35
- package/index.js +5 -1
- package/package.json +9 -2
- package/problems.txt +1 -0
|
@@ -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
|
+
}
|
package/UIComponents/Input.js
CHANGED
|
@@ -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}
|
|
20
|
-
* @param {number}
|
|
21
|
-
* @param {number}
|
|
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,
|
|
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,
|
|
33
|
-
|
|
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());
|