float gamma_r = 2.2; float gamma_g = 2.2; float gamma_b = 2.2; float gamma_bw = 2.2; int GAMMA_STEPS = 42; float GAMMA_MIN = 1.0; float GAMMA_MAX = 3.0; boolean USE_WEB_SAFE = false; float munsell_intensity = 0.5; float last_munsell_i = 0.5; boolean needsRendered = false; int pagenumber = 0; PFont font16; PFont font12; PFont font12b; PFont font8; color[] colorHistory = { color(0), color(0), color(0), color(0), color(0), color(0), color(0), color(0) }; color lastMouseColor = color(0); float lastPickedHue = PI; float lastPickedSat = 0.5; void setup() { font16 = loadFont("HelveticaNeue-16.vlw"); font12 = loadFont("HelveticaNeue-12.vlw"); font12b = loadFont("DroidSansMono-12.vlw"); font8 = loadFont("HelveticaNeue-8.vlw"); size(600, 750); background(0, 0, 0); frameRate(15); } void draw() { if (pagenumber == 0) { background(0, 0, 0); fill(0, 0, 0); rect(0, 0, width, height); pushMatrix(); resetMatrix(); // move to intuitive coordinate system translate(width/2, height*(1.0/4.0)); textFont(font16, 16); fill(255, 255, 255); text("Munsell Color Selector \n\n Click to start calibration with an RGB Gamma test.", -225, 100); popMatrix(); delay(30); return; } if (needsRendered) { if (pagenumber == 1) { renderRGBGammaTest(); needsRendered = false; return; } if (pagenumber == 2) { renderMunsellSquare(munsell_intensity); drawColorHistory(); drawResetButton(); drawWebSafeToggle(); drawIntensitySlider(); needsRendered = false; return; } } } void mouseMoved() { if (pagenumber == 2) { if (mouseX >= 50 && mouseX <= 350 && mouseY >= 50 && mouseY <= 350) { // mouse is over Munsell color square loadPixels(); color m_color = pixels[mouseX + mouseY*width]; lastMouseColor = m_color; /* pushMatrix(); resetMatrix(); int i = mouseX - 50; int j = mouseY - 50; stroke(128, 128, 128); color m_color = munsellToRGB(((float)i)/300.0*2.0*PI, 1.0-(float)j/300.0, munsell_intensity); lastMouseColor = m_color; */ stroke(128, 128, 128); fill(m_color); rect(395, 150, 50, 50); rect(375, 275, 200, 25); drawRGBInfo(m_color, 450, 150); //popMatrix(); } if(mouseY >= 450){ // over bottom munsell squares... loadPixels(); color m_color = pixels[mouseX + mouseY*width]; lastMouseColor = m_color; stroke(128, 128, 128); fill(m_color); rect(395, 150, 50, 50); rect(375, 275, 200, 25); drawRGBInfo(m_color, 450, 150); } if (mouseX >=375 && mouseX < 575 && mouseY >= 300 && mouseY <= 325) { // mouse click on color history int offset = mouseX - 375; int colorIndex = floor(offset/25); pushMatrix(); resetMatrix(); stroke(128, 128, 128); fill(colorHistory[colorIndex]); rect(395, 150, 50, 50); rect(375, 275, 200, 25); drawRGBInfo(colorHistory[colorIndex], 450, 150); popMatrix(); } } } void drawRGBInfo(color c, int c_x, int c_y) { noStroke(); fill(0, 0, 0); rect(c_x, c_y, 100, 50); fill(128, 128, 128); textFont(font12b, 12); // make decimal strings pretty String s_r, s_g, s_b; if (int(red(c)) < 10) { s_r = " " + str(int(red(c))); } else { if (int(red(c)) < 100) { s_r = " " + str(int(red(c))); } else { s_r = str(int(red(c))); } } if (int(green(c)) < 10) { s_g = " " + str(int(green(c))); } else { if (int(green(c)) < 100) { s_g = " " + str(int(green(c))); } else { s_g = str(int(green(c))); } } if (int(blue(c)) < 10) { s_b = " " + str(int(blue(c))); } else { if (int(blue(c)) < 100) { s_b = " " + str(int(blue(c))); } else { s_b = str(int(blue(c))); } } text("r: 0x" + hex(int(red(c)), 2) + " " + s_r + "\ng: 0x" + hex(int(green(c)), 2) + " " + s_g + "\nb: 0x" + hex(int(blue(c)), 2) + " " + s_b, c_x, c_y+15); } void mousePressed() { last_munsell_i = munsell_intensity; if (pagenumber == 0) { // startup text background(0, 0, 0); delay(30); pagenumber = 1; needsRendered = true; return; } if (pagenumber == 1) { // RGB gamma test if (mouseX > (width-100) && mouseY < 30) { // Done pagenumber = 2; needsRendered = true; return; } if (mouseY > 50 && mouseY < 125 && mouseX > 50) { // Red Gamma gamma_r = getGammaFromX(width-100, mouseX-50); needsRendered = true; return; } if (mouseY > 150 && mouseY < 225 && mouseX > 50) { // Red Gamma gamma_g = getGammaFromX(width-100, mouseX-50); needsRendered = true; return; } if (mouseY > 250 && mouseY < 325 && mouseX > 50) { // Red Gamma gamma_b = getGammaFromX(width-100, mouseX-50); needsRendered = true; return; } } if (pagenumber == 2) { if (mouseX >= 50 && mouseX <= 350 && mouseY >= 50 && mouseY <= 350) { // mouse is over Munsell color square pushMatrix(); resetMatrix(); int i = mouseX - 50; int j = mouseY - 50; stroke(128, 128, 128); lastPickedHue = ((float)i)/300.0*2.0*PI; lastPickedSat = 1.0-(float)j/300.0; color m_color = munsellToRGB(lastPickedHue, lastPickedSat, munsell_intensity); fill(m_color); rect(395, 200, 50, 50); rect(375, 325, 200, 25); drawRGBInfo(m_color, 450, 200); popMatrix(); pushColorOnHistory(m_color); drawColorHistory(); renderSecondSquares(); } if(mouseY >= 450){ // over bottom munsell squares... loadPixels(); color m_color = pixels[mouseX + mouseY*width]; lastMouseColor = m_color; if(mouseX < 300){ lastPickedSat = (750.0-mouseY)/300.0; }else{ lastPickedHue = ((float)(mouseX-300))/300.0*2.0*PI; } stroke(128, 128, 128); fill(m_color); rect(395, 150, 50, 50); rect(375, 275, 200, 25); drawRGBInfo(m_color, 450, 150); pushColorOnHistory(m_color); drawColorHistory(); renderSecondSquares(); } if (mouseX >=375 && mouseX < 575 && mouseY >= 275 && mouseY <= 350) { // mouse click on color history color clickedColor = color(0); if (mouseY <= 300) { clickedColor = lastMouseColor; } if (mouseY > 300 && mouseY < 325) { // read color from history int offset = mouseX - 375; int colorIndex = floor(offset/25); clickedColor = colorHistory[colorIndex]; } if (mouseY > 325) { clickedColor = colorHistory[0]; } pushMatrix(); resetMatrix(); stroke(128, 128, 128); fill(clickedColor); rect(395, 200, 50, 50); rect(375, 325, 200, 25); drawRGBInfo(clickedColor, 450, 200); popMatrix(); pushColorOnHistory(clickedColor); drawColorHistory(); } if (mouseX >= 525 && mouseX <= 575 && mouseY >= 365 && mouseY <= 385) { // "reset" button was pressed resetColorHistory(); } if (mouseX >= 375 && mouseX <= 515 && mouseY >= 365 && mouseY <= 385) { // "toggle websafe" button was pressed if (USE_WEB_SAFE) { USE_WEB_SAFE = false; } else { USE_WEB_SAFE = true; } textFont(font12, 12); fill(255, 255, 255); text("Updating...", 400, 90); repaint(); needsRendered = true; } if (mouseX >= 375 && mouseX <= 575 && mouseY >= 93 && mouseY <= 110) { // dragging the intensity slider... munsell_intensity = (float)(mouseX - 375)/200.0; drawIntensitySlider(); } } } void mouseDragged() { if (pagenumber == 2) { if (mouseX >= 375 && mouseX <= 575 && mouseY >= 93 && mouseY <= 110) { // dragging the intensity slider... munsell_intensity = (float)(mouseX - 375)/200.0; drawIntensitySlider(); } } } void mouseReleased() { if (last_munsell_i != munsell_intensity) { textFont(font12, 12); fill(255, 255, 255); text("Updating...", 400, 90); repaint(); needsRendered = true; } } void resetColorHistory() { int i = 0; for (i=0; i<8; i++) { colorHistory[i] = color(0); } pushMatrix(); noStroke(); fill(0, 0, 0); rect(373, 148, 250, 300); // clobber all color info drawColorHistory(); drawResetButton(); drawWebSafeToggle(); popMatrix(); } void drawResetButton() { int t_x = 525; int t_y = 365; pushMatrix(); resetMatrix(); stroke(128, 128, 128); strokeWeight(2.0); fill(0, 0, 0); rect(t_x, t_y, 50, 20); textFont(font12, 12); fill(128, 128, 128); text("Reset", t_x+8, t_y+14); strokeWeight(1.0); popMatrix(); } void drawWebSafeToggle() { int t_x = 375; int t_y = 365; pushMatrix(); resetMatrix(); stroke(128, 128, 128); strokeWeight(2.0); fill(0, 0, 0); rect(t_x, t_y, 140, 20); textFont(font12, 12); fill(128, 128, 128); if (USE_WEB_SAFE) { text("Show all Colors", t_x+25, t_y+14); } else { text("Only Show WebSafe", t_x+8, t_y+14); } strokeWeight(1.0); popMatrix(); } void drawIntensitySlider() { int slider_width = 200; int slider_pos = int(munsell_intensity*slider_width); noStroke(); fill(0, 0, 0); rect(370, 90, 210, 60); // clobber old slider strokeWeight(1.0); stroke(128, 128, 128); fill(0, 0, 0); rect(375, 100, slider_width, 3); noStroke(); fill(64, 64, 64, 128); triangle(375, 100+3, 375+slider_width+1, 100-7, 375+slider_width+1, 100+3); stroke(128, 128, 128); fill(128, 128, 128); rect(375+slider_pos-2, 100-7, 4, 17); textFont(font12, 12); text("Intensity: " + str(munsell_intensity), 425, 130); } void pushColorOnHistory(color newColor) { int i=0; for (i=7; i>0; i--) { colorHistory[i] = colorHistory[i-1]; } colorHistory[0] = newColor; } void renderDarkGammaTest() { background(0); noStroke(); int i = 0; for (i=1; i<34; i++) { fill(i, i, i); rect(35+i*15, 50, 15, height-100); fill(30, 30, 30); rect(40+i*15, height-45, 5, 5); } } void renderLightGammaTest() { background(255); noStroke(); int i = 0; for (i=1; i<34; i++) { fill(255-i, 255-i, 255-i); rect(35+i*15, 50, 15, height-100); fill(30, 30, 30); rect(40+i*15, height-45, 5, 5); } } void renderRGBGammaTest() { background(0); noStroke(); int i = 0; int j = 0; // checkerboard b/w --> grey checkBox(color(255, 0, 0), color(0, 0, 0), 50, 50, width-100, 25); gammaBox(0.50, 50, 75, width-100, 25, 1.0, 0.0, 0.0); checkBox(color(255, 0, 0), color(0, 0, 0), 50, 100, width-100, 25); checkBox(color(0, 255, 0), color(0, 0, 0), 50, 150, width-100, 25); gammaBox(0.50, 50, 175, width-100, 25, 0.0, 1.0, 0.0); checkBox(color(0, 255, 0), color(0, 0, 0), 50, 200, width-100, 25); checkBox(color(0, 0, 255), color(0, 0, 0), 50, 250, width-100, 25); gammaBox(0.50, 50, 275, width-100, 25, 0.0, 0.0, 1.0); checkBox(color(0, 0, 255), color(0, 0, 0), 50, 300, width-100, 25); checkBox(color(255, 255, 255), color(0, 0, 0), 10, 50, 10, 275); noStroke(); fill(luminanceToValue(0.5, gamma_r)*255, luminanceToValue(0.5, gamma_g)*255, luminanceToValue(0.5, gamma_b)*255); rect(20, 50, 10, 276); checkBox(color(255, 255, 255), color(0, 0, 0), 30, 50, 10, 275); noStroke(); fill(96, 96, 96); text("RGB Gamma Test\n Click on each color where the center blends with checked pattern.\n The bar on the left should converge to solid grey.", 25, 400); // red gamma position pointer rect(50+getXfromGamma(gamma_r, width-100)-6, 40, 12, 5); rect(50+getXfromGamma(gamma_r, width-100)-3, 45, 6, 5); // green gamma position pointer rect(50+getXfromGamma(gamma_g, width-100)-6, 140, 12, 5); rect(50+getXfromGamma(gamma_g, width-100)-3, 145, 6, 5); // blue gamma position pointer rect(50+getXfromGamma(gamma_b, width-100)-6, 240, 12, 5); rect(50+getXfromGamma(gamma_b, width-100)-3, 245, 6, 5); // gamma decimal labels to the right of each color rotate(-PI/2.0); text(gamma_r, -115, width-20); text(gamma_g, -215, width-20); text(gamma_b, -315, width-20); rotate(PI/2.0); // "Done" button stroke(96, 96, 96); strokeWeight(2.0); fill(0, 0, 0); rect(width-100, 1, 98, 28); fill(128, 128, 128); text("Done", width-70, 20); } void renderMunsellSquare(float intensity) { background(0); int i = 0; int j = 0; strokeWeight(1.0); for (i=0; i<300; i++) { for (j=0; j<300; j++) { stroke(munsellToRGB(((float)i)/300.0*2.0*PI, 1.0-(float)j/300.0, intensity)); point(i+50, j+50); } } renderSecondSquares(); noStroke(); fill(128, 128, 128); textFont(font16, 16); if (USE_WEB_SAFE) { text("Munsell \"WebSafe\" Color Space", 80, 30); } else { text("Munsell Color Space", 115, 30); } text("All colors shown\nhave equal intensity.", 375, 50); text("Same Intensity", 150, 370); //pushMatrix(); //rotate(-PI/2.0); text("Same Hue", 100, 440); text("Same Saturation", 400, 440); //rotate(PI/2.0); //popMatrix(); //stroke(128, 128, 128); //drawArrow(25, 200, 125, PI, 8); //drawArrow(150, 375, 150, -PI/2.0, 8); } void renderSecondSquares() { for (int i=0; i<300; i++) { for (int j=0; j<300; j++) { stroke(munsellToRGB(lastPickedHue, 1.0-(float)j/300.0, i/299.0)); point(i+0, j+450); } } for (int i=0; i<300; i++) { for (int j=0; j<300; j++) { stroke(munsellToRGB(((float)i)/300.0*2.0*PI, lastPickedSat, j/299.0)); point(i+300, j+450); } } } void drawColorHistory() { int i = 0; strokeWeight(1.0); stroke(128, 128, 128); for (i=0; i<8; i++) { fill(colorHistory[i]); rect(375+i*25, 300, 25, 25); } fill(colorHistory[0]); rect(375, 325, 200, 25); } void drawArrow(int x1, int y1, int arrowLength, float rotation, int arrowSize) { strokeWeight(1.0); pushMatrix(); translate(x1, y1); rotate(rotation); line(0, 0, 0, arrowLength); translate(0, arrowLength); pushMatrix(); rotate(5.0*PI/6.0); line(0, 0, 0, arrowSize); rotate(2.0*PI/6.0); line(0, 0, 0, arrowSize); popMatrix(); popMatrix(); } /** * Convert a Munsell color to RGB (at the gamma_r, gamma_g, gamma_b stored globally) * m_hue Munsell hue value: [0,2PI] * m_saturation Munsell saturation value: [0,1] * m_intensity Munsell intensity value: [0,1] */ color munsellToRGB(float m_hue, float m_saturation, float m_intensity) { float rgb_r = 0.0; float rgb_g = 0.0; float rgb_b = 0.0; // algorithm from: http://www.profc.udec.cl/~gabriel/tutoriales/rsnote/cp10/cp10-6.htm // but fixed error on h=4 case m_hue = m_hue % (2*PI); float Hprime = (3*m_hue) / PI; int h_floor = floor(Hprime); float P = m_intensity*(1-m_saturation); float Q = m_intensity*(1-m_saturation*(Hprime - h_floor)); float T = m_intensity*(1-m_saturation*(1 - Hprime + h_floor)); switch(h_floor) { case(0): { rgb_r = m_intensity; rgb_g = T; rgb_b = P; break; } case(1): { rgb_r = Q; rgb_g = m_intensity; rgb_b = P; break; } case(2): { rgb_r = P; rgb_g = m_intensity; rgb_b = T; break; } case(3): { rgb_r = P; rgb_g = Q; rgb_b = m_intensity; break; } case(4): { rgb_r = T; rgb_g = P; rgb_b = m_intensity; break; } case(5): { rgb_r = m_intensity; rgb_g = P; rgb_b = Q; break; } default: { break; } } color munRGB = color(luminanceToValue(rgb_r, gamma_r)*255, luminanceToValue(rgb_g, gamma_g)*255, luminanceToValue(rgb_b, gamma_b)*255); if (USE_WEB_SAFE) { return RGBtoWebSafe(munRGB); } return munRGB; //return color(rgb_r*255, rgb_g*255,rgb_b*255); // simple method without gamma (bad idea) } color RGBtoWebSafe(color rgbColor) { float ws_r = ((int)(red(rgbColor) + 0x19) / 0x33) * 0x33; float ws_g = ((int)(green(rgbColor) + 0x19) / 0x33) * 0x33; float ws_b = ((int)(blue(rgbColor) + 0x19) / 0x33) * 0x33; return color(ws_r, ws_g, ws_b); } float luminanceToValue(float luminance, float gamma) { return pow(luminance, 1/gamma); } void checkBox(color a, color b, int b_x, int b_y, int b_width, int b_height) { int i, j; strokeWeight(1.0); for (i=b_y; i<(b_y+b_height); i+=2) { for (j=b_x; j<(b_x+b_width); j+=2) { stroke(a); line(j, i, j, i); stroke(b); line(1+j, i, 1+j, i); } for (j=b_x; j<(b_x+b_width); j+=2) { stroke(b); line(j, 1+i, j, 1+i); stroke(a); line(1+j, 1+i, 1+j, 1+i); } } } void gammaBox(float luminance, int b_x, int b_y, int b_width, int b_height, float mul_r, float mul_g, float mul_b) { float gamma; float gamma_width = (GAMMA_MAX-GAMMA_MIN)/(float)(GAMMA_STEPS-1); float step_width = (float)b_width/(float)GAMMA_STEPS; noStroke(); int i = 0; for (gamma=GAMMA_MIN; gamma<=GAMMA_MAX; gamma+=gamma_width) { float value = luminanceToValue(0.5, gamma)*255; fill(mul_r*value, mul_g*value, mul_b*value); rect(b_x+i*step_width, b_y, step_width, b_height); i++; } } float getXfromGamma(float gamma, int b_width) { float gamma_width = (GAMMA_MAX-GAMMA_MIN)/(float)(GAMMA_STEPS-1); float step_width = (float)b_width/(float)GAMMA_STEPS; return ((gamma-GAMMA_MIN)/gamma_width)*step_width + step_width/2.0; } float getGammaFromX(int b_width, int x) { float gamma_width = (GAMMA_MAX-GAMMA_MIN)/(float)(GAMMA_STEPS-1); float step_width = (float)b_width/(float)GAMMA_STEPS; return (((float)x) - step_width/2.0)/step_width*gamma_width + GAMMA_MIN; }