Event and paint cascading for Processing, implemented using Processing.js
Processing, the visualisation programming language, uses
a javalike syntax, with full support for Object Oriented programming, but does not come with its own
cascaded event and paint model. These are relatively easy to implement (at least naively), and so
what follows is a simple framework that you can download and use in your own code. I personally use
it for work that I do in processing.js, the javascript library
for using raw Processing (or P5, as it's also known) code inside script tags, or by script file inclusion.
Component cascading
Basic processing code
The main key in this framework is that every event runs through a master "Components"
container, so all draw instructions, mouse events and key events are sent to it:
toggle code
703 | /**
|
704 | * All events are routed through a single "components" instance
|
705 | */
|
706 | Components components;
|
707 |
|
708 | /**
|
709 | * Only does initialisation, then hands off all event work to the components handler
|
710 | */
|
711 | void setup() {
|
712 | noLoop();
|
713 | size(600,600);
|
714 | components = new Components();
|
715 | initUI();
|
716 | }
|
717 |
|
718 | /**
|
719 | * Wrapper for component's draw()
|
720 | */
|
721 | void draw() { background(255); components.draw(); }
|
722 |
|
723 | /**
|
724 | * Wrappers for event handling
|
725 | */
|
726 | void mouseMoved() { components.mouseMoved(mouseX, mouseY); }
|
727 | void mouseDragged() { components.mouseDragged(mouseX, mouseY); }
|
728 | void mouseClicked() { components.mouseClicked(mouseX, mouseY); }
|
729 | void mousePressed() { components.mousePressed(mouseX, mouseY); }
|
730 | void mouseReleased() { components.mouseReleased(mouseX, mouseY); }
|
731 | void keyPressed() { components.keyPressed(key, keyCode); }
|
732 | void keyReleased() { components.keyReleased(key, keyCode); }
|
733 |
|
You will notice a reference to the method initUI(), which is a convenient place to put all your UI
initialisation and component filling.
Components container
The code for the Components object is relatively straight forward, and takes care of three things: 1) event
cascading, 2) draw() cascading and 3) focus tracking based on a "focus follows the mouse" principle.
toggle code
65 | class Components {
|
66 | ArrayList components;
|
67 | Component focussed = null;
|
68 |
|
69 | Components() { components = new ArrayList(); }
|
70 |
|
71 | void add(Component component) { components.add(component); }
|
72 | Component get(int index) { return (Component) components.get(index); }
|
73 | int size() { return components.size(); }
|
74 |
|
75 | // cascades a debug down
|
76 | void setDebug(boolean debug) {
|
77 | for(int c=0; c<components.size(); c++) {
|
78 | Component component = ((Component)components.get(c));
|
79 | component.setDebug(debug); }}
|
80 |
|
81 | // draws all components, lowest first
|
82 | void draw(){
|
83 | for(int c=0; c<components.size(); c++) {
|
84 | Component component = ((Component)components.get(c));
|
85 | if (component.isVisible()) { component.draw(); }}}
|
86 |
|
87 | // we look for the last (=top most) component that will accept this mouseMove event.
|
88 | // if it's not the currently focussed component, send the old component a focusLost(),
|
89 | // and the new component a focusReceived() event.
|
90 | boolean mouseMoved(int mouseX, int mouseY) {
|
91 | boolean cfound = false;
|
92 | for(int c=components.size()-1; c>=0; c--) {
|
93 | Component component = ((Component)components.get(c));
|
94 | if(component.listensAt(mouseX,mouseY)) {
|
95 | if(focussed!=component) {
|
96 | if(focussed!=null) {
|
97 | focussed.setFocus(false);
|
98 | focussed.focusLost(); }
|
99 | component.setFocus(true);
|
100 | component.focusReceived();
|
101 | focussed = component; }
|
102 | else if(!focussed.hasFocus()) {
|
103 | focussed.setFocus(true);
|
104 | focussed.focusReceived(); }
|
105 | component.mouseMoved(mouseX,mouseY);
|
106 | cfound = true;
|
107 | break; }}
|
108 | if(!cfound && focussed!=null) { focussed.focusLost(); focussed=null; }
|
109 | redraw();
|
110 | return cfound; }
|
111 |
|
112 | // standard cascade
|
113 | void mouseClicked(int mouseX, int mouseY) {
|
114 | for(int c=components.size()-1; c>=0; c--) {
|
115 | Component component = ((Component)components.get(c));
|
116 | if(component.listensAt(mouseX,mouseY)) {
|
117 | component.mouseClicked(mouseX,mouseY); break; }} redraw(); }
|
118 |
|
119 | // standard cascade
|
120 | void mousePressed(int mouseX, int mouseY) {
|
121 | for(int c=components.size()-1; c>=0; c--) {
|
122 | Component component = ((Component)components.get(c));
|
123 | if(component.listensAt(mouseX,mouseY)) {
|
124 | component.mousePressed(mouseX,mouseY); break; }} redraw(); }
|
125 |
|
126 | // standard cascade
|
127 | void mouseDragged(int mouseX, int mouseY) {
|
128 | for(int c=components.size()-1; c>=0; c--) {
|
129 | Component component = ((Component)components.get(c));
|
130 | if(component.listensAt(mouseX,mouseY)) {
|
131 | component.mouseDragged(mouseX,mouseY); break; }} redraw(); }
|
132 |
|
133 | // standard cascade
|
134 | void mouseReleased(int mouseX, int mouseY) {
|
135 | for(int c=components.size()-1; c>=0; c--) {
|
136 | Component component = ((Component)components.get(c));
|
137 | if(component.listensAt(mouseX,mouseY)) {
|
138 | component.mouseReleased(mouseX,mouseY); break; }} redraw(); }
|
139 |
|
140 | // standard cascade
|
141 | void keyPressed(int key, int keyCode) {
|
142 | for(int c=0; c<components.size(); c++) {
|
143 | Component component = ((Component)components.get(c));
|
144 | if(component.listensForKeyPress()) {
|
145 | component.keyPressed(key, keyCode); }} redraw(); }
|
146 |
|
147 | // standard cascade
|
148 | void keyReleased(int key, int keyCode) {
|
149 | for(int c=0; c<components.size(); c++) {
|
150 | Component component = ((Component)components.get(c));
|
151 | if(component.listensForKeyRelease()) {
|
152 | component.keyReleased(key, keyCode); }} redraw(); }
|
153 | }
|
The component class
Of course, we can't do any event/draw cascading without Component objects, so there is also
a superancestor for components. Components can have focus or not, be visible or not, be set to debug
mode or not, and can indicate whether they are interested in mouse events, key presses, and key releases:
toggle code
20 | class Component {
|
21 | boolean visible = true; // visibility
|
22 | boolean hasfocus = false; // focus
|
23 | boolean debug = false; // debug status
|
24 | int xoffset = 0; // nested positioning x-offset
|
25 | int yoffset = 0; // nested positioning y-offset
|
26 | // empty constructor
|
27 | Component() {}
|
28 | // draw method
|
29 | void draw() {}
|
30 | // set offsets
|
31 | void setOffsets(int x, int y) { xoffset=x; yoffset=y; }
|
32 | int getXOffset() { return xoffset; }
|
33 | int getYOffset() { return yoffset; }
|
34 | // debug
|
35 | void setDebug(boolean d) { debug = d; }
|
36 | // handle visibility
|
37 | void setVisible(boolean v) { visible = v; }
|
38 | boolean isVisible() { return visible; }
|
39 | // event handling checks: mouse
|
40 | boolean listensAt(int x, int y) { return false; }
|
41 | // event handling checks: keybaord
|
42 | boolean listensForKeyPress() { return false; }
|
43 | // event handling checks: keyboard
|
44 | boolean listensForKeyRelease() { return false; }
|
45 | // event handling super methods
|
46 | void mouseMoved(int mouseX, int mouseY) {}
|
47 | void mouseClicked(int mouseX, int mouseY) {}
|
48 | void mouseDragged(int mouseX, int mouseY) {}
|
49 | void mousePressed(int mouseX, int mouseY) {}
|
50 | void mouseReleased(int mouseX, int mouseY) {}
|
51 | void keyPressed(int key, int keyCode) {}
|
52 | void keyReleased(int key, int keyCode) {}
|
53 | // action listening from other components
|
54 | void actionPerformed(Component source, int event, String action) {}
|
55 | // focus listening
|
56 | void setFocus(boolean f) { hasfocus = f; }
|
57 | boolean hasFocus() { return hasfocus; }
|
58 | void focusReceived() {}
|
59 | void focusLost() {}
|
60 | }
|
Drawable components
While reasonably a component should be drawable, those properties are actually part of things that
are "more" than just a component, so we stick those properties in a new superancestor, called "Drawable".
This is an extension on Component with things like stroke and fill properties, as well as methods that
offer an interface to the active drawable surface:
toggle code
193 | class Drawable extends Component {
|
194 | int stroke_r, stroke_g, stroke_b, stroke_a; // stroke color
|
195 | int fill_r, fill_g, fill_b, fill_a; // fill color
|
196 | int x, y; // component's location on a surface
|
197 | BoundingBox bounds = null;
|
198 | Drawable(int x, int y) {
|
199 | super();
|
200 | setStroke(0,0,0,255);
|
201 | setFill(255,255,255,255);
|
202 | setX(x);
|
203 | setY(y);
|
204 | setupBoundingBox(); }
|
205 | // all drawable components have a bounding box
|
206 | void setupBoundingBox() {
|
207 | bounds = new BoundingBox(x,y,x,y); }
|
208 | void setOffsets(int x, int y) {
|
209 | super.setOffsets(x,y);
|
210 | bounds.move(x,y); }
|
211 | // draw means setting colors, and if debugging, drawing the bounding box
|
212 | void draw() {
|
213 | if(debug) {
|
214 | stroke(255,0,0,100);
|
215 | fill(255,0,0,10);
|
216 | bounds.draw(); }
|
217 | stroke(stroke_r,stroke_g,stroke_b,stroke_a);
|
218 | fill(fill_r,fill_g,fill_b,fill_a); }
|
219 | void setStroke(int r, int g, int b, int a) {
|
220 | stroke_r = r; stroke_g = g; stroke_b = b; stroke_a = a; }
|
221 | void setFill(int r, int g, int b, int a) {
|
222 | fill_r = r; fill_g = g; fill_b = b; fill_a = a; }
|
223 | // standard getter/setters
|
224 | int getX() { return x; }
|
225 | int getY() { return y; }
|
226 | void setX(int v) { x=v; }
|
227 | void setY(int v) { y=v; }
|
228 | BoundingBox getBoundingBox() { return bounds; }
|
229 | // moved a drawable component around on the surface
|
230 | void move(int dx, int dy) { x += dx; y += dy; bounds.move(dx,dy); }
|
231 | // superancestral methods for determining whether focus and events should propagate
|
232 | boolean listensAt(int x, int y) { return inArea(x,y); }
|
233 | boolean inArea(int x, int y) { return false; }
|
234 | }
|
Drawable components have a location in their parent container, and have stroke and fill color
properties. In addition, the "listensAt" method now refers to the inArea method, so that subclasses
only have to implement an "inArea" check for interfacing with the drawable surface
Bounding boxes
All drawable components have a bounding box, which is basically just a rectange that indicates the
extremities of a shape. For some shapes the bounding box is simply defined by the x and y coordinates
of the points on the shape, such as lines and rectangles, but for some shapes the bounding box depends
on the shape of the line segments drawn, rather than the shape's points, such as for circles and bezier
curves.
toggle code
163 | class BoundingBox
|
164 | {
|
165 | int x, y, X, Y;
|
166 | BoundingBox(int x, int y, int X, int Y) { this.x=x; this.y=y; this.X=X; this.Y=Y; }
|
167 | // draw
|
168 | void draw() { rectMode(CORNER); rect(x, y, X-x, Y-y); }
|
169 | // getters
|
170 | int getMinX() { return x; }
|
171 | int getMinY() { return y; }
|
172 | int getMaxX() { return X; }
|
173 | int getMaxY() { return Y; }
|
174 | int getWidth() { return X-x; }
|
175 | int getHeight() { return Y-y; }
|
176 | // setters
|
177 | void setMinX(int v) { x = v; }
|
178 | void setMinY(int v) { y = v; }
|
179 | void setMaxX(int v) { X = v; }
|
180 | void setMaxY(int v) { Y = v; }
|
181 | // movers
|
182 | void move(int dx, int dy) { x+=dx; y+=dy; X+=dx; Y+=dy; }
|
183 | // in bound check
|
184 | boolean inBoundsX(int v) { return (x<=v && v<=X); }
|
185 | boolean inBoundsY(int v) { return (y<=v && v<=Y); }
|
186 | boolean inArea(int x, int y) { return (inBoundsX(x) && inBoundsY(y)); }
|
187 | }
|
Normally, the bounding box is hidden, but because it's always good to be able to play around with, it can
be made visible by setting a component's debug flag to true
Using objects instead of draw primitives
The point of an event/draw tree is that you can treat what you draw as individual components. So let's
make that happen: lets define the basic drawing primitives in Processing as proper objects instead:
Ellipse
toggle code
239 | class Ellipse extends Drawable
|
240 | {
|
241 | int width, height;
|
242 | Ellipse(int xv, int yv, int w, int h) {
|
243 | super(xv,yv);
|
244 | width = w;
|
245 | height = h;
|
246 | setupBoundingBox(); }
|
247 | void setupBoundingBox() {
|
248 | int hw = (width/2);
|
249 | int hh = (height/2);
|
250 | bounds = new BoundingBox(x-hw, y-hh, x+hw, y+hh); }
|
251 | // draw
|
252 | void draw() { super.draw(); ellipse(x+xoffset, y+yoffset, width, height); }
|
253 | // getters
|
254 | int getWidth() { return width; }
|
255 | int getHeight() { return height; }
|
256 | // setters
|
257 | void setWidth(int v) { width = v; }
|
258 | void setHeight(int v) { height = v; }
|
259 | // poly check (sort of) - Ellipse cartesian formula: (x-h)²/a² + (y-k)²/b² = 1
|
260 | // in this formula {h,k} = center, a = half width, and b = half height
|
261 | boolean inArea(int xv, int yv) {
|
262 | int a = width/2; int sqa = a*a;
|
263 | int b = height/2; int sqb = b*b;
|
264 | int xh = x - xv; int sqxh = xh*xh;
|
265 | int yk = y - yv; int sqyk = yk*yk;
|
266 | return (sqxh/sqa + sqyk/sqb) <= 1; }
|
267 | }
|
Circle
technically this is not a Processing primitive, you'd use ellipse(x,y,d,d), but it's worth giving its own class.
toggle code
278 | class Circle extends Drawable
|
279 | {
|
280 | int radius;
|
281 | Circle(int xv, int yv, int r) {
|
282 | super(xv,yv);
|
283 | radius = (int)r;
|
284 | setupBoundingBox(); }
|
285 | void setupBoundingBox() {
|
286 | bounds = new BoundingBox(x-radius, y-radius, x+radius, y+radius); }
|
287 | // draw
|
288 | void draw() { super.draw(); ellipse(x+xoffset, y+yoffset, 2*radius, 2*radius); }
|
289 | // getters
|
290 | int getRadius() { return radius; }
|
291 | // setters
|
292 | void setRadius(int v) { radius = v; }
|
293 | // poly check (sort of)
|
294 | boolean inArea(int xv, int yv) {
|
295 | int dx = xv-x;
|
296 | int sqx = dx*dx;
|
297 | int dy = yv-y;
|
298 | int sqy = dy*dy;
|
299 | int sqr = radius*radius;
|
300 | // aways use multiplication rather than sqrt when possible. Much, much cheaper
|
301 | return sqx+sqy <= sqr; }
|
302 | }
|
Rect
toggle code
308 | class Rect extends Drawable
|
309 | {
|
310 | int x2, y2;
|
311 | Rect(int x, int y, int w, int h) {
|
312 | super(x,y);
|
313 | x2 = x+w;
|
314 | y2 = y+h;
|
315 | setupBoundingBox(); }
|
316 | void setupBoundingBox() {
|
317 | bounds = new BoundingBox(x,y,x2,y2); }
|
318 | // draw
|
319 | void draw() {
|
320 | super.draw();
|
321 | rectMode(CORNER);
|
322 | rect(x+xoffset, y+yoffset, getWidth(), getHeight()); }
|
323 | // getters
|
324 | int getMinX() { return x; }
|
325 | int getMinY() { return y; }
|
326 | int getMaxX() { return x2; }
|
327 | int getMaxY() { return y2; }
|
328 | int getWidth() { return x2-x; }
|
329 | int getHeight() { return y2-y; }
|
330 | // setters
|
331 | void setMinX(int v) { setX(v); }
|
332 | void setMinY(int v) { setY(v); }
|
333 | void setMaxX(int v) { x2 = v; }
|
334 | void setMaxY(int v) { y2 = v; }
|
335 | // move override, because we have more than just x/y to move
|
336 | void move(int dx, int dy) {
|
337 | super.move(dx,dy);
|
338 | x2 += dx;
|
339 | y2 += dy; }
|
340 | // poly check (sort of)
|
341 | boolean inBoundsX(int v) { return (x<=v && v<=x2); }
|
342 | boolean inBoundsY(int v) { return (y<=v && v<=y2); }
|
343 | boolean inArea(int x, int y) { return (inBoundsX(x) && inBoundsY(y)); }
|
344 | }
|
Line
toggle code
393 | class Line extends Drawable
|
394 | {
|
395 | int bounding_thickness;
|
396 | int x2, y2, xdif, ydif;
|
397 |
|
398 | Line(int x1, int y1, int x2, int y2) {
|
399 | super(x1,y1);
|
400 | this.x2 = x2; this.y2 = y2;
|
401 | bounding_thickness = 3;
|
402 | setupBoundingBox(); }
|
403 |
|
404 | void setupBoundingBox() {
|
405 | bounds = new BoundingBox(min(x,x2)-bounding_thickness,
|
406 | min(y,y2)-bounding_thickness,
|
407 | max(x,x2)+bounding_thickness,
|
408 | max(y,y2)+bounding_thickness); }
|
409 |
|
410 | void setStartX(int v) { setX(v); setupBoundingBox(); }
|
411 | void setStartY(int v) { setY(v); setupBoundingBox(); }
|
412 | void setEndX(int v) { x2=v; setupBoundingBox(); }
|
413 | void setEndY(int v) { y2=v; setupBoundingBox(); }
|
414 |
|
415 | void move(int dx, int dy) {
|
416 | super.move(dx,dy);
|
417 | x2+=dx;
|
418 | y2+=dy; }
|
419 |
|
420 | void setBoundingThickness(int t) {
|
421 | bounding_thickness = t;
|
422 | setupBoundingBox(); }
|
423 |
|
424 | void draw() { super.draw(); drawLine(); }
|
425 | void drawLine() { line(x+xoffset,y+yoffset,x2+xoffset,y2+yoffset); }
|
426 |
|
427 | // compute the y we get for this x, and see if it's close enough to the y we *should* get.
|
428 | boolean onLine(int xv, int yv) {
|
429 | if(bounds.inArea(xv,yv)) {
|
430 | if(abs(x2-x)<=bounding_thickness) { return (min(y,y2)<=yv && yv<=max(y,y2)); }
|
431 | else if(abs(y2-y)<=bounding_thickness) { return (min(x,x2)<=yv && yv<=max(x,x2)); }
|
432 | else {
|
433 | double ydif = y2-y;
|
434 | double xdif = x2-x;
|
435 | double coeff = ydif/xdif;
|
436 | int computed_y = (int) (y + (((xv-bounding_thickness)-x)*coeff));
|
437 | int computed_y2 = (int) (y + (((xv+bounding_thickness)-x)*coeff));
|
438 | boolean online = (min(computed_y,computed_y2)<=yv && yv<=max(computed_y,computed_y2));
|
439 | return online; }}
|
440 | return false; }
|
441 |
|
442 | // lines strictly speaking have no area, so an inArea check is the same as an onLine check
|
443 | boolean inArea(int x, int y) { return onLine(x,y); }
|
444 | }
|
Bezier
The bezier curve is a spectacular beast, mostly because its bounding box and "is the mouse hovering
over it" computation are rather funky bits of math:
toggle code
450 | class Bezier extends Line
|
451 | {
|
452 | int cx1, cy1, cx2, cy2; // control coordinates
|
453 |
|
454 | // third order bezier (cubic)
|
455 | Bezier(int x1, int y1, int cx1, int cy1, int cx2, int cy2, int x2, int y2) {
|
456 | super(x1,y1,x2,y2);
|
457 | this.cx1 = cx1; this.cy1 = cy1; this.cx2 = cx2; this.cy2 = cy2;
|
458 | setupBezierBounds(); }
|
459 |
|
460 | // bounds computation for Bezier curves is not quite as straight forward as lines...
|
461 | // in fact, it's downright complex.
|
462 | void setupBezierBounds()
|
463 | {
|
464 | int x1=x;
|
465 | int y1=y;
|
466 |
|
467 | // I don't know why, but when we get to this point, bounds is not guaranteed to have been instantiated...
|
468 | // perhaps a processing.js bug, perhaps an inherent feature. Don't know, easy to work around.
|
469 | bounds = new BoundingBox(min(x1,x2), min(y1,y2), max(x1,x2), max(y1,y2));
|
470 |
|
471 | //
|
472 | // The next bit is technical. See the comment on this topic on
|
473 | // http://newsgroups.derkeiler.com/Archive/Comp/comp.graphics.algorithms/2005-07/msg00334.html
|
474 | // and the worked out mathematics at http://pomax.nihongoresources.com/downloads/bezierbounds.html
|
475 | // for an explanation of why the following code is being used, and why it works.
|
476 | //
|
477 |
|
478 | double dcx0 = (double) (cx1-x1);
|
479 | double dcy0 = (double) (cy1-y1);
|
480 | double dcx1 = (double) (cx2 - cx1);
|
481 | double dcy1 = (double) (cy2 - cy1);
|
482 | double dcx2 = (double) (x2 - cx2);
|
483 | double dcy2 = (double) (y2 - cy2);
|
484 |
|
485 | // recompute bounds projected on the x-axis, if the control points lie outside the bounding box x-bounds
|
486 | if(!bounds.inBoundsX(cx1) || !bounds.inBoundsX(cx2)) {
|
487 | double a = dcx0;
|
488 | double b = dcx1;
|
489 | double c = dcx2;
|
490 |
|
491 | // Do we have a problematic discriminator if we use these values?
|
492 | // If we do, because we're computing at sub-pixel level anyway, simply salt 'b' a tiny bit.
|
493 | if(a+c != 2*b) { b+=0.01; }
|
494 |
|
495 | double numerator = 2*(a - b);
|
496 | double denominator = 2*(a - 2*b + c);
|
497 | double doubleroot = (2*b-2*a)*(2*b-2*a) - 2*a*denominator;
|
498 | double root = sqrt((float)doubleroot);
|
499 |
|
500 | // there are two possible values for t that yield inflection points
|
501 | double t1 = (numerator + root) / denominator;
|
502 | double t2 = (numerator - root) / denominator;
|
503 |
|
504 | // so, which of these is the useful point? (t must lie in [0,1])
|
505 | if(0<=t1 && t1<=1) {
|
506 | double inflectionpoint = evaluateX(t1);
|
507 | if(inflectionpoint>=0) {
|
508 | if(bounds.getMinX() > inflectionpoint) { bounds.setMinX((int)inflectionpoint); }
|
509 | else if(bounds.getMaxX() < inflectionpoint) { bounds.setMaxX((int)inflectionpoint); }}}
|
510 | if(0<=t2 && t2<=1) {
|
511 | double inflectionpoint = evaluateX(t2);
|
512 | if(inflectionpoint>=0) {
|
513 | if(bounds.getMinX() > inflectionpoint) { bounds.setMinX((int)inflectionpoint); }
|
514 | else if(bounds.getMaxX() < inflectionpoint) { bounds.setMaxX((int)inflectionpoint); }}}
|
515 | }
|
516 |
|
517 | // recompute bounds projected on the y-axis, if the control points lie outside the bounding box y-bounds
|
518 | // no comments, because it's virtually identical code. Kept as duplicate, though, to emphasise that we have
|
519 | // to do this for the x-axis and y-axis separately.
|
520 | if(!bounds.inBoundsY(cy1) || !bounds.inBoundsY(cy2)) {
|
521 | double a = dcy0;
|
522 | double b = dcy1;
|
523 | double c = dcy2;
|
524 | if(a+c != 2*b) { b+=0.01; }
|
525 | double numerator = 2*(a - b);
|
526 | double denominator = 2*(a - 2*b + c);
|
527 | double doubleroot = (2*b-2*a)*(2*b-2*a) - 2*a*denominator;
|
528 | double root = sqrt((float)doubleroot);
|
529 | double t1 = (numerator + root) / denominator;
|
530 | double t2 = (numerator - root) / denominator;
|
531 | if(0<=t1 && t1<=1) {
|
532 | double inflectionpoint = evaluateY(t1);
|
533 | if(inflectionpoint>=0) {
|
534 | if(bounds.getMinY() > inflectionpoint) { bounds.setMinY((int)inflectionpoint); }
|
535 | else if(bounds.getMaxY() < inflectionpoint) { bounds.setMaxY((int)inflectionpoint); }}}
|
536 | if(0<=t2 && t2<=1) {
|
537 | double inflectionpoint = evaluateY(t2);
|
538 | if(inflectionpoint>=0) {
|
539 | if(bounds.getMinY() > inflectionpoint) { bounds.setMinY((int)inflectionpoint); }
|
540 | else if(bounds.getMaxY() < inflectionpoint) { bounds.setMaxY((int)inflectionpoint); }}}
|
541 | }
|
542 | }
|
543 | // evaluates the x-projection of the bezier curve for parameter t
|
544 | double evaluateX(double t) {
|
545 | double it = (1-t);
|
546 | return ((double)x*it*it*it + 3.0*(double)cx1*t*it*it + 3.0*(double)cx2*it*t*t + (double)x2*t*t*t); }
|
547 |
|
548 | // evaluates the y-projection of the bezier curve for parameter t
|
549 | double evaluateY(double t) {
|
550 | double it = (1-t);
|
551 | return ((double)y*it*it*it + 3.0*(double)cy1*t*it*it + 3.0*(double)cy2*it*t*t + (double)y2*t*t*t); }
|
552 |
|
553 | // override - instead of drawing a straight line, we draw the bezier curve
|
554 | void drawLine() { bezier(x+xoffset,y+yoffset, cx1+xoffset,cy1+yoffset, cx2+xoffset,cy2+yoffset, x2+xoffset,y2+yoffset); }
|
555 |
|
556 | // override on move, because not only do we have to move our start and end point, but also our
|
557 | // bezier control points.
|
558 | void move(int dx, int dy) {
|
559 | super.move(dx,dy);
|
560 | this.cx1 += dx;
|
561 | this.cy1 += dy;
|
562 | this.cx2 += dx;
|
563 | this.cy2 += dy; }
|
564 |
|
565 | // run through the bezier parametric curve and see if we pass the provided coordinate.
|
566 | // this check can be optimised by setting the step size for t based on what the bezier
|
567 | // parameters in the constructor are. however, for simplicity (right now), we simply use
|
568 | // step 1/100 so we'll probably always catch any mouseover
|
569 | boolean onLine(int x, int y) {
|
570 | for(double t=0.0; t<=1.0; t+=0.01) {
|
571 | int ftx = (int) evaluateX(t);
|
572 | int fty = (int) evaluateY(t);
|
573 | if(abs(ftx-x)<=bounding_thickness && abs(fty-y)<=bounding_thickness) { return true; }}
|
574 | return false; }
|
575 | }
|
Extended components
Just primitives only get us so far, so in this framework there are also two extra, extended components: The
Panel, and the Button.
Panels
Panels allow us to define a region on the screen, and place components relative to that region. For example,
if we define a 200x200 region of the screen, with its top-left corner at 150x250, then we can add a component
to the panel docked nicely to the upper-left corner by giving the component coordinates 0x0, rather than
coordinates 150x250. This is really useful! The code for this is not very complicated:
toggle code
351 | class Panel extends Rect
|
352 | {
|
353 | Components components;
|
354 | Component focussed = null;
|
355 | Panel(int x, int y, int w, int h) {
|
356 | super(x,y,w,h);
|
357 | components = new Components();
|
358 | setStroke(0,0,0,0);
|
359 | setFill(255,255,255,255); }
|
360 | // panels don't have a stroke, only a background color
|
361 | void setBackground(int r, int g, int b) { setFill(r,g,b,255); }
|
362 | void add(Component component) {
|
363 | component.setOffsets(x+xoffset,y+yoffset);
|
364 | components.add(component); }
|
365 | void setDebug(boolean debug) { components.setDebug(debug); }
|
366 | Component get(int index) { return (Component) components.get(index); }
|
367 | int size() { return components.size(); }
|
368 | void draw(){
|
369 | super.draw();
|
370 | for(int c=0; c<components.size(); c++) {
|
371 | Component component = ((Component)components.get(c));
|
372 | if (component.isVisible()) { component.draw(); }}}
|
373 |
|
374 | // movemoved events inside a canvas must be propagated to the components
|
375 | // inside the panel - note the offset corrections in the passed coordinates!
|
376 | void mouseMoved(int x, int y) {
|
377 | boolean handled = components.mouseMoved(x-(this.x+xoffset),y-(this.y+yoffset));
|
378 | if(handled&&hasfocus) { setFocus(false); focusLost(); }}
|
379 |
|
380 | // generic propagation - note the offset corrections in the passed coordinates!
|
381 | void mouseClicked(int x, int y) { components.mouseClicked(x-(this.x+xoffset),y-(this.y+yoffset)); }
|
382 | void mousePressed(int x, int y) { components.mousePressed(x-(this.x+xoffset),y-(this.y+yoffset)); }
|
383 | void mouseDragged(int x, int y) { components.mouseDragged(x-(this.x+xoffset),y-(this.y+yoffset)); }
|
384 | void mouseReleased(int x, int y) { components.mouseReleased(x-(this.x+xoffset),y-(this.y+yoffset)); }
|
385 | void keyPressed(int k, int kc) { components.keyPressed(k,kc); }
|
386 | void keyReleased(int k, int kc) { components.keyReleased(k,kc); }
|
387 | }
|
Buttons
Buttons are generally useful when it comes to making your code do something based on functional mouse
events. Of course, in processing.js you can use on-page UI elements, rather than in-sketch elements, but then
you are making your sketch impossible to load in Processing... so let's look at the button definition too:
toggle code
594 | class Button extends Component
|
595 | {
|
596 | public int BUTTON_PRESSED = 0;
|
597 | public int BUTTON_UNPRESSED = 1;
|
598 | private boolean pressed = false;
|
599 | ArrayList actionlisteners = new ArrayList();
|
600 | private Drawable face;
|
601 | private String action;
|
602 | public Button(Drawable face, String action) {
|
603 | this.face=face;
|
604 | this.action=action;
|
605 | release(); }
|
606 | void draw() { face.draw(); }
|
607 | void setDebug(boolean debug) { face.setDebug(debug); }
|
608 | void setStroke(int r, int g, int b, int a) { face.setStroke(r,g,b,a); }
|
609 | void setFill(int r, int g, int b, int a) { face.setFill(r,g,b,a); }
|
610 | void setOffsets(int x, int y) { face.setOffsets(x,y); }
|
611 | // standard getter/setters
|
612 | int getX() { return face.getX(); }
|
613 | int getY() { return face.getY(); }
|
614 | void setX(int v) { face.setX(v); }
|
615 | void setY(int v) { face.setY(v); }
|
616 | BoundingBox getBoundingBox() { return face.getBoundingBox(); }
|
617 | void move(int dx, int dy) { if(face!=null) { face.move(dx,dy); }}
|
618 | // superancestral methods for determining whether focus and events should propagate
|
619 | boolean listensAt(int x, int y) { return face.listensAt(x,y); }
|
620 | boolean inArea(int x, int y) { return face.inArea(x,y); }
|
621 | // getter
|
622 | Drawable getFace() { return face; }
|
623 | // action listeners
|
624 | void addActionListener(Component c) { actionlisteners.add(c); }
|
625 | String getActionString() { return action; }
|
626 | // coloring
|
627 | void setNormalColors() { setStroke(0,0,0,255); setFill(255,255,255,255); }
|
628 | void setMouseoverColors() { setStroke(255,0,175,255); }
|
629 | void setClickedColors() { setStroke(0,0,0,255); setFill(200,200,255,255); }
|
630 | // button has been pressed
|
631 | void press() {
|
632 | pressed = true;
|
633 | setClickedColors();
|
634 | for(int a=0; a<actionlisteners.size(); a++) {
|
635 | ((Component)actionlisteners.get(a)).actionPerformed(this, BUTTON_PRESSED, action); }}
|
636 | // button has been released
|
637 | void release() {
|
638 | pressed = false;
|
639 | setNormalColors();
|
640 | for(int a=0; a<actionlisteners.size(); a++) {
|
641 | ((Component)actionlisteners.get(a)).actionPerformed(this, BUTTON_UNPRESSED, action); }}
|
642 | // state check
|
643 | boolean isPressed() { return pressed; }
|
644 | // mouse handler for clicking on the button
|
645 | void mousePressed(int x, int y) { if(pressed) { release(); } else { press(); }}
|
646 | // what to do when focus is lost
|
647 | void focusLost() {
|
648 | if(pressed) { setClickedColors(); }
|
649 | else { setNormalColors(); }}
|
650 | // what to do when focus is granted
|
651 | void focusReceived() { setMouseoverColors(); }
|
652 | }
|
In order for a button to trigger functionality in another Component, that component needs to have been
added as a listener to the button, using button.addActionListener(component).
This will then automatically make the button trigger the component's actionPerformed(source, action)
method, with the "source" being the button, and "action" being an action string as defined by the button, for switch running.
Putting it together
Let's look at a practical example - A panel with lots of clickable "buttons". For the purpose of this
exercise, we'll use rect, ellipse, circle and bezier buttons. First off, the code we'll use in our initUI():
toggle code
738 | void initUI()
|
739 | {
|
740 | // make a 400x400 panel, placed at {100, 100}
|
741 | RandomColorPanel randomcolorpanel = new RandomColorPanel(100,550,400,25);
|
742 | MainPanel mainpanel = new MainPanel(100,100,400,400);
|
743 |
|
744 | int howmanyofeach=10;
|
745 |
|
746 | // make a couple of (almost) randomly placed 25x25 buttons, inside the panel.
|
747 | for(int i=0; i<howmanyofeach; i++) {
|
748 | RectButton button = new RectButton((int)random(0,300)+25,(int)random(0,300)+25,25,25, "rectangle");
|
749 | button.addActionListener(randomcolorpanel);
|
750 | mainpanel.add(button); }
|
751 |
|
752 | // make a couple of (almost) randomly placed 13px radius circle buttons, inside the panel.
|
753 | for(int i=0; i<howmanyofeach; i++) {
|
754 | CircleButton button = new CircleButton((int)random(0,300)+25,(int)random(0,300)+25,13, "circle");
|
755 | button.addActionListener(randomcolorpanel);
|
756 | mainpanel.add(button); }
|
757 |
|
758 | // make a couple of almost randomly placed randomly bulged ellipse buttons, too.
|
759 | for(int i=0; i<howmanyofeach; i++) {
|
760 | EllipseButton button = new EllipseButton((int)random(0,300)+25,(int)random(0,300)+25,(int)random(10,30), (int)random(10,30), "ellipse");
|
761 | button.addActionListener(randomcolorpanel);
|
762 | mainpanel.add(button); }
|
763 |
|
764 | // and finally, make a couple of randomly curved clickable bezier curves, in a pretty configuration
|
765 | for(int i=0; i<howmanyofeach; i++) {
|
766 | BezierLineButton button = new BezierLineButton(0,0,
|
767 | (int)random(100,400),(int)random(100,400),
|
768 | (int)random(-200,600),(int)random(-200,600),
|
769 | 380,380,
|
770 | "bezier line");
|
771 | button.addActionListener(randomcolorpanel);
|
772 | mainpanel.add(button); }
|
773 |
|
774 | // add panel to components, and we're done
|
775 | components.add(mainpanel);
|
776 | components.add(randomcolorpanel);
|
777 | } |
And then the code that actually defines our custom clickable panel, and the button objects we will be using.
We want our main panel to be a dull grey that changes to white when it has focus, changing to blue when it is
clicked. We want our buttons white with a black stroke, unless they receive focus, in which case their stroke should
be pink. A pressed button is a dull blue-grey. Bezier curve "buttons" are black, unless they have focus, in which
case they're pink, or they've been pressed, in which case they're dull blue-grey. Finally, we also want an extra
"nonsense" panel below our main panel, and we want it to change to a random colour every time any button is
clicked, but not when the main panel is clicked.
toggle code
658 | class RandomColorPanel extends Panel {
|
659 | RandomColorPanel(int x, int y, int w, int h) {
|
660 | super(x,y,w,h);
|
661 | colorBackground();}
|
662 | void colorBackground() {
|
663 | setBackground((int)random(255),(int)random(255),(int)random(255)); }
|
664 | // when something notifies us that an action occured, change color
|
665 | void actionPerformed(Component source, int event, String action) { colorBackground(); }
|
666 | }
|
667 |
|
668 | class MainPanel extends Panel {
|
669 | MainPanel(int x, int y, int w, int h) { super(x,y,w,h); setBackground(230,230,230);}
|
670 | void focusLost() { setBackground(230,230,230); }
|
671 | void focusReceived() { setBackground(255,255,255); }
|
672 | void mousePressed(int x, int y) {
|
673 | if(hasFocus()) { setBackground(100,200,255); }
|
674 | super.mousePressed(x,y); }
|
675 | void mouseReleased(int x, int y) {
|
676 | if(hasFocus()) { setBackground(255,255,255); }
|
677 | super.mouseReleased(x,y); }
|
678 | }
|
679 |
|
680 | class RectButton extends Button {
|
681 | RectButton(int x, int y, int w, int h, String action) {
|
682 | super(new Rect(x,y,w,h), action); }}
|
683 |
|
684 | class EllipseButton extends Button {
|
685 | EllipseButton(int x, int y, int r1, int r2, String action) {
|
686 | super(new Ellipse(x,y,r1,r2), action); }}
|
687 |
|
688 | class CircleButton extends Button {
|
689 | CircleButton(int x, int y, int r, String action) {
|
690 | super(new Circle(x,y,r), action); }}
|
691 |
|
692 | class BezierLineButton extends Button {
|
693 | BezierLineButton(int x1, int y1, int cx1, int cy1, int cx2, int cy2, int x2, int y2, String action) {
|
694 | super(new Bezier(x1,y1,cx1,cy1,cx2,cy2,x2,y2), action); }
|
695 | void setNormalColors() { setStroke(0,0,0,255); setFill(0,0,0,0); }
|
696 | void setMouseoverColors() { setStroke(255,0,175,255); setFill(0,0,0,0); }
|
697 | void setClickedColors() { setStroke(150,150,255,255); setFill(0,0,0,0); }}
|
So... what does this sketch look like?
I'll let you come up with a name for this sketch yourself, but my associations are marine biology inspired...
Note that the big white border is actually still part of the sketch - the sketch itself takes up 600x600 pixels, but
the panel with content is only 400x400, and placed at an offset of 100x100. Note that the bezier curves may be bigger
than the main panel, so they'll be drawn "outside" what you might think is the drawing surface!
Get the code
To save you some copy/paste work, click here to download the complete source code file!