/** CSC480PixelPhotosAssn2FA21, D. Parson, Fall 2021, CSC480, derived from CSC480DemoScreenshot1. DUE October 10, 2021, 11:59 PM via D2L. ENTER STUDENT NAME HERE: This assignment uses multithreading to assist with taking "photos" of the Processing display and of PImages to use as paintbrushes. It allows uses images from prior frames, accumulated over draw() calls, as a composite canvas amednable to rotation and scaling. See keyword and mouse commands: INTERACTION: Use mousePress and mouseRelease to sweep the extents of the following, see isBrushKey(char key): 'e' ellipse 'r' rectangle 'c' recursive canvas paintbrush 'T' subtractive Tinted recursive canvas paintbrush 't' triangle (STUDENT must add this: startx,starty, endx,endy, (startx+endx)/2, Y_BASED_ON_THESE_4_VARS 'l' line (STUDENT add from startx,starty to endx,endy, thick strokeWeight) 'v' vector (PShape), STUDENT add, shape takes same args as ellipse() 'i' image (PImage), STUDENT add, shape takes same args as ellipse() GLOBAL COMMANDS 'f' toggles freezing the display for debugging & snapshots 'C' clears the painted canvas; brushes and canvas transforms unchanged 'L' lifts and removes paintbrushes; painted canvas transforms unchanged 'Z' zeroes rotations and scales of the canvas; brushes & paint untouched 'O' makes clipping mask a centered circle 'Q' makes clipping mask a centered square 'N' eliminates clipping mask until next 'O' or 'Q' '|' toggles reflection of current paintbrush around Y axis '_' toggles reflection of current paintbrush around X axis '/' shuts off both '|' and '_' paintbrush reflections 'o' Make the recursive paintbrush for 'c' and 'T' a circle (default) 'q' Make the recursive paintbrush for 'c' and 'T' a square 'w' Make palette white -> SAT 0, BRIGHTNESS 99 'b' Make palette black -> SAT 99, BRIGHTNESS 0 'h' Make palette use Hue, SAT 99, BRIGHTNESS 99 GLOBAL COMMANDS THAT USE THE cmdbuffer 'RNN.nn' degrees for rotating canvas an exact degree amount, may be - 'SNN.nn' for scaling canvas, may be -, STUDENT MUST COMPLETE 'AN' for setting erasure of canvas, 0 means none, 99 means erase every time (Alpha) 'Mxx.yy' Set all current Brushes in motion, where xx is x speed, yy is y speed, and either may be 0 or negative. Just running 'M\n' sets to random x & y speeds. STUDENT MUST COMPLETE. 'E.NN.nn.NN.nn.NN...' for etching the recursive paintbrush in bands, where each NN or nn must be in range [0, 100] in ascending order, and the NNs mean opaque band, and nn means transparent, and a value [0, 100] is interpreted as a percentage % out from the center. The NN sets the start % of an opaque band, and the nn sets the start % of a transparent band. To make the center band transparent, start out 'E.0.0.NN' so that innermost opaque band has 0 width, and first transparent starts at 0% from center. Dr. Parson will write the command parser. Just running 'E\n' sets back to default state. STUDENT can earn 10% bonus points by fully implementing this etcher. There is also a REQUIRED STUDENT block of code for square etching. CONTINUOUSLY POLLED commands, see keyPoll() below '>' cycles the HSB color palette forward, swatch in upper left corner '<' cycles the HSB color palette backward, swatch in upper left corner RIGHT ARROW adds small clockwise increment to canvas rotation speed LEFT ARROW adds small counterclockwise increment to canvas rotation speed UP adds small increment to canvas scaling DOWN adds small decrement to canvas scaling **/ import java.util.concurrent.CopyOnWriteArrayList ; import java.util.LinkedList ; int globalRadius = 0 ; // setup() to min(width/height)/2 PImage clippingCircle = null ; // build this in setup() after we have size PImage clippingSquare = null ; // build this in setup() after we have size enum ClippingType { CLIPCIRCLE, CLIPSQUARE, CLIPNONE ; }; ClippingType cliptyle = ClippingType.CLIPCIRCLE ; PImage lastCanvas = null ; // snapshot at the previous end of draw() PImage canvasBrushCropped = null ; // Use a cropped version of lastCanvas as a paintbrush char shapeToDraw = 'e' ; // ellipse default, see isBrushKey() float rotateCanvasAmount = 0.0, rotateCanvasSpeed = 0.0 ; final float rotateCanvasIncr = 0.1; // degrees float scaleCanvas = 1.0 ; final float scaleCanvasIncr = 0.1 ; // increment for the UP and DOWN keys // Keep mousePressd() & mouseReleased() x,y for determining brush size int mousePressedX = -1, mousePressedY = -1, mouseReleasedX = -1, mouseReleasedY = -1 ; int Hue = 60 ; // current swatch in upper left corner, '<' or '>' to change it int Saturation = 99 ; // HSB default, changed by 'w' 'b' 'h' int Brightness = 99 ; // HSB default, changed by 'w' 'b' 'h' Brush [] brushes = new Brush [ 0 ] ; // all brushes currently bolted onto canvas boolean reflectx = false, reflecty = false ; // make reflected brush when true boolean isKeyDown = false ; // eliminate sticky keys in keyPoll() PImage STUDENTIMAGE = null ; PShape STUDENTSHAPE = null ; boolean isFreeze = false ; int globalAlpha = 0 ; // up to 99, where 99 is background 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(); clippingSquare = createImage(width, height, ARGB); clippingSquare.loadPixels(); int squareDiameter = min(width, height); int xmargin = (width-squareDiameter)/2 ; int ymargin = (height-squareDiameter)/2; 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 } if (col < xmargin || col >= (width-xmargin) || row < ymargin || row >= (height-ymargin)) { clippingSquare.pixels[pix] = 0x0ff000000 ; // all black, max alpha } else { clippingSquare.pixels[pix] = 0x000000000 ; // min alpha is transparent } } } clippingCircle.updatePixels(); clippingSquare.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 draw() { if (isFreeze) { return ; } PImage oldbrush = croppedCanvasBrushQueue.poll(); // Poll to retrieve last recurrent canvas paintbrush i.f.f. there is one. if (oldbrush != null) { canvasBrushCropped = oldbrush ; } push(); translate(width/2, height/2); // 0,0 is at center of display keyPoll(); // These are for keys to be held down. if (lastCanvas != null) { push();// This is the manipulation/painting of prior painting on canvas. scale(scaleCanvas); rotate(radians(rotateCanvasAmount)); image(lastCanvas, 0, 0, lastCanvas.width, lastCanvas.height); pop(); } if (rotateCanvasSpeed != 0) { rotateCanvasAmount += rotateCanvasSpeed ; } // Do this screening after painting prior canvas. if (globalAlpha == 99) { background(0, 0, 0); // erase old canvas, set with 'A' } else if (globalAlpha > 0) { fill(0,0,0,globalAlpha); // translucent screen of old canvas stroke(0,0,0,globalAlpha); rectMode(CENTER); rect(0, 0, width, height); } for (Brush brush : brushes) { // Display all brushes, and their reflections when that is set. brush.move() ; 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(); } } if (cliptyle == ClippingType.CLIPCIRCLE) { image(clippingCircle, 0, 0, width, height); } else if (cliptyle == ClippingType.CLIPSQUARE) { image(clippingSquare, 0, 0, width, height); } // Take snapshot of current display buffer. lastCanvas = photographCanvas(null); // Offer a copy to the cropper, only when it is ready to accept. // Blocking with a put() here or a take() at the top of draw() can // and often does block, or at least slow down the Processing thread. uncroppedCanvasBrushQueue.offer(lastCanvas); pop(); // After all snapshot & clipping is done, paint color palette in upper left. Brush palette = new Brush('r', 0, 0, 100, 100, Hue, Saturation, Brightness, null, null); palette.display(); } /* keyPoll is for keys that update variables continuously. */ void keyPoll() { if (keyPressed && isKeyDown) { // isKeyDown reset in keyReleased to eliminate sticking on Windows 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 ; } } } String cmdbuffer = null ; void keyPressed() { // keyPressed() is called only one when key is first pressed. isKeyDown = true ; // isKeyDown reset in keyReleased to eliminate stiCLcking on Windows if (key == 'R' || key == 'S' || key == 'A' || key == 'M' || key == 'E') { cmdbuffer = "" + key ; println("START CMD: " + cmdbuffer); } else if (cmdbuffer != null && ((key >= '0' && key <= '9') || key == '-' || key == '.')) { // numeric key, optional decimal point or sign, attach to cmdbuffer cmdbuffer = cmdbuffer + key ; println("CMD SO FAR: " + cmdbuffer); } else if (cmdbuffer != null && key == '\n') { println("INTERPRETING CMD: " + cmdbuffer); try { if (cmdbuffer.charAt(0) != 'E') { float value = 0 ; if (cmdbuffer.charAt(0) == 'R') { value = Float.parseFloat(cmdbuffer.substring(1)); rotateCanvasAmount = value ; rotateCanvasSpeed = 0.0 ; println("DEBUG ROTATE = " + rotateCanvasAmount); } else if (cmdbuffer.charAt(0) == 'S') { // STUDENT MUST IMPLEMENT SCALE BASED ON value value = Float.parseFloat(cmdbuffer.substring(1)); } else if (cmdbuffer.charAt(0) == 'A') { value = Float.parseFloat(cmdbuffer.substring(1)); globalAlpha = constrain(round(value), 0, 99); // println("DEBUG globalAlpha = " + globalAlpha); } else if (cmdbuffer.charAt(0) == 'M') { // There may be a '-' sign on either or both sides of the '.'. int xspeed = 0, yspeed = 0 ; if (cmdbuffer.length() < 2) { xspeed = round(random(-10, 10)); yspeed = round(random(-10,10)); } else { String [] field2 = cmdbuffer.substring(1).split("\\.") ; println("DEBUG LEN OF cmdbuffer " + cmdbuffer + " = " + field2.length); if (field2.length == 1) { xspeed = yspeed = Integer.parseInt(field2[0]); } else if (field2.length != 2) { throw new NumberFormatException("ERROR IN NUMERIC FIELD: " + cmdbuffer); } else { xspeed = Integer.parseInt(field2[0]); yspeed = Integer.parseInt(field2[1]); } } println("DEBUG 'M', xspeed = " + xspeed + ", yspeed = " + yspeed); // STUDENT: Call setSpeed() on every Brush object in brushes. } } else { LinkedList Eseq = new LinkedList(); String [] substrings = null ; int [] subfields = null ; if (cmdbuffer.length() < 3) { Esequence = null ; // E. without any NN or nn return ; } if (cmdbuffer.charAt(1) == '.') { substrings = cmdbuffer.substring(2).split("\\.") ; } else { substrings = cmdbuffer.substring(1).split("\\.") ; } if (substrings.length < 2) { Esequence = null ; return ; } subfields = new int [ substrings.length ] ; int lastval = 0 ; for (int i = 0 ; i < substrings.length ; i++) { int value = Integer.parseInt(substrings[i]); if (value == 0 && (i == 0 || (i == 1 && subfields[0] == 0))) { subfields[i] = value ; lastval = value ; } else if (value < 0 || value <= lastval || value > 100) { throw new NumberFormatException("Invalid E sequence: " + cmdbuffer); } else { subfields[i] = value ; lastval = value ; } } for (int i = 0 ; i < subfields.length ; i++) { Eseq.add(subfields[i]/100.0); println("DEBUG E[" + i + "] = " + subfields[i]); } Esequence = new CopyOnWriteArrayList(Eseq); } } catch (NumberFormatException fx) { println("Invalid number in command, ignored: " + cmdbuffer); cmdbuffer = null ; } finally { cmdbuffer = null ; } } else { cmdbuffer = null ; if (isBrushKey(key)) { shapeToDraw = key ; } else if (key == 'C') { // clear background(0); canvasBrushCropped = null ; lastCanvas = null ; } else if (key == 'Z') { rotateCanvasAmount = rotateCanvasSpeed = 0.0 ; scaleCanvas = 1.0 ; } 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"); } else if (key == 'O') { cliptyle = ClippingType.CLIPCIRCLE ; println("Setting clipping circle on 'O'"); } else if (key == 'Q') { cliptyle = ClippingType.CLIPSQUARE ; println("Setting clipping square on 'Q'"); } else if (key == 'N') { cliptyle = ClippingType.CLIPNONE ; println("Setting no clipping on 'N'"); } else if (key == 'o') { isCroppedCanvasCircle = true ; } else if (key == 'q') { isCroppedCanvasCircle = false ; } else if (key == 'w') { Saturation = 0 ; Brightness = 99 ; } else if (key == 'b') { Saturation = 0 ; Brightness = 0 ; } else if (key == 'h') { Saturation = 99 ; Brightness = 99 ; } else if (key == 'f') { isFreeze = !isFreeze ; } } } void keyReleased() { // keyReleased() is called only one when key is first released. isKeyDown = false ; // isKeyDown reset in keyReleased to eliminate sticking on Windows } void mousePressed() { // record start of a mouse sweep for adding a Brush object mousePressedX = mouseX - width/2 ; // 0,0 is at center of display mousePressedY = mouseY - height/2 ; } void mouseReleased() { // record end of a mouse sweep for adding a Brush object, then add the Brush mouseReleasedX = mouseX - width/2 ; mouseReleasedY = mouseY - height/2 ; try { brushes = (Brush []) append(brushes, new Brush(shapeToDraw, mousePressedX, mousePressedY, mouseReleasedX, mouseReleasedY, Hue, Saturation, Brightness, STUDENTIMAGE, STUDENTSHAPE)); // No STUDENT requirement here. } catch (Exception bx) { println("ERROR CREATING BRUSH " + shapeToDraw + ": " + bx.getMessage()); } } /** * STUDENT must update Brush.display per the assignment handout. */ class Brush { char brushtype ; int startx, starty, endx, endy ; int speedx = 0 ; int speedy = 0 ; // Brushes are stationary when first constructed. int Myhue, Mysat, Mybright ; PImage i ; // to be used by STUDENT for adding an image loaded in setup PShape v ; // used by STUDENT for adding a PShape vector init'd in setup() Brush (char brushtype, int startx, int starty, int endx, int endy, int Hue, int Saturation, int Brightness, PImage img, PShape vector) { this.brushtype = brushtype ; this.startx = startx ; this.starty = starty ; this.endx = endx ; this.endy = endy ; this.Myhue = Hue ; this.Mysat = Saturation ; this.Mybright = Brightness ; this.i = img ; // to be used by STUDENT this.v = vector ; // to be used by STUDENT if (! isBrushKey(brushtype)) { throw new RuntimeException("ERROR, invalid brush type: " + brushtype); } } 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(Myhue, Mysat, Mybright, 99); int w = abs(startx-endx) ; int h = abs(starty-endy) ; switch (brushtype) { case 'e': ellipse(0, 0, w, h); break ; case 'r': if (Brightness == 0) { stroke(255); // Make black swatch visible strokeWeight(1); } rect(0, 0, w, h); break ; case 'c': case 'T': if (canvasBrushCropped != null) { if (brushtype == 'T') { // Vary through global Hue,Saturation,Brightness. tint(Hue,Saturation,Brightness); if (Brightness == 0) { stroke(255); strokeWeight(1); } } image(canvasBrushCropped, 0, 0, w, h); } break ; } pop(); } /* Speeds are in pixels per draw(), 0 to stop movement. */ void setSpeed(int Speedx, int Speedy) { speedx = Speedx ; speedy = Speedy ; } void move() { startx += speedx ; endx += speedx ; starty += speedy ; endy += speedy ; // Translated screen extents are -width/2,-height/2 to // width/2, height/2, with 0,0 in center screen. if (speedx > 0) { // Going off right side, bring back on the left. if (startx > width/2 || endx > width/2) { startx = startx - width + 1 ; endx = endx - width + 1 ; } } else if (speedx < 0) { // Going off left side, bring back on the right. if (startx < -width/2 || endx < -width/2) { startx = startx + width - 1 ; endx = endx + width - 1 ; } } if (speedy > 0) { // Going off bottom side, bring back on the top. if (starty > height/2 || endy > height/2) { starty = starty - height + 1 ; endy = endy - height + 1 ; } } else if (speedy < 0) { // Going off top side, bring back on the bottom. if (starty < -height/2 || endy < -height/2) { starty = starty + height - 1 ; endy = endy + height - 1 ; } } } } boolean isBrushKey(char key) { // ellipse, rectangle, canvas, Tinted canvas // STUDENT must add some brush types here and in class brush.display() return (key == 'e' || key == 'r' || key == 'c' || key == 'T'); } 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) { result = canvas.copy(); return result ; } 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(); 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(); volatile boolean isCroppedCanvasCircle = true ; // Must be volatile to communicate with cropper thread, false means a square. volatile CopyOnWriteArrayList Esequence = null ; // Esequence is list of E.NN.nn numbers or null when no E command, // where each NN or nn is a fraction [0.0, 1.0]/ // 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 CopyOnWriteArrayList eseq = Esequence ; // Copy volatile atomic, then clone() for stability if (eseq != null) { eseq = new CopyOnWriteArrayList(eseq); } // STUDENT 10 bonus points to implement E.NN.nn.etc as documented. 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 if (isCroppedCanvasCircle) { 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]; } } } } else { // STUDENT IS REQUIRED TO IMPLEMENT THIS PORTION OF THIS METHOD. // Copy pixels in the square centered in the incoming into outgoing. // Leave the outside margins transparent - they already are. // Use nested for loops *similar* (not identical) to above block of code. } croppedCanvasBrushQueue.put(outgoing); } catch (InterruptedException iex) { println("WARNING, unexpected interrupted exception: " + iex.getMessage()); } } } }