// CSC480DemoScreenshot1FA2021, D. Parson, warmup to CSC480 assn2, fall 2021 //<>// /***** MOUSE & KEYBOARD COMMANDS: PRESSING, DRAGGING, AND RELEASING THE MOUSE DRAWS THE CURRENT PAINTBRUSH CONTINUOUSLY POLLED KEY HOLDS UP scales the '~painted canvas up. DOWN scales the painted canvas up. RIGHT increases clockwise rotation of the painted canvas. LEFT increases counterclockwise rotation of the painted canvas. '>' advances clockwise rotation of the paintbrush hue. '<' advances counterclockwise rotation of the paintbrush hue. DISCRETE KEY PRESSES 'a' sets current paintbrush to an ellipse 'b' sets current paintbrush to a rectangle 'c' or 'd' set current paintbrush to a copy of the display 'R' resets canvas rotation to 0 and scaling to 1 'C' clears (erases) the display and removes any recursive paintbrush 'L' lifts (removes) all current paintbrushes 'F' freezes or unfreezes the display for debugging '|' toggles reflection of x coord around the Y axis for current brushes '_' toggles reflection of y coord around the X axis for current brushes '/' removes any '|' and '_' reflection of brushes 'r' reverses the direction of rotation 's' inverts scaling of canvas (divides into 1.0) 'S' sets scaling = - scaling of canvas *****/ int globalRadius = 0 ; // setup() to min(width/height)/2 PImage clippingCircle = null ; // build this in setup() after we have size PImage lastCanvas = null ; // snapshot at the previous end of draw() PImage canvasBrushCropped = null ; // Use a cropped version of lastCanvas as a paintbrush char shapeToDraw = 'a' ; final char topShape = 'd' ; // valid shapes are 'a' through 'd' for now float rotateCanvasAmount = 0.0, rotateCanvasSpeed = 0.0 ; final float rotateCanvasIncr = radians(0.1); float scaleCanvas = 1.0 ; final float scaleCanvasIncr = 0.1 ; // Keep mousePressd() & mouseReleased() x,y for determining brush size int mousePressedX = -1, mousePressedY = -1, mouseReleasedX = -1, mouseReleasedY = -1 ; int hue ; Brush [] brushes = new Brush [ 0 ] ; boolean reflectx = false, reflecty = false ; boolean isFrozen = false ; // Freeze display on 'F' for debug, unfreeze on next 'F' void setup() { size(1500, 1000, P2D); globalRadius = min(width,height)/2; // Build a clipping circle based on screen size. int midx = width / 2 ; int midy = height / 2 ; colorMode(RGB, 256, 256, 256, 256); // Just to build clippingCircle clippingCircle = createImage(width, height, ARGB); clippingCircle.loadPixels(); for (int col = 0 ; col < width ; col++) { for (int row = 0 ; row < height ; row++) { int pix = row * width + col ; if (dist(col, row, midx, midy) > globalRadius) { clippingCircle.pixels[pix] = 0x0ff000000 ; // all black, max alpha } else { clippingCircle.pixels[pix] = 0x000000000 ; // min alpha is transparent } } } clippingCircle.updatePixels(); colorMode(HSB, 360, 100, 100, 100); rectMode(CENTER); ellipseMode(CENTER); shapeMode(CENTER); imageMode(CENTER); background(0); Thread cropthread = new Thread(cropper); cropthread.start(); } void keyPoll() { if (keyPressed) { if (key == CODED) { if (keyCode == UP) { scaleCanvas += scaleCanvasIncr ; } else if (keyCode == DOWN) { scaleCanvas -= scaleCanvasIncr ; } else if (keyCode == LEFT) { rotateCanvasSpeed -= rotateCanvasIncr ; } else if (keyCode == RIGHT) { rotateCanvasSpeed += rotateCanvasIncr ; } } else if (key == '<') { hue = (hue + 360 - 4) % 360 ; // same as -1 in modulo 360 circle } else if (key == '>') { hue = (hue + 4) % 360 ; } } } void keyPressed() { if (key >= 'a' && key <= topShape) { shapeToDraw = key ; } else if (key == 'C') { // clear background(0); canvasBrushCropped = null ; lastCanvas = null ; } else if (key == 'R') { rotateCanvasAmount = rotateCanvasSpeed = 0.0 ; scaleCanvas = 1.0 ; } else if (key == 'r') { rotateCanvasSpeed = - rotateCanvasSpeed ; } else if (key == 's') { scaleCanvas = 1.0 / scaleCanvas ; } else if (key == 'S') { scaleCanvas = - scaleCanvas ; } else if (key == 'F') { isFrozen = ! isFrozen ; } else if (key == 'L') { // lift the brushes brushes = new Brush [0]; } else if (key == '_') { reflecty = ! reflecty ; println("reflecty = " + reflecty); } else if (key == '|') { reflectx = ! reflectx ; println("reflectx = " + reflectx); } else if (key == '/') { reflectx = reflecty = false ; println("no reflection"); } } void mousePressed() { mousePressedX = mouseX - width/2 ; // 0,0 is at center of display mousePressedY = mouseY - height/2 ; } void mouseReleased() { mouseReleasedX = mouseX - width/2 ; mouseReleasedY = mouseY - height/2 ; brushes = (Brush []) append(brushes, new Brush(shapeToDraw, mousePressedX, mousePressedY, mouseReleasedX, mouseReleasedY, hue)); } void draw() { if (isFrozen) { return ; // Freeze display on 'F' for debug, unfreeze on next 'F' } PImage oldbrush = croppedCanvasBrushQueue.poll(); if (oldbrush != null) { canvasBrushCropped = oldbrush ; } push(); translate(width/2, height/2); // 0,0 is at center of display keyPoll(); if (lastCanvas != null) { push(); scale(scaleCanvas); rotate(rotateCanvasAmount); image(lastCanvas, 0, 0, lastCanvas.width, lastCanvas.height); pop(); } if (rotateCanvasSpeed != 0) { rotateCanvasAmount += rotateCanvasSpeed ; } for (Brush brush : brushes) { brush.display(); if (reflectx) { pushMatrix(); scale(-1, 1); brush.display(); popMatrix(); } if (reflecty) { pushMatrix(); scale(1, -1); brush.display(); popMatrix(); } if (reflectx && reflecty) { pushMatrix(); scale(-1, -1); brush.display(); popMatrix(); } } image(clippingCircle, 0, 0, width, height); lastCanvas = photographCanvas(null); uncroppedCanvasBrushQueue.offer(lastCanvas); pop(); // After all snapshot and clipping is done, paint the color pallette in the upper left. Brush pallette = new Brush('b', 0, 0, 100, 100, hue); pallette.display(); } class Brush { char brushtype ; int startx, starty, endx, endy ; int Hue ; Brush (char brushtype, int startx, int starty, int endx, int endy, int Hue) { this.brushtype = brushtype ; this.startx = startx ; this.starty = starty ; this.endx = endx ; this.endy = endy ; this.Hue = Hue ; } void display() { push() ; rectMode(CENTER); ellipseMode(CENTER); shapeMode(CENTER); imageMode(CENTER); colorMode(HSB, 360, 100, 100, 100); translate((startx+endx)/2, (starty+endy)/2); noStroke(); fill(Hue, 99, 99, 99); int w = abs(startx-endx) ; int h = abs(starty-endy) ; switch (brushtype) { case 'a': ellipse(0, 0, w, h); break ; case 'b': rect(0, 0, w, h); break ; case 'c': case 'd': default: if (canvasBrushCropped != null) { if (brushtype == 'd') { tint(hue,99,99); } image(canvasBrushCropped, 0, 0, w, h); } break ; } pop(); } } PImage photographCanvas(PImage canvas) { int [] pix ; // 0xAARRGGBB A=alpha 0-255(oxff), R, G, B // pix arrray laid out like this: // row[0]: col[0], col[1], ..., col[width-1] // row[1]: col[0], col[1], ..., col[width-1] // row[height-1]: col[0], col[1], ..., col[width-1] PImage result ; if (canvas != null) { canvas.loadPixels(); pix = canvas.pixels ; result = createImage(canvas.width, canvas.height, ARGB); } else { loadPixels(); // from display buffer pix = pixels ; result = createImage(width, height, ARGB); } result.loadPixels(); java.lang.System.arraycopy(pix, 0, result.pixels, 0, pix.length); result.updatePixels(); if (canvas != null) { canvas.updatePixels(); } else { updatePixels(); // main display, always match a loadPixels() call with updatePixels(); } return result ; } import java.util.concurrent.SynchronousQueue ; // https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/SynchronousQueue.html final SynchronousQueue uncroppedCanvasBrushQueue = new SynchronousQueue(); final SynchronousQueue croppedCanvasBrushQueue = new SynchronousQueue(); final Cropper cropper = new Cropper(); // Always make variables that communicate between threads final or volatile. // make them final if they never to change. Final & volatile variables get flushed // to memory so other threads can see them. // Also, if you use containers like Queues to pass data between threads, // use thread-safe containers from package java.util.concurrent. class Cropper implements java.lang.Runnable { void run() { while (true) { // This server runs forever to crop PImages doesn to their central circle. PImage incoming, outgoing ; try { incoming = uncroppedCanvasBrushQueue.take(); // blocks til new screenshot arrives int diameter = min(incoming.width, incoming.height); int radius = diameter / 2 ; int xmargin = (incoming.width - diameter) /2 ; int ymargin = (incoming.height - diameter) / 2 ; outgoing = createImage(diameter, diameter, ARGB); incoming.loadPixels(); outgoing.loadPixels(); int incenterx = incoming.width/2 ; int incentery = incoming.height/2 ; java.util.Arrays.fill(outgoing.pixels, 0); // all transparent except central circle for (int inrow = ymargin, outrow = 0 ; inrow < incoming.height - ymargin ; inrow++, outrow++) { for (int incol = xmargin, outcol = 0 ; incol < incoming.width - xmargin ; incol++, outcol++) { if (dist(incol, inrow, incenterx, incentery) < radius) { outgoing.pixels[outrow * outgoing.width + outcol] = incoming.pixels[inrow * incoming.width + incol]; } } } croppedCanvasBrushQueue.put(outgoing); } catch (InterruptedException iex) { println("WARNING, unexpected interrupted exception: " + iex.getMessage()); } } } }