× Project Code

// Dynamic SCAA 2016 Flavor Wheel // Jared S Tarbell // October 14, 2016 // // Implemented a recursive polar hierarchy visualization to display the various // coffee flavor notes and faults with the graphic style, lexicon, and colors // of the SCAA 2016 Coffee Taster's Flavor Wheel www.scaa.org // // TODO fix font loading FlavorWheel flavorWheel; // PFont font; float arcResolution = 10.0; int restoreTimer = 0; void setup() { size(2000,2000); fullScreen(); background(#e8e4dc); background(0); smooth(); // font = createFont("URWTopic.otf"); // this is not working! // textFont(font,22); flavorWheel = new FlavorWheel(); Sector neoFloral = flavorWheel.addSector("floral", 0xFFed028c); Sector neoBlacktea = neoFloral.addSector("black tea", 0xFFad657c); Sector neoFFloral = neoFloral.addSector("floral", 0xFFef4b8e); neoFFloral.addSector("chamomile", 0xFFf7a834); neoFFloral.addSector("rose", 0xFFe2749f); neoFFloral.addSector("jasmine", 0xFFfffde5); Sector neoFruity = flavorWheel.addSector("fruity", 0xFFee1d23); Sector neoBerry = neoFruity.addSector("berry", 0xFFed2c41); neoBerry.addSector("blackberry", 0xFF090819); neoBerry.addSector("raspberry", 0xFFe32886); neoBerry.addSector("blueberry", 0xFF9799c0); neoBerry.addSector("strawberry", 0xFFee283b); Sector neoDried = neoFruity.addSector("dried fruit", 0xFFd6444e); neoDried.addSector("raisin", 0xFF9e1d78); neoDried.addSector("prune", 0xFF84548e); Sector neoOther = neoFruity.addSector("other fruit", 0xFFf26648); neoOther.addSector("coconut", 0xFFe38f29); neoOther.addSector("cherry", 0xFFe71256); neoOther.addSector("pomegranate", 0xFFee3c5c); neoOther.addSector("pineapple", 0xFFf89c1c); neoOther.addSector("grape", 0xFF9ec536); neoOther.addSector("apple", 0xFF69c071); neoOther.addSector("peach", 0xFFf27f51); neoOther.addSector("pear", 0xFFb2a91d); Sector neoCitrus = neoFruity.addSector("citrus fruit", 0xFFfcb814); neoCitrus.addSector("grapefruit", 0xFFf05961); neoCitrus.addSector("orange", 0xFFf47921); neoCitrus.addSector("lemon", 0xFFf4d902); neoCitrus.addSector("lime", 0xFF8fc254); Sector neoSweet = flavorWheel.addSector("sweet", 0xFFf36421); Sector neoBrownSugar = neoSweet.addSector("brown sugar", 0xFFcc7b89); neoBrownSugar.addSector("molasses",0xFF020f09); neoBrownSugar.addSector("maple syrup",0xFFce5e31); neoBrownSugar.addSector("carmelized",0xFFdd9c34); neoBrownSugar.addSector("honey",0xFFee7933); neoSweet.addSector("vanilla", 0xFFf39477); neoSweet.addSector("vanillin", 0xFFef7d80); neoSweet.addSector("overall sweet", 0xFFda7174); neoSweet.addSector("sweet aromatics", 0xFFc74465); neoSweet.addSector("pez", 0xFFb668e7); Sector neoNC = flavorWheel.addSector("nutty/cocoa", 0xFF997b78); Sector neoCocoa = neoNC.addSector("cocoa",0xFFac6e30); neoCocoa.addSector("dark chocolate",0xFF4a2c21); neoCocoa.addSector("chocolate",0xFF66332c); Sector neoNutty = neoNC.addSector("nutty",0xFFb58c7d); neoNutty.addSector("almond",0xFFd8a493); neoNutty.addSector("hazelnut",0xFF8e5e2f); neoNutty.addSector("peanuts",0xFFdeaf29); Sector neoSpices = flavorWheel.addSector("spices", 0xFFb80e40); Sector neoBrownSpice = neoSpices.addSector("brown spice",0xFFb4464b); neoBrownSpice.addSector("clove",0xFFb27566); neoBrownSpice.addSector("cinnamon",0xFFe08d39); neoBrownSpice.addSector("nutmeg",0xFF9a2e28); neoBrownSpice.addSector("anise",0xFFc3992f); Sector neoPepper = neoSpices.addSector("pepper",0xFFd53136); Sector neoPungent = neoSpices.addSector("pungent",0xFF724a5a); Sector neoRoasted = flavorWheel.addSector("roasted", 0xFFd33627); Sector neoCereal = neoRoasted.addSector("cereal",0xFFedc13c); neoCereal.addSector("malt",0xFFe89663); neoCereal.addSector("grain",0xFFcfa080); Sector neoBurnt = neoRoasted.addSector("burnt",0xFFb17c4e); neoBurnt.addSector("brown, roast",0xFF7f572e); neoBurnt.addSector("smoky",0xFFa47c3c); neoBurnt.addSector("ashy",0xFF98a187); neoBurnt.addSector("acrid",0xFFae9d67); Sector neoTobacco = neoRoasted.addSector("tabacco",0xFFcdb07b); Sector neoPipe = neoRoasted.addSector("pipe tabacco",0xFFa3905f); Sector neoVegetal = flavorWheel.addSector("vegetal", 0xFF167f3b); Sector neoBeany = neoVegetal.addSector("beany",0xFF799887); Sector neoGreen = neoVegetal.addSector("green",0xFF31a447); neoGreen.addSector("herb-like",0xFF7eb950); neoGreen.addSector("hay-like",0xFF9d9c33); neoGreen.addSector("vegetative",0xFF2ea55e); neoGreen.addSector("dark green",0xFF15583a); neoGreen.addSector("fresh",0xFF009d62); neoGreen.addSector("peapod",0xFF55a941); neoGreen.addSector("under-ripe",0xFFa8c346); Sector neoFault = flavorWheel.addSector("fault", 0xFF00a6d1); Sector neoChem = neoFault.addSector("chemical", 0xFF7ac0cd); neoChem.addSector("rubber",0xFF1e232d); neoChem.addSector("skunky",0xFF6b7f8a); neoChem.addSector("petroleum",0xFF009ba9); neoChem.addSector("medicinal",0xFF73a3b6); neoChem.addSector("salty",0xFFf9fbfc); neoChem.addSector("bitter",0xFF7fc2af); Sector neoPapery = neoFault.addSector("papery/musty",0xFFa6bac3); neoPapery.addSector("phenolic",0xFFe57c82); neoPapery.addSector("metay brothy",0xFFc97e72); neoPapery.addSector("animalic",0xFFa29c70); neoPapery.addSector("rusty",0xFF938149); neoPapery.addSector("dusty",0xFFc8a065); neoPapery.addSector("damp",0xFFa3a66e); neoPapery.addSector("woody",0xFF725c31); neoPapery.addSector("papery",0xFFfffefd); neoPapery.addSector("cardboard",0xFFd6bf4c); neoPapery.addSector("stale",0xFF6c7662); flavorWheel.render(); } void draw() { //Sector neo = flavorWheel.pickRandomSector(); //neo.mag = random(1.0,100.0); //background(#e8e4dc); background(0); flavorWheel.move(); flavorWheel.render(); restoreTimer++; if (restoreTimer==0) { flavorWheel.restore(); } } void mousePressed() { if (mouseButton==RIGHT) { // right clicked Sector neo = flavorWheel.pickSectorAtMouse(); if (neo!=null) { println("right picked:"+neo.label); neo.removeMe(); } } else { // left clicked Sector neo = flavorWheel.pickSectorAtMouse(); if (neo!=null) { println("picked:"+neo.label); neo.dmag += 10.0; neo.dthw += .618; restoreTimer = -100; } } } void keyPressed() { if (key=='s') { saveFrame(); println("Saved frame."); } } class FlavorWheel { Sector motherSector; FlavorWheel() { motherSector = new Sector(null, "COFFEE", 0xFF3d3d3d); } void move() { motherSector.move(); } void render() { pushMatrix(); translate(width/2,height/2); rotate(-HALF_PI); motherSector.render(); popMatrix(); } void restore() { motherSector.restore(1.0,1.0); } Sector addSector(String _label, color _myc) { Sector neo = motherSector.addSector(_label, _myc); return neo; } Sector pickRandomSector() { Sector obj = motherSector; for (int k=0;k<3;k++) { if (obj.children.size()==0) return obj; if (random(100)>obj.children.size()*10) { int n = floor(random( obj.children.size() )); obj = obj.children.get(n); } } return obj; } Sector pickSectorAtMouse() { Sector obj = null; obj = motherSector.didClick(); return obj; } } class Sector { Sector mother = null; // mother ArrayList<Sector> children; String label; color myc; int gen; // radial location in space float r0; float r1; float t0; float t1; // weighted pie slice rendering float mag = 1.0; float dmag = 1.0; // weighted radial rednering float thw = 1.0; float dthw = 1.0; Sector(Sector _mother, String _label, color _myc) { mother = _mother; label = _label; myc = _myc; children = new ArrayList<Sector>(); // special configuration for mother sector if (mother==null) { t0 = 0; t1 = TWO_PI; r0 = 0; r1 = 300; gen = 0; } else { gen = mother.gen + 1; } } Sector addSector(String _label, color _myc) { Sector neo = new Sector(this, _label, _myc); children.add(neo); return neo; } void restore(float _mag, float _thw) { dmag = _mag; dthw = _thw; for (int n=0;n<children.size();n++) { Sector obj = children.get(n); obj.restore(_mag, _thw); } } void move() { if (dmag<mag) { mag += (dmag-mag)*.255; } else { mag += (dmag-mag)*.333; } if (dthw<thw) { thw += (dthw-thw)*.255; } else { thw += (dthw-thw)*.333; } for (int n=0;n<children.size();n++) { Sector obj = children.get(n); obj.move(); } } void render() { // compute 2d coordinates and count number of points if (mother==null) { fill(myc); noStroke(); float diam = (r1-3)*2; ellipse(0,0,diam,diam); fill(255); textAlign(CENTER,CENTER); pushMatrix(); rotate(HALF_PI); text(label.toUpperCase(),0,0); popMatrix(); } else { float tinc = atan(arcResolution/r1); // padding substitution float tpad = .02 / (gen+1); float pt0 = t0 + tpad; float pt1 = t1 - tpad; float rpad = 3; float pr0 = r0 + rpad; float pr1 = r1 - rpad; // stupid hack to temporarily keep radius of gen 3 sector float tempr0 = pr0; if (gen==3) pr0 = pr1-20; // draw solid arc and label fill(myc); noStroke(); beginShape(); float x0 = pr0*cos(pt0); float y0 = pr0*sin(pt0); vertex(x0,y0); float x1 = pr1*cos(pt0); float y1 = pr1*sin(pt0); vertex(x1,y1); for (float t=pt0;t<=pt1;t+=tinc) { float xt = pr1*cos(t); float yt = pr1*sin(t); vertex(xt,yt); } float x2 = pr1*cos(pt1); float y2 = pr1*sin(pt1); vertex(x2,y2); float x3 = pr0*cos(pt1); float y3 = pr0*sin(pt1); vertex(x3,y3); for (float t=pt1;t>=pt0;t-=tinc) { float xtt = pr0*cos(t); float ytt = pr0*sin(t); vertex(xtt,ytt); } vertex(x0,y0); endShape(); if (gen==3) { // draw solid arc and label pr1 = pr0; pr0 = tempr0; fill(myc,64); noStroke(); beginShape(); float nx0 = pr0*cos(pt0); float ny0 = pr0*sin(pt0); vertex(nx0,ny0); float nx1 = pr1*cos(pt0); float ny1 = pr1*sin(pt0); vertex(nx1,ny1); for (float t=pt0;t<=pt1;t+=tinc) { float nxt = pr1*cos(t); float nyt = pr1*sin(t); vertex(nxt,nyt); } float nx2 = pr1*cos(pt1); float ny2 = pr1*sin(pt1); vertex(nx2,ny2); float nx3 = pr0*cos(pt1); float ny3 = pr0*sin(pt1); vertex(nx3,ny3); for (float t=pt1;t>=pt0;t-=tinc) { float nxtt = pr0*cos(t); float nytt = pr0*sin(t); vertex(nxtt,nytt); } vertex(nx0,ny0); endShape(); } // place label pushMatrix(); fill(255); if (gen==3) fill(myc); noStroke(); float tt = (t0 + t1)/2.0; float fx = 0; float fy = 0; // place and transform label according to generation if (gen==1) { fx = (r1 - 40)*cos(tt); fy = (r1 - 40)*sin(tt); } else if (gen==2) { fx = (r0 + 10)*cos(tt); fy = (r0 + 10)*sin(tt); } else if (gen==3) { fx = (r1 + 10)*cos(tt); fy = (r1 + 10)*sin(tt); } translate(fx,fy); if (gen==1) { rotate(tt+HALF_PI); textAlign(CENTER,BOTTOM); } else { if (tt<=PI || tt<0) { rotate(tt); textAlign(LEFT,CENTER); } else { rotate(tt+PI); textAlign(RIGHT, CENTER); } } float maxTextHeight = abs(.5*(pr0+pr1)*tan(pt1-pt0)); if (maxTextHeight>1) { if (maxTextHeight<28) scale(maxTextHeight/28); text(label.toUpperCase(),0,0); } popMatrix(); } fitChildren(); renderChildren(); } void renderChildren() { for (int n=0;n<children.size();n++) { Sector obj = children.get(n); // calculate radii on the fly obj.r0 = r1; obj.r1 = r1 + 100*obj.thw; obj.render(); } } void fitChildren() { // adjust all children radial angles to fit float omega = t1 - t0; // count total weight of children float childWeight = 0; for (int n=0;n<children.size();n++) { childWeight+=children.get(n).getWeight(); } // fit theta angles float theta = t0; for (int n=0;n<children.size();n++) { Sector obj = children.get(n); obj.t0 = theta; // adjust theta according to weight and space available theta += omega * obj.getWeight()/childWeight; obj.t1 = theta; } } float getWeight() { float wt = mag; for (int n=0;n<children.size();n++) { Sector obj = children.get(n); wt += obj.getWeight(); } return wt; } float getThrow() { float tt = thw; for (int n=0;n<children.size();n++) { Sector obj = children.get(n); tt += obj.getThrow(); } return tt; } Sector didClick() { // did the mouse click this sector float mt = atan2(mouseY-height/2,mouseX-width/2)+HALF_PI; float mr = dist(width/2,height/2,mouseX,mouseY); // normalize thetas float normt0 = t0 + TWO_PI; if (normt0>TWO_PI) normt0-=TWO_PI; float normt1 = t1 + TWO_PI; if (normt1>TWO_PI) normt1-=TWO_PI; float normt = mt + TWO_PI; if (normt>TWO_PI) normt-=TWO_PI; //println("R:"+r0+" .. "+mr+" .. "+r1+" T:"+t0+" .. "+mt+" .. "+t1); if (normt>=normt0 && normt<normt1) { if (mr>=r0 && mr<r1) { return this; } } // not this one, check children for (int n=0;n<children.size();n++) { Sector hmm = children.get(n).didClick(); if (hmm!=null) return hmm; } return null; } void removeMe() { // remove this sector if (mother!=null) mother.removeSector(this); } void removeSector(Sector target) { for (int i=0;i<children.size();i++) { Sector obj = children.get(i); if (target==obj) { children.remove(i); } } } }