diff --git a/Simulators/TriangleSimulator/TriangleSimulator.pde b/Simulators/TriangleSimulator/TriangleSimulator.pde index ccfd963..ade5d89 100644 --- a/Simulators/TriangleSimulator/TriangleSimulator.pde +++ b/Simulators/TriangleSimulator/TriangleSimulator.pde @@ -1,60 +1,36 @@ /* Playing around with hex drawing, as described at: http://www.redblobgames.com/grids/hexagons/ - + Grid shapes are described in arrays of arrays: {skip_count, fill_count, skip_count ...} */ import processing.net.*; import java.util.regex.*; -Table table; boolean DRAW_LABELS = true; -//boolean POINTY_TOP = true; -boolean DARK_MODE = false; - - - - - // model vars -HexForm honeycomb = null; -HashMap labels = null; +TriangleForm grid = null; // network vars int port = 4444; Server _server; StringBuffer _buf = new StringBuffer(); -color defaultHexLine() { - return color(255,255,255); -} - -color defaultHexFill() { - return color(255,0,255); -} - -PVector a = new PVector(400, 100); -PVector b = new PVector(100, 600); -PVector c = new PVector(700, 600); -float hue; - +color defaultLineColor = color(255, 255, 255); +color defaultFillColor = color(255, 0, 255); void setup() { size(800,700); rotate(radians(0)); frameRate(30); - // println(PFont.list()); PFont f = createFont("Helvetica", 12, true); textFont(f, 12); - labels = loadLabels("mapping_tri.csv"); - - honeycomb = makeSimpleGrid(8,1,500,300); - //honeycomb = makeHexForm(SNOWFLAKE, 50, 50); + grid = makeSimpleGrid(); _server = new Server(this, port); println("server listening:" + _server); @@ -64,49 +40,40 @@ void drawCheckbox(int x, int y, boolean checked) { int size = 20; stroke(0); fill(255); - rect(x,y,size,size); + rect(x, y, size, size); if (checked) { - line(x,y,x+size,y+size); - line(x+size,y,x,y+size); + line(x, y, x+size, y+size); + line(x+size, y, x, y+size); } } - void drawBottomControls() { // draw a bottom white region - fill(255,255,255); - rect(0,500,500,50); - + fill(255, 255, 255); + rect(0, 500, 500, 50); + // draw checkboxes stroke(0); fill(255); - drawCheckbox(20,510, DRAW_LABELS); // label checkbox - // drawCheckbox(190,510, POINTY_TOP); // pointy-top checkbox - drawCheckbox(360,510, DARK_MODE); // dark mode - + drawCheckbox(20, 510, DRAW_LABELS); // label checkbox + // draw text labels fill(0); textAlign(LEFT); text("Draw Labels", 50, 525); - text("Pointy Top", 220, 525); - text("Dark Mode", 390, 525); } - void mouseClicked() { - //println("click! x:" + mouseX + " y:" + mouseY); if (mouseX > 20 && mouseX < 40 && mouseY > 510 && mouseY < 530) { // clicked draw labels button DRAW_LABELS = !DRAW_LABELS; - } else if (mouseX > 360 && mouseX < 380 && mouseY > 510 && mouseY < 530) { - DARK_MODE = !DARK_MODE; } } void draw() { background(250); drawBottomControls(); - honeycomb.draw(); + grid.draw(); pollServer(); } @@ -125,7 +92,6 @@ void pollServer() { while (ix > -1) { String msg = _buf.substring(0, ix); msg = msg.trim(); - //println(msg); processCommand(msg); _buf.delete(0, ix+1); ix = _buf.indexOf("\n"); @@ -136,8 +102,7 @@ void pollServer() { } } -//Pattern cmd_pattern = Pattern.compile("^\\s*(\\d+)\\s+(\\d+),(\\d+),(\\d+)\\s*$"); -Pattern cmd_pattern = Pattern.compile("^\\s*(p|b|a|f )\\s+(\\d+)\\s+(\\d+),(\\d+),(\\d+)\\s*$"); +Pattern cmd_pattern = Pattern.compile("^\\s*(\\d+)\\s+(\\d+),(\\d+),(\\d+)\\s*$"); void processCommand(String cmd) { Matcher m = cmd_pattern.matcher(cmd); @@ -145,38 +110,17 @@ void processCommand(String cmd) { println("ignoring input!"); return; } - String side = m.group(1); - int cell = Integer.valueOf(m.group(2)); - int r = Integer.valueOf(m.group(3)); - int g = Integer.valueOf(m.group(4)); - int b = Integer.valueOf(m.group(5)); + int cell = Integer.valueOf(m.group(1)); + int r = Integer.valueOf(m.group(2)); + int g = Integer.valueOf(m.group(3)); + int b = Integer.valueOf(m.group(4)); - honeycomb.setCellColor(cell, color(r,g,b)); + grid.setCellColor(cell, color(r,g,b)); } -/* -mappings* Load label mapping file - */ -HashMap loadLabels(String labelFile) { - HashMap labels = new HashMap(); - Table table = loadTable(labelFile); - - println(table.getRowCount() + " total rows in table"); - - for (TableRow row : table.rows()) { - int id = row.getInt(0); - String coord = row.getString(1); - labels.put(id, coord); - } - return labels; -} - - - - -HexForm makeSimpleGrid(int rows, int cols, int start_x, int start_y) { - HexForm form = new HexForm(); - table = loadTable("triangleCellMapping.csv", "header"); +TriangleForm makeSimpleGrid() { + TriangleForm form = new TriangleForm(); + Table table = loadTable("triangleCellMapping.csv", "header"); for (TableRow row : table.rows()) { String shape = row.getString("shape"); int x1 = row.getInt("x1"); @@ -186,59 +130,47 @@ HexForm makeSimpleGrid(int rows, int cols, int start_x, int start_y) { int x3 = row.getInt("x3"); int y3 = row.getInt("y3"); int id = row.getInt("id"); - //triangle(x1,y1,x2,y2,x3,y3); - // print(shape); - form.add(new Hex(x1,y1,x2,y2,x3,y3,id),id); - } + form.add(new Triangle(x1, y1, x2, y2, x3, y3, id), id); + } return form; } -class HexForm { - ArrayList hexes; - //HashMap hexesById; - - HexForm() { - hexes = new ArrayList(); - //hexesById = new HashMap(); +class TriangleForm { + ArrayList triangles = new ArrayList(); + + TriangleForm() { } - - void add(Hex h, int hexId) { -// int hexId = hexes.size(); -// if (labels != null) { -// h.setId(labels.get(hexId)); -// } else { -// h.setId(String.valueOf(hexId)); -// } - print("HEXID:", hexId); - h.setId(hexId); - hexes.add(h); + + void add(Triangle triangle, int id) { + print("Triangle ID:", id); + triangle.setId(id); + triangles.add(triangle); } - + int size() { - return hexes.size(); + return triangles.size(); } - + void draw() { - for (Hex h : hexes) { - h.draw(); + for (Triangle triangle : triangles) { + triangle.draw(); } } - + // XXX probably need a better API here! void setCellColor(int i, color c) { - if (i >= hexes.size()) { - println("invalid offset for HexForm.setColor: i only have " + hexes.size() + " hexes"); -// hexes.get(1).setColor(255); - - } else { - hexes.get(i).setColor(c); + if (i >= triangles.size()) { + println("invalid offset for HexForm.setColor: i only have " + triangles.size() + " hexes"); + return; } + + triangles.get(i).setColor(c); } } -class Hex { +class Triangle { int id = 0; // optional int x1; int y1; @@ -250,8 +182,8 @@ class Hex { Integer c; // can store color/int or null - Hex(int x1, int y1,int x2, int y2,int x3, int y3, int id) { - print("CreateHex\n"); + Triangle(int x1, int y1,int x2, int y2,int x3, int y3, int id) { + print("CreateTriangle\n"); this.x1 = x1; this.y1 = y1; this.x2 = x2; @@ -267,28 +199,25 @@ class Hex { this.id = id; print("pass"); } - + void setColor(color c) { this.c = c; } - void draw() { - color fill_color = (this.c != null) ? c : defaultHexFill(); + color fill_color = (this.c != null) ? c : defaultFillColor; fill(fill_color); - stroke(defaultHexLine()); + stroke(defaultLineColor); beginShape(); - triangle(this.x1,this.y1,this.x2,this.y2,this.x3,this.y3); - + triangle(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); + endShape(CLOSE); - + // draw text label if (DRAW_LABELS && this.id != 0) { -// fill(defaultHexLine()); -// if (this.cell == 0) { - fill(defaultHexLine()); - + fill(defaultLineColor); + if (this.y1 == this.y2) { textAlign(CENTER); print(this.id,"pointy"); @@ -298,26 +227,8 @@ class Hex { print(this.id,"flat"); text(this.id,this.x1,this.y1+30); } -// this.cell = 1; -// } else { -// textAlign(BOTTOM); -// text(this.id,this.x1+20,this.y3); -// this.cell = 0; - // } - // if (this.id % 2 == 0) { - // textAlign(CENTER); - // text(this.id,this.x1,this.y1);/ -// - // } else { - // textAlign(CENTER); - // text(this.id,this.x3+5,this.y3); -// - // } - - // print(this.id, this.x1, this.y2,this.x2, this.y2,this.x3, this.y3 ); } + noFill(); - - } } diff --git a/Simulators/TriangleSimulator/mapping_tri.csv b/Simulators/TriangleSimulator/mapping_tri.csv deleted file mode 100644 index 3e8ac1a..0000000 --- a/Simulators/TriangleSimulator/mapping_tri.csv +++ /dev/null @@ -1,270 +0,0 @@ -0,"0" -1,"1" -2,"2" -3,"3" -4,"4" -5,"5" -6,"6" -7,"7" -8,"8" -9,"9" -10,"10" -11,"11" -12,"12" -13,"13" -14,"14" -15,"15" -16,"16" -17,"17" -18,"18" -19,"19" -20,"20" -21,"21" -22,"22" -23,"23" -24,"24" -25,"25" -26,"26" -27,"27" -28,"28" -29,"29" -30,"30" -31,"31" -32,"32" -33,"33" -34,"34" -35,"35" -36,"36" -37,"37" -38,"38" -39,"39" -40,"40" -41,"41" -42,"42" -43,"43" -44,"44" -45,"45" -46,"46" -47,"47" -48,"48" -49,"49" -50,"50" -51,"51" -52,"52" -53,"53" -54,"54" -55,"55" -56,"56" -57,"57" -58,"58" -59,"59" -60,"60" -61,"61" -62,"62" -63,"63" -64,"64" -65,"65" -66,"66" -67,"67" -68,"68" -69,"69" -70,"70" -71,"71" -72,"72" -73,"73" -74,"74" -75,"75" -76,"76" -77,"77" -78,"78" -79,"79" -80,"80" -81,"81" -82,"82" -83,"83" -84,"84" -85,"85" -86,"86" -87,"87" -88,"88" -89,"89" -90,"90" -91,"91" -92,"92" -93,"93" -94,"94" -95,"95" -96,"96" -97,"97" -98,"98" -99,"99" -100,"100" -101,"101" -102,"102" -103,"103" -104,"104" -105,"105" -106,"106" -107,"107" -108,"108" -109,"109" -110,"110" -111,"111" -112,"112" -113,"113" -114,"114" -115,"115" -116,"116" -117,"117" -118,"118" -119,"119" -120,"120" -121,"121" -122,"122" -123,"123" -124,"124" -125,"125" -126,"126" -127,"127" -128,"128" -129,"129" -130,"130" -131,"131" -132,"132" -133,"133" -134,"134" -135,"135" -136,"136" -137,"137" -138,"138" -139,"139" -140,"140" -141,"141" -142,"142" -143,"143" -144,"144" -145,"145" -146,"146" -147,"147" -148,"148" -149,"149" -150,"150" -151,"151" -152,"152" -153,"153" -154,"154" -155,"155" -156,"156" -157,"157" -158,"158" -159,"159" -160,"160" -161,"161" -162,"162" -163,"163" -164,"164" -165,"165" -166,"166" -167,"167" -168,"168" -169,"169" -170,"170" -171,"171" -172,"172" -173,"173" -174,"174" -175,"175" -176,"176" -177,"177" -178,"178" -179,"179" -180,"180" -181,"181" -182,"182" -183,"183" -184,"184" -185,"185" -186,"186" -187,"187" -188,"188" -189,"189" -190,"190" -191,"191" -192,"192" -193,"193" -194,"194" -195,"195" -196,"196" -197,"197" -198,"198" -199,"199" -200,"200" -201,"201" -202,"202" -203,"203" -204,"204" -205,"205" -206,"206" -207,"207" -208,"208" -209,"209" -210,"210" -211,"211" -212,"212" -213,"213" -214,"214" -215,"215" -216,"216" -217,"217" -218,"218" -219,"219" -220,"220" -221,"221" -222,"222" -223,"223" -224,"224" -225,"225" -226,"226" -227,"227" -228,"228" -229,"229" -230,"230" -231,"231" -232,"232" -233,"233" -234,"234" -235,"235" -236,"236" -237,"237" -238,"238" -239,"239" -240,"240" -241,"241" -242,"242" -243,"243" -244,"244" -245,"245" -246,"246" -247,"247" -248,"248" -249,"249" -250,"250" -251,"251" -252,"252" -253,"253" -254,"254" -255,"255" -256,"256" -257,"257" -258,"258" -259,"259" -260,"260" -261,"261" -262,"262" -263,"263" -264,"264" -265,"265" -266,"266" -267,"267" -268,"268" -269,"269" diff --git a/Simulators/TriangleSimulator/triangleCellMapping.csv b/Simulators/TriangleSimulator/triangleCellMapping.csv index 52e45ac..f4cc1f4 100644 --- a/Simulators/TriangleSimulator/triangleCellMapping.csv +++ b/Simulators/TriangleSimulator/triangleCellMapping.csv @@ -1,258 +1,122 @@ shape,x1,y1,x2,y2,x3,y3,id -triangle,0,0,0,0,0,0,0 -triangle,400.0,100.0,381.25,131.25,418.75,131.25,1 -triangle,381.25,131.25,362.5,162.5,400.0,162.5,2 -triangle,381.25,131.25,418.75,131.25,400.0,162.5,3 -triangle,418.75,131.25,400.0,162.5,437.5,162.5,4 -triangle,362.5,162.5,343.75,193.75,381.25,193.75,5 -triangle,362.5,162.5,400.0,162.5,381.25,193.75,6 -triangle,400.0,162.5,381.25,193.75,418.75,193.75,7 -triangle,400.0,162.5,437.5,162.5,418.75,193.75,8 -triangle,437.5,162.5,418.75,193.75,456.25,193.75,9 -triangle,343.75,193.75,325.0,225.0,362.5,225.0,10 -triangle,343.75,193.75,381.25,193.75,362.5,225.0,11 -triangle,381.25,193.75,362.5,225.0,400.0,225.0,12 -triangle,381.25,193.75,418.75,193.75,400.0,225.0,13 -triangle,418.75,193.75,400.0,225.0,437.5,225.0,14 -triangle,418.75,193.75,456.25,193.75,437.5,225.0,15 -triangle,456.25,193.75,437.5,225.0,475.0,225.0,16 -triangle,325.0,225.0,306.25,256.25,343.75,256.25,17 -triangle,325.0,225.0,362.5,225.0,343.75,256.25,18 -triangle,362.5,225.0,343.75,256.25,381.25,256.25,19 -triangle,362.5,225.0,400.0,225.0,381.25,256.25,20 -triangle,400.0,225.0,381.25,256.25,418.75,256.25,21 -triangle,400.0,225.0,437.5,225.0,418.75,256.25,22 -triangle,437.5,225.0,418.75,256.25,456.25,256.25,23 -triangle,437.5,225.0,475.0,225.0,456.25,256.25,24 -triangle,475.0,225.0,456.25,256.25,493.75,256.25,25 -triangle,306.25,256.25,287.5,287.5,325.0,287.5,26 -triangle,306.25,256.25,343.75,256.25,325.0,287.5,27 -triangle,343.75,256.25,325.0,287.5,362.5,287.5,28 -triangle,343.75,256.25,381.25,256.25,362.5,287.5,29 -triangle,381.25,256.25,362.5,287.5,400.0,287.5,30 -triangle,381.25,256.25,418.75,256.25,400.0,287.5,31 -triangle,418.75,256.25,400.0,287.5,437.5,287.5,32 -triangle,418.75,256.25,456.25,256.25,437.5,287.5,33 -triangle,456.25,256.25,437.5,287.5,475.0,287.5,34 -triangle,456.25,256.25,493.75,256.25,475.0,287.5,35 -triangle,493.75,256.25,475.0,287.5,512.5,287.5,36 -triangle,287.5,287.5,268.75,318.75,306.25,318.75,37 -triangle,287.5,287.5,325.0,287.5,306.25,318.75,38 -triangle,325.0,287.5,306.25,318.75,343.75,318.75,39 -triangle,325.0,287.5,362.5,287.5,343.75,318.75,40 -triangle,362.5,287.5,343.75,318.75,381.25,318.75,41 -triangle,362.5,287.5,400.0,287.5,381.25,318.75,42 -triangle,400.0,287.5,381.25,318.75,418.75,318.75,43 -triangle,400.0,287.5,437.5,287.5,418.75,318.75,44 -triangle,437.5,287.5,418.75,318.75,456.25,318.75,45 -triangle,437.5,287.5,475.0,287.5,456.25,318.75,46 -triangle,475.0,287.5,456.25,318.75,493.75,318.75,47 -triangle,475.0,287.5,512.5,287.5,493.75,318.75,48 -triangle,512.5,287.5,493.75,318.75,531.25,318.75,49 -triangle,268.75,318.75,250.0,350.0,287.5,350.0,50 -triangle,268.75,318.75,306.25,318.75,287.5,350.0,51 -triangle,306.25,318.75,287.5,350.0,325.0,350.0,52 -triangle,306.25,318.75,343.75,318.75,325.0,350.0,53 -triangle,343.75,318.75,325.0,350.0,362.5,350.0,54 -triangle,343.75,318.75,381.25,318.75,362.5,350.0,55 -triangle,381.25,318.75,362.5,350.0,400.0,350.0,56 -triangle,381.25,318.75,418.75,318.75,400.0,350.0,57 -triangle,418.75,318.75,400.0,350.0,437.5,350.0,58 -triangle,418.75,318.75,456.25,318.75,437.5,350.0,59 -triangle,456.25,318.75,437.5,350.0,475.0,350.0,60 -triangle,456.25,318.75,493.75,318.75,475.0,350.0,61 -triangle,493.75,318.75,475.0,350.0,512.5,350.0,62 -triangle,493.75,318.75,531.25,318.75,512.5,350.0,63 -triangle,531.25,318.75,512.5,350.0,550.0,350.0,64 -triangle,250.0,350.0,231.25,381.25,268.75,381.25,65 -triangle,250.0,350.0,287.5,350.0,268.75,381.25,66 -triangle,287.5,350.0,268.75,381.25,306.25,381.25,67 -triangle,287.5,350.0,325.0,350.0,306.25,381.25,68 -triangle,325.0,350.0,306.25,381.25,343.75,381.25,69 -triangle,325.0,350.0,362.5,350.0,343.75,381.25,70 -triangle,362.5,350.0,343.75,381.25,381.25,381.25,71 -triangle,362.5,350.0,400.0,350.0,381.25,381.25,72 -triangle,400.0,350.0,381.25,381.25,418.75,381.25,73 -triangle,400.0,350.0,437.5,350.0,418.75,381.25,74 -triangle,437.5,350.0,418.75,381.25,456.25,381.25,75 -triangle,437.5,350.0,475.0,350.0,456.25,381.25,76 -triangle,475.0,350.0,456.25,381.25,493.75,381.25,77 -triangle,475.0,350.0,512.5,350.0,493.75,381.25,78 -triangle,512.5,350.0,493.75,381.25,531.25,381.25,79 -triangle,512.5,350.0,550.0,350.0,531.25,381.25,80 -triangle,550.0,350.0,531.25,381.25,568.75,381.25,81 -triangle,231.25,381.25,212.5,412.5,250.0,412.5,82 -triangle,231.25,381.25,268.75,381.25,250.0,412.5,83 -triangle,268.75,381.25,250.0,412.5,287.5,412.5,84 -triangle,268.75,381.25,306.25,381.25,287.5,412.5,85 -triangle,306.25,381.25,287.5,412.5,325.0,412.5,86 -triangle,306.25,381.25,343.75,381.25,325.0,412.5,87 -triangle,343.75,381.25,325.0,412.5,362.5,412.5,88 -triangle,343.75,381.25,381.25,381.25,362.5,412.5,89 -triangle,381.25,381.25,362.5,412.5,400.0,412.5,90 -triangle,381.25,381.25,418.75,381.25,400.0,412.5,91 -triangle,418.75,381.25,400.0,412.5,437.5,412.5,92 -triangle,418.75,381.25,456.25,381.25,437.5,412.5,93 -triangle,456.25,381.25,437.5,412.5,475.0,412.5,94 -triangle,456.25,381.25,493.75,381.25,475.0,412.5,95 -triangle,493.75,381.25,475.0,412.5,512.5,412.5,96 -triangle,493.75,381.25,531.25,381.25,512.5,412.5,97 -triangle,531.25,381.25,512.5,412.5,550.0,412.5,98 -triangle,531.25,381.25,568.75,381.25,550.0,412.5,99 -triangle,568.75,381.25,550.0,412.5,587.5,412.5,100 -triangle,212.5,412.5,193.75,443.75,231.25,443.75,101 -triangle,212.5,412.5,250.0,412.5,231.25,443.75,102 -triangle,250.0,412.5,231.25,443.75,268.75,443.75,103 -triangle,250.0,412.5,287.5,412.5,268.75,443.75,104 -triangle,287.5,412.5,268.75,443.75,306.25,443.75,105 -triangle,287.5,412.5,325.0,412.5,306.25,443.75,106 -triangle,325.0,412.5,306.25,443.75,343.75,443.75,107 -triangle,325.0,412.5,362.5,412.5,343.75,443.75,108 -triangle,362.5,412.5,343.75,443.75,381.25,443.75,109 -triangle,362.5,412.5,400.0,412.5,381.25,443.75,110 -triangle,400.0,412.5,381.25,443.75,418.75,443.75,111 -triangle,400.0,412.5,437.5,412.5,418.75,443.75,112 -triangle,437.5,412.5,418.75,443.75,456.25,443.75,113 -triangle,437.5,412.5,475.0,412.5,456.25,443.75,114 -triangle,475.0,412.5,456.25,443.75,493.75,443.75,115 -triangle,475.0,412.5,512.5,412.5,493.75,443.75,116 -triangle,512.5,412.5,493.75,443.75,531.25,443.75,117 -triangle,512.5,412.5,550.0,412.5,531.25,443.75,118 -triangle,550.0,412.5,531.25,443.75,568.75,443.75,119 -triangle,550.0,412.5,587.5,412.5,568.75,443.75,120 -triangle,587.5,412.5,568.75,443.75,606.25,443.75,121 -triangle,193.75,443.75,175.0,475.0,212.5,475.0,122 -triangle,193.75,443.75,231.25,443.75,212.5,475.0,123 -triangle,231.25,443.75,212.5,475.0,250.0,475.0,124 -triangle,231.25,443.75,268.75,443.75,250.0,475.0,125 -triangle,268.75,443.75,250.0,475.0,287.5,475.0,126 -triangle,268.75,443.75,306.25,443.75,287.5,475.0,127 -triangle,306.25,443.75,287.5,475.0,325.0,475.0,128 -triangle,306.25,443.75,343.75,443.75,325.0,475.0,129 -triangle,343.75,443.75,325.0,475.0,362.5,475.0,130 -triangle,343.75,443.75,381.25,443.75,362.5,475.0,131 -triangle,381.25,443.75,362.5,475.0,400.0,475.0,132 -triangle,381.25,443.75,418.75,443.75,400.0,475.0,133 -triangle,418.75,443.75,400.0,475.0,437.5,475.0,134 -triangle,418.75,443.75,456.25,443.75,437.5,475.0,135 -triangle,456.25,443.75,437.5,475.0,475.0,475.0,136 -triangle,456.25,443.75,493.75,443.75,475.0,475.0,137 -triangle,493.75,443.75,475.0,475.0,512.5,475.0,138 -triangle,493.75,443.75,531.25,443.75,512.5,475.0,139 -triangle,531.25,443.75,512.5,475.0,550.0,475.0,140 -triangle,531.25,443.75,568.75,443.75,550.0,475.0,141 -triangle,568.75,443.75,550.0,475.0,587.5,475.0,142 -triangle,568.75,443.75,606.25,443.75,587.5,475.0,143 -triangle,606.25,443.75,587.5,475.0,625.0,475.0,144 -triangle,175.0,475.0,156.25,506.25,193.75,506.25,145 -triangle,175.0,475.0,212.5,475.0,193.75,506.25,146 -triangle,212.5,475.0,193.75,506.25,231.25,506.25,147 -triangle,212.5,475.0,250.0,475.0,231.25,506.25,148 -triangle,250.0,475.0,231.25,506.25,268.75,506.25,149 -triangle,250.0,475.0,287.5,475.0,268.75,506.25,150 -triangle,287.5,475.0,268.75,506.25,306.25,506.25,151 -triangle,287.5,475.0,325.0,475.0,306.25,506.25,152 -triangle,325.0,475.0,306.25,506.25,343.75,506.25,153 -triangle,325.0,475.0,362.5,475.0,343.75,506.25,154 -triangle,362.5,475.0,343.75,506.25,381.25,506.25,155 -triangle,362.5,475.0,400.0,475.0,381.25,506.25,156 -triangle,400.0,475.0,381.25,506.25,418.75,506.25,157 -triangle,400.0,475.0,437.5,475.0,418.75,506.25,158 -triangle,437.5,475.0,418.75,506.25,456.25,506.25,159 -triangle,437.5,475.0,475.0,475.0,456.25,506.25,160 -triangle,475.0,475.0,456.25,506.25,493.75,506.25,161 -triangle,475.0,475.0,512.5,475.0,493.75,506.25,162 -triangle,512.5,475.0,493.75,506.25,531.25,506.25,163 -triangle,512.5,475.0,550.0,475.0,531.25,506.25,164 -triangle,550.0,475.0,531.25,506.25,568.75,506.25,165 -triangle,550.0,475.0,587.5,475.0,568.75,506.25,166 -triangle,587.5,475.0,568.75,506.25,606.25,506.25,167 -triangle,587.5,475.0,625.0,475.0,606.25,506.25,168 -triangle,625.0,475.0,606.25,506.25,643.75,506.25,169 -triangle,156.25,506.25,137.5,537.5,175.0,537.5,170 -triangle,156.25,506.25,193.75,506.25,175.0,537.5,171 -triangle,193.75,506.25,175.0,537.5,212.5,537.5,172 -triangle,193.75,506.25,231.25,506.25,212.5,537.5,173 -triangle,231.25,506.25,212.5,537.5,250.0,537.5,174 -triangle,231.25,506.25,268.75,506.25,250.0,537.5,175 -triangle,268.75,506.25,250.0,537.5,287.5,537.5,176 -triangle,268.75,506.25,306.25,506.25,287.5,537.5,177 -triangle,306.25,506.25,287.5,537.5,325.0,537.5,178 -triangle,306.25,506.25,343.75,506.25,325.0,537.5,179 -triangle,343.75,506.25,325.0,537.5,362.5,537.5,180 -triangle,343.75,506.25,381.25,506.25,362.5,537.5,181 -triangle,381.25,506.25,362.5,537.5,400.0,537.5,182 -triangle,381.25,506.25,418.75,506.25,400.0,537.5,183 -triangle,418.75,506.25,400.0,537.5,437.5,537.5,184 -triangle,418.75,506.25,456.25,506.25,437.5,537.5,185 -triangle,456.25,506.25,437.5,537.5,475.0,537.5,186 -triangle,456.25,506.25,493.75,506.25,475.0,537.5,187 -triangle,493.75,506.25,475.0,537.5,512.5,537.5,188 -triangle,493.75,506.25,531.25,506.25,512.5,537.5,189 -triangle,531.25,506.25,512.5,537.5,550.0,537.5,190 -triangle,531.25,506.25,568.75,506.25,550.0,537.5,191 -triangle,568.75,506.25,550.0,537.5,587.5,537.5,192 -triangle,568.75,506.25,606.25,506.25,587.5,537.5,193 -triangle,606.25,506.25,587.5,537.5,625.0,537.5,194 -triangle,606.25,506.25,643.75,506.25,625.0,537.5,195 -triangle,643.75,506.25,625.0,537.5,662.5,537.5,196 -triangle,137.5,537.5,118.75,568.75,156.25,568.75,197 -triangle,137.5,537.5,175.0,537.5,156.25,568.75,198 -triangle,175.0,537.5,156.25,568.75,193.75,568.75,199 -triangle,175.0,537.5,212.5,537.5,193.75,568.75,200 -triangle,212.5,537.5,193.75,568.75,231.25,568.75,201 -triangle,212.5,537.5,250.0,537.5,231.25,568.75,202 -triangle,250.0,537.5,231.25,568.75,268.75,568.75,203 -triangle,250.0,537.5,287.5,537.5,268.75,568.75,204 -triangle,287.5,537.5,268.75,568.75,306.25,568.75,205 -triangle,287.5,537.5,325.0,537.5,306.25,568.75,206 -triangle,325.0,537.5,306.25,568.75,343.75,568.75,207 -triangle,325.0,537.5,362.5,537.5,343.75,568.75,208 -triangle,362.5,537.5,343.75,568.75,381.25,568.75,209 -triangle,362.5,537.5,400.0,537.5,381.25,568.75,210 -triangle,400.0,537.5,381.25,568.75,418.75,568.75,211 -triangle,400.0,537.5,437.5,537.5,418.75,568.75,212 -triangle,437.5,537.5,418.75,568.75,456.25,568.75,213 -triangle,437.5,537.5,475.0,537.5,456.25,568.75,214 -triangle,475.0,537.5,456.25,568.75,493.75,568.75,215 -triangle,475.0,537.5,512.5,537.5,493.75,568.75,216 -triangle,512.5,537.5,493.75,568.75,531.25,568.75,217 -triangle,512.5,537.5,550.0,537.5,531.25,568.75,218 -triangle,550.0,537.5,531.25,568.75,568.75,568.75,219 -triangle,550.0,537.5,587.5,537.5,568.75,568.75,220 -triangle,587.5,537.5,568.75,568.75,606.25,568.75,221 -triangle,587.5,537.5,625.0,537.5,606.25,568.75,222 -triangle,625.0,537.5,606.25,568.75,643.75,568.75,223 -triangle,625.0,537.5,662.5,537.5,643.75,568.75,224 -triangle,662.5,537.5,643.75,568.75,681.25,568.75,225 -triangle,118.75,568.75,100.0,600.0,137.5,600.0,226 -triangle,118.75,568.75,156.25,568.75,137.5,600.0,227 -triangle,156.25,568.75,137.5,600.0,175.0,600.0,228 -triangle,156.25,568.75,193.75,568.75,175.0,600.0,229 -triangle,193.75,568.75,175.0,600.0,212.5,600.0,230 -triangle,193.75,568.75,231.25,568.75,212.5,600.0,231 -triangle,231.25,568.75,212.5,600.0,250.0,600.0,232 -triangle,231.25,568.75,268.75,568.75,250.0,600.0,233 -triangle,268.75,568.75,250.0,600.0,287.5,600.0,234 -triangle,268.75,568.75,306.25,568.75,287.5,600.0,235 -triangle,306.25,568.75,287.5,600.0,325.0,600.0,236 -triangle,306.25,568.75,343.75,568.75,325.0,600.0,237 -triangle,343.75,568.75,325.0,600.0,362.5,600.0,238 -triangle,343.75,568.75,381.25,568.75,362.5,600.0,239 -triangle,381.25,568.75,362.5,600.0,400.0,600.0,240 -triangle,381.25,568.75,418.75,568.75,400.0,600.0,241 -triangle,418.75,568.75,400.0,600.0,437.5,600.0,242 -triangle,418.75,568.75,456.25,568.75,437.5,600.0,243 -triangle,456.25,568.75,437.5,600.0,475.0,600.0,244 -triangle,456.25,568.75,493.75,568.75,475.0,600.0,245 -triangle,493.75,568.75,475.0,600.0,512.5,600.0,246 -triangle,493.75,568.75,531.25,568.75,512.5,600.0,247 -triangle,531.25,568.75,512.5,600.0,550.0,600.0,248 -triangle,531.25,568.75,568.75,568.75,550.0,600.0,249 -triangle,568.75,568.75,550.0,600.0,587.5,600.0,250 -triangle,568.75,568.75,606.25,568.75,587.5,600.0,251 -triangle,606.25,568.75,587.5,600.0,625.0,600.0,252 -triangle,606.25,568.75,643.75,568.75,625.0,600.0,253 -triangle,643.75,568.75,625.0,600.0,662.5,600.0,254 -triangle,643.75,568.75,681.25,568.75,662.5,600.0,255 -triangle,681.25,568.75,662.5,600.0,700.0,600.0,256 +triangle,400.0,100.0,381.25,131.25,418.75,131.25,0 +triangle,381.25,131.25,362.5,162.5,400.0,162.5,1 +triangle,381.25,131.25,418.75,131.25,400.0,162.5,2 +triangle,418.75,131.25,400.0,162.5,437.5,162.5,3 +triangle,362.5,162.5,343.75,193.75,381.25,193.75,4 +triangle,362.5,162.5,400.0,162.5,381.25,193.75,5 +triangle,400.0,162.5,381.25,193.75,418.75,193.75,6 +triangle,400.0,162.5,437.5,162.5,418.75,193.75,7 +triangle,437.5,162.5,418.75,193.75,456.25,193.75,8 +triangle,343.75,193.75,325.0,225.0,362.5,225.0,9 +triangle,343.75,193.75,381.25,193.75,362.5,225.0,10 +triangle,381.25,193.75,362.5,225.0,400.0,225.0,11 +triangle,381.25,193.75,418.75,193.75,400.0,225.0,12 +triangle,418.75,193.75,400.0,225.0,437.5,225.0,13 +triangle,418.75,193.75,456.25,193.75,437.5,225.0,14 +triangle,456.25,193.75,437.5,225.0,475.0,225.0,15 +triangle,325.0,225.0,306.25,256.25,343.75,256.25,16 +triangle,325.0,225.0,362.5,225.0,343.75,256.25,17 +triangle,362.5,225.0,343.75,256.25,381.25,256.25,18 +triangle,362.5,225.0,400.0,225.0,381.25,256.25,19 +triangle,400.0,225.0,381.25,256.25,418.75,256.25,20 +triangle,400.0,225.0,437.5,225.0,418.75,256.25,21 +triangle,437.5,225.0,418.75,256.25,456.25,256.25,22 +triangle,437.5,225.0,475.0,225.0,456.25,256.25,23 +triangle,475.0,225.0,456.25,256.25,493.75,256.25,24 +triangle,306.25,256.25,287.5,287.5,325.0,287.5,25 +triangle,306.25,256.25,343.75,256.25,325.0,287.5,26 +triangle,343.75,256.25,325.0,287.5,362.5,287.5,27 +triangle,343.75,256.25,381.25,256.25,362.5,287.5,28 +triangle,381.25,256.25,362.5,287.5,400.0,287.5,29 +triangle,381.25,256.25,418.75,256.25,400.0,287.5,30 +triangle,418.75,256.25,400.0,287.5,437.5,287.5,31 +triangle,418.75,256.25,456.25,256.25,437.5,287.5,32 +triangle,456.25,256.25,437.5,287.5,475.0,287.5,33 +triangle,456.25,256.25,493.75,256.25,475.0,287.5,34 +triangle,493.75,256.25,475.0,287.5,512.5,287.5,35 +triangle,287.5,287.5,268.75,318.75,306.25,318.75,36 +triangle,287.5,287.5,325.0,287.5,306.25,318.75,37 +triangle,325.0,287.5,306.25,318.75,343.75,318.75,38 +triangle,325.0,287.5,362.5,287.5,343.75,318.75,39 +triangle,362.5,287.5,343.75,318.75,381.25,318.75,40 +triangle,362.5,287.5,400.0,287.5,381.25,318.75,41 +triangle,400.0,287.5,381.25,318.75,418.75,318.75,42 +triangle,400.0,287.5,437.5,287.5,418.75,318.75,43 +triangle,437.5,287.5,418.75,318.75,456.25,318.75,44 +triangle,437.5,287.5,475.0,287.5,456.25,318.75,45 +triangle,475.0,287.5,456.25,318.75,493.75,318.75,46 +triangle,475.0,287.5,512.5,287.5,493.75,318.75,47 +triangle,512.5,287.5,493.75,318.75,531.25,318.75,48 +triangle,268.75,318.75,250.0,350.0,287.5,350.0,49 +triangle,268.75,318.75,306.25,318.75,287.5,350.0,50 +triangle,306.25,318.75,287.5,350.0,325.0,350.0,51 +triangle,306.25,318.75,343.75,318.75,325.0,350.0,52 +triangle,343.75,318.75,325.0,350.0,362.5,350.0,53 +triangle,343.75,318.75,381.25,318.75,362.5,350.0,54 +triangle,381.25,318.75,362.5,350.0,400.0,350.0,55 +triangle,381.25,318.75,418.75,318.75,400.0,350.0,56 +triangle,418.75,318.75,400.0,350.0,437.5,350.0,57 +triangle,418.75,318.75,456.25,318.75,437.5,350.0,58 +triangle,456.25,318.75,437.5,350.0,475.0,350.0,59 +triangle,456.25,318.75,493.75,318.75,475.0,350.0,60 +triangle,493.75,318.75,475.0,350.0,512.5,350.0,61 +triangle,493.75,318.75,531.25,318.75,512.5,350.0,62 +triangle,531.25,318.75,512.5,350.0,550.0,350.0,63 +triangle,250.0,350.0,231.25,381.25,268.75,381.25,64 +triangle,250.0,350.0,287.5,350.0,268.75,381.25,65 +triangle,287.5,350.0,268.75,381.25,306.25,381.25,66 +triangle,287.5,350.0,325.0,350.0,306.25,381.25,67 +triangle,325.0,350.0,306.25,381.25,343.75,381.25,68 +triangle,325.0,350.0,362.5,350.0,343.75,381.25,69 +triangle,362.5,350.0,343.75,381.25,381.25,381.25,70 +triangle,362.5,350.0,400.0,350.0,381.25,381.25,71 +triangle,400.0,350.0,381.25,381.25,418.75,381.25,72 +triangle,400.0,350.0,437.5,350.0,418.75,381.25,73 +triangle,437.5,350.0,418.75,381.25,456.25,381.25,74 +triangle,437.5,350.0,475.0,350.0,456.25,381.25,75 +triangle,475.0,350.0,456.25,381.25,493.75,381.25,76 +triangle,475.0,350.0,512.5,350.0,493.75,381.25,77 +triangle,512.5,350.0,493.75,381.25,531.25,381.25,78 +triangle,512.5,350.0,550.0,350.0,531.25,381.25,79 +triangle,550.0,350.0,531.25,381.25,568.75,381.25,80 +triangle,231.25,381.25,212.5,412.5,250.0,412.5,81 +triangle,231.25,381.25,268.75,381.25,250.0,412.5,82 +triangle,268.75,381.25,250.0,412.5,287.5,412.5,83 +triangle,268.75,381.25,306.25,381.25,287.5,412.5,84 +triangle,306.25,381.25,287.5,412.5,325.0,412.5,85 +triangle,306.25,381.25,343.75,381.25,325.0,412.5,86 +triangle,343.75,381.25,325.0,412.5,362.5,412.5,87 +triangle,343.75,381.25,381.25,381.25,362.5,412.5,88 +triangle,381.25,381.25,362.5,412.5,400.0,412.5,89 +triangle,381.25,381.25,418.75,381.25,400.0,412.5,90 +triangle,418.75,381.25,400.0,412.5,437.5,412.5,91 +triangle,418.75,381.25,456.25,381.25,437.5,412.5,92 +triangle,456.25,381.25,437.5,412.5,475.0,412.5,93 +triangle,456.25,381.25,493.75,381.25,475.0,412.5,94 +triangle,493.75,381.25,475.0,412.5,512.5,412.5,95 +triangle,493.75,381.25,531.25,381.25,512.5,412.5,96 +triangle,531.25,381.25,512.5,412.5,550.0,412.5,97 +triangle,531.25,381.25,568.75,381.25,550.0,412.5,98 +triangle,568.75,381.25,550.0,412.5,587.5,412.5,99 +triangle,212.5,412.5,193.75,443.75,231.25,443.75,100 +triangle,212.5,412.5,250.0,412.5,231.25,443.75,101 +triangle,250.0,412.5,231.25,443.75,268.75,443.75,102 +triangle,250.0,412.5,287.5,412.5,268.75,443.75,103 +triangle,287.5,412.5,268.75,443.75,306.25,443.75,104 +triangle,287.5,412.5,325.0,412.5,306.25,443.75,105 +triangle,325.0,412.5,306.25,443.75,343.75,443.75,106 +triangle,325.0,412.5,362.5,412.5,343.75,443.75,107 +triangle,362.5,412.5,343.75,443.75,381.25,443.75,108 +triangle,362.5,412.5,400.0,412.5,381.25,443.75,109 +triangle,400.0,412.5,381.25,443.75,418.75,443.75,110 +triangle,400.0,412.5,437.5,412.5,418.75,443.75,111 +triangle,437.5,412.5,418.75,443.75,456.25,443.75,112 +triangle,437.5,412.5,475.0,412.5,456.25,443.75,113 +triangle,475.0,412.5,456.25,443.75,493.75,443.75,114 +triangle,475.0,412.5,512.5,412.5,493.75,443.75,115 +triangle,512.5,412.5,493.75,443.75,531.25,443.75,116 +triangle,512.5,412.5,550.0,412.5,531.25,443.75,117 +triangle,550.0,412.5,531.25,443.75,568.75,443.75,118 +triangle,550.0,412.5,587.5,412.5,568.75,443.75,119 +triangle,587.5,412.5,568.75,443.75,606.25,443.75,120 \ No newline at end of file diff --git a/color.py b/color.py index 3232311..5f84cdf 100644 --- a/color.py +++ b/color.py @@ -1,44 +1,34 @@ """ Color -Color class that can be used interchangably as RGB or HSV org RGBW or HEX with -seamless translation. Use whichever is more convenient at the -time - RGB for familiarity, HSV to fade colors easily, RGBW as the new hotness +Color class that allows you to initialize a color in any of HSV, HSL, HSI, RGB, Hex color spaces. Once initialized, the corresponding RGBW values are calculated and you may modify the object in RGB or HSV color spaces( ie: by re-setting any component of HSV or RGB (ie, just resetting the R value) and all RGB/HSV/RGBW values will be recalculated. As of now, you can not work in RGBW directly as we have not written the conversions from RGBW back to one of the standard color spaces. (annoying, but so it goes). -The Color object takes rgb, hsv, rgbw, hex constructors and represents them all as hsv, doing the appropriate transformations when you wish to pull back any of the color types supported. This meanst the core data was stored as a 3 member hsv tuple. To enable RGBW support, I had to bolt on a 4th member of the tuple, holding the W value. Since all of our code emits colors as RGB(W now), I accepted this bit of dirty business b/c the shows we write will expect tuples of 4 to map properly to our pixes. The RGBW tuples will have the correct W value, even if the 4th value in HSV makes no sense (if you wish to grab the valid hsv tuple, you may still do so with tuple()[0:3] -So, when constructing Color objects, beside the original constructor signature, I've added 2 optional fields: -w=int (0 by default) -recalculate_w = boolean (True by default) - -w is set to zero by default, but will be calculated for the RGB values generated during the Color object construction. UNLESS you set recalculate_w=False, in which case it will be set to zero and remain zero even if you update the Color object - -This is the cool part of the Color object, once created, you have RGB & HSV values available to you interchangibly. If you change Color.r = 255, the corresponding Color.hsv will aslo change. If you have set recalculate_w == True, then when you reset any color value after object creation, the new appropriate RGBW w value will be calc'd and set. If set to False, it will remain as you set it. If you directly change the w value, it will not trigger recalculation. You may also at any time use Color.recalculate_w(True/False) to globally turn on/off w recalculation. - -So how does this all work with our models? Our original models expected a 3 member tuple of (r,g,b) per LED. Now, all LEDs expect a 4 member tuple(r,g,b,w). So, all of the existing shows that use RGB/HEX/HSV native color objects will send the correct (r,g,b,w) tuple now. - -I DID NOT make the choice to make this module support 3 chanel LEDs at this time. +The main goal of this class is to translate various color spaces into RGBW for use in RGBW pixels. +NOTE! this package will not control 3 channel RGB LEDs properly. +The color representation is maintained in HSV interanlly and translated to RGB (and RGBW, but only for retrieval). +Use whichever is more convenient at the time - RGB for familiarity, HSV to fade colors easily. RGB values range from 0 to 255 -HSV values range from 0.0 to 1.0 -RGBW values range from 0 to 255 +HSV values range from 0.0 to 1.0 *Note the H value has been normalized to range between 0-1 in instead of 0-360 to allow for easier cycling of values. +HSL/HSI values range from 0-360 for H, 0-1 for S/[L|I] - >>> red = RGB(255, 0 ,0) - >>> green = HSV(0.33, 1.0, 1.0) - >>> ref = RGBW(255,0,0,85) *More on how W is dealt with latter + >>> red = RGB(255, 0 ,0) (RGBW = 255,0,0,0) + >>> green = HSV(0.33, 1.0, 1.0) (RGBW = 5, 254, 0, 0) + >>> fuschia = RGB(180, 48, 229) (RGBW = 130, 0 , 182, 47) Colors may also be specified as hexadecimal string: >>> blue = Hex('#0000ff') -All three RGBW, RGB and HSV components are available as attributes +Both RGB and HSV components are available as attributes and may be set. - >>> red.r + >>> red.rgb_r 255 - >>> red.g = 128 + >>> red.rgb_g = 128 >>> red.rgb (255, 128, 0) @@ -50,7 +40,7 @@ >>> red = RGB(255,0,0) >>> purple = red.copy() - >>> purple.b = 255 + >>> purple.rgb_b = 255 >>> red.rgb (255, 0, 0) >>> purple.rgb @@ -67,54 +57,27 @@ ... print col.rgb ... col.v -= 0.1 ... - (0, 255, 0, 85) - (0, 229, 0, 76) - (0, 204, 0, 68) - (0, 178, 0, 59) - (0, 153, 0, 51) - (0, 127, 0, 42) - (0, 102, 0, 34) - (0, 76, 0, 25) - (0, 51, 0, 17) - (0, 25, 0, 8) -NOTE the W value is also calculated as it is expected to be used. - + (0, 255, 0) + (0, 229, 0) + (0, 204, 0) + (0, 178, 0) + (0, 153, 0) + (0, 127, 0) + (0, 102, 0) + (0, 76, 0) + (0, 51, 0) + (0, 25, 0) -RGBW Handling -To keep the core functionality of this module, and include support for RGBW, I increased the expected tuple from 3 to 4 in length, with the last member of the tuple being the W value (in RGB space). +RGBW -Since the core of this module uses HSV as the reference point (THANKS GREG!), I had to do a LOT of hoop jumping to make this all work. +To get the (r,g,b,w) tuples back from a Color object, simpy call Color.rgbw and you will return the (r,g,b,w) tuple. -Basically, you can instantiate any of the non-RGBW types as usual and they will automatically calculate the appropriate W value. This recalculation can get tricky, but more on that in a moment. - -So, to set red: - r = = RGB(255,0,0) - ...the w value will be caluclated and appended - print.rgb - (255, 0, 0, 85) - -I h """ import colorsys +import math from copy import deepcopy -__all__=['RGB', 'HSV', 'Hex', 'Color', 'RGBW'] - -def saturation(rgb): - low = float(min(rgb.r, rgb.g, rgb.b)) - high = float(max(rgb.r, rgb.g, rgb.b)) - ret = 0 - if low > 0 and high > 0: - ret = round(100.0*((high-low)/high)) - return ret - -def getWhiteColor(rgb): - ret = 0 - try: - ret = int(round((255.0-saturation(rgb)) / 255.0 * (float(rgb.r) + float(rgb.b) + float(rgb.g))/3.0)) - except: - print("Error", rgb) - return ret +__all__=['RGB', 'HSV', 'Hex', 'Color', 'HSI', 'RGBW'] def clamp(val, min_value, max_value): "Restrict a value between a minimum and a maximum value" @@ -122,105 +85,277 @@ def clamp(val, min_value, max_value): def is_hsv_tuple(hsv): "check that a tuple contains 3 values between 0.0 and 1.0" - return len(hsv) == 4 and all([(0.0 <= t <= 1.0) for t in hsv[0:3]]) + return len(hsv) == 3 and all([(0.0 <= t <= 1.0) for t in hsv]) -def is_rgb_tuple(rgb): - "check that a tuple contains 3 values between 0 and 255" - return len(rgb) == 4 and all([(0 <= t <= 255) for t in rgb]) +def is_hsi_hsl_tuple(hsi): + ret = True + if len(hsi) != 3: + ret = False + if hsi[0] < 0 or hsi[0] > 360: + ret = False + if hsi[1] <0.0 or hsi[1] > 1.0: + ret = False + if hsi[2] <0.0 or hsi[2] > 1.0: + ret = False + + return(ret) def is_rgbw_tuple(rgbw): - "check for valid RGBW tuple" + "check that rgbw tuple is as expected" return len(rgbw) == 4 and all([(0 <= t <= 255) for t in rgbw]) +def is_rgb_tuple(rgb): + "check that a tuple contains 3 values between 0 and 255" + return len(rgb) == 3 and all([(0 <= t <= 255) for t in rgb]) + def rgb_to_hsv(rgb): "convert a rgb[0-255] tuple to hsv[0.0-1.0]" f = float(255) - ret = list(colorsys.rgb_to_hsv(rgb[0]/f, rgb[1]/f, rgb[2]/f)) - ret.append(rgb[-1]) - return tuple(ret) + return colorsys.rgb_to_hsv(rgb[0]/f, rgb[1]/f, rgb[2]/f) -def rgbw_to_hsv(rgbw): - "convert a rgbw[0:3][0-255] tuple to hsv[0.0-1.0], plus w" - f = float(255) - ret = colorsys.rgb_to_hsv(rgbw[0]/f, rgbw[1]/f, rgbw[2]/f) - ret.append(rgbw[-1]) - return tuple(ret) - -def hsv_to_rgbw(hsv): +def hsv_to_rgb(hsv): assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) - _rgb = colorsys.hsv_to_rgb(*tuple(hsv[0:3])) + _rgb = colorsys.hsv_to_rgb(*hsv) r = int(_rgb[0] * 0xff) g = int(_rgb[1] * 0xff) b = int(_rgb[2] * 0xff) - return (r, g, b, hsv[-1]) + return (r,g,b) + +def constrain(val, min, max): + ret = val + if val <= min: + ret = min + if val >= max: + ret=max + return ret + +#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white +def hsi2rgb(H,S,I): + r = 0.0 + g = 0.0 + b = 0.0 + + H = math.fmod(H,360.0) + H = 3.14159*H/180.0 + S = constrain(S, 0.0,1.0) + I = constrain(I, 0.0,1.0) + + if H < 2.09439: + r = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + g = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + b = 255.0*I/3.0*(1.0-S) + elif H < 4.188787: + H = H - 2.09439 + g = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + b = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + r = 255.0*I/3.0*(1.0-S) + else: + H = H - 4.188787 + b = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + r = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + g = 255.0*I/3.0*(1.0-S) + + return ( constrain(int(r*3.0),0,255), constrain(int(g*3.0),0,255), constrain(int(b*3.0)\ +,0,255 )) #for some reason, the rgb numbers need to be X3... + + +#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white +def hsi2rgb(H,S,I): + r = 0.0 + g = 0.0 + b = 0.0 + + H = math.fmod(H,360.0) + H = 3.14159*H/180.0 + S = constrain(S, 0.0,1.0) + I = constrain(I, 0.0,1.0) + + if H < 2.09439: + r = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + g = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + b = 255.0*I/3.0*(1.0-S) + elif H < 4.188787: + H = H - 2.09439 + g = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + b = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + r = 255.0*I/3.0*(1.0-S) + else: + H = H - 4.188787 + b = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + r = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + g = 255.0*I/3.0*(1.0-S) + + return ( constrain(int(r*3.0),0,255), constrain(int(g*3.0),0,255), constrain(int(b*3.0)\ +,0,255 )) #for some reason, the rgb numbers need to be X3... + + +#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white +def hsi2rgbw(H,S,I): + r = 0 + g = 0 + b = 0 + w = 0 + cos_h = 0.0 + cos_1047_h = 0.0 + + H = float(math.fmod(H,360)) # cycle H around to 0-360 degrees + H = 3.14159*H/180.0 # Convert to radians. + S = constrain(S,0.0,1.0) + I = constrain(I,0.0,1.0) + + if(H < 2.09439): + cos_h = math.cos(H) + cos_1047_h = math.cos(1.047196667-H) + r = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) + g = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) + b = 0.0 + w = 255.0*(1.0-S)*I + elif(H < 4.188787): + H = H - 2.09439 + cos_h = math.cos(H) + cos_1047_h = math.cos(1.047196667-H) + g = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) + b = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) + r = 0.0 + w = 255.0*(1.0-S)*I + else: + H = H - 4.188787 + cos_h = math.cos(H) + cos_1047_h = math.cos(1.047196667-H) + b = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) + r = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) + g = 0.0 + w = 255.0*(1.0-S)*I + + return (int(constrain(r*3,0,255)), int(constrain(g*3,0,255)), int(constrain(b*3,0,255)) , int(constrain(w,0,255))) #for some reason, the rgb numbers need to be X3... + + +#https://en.wikipedia.org/wiki/HSL_and_HSV +def hsv2hsl(h,s,v): + h = constrain(h,0.0,360.0) + s = constrain(s,0.0,1.0) + v = constrain(v,0.0,1.0) + + Hhsl = h + Lhsl = v-(v*s/2.0) + Shsl = 0 + if Lhsl > 0.0 and Lhsl < 1.0: + Shsl = (v-Lhsl)/min(Lhsl, 1.0-Lhsl) + + return(Hhsl,Lhsl,Shsl) + + +#https://en.wikipedia.org/wiki/HSL_and_HSV +def hsl2hsv(h,s,l): + h =constrain(h,0.0,360.0) + s =constrain(s,0.0,1.0) + l =constrain(l,0.0,1.0) + + Hhsv = h + Vhsv = l + (s*min(l, 1.0-l)) + Shsv = 0 + if Vhsv > 0.0: + Shsv = 2.0-(2.0*l/Vhsv) + return(Hhsv,Shsv,Vhsv) + + +#https://en.wikipedia.org/wiki/HSL_and_HSV +def rgb2hsi(r, g, b): + r = constrain(float(r)/255.0,0.0,1.0) + g = constrain(float(g)/255.0, 0.0,1.0) + b = constrain(float(b)/255.0,0.0,1.0) + intensity = 0.33333*(r+g+b) + + M = max(r,g,b) + m = min(r,g,b) + C = M - m + + saturation = 0.0 + if intensity == 0.0: + saturation = 0.0 + else: + saturation = (1.0-(m/intensity)) + + hue = 0 + if M == m: + hue = 0 + if M == r: + if M == m: + hue = 0.0 + else: + hue = 60.0* (0.0 + ((g-b) / (M-m))) + if M == g: + if M == m: + hue = 0.0 + else: + hue = 60.0* (2.0 + ((b-r) / (M-m))) + if M == b: + if M == m: + hue = 0.0 + else: + hue = 60.0 * (4.0 + ((r-g) / (M-m))) + if hue < 0.0: + hue = hue + 360 + return(hue,abs(saturation),intensity) -def hsv_to_rgb(hsv): - assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) -# from IPython import embed; embed() - _rgb = colorsys.hsv_to_rgb(*tuple(hsv[0:3])) - r = int(_rgb[0] * 0xff) - g = int(_rgb[1] * 0xff) - b = int(_rgb[2] * 0xff) - return (r, g, b, hsv[-1]) +def RGBW(r, g, b, w): + "Create RGBW color" + raise Exception("Gotcha! We can't yet reverse calculate RGBW back to any other color spaces.... work in one of the other spaces and get your RGBW values back from Color.rgbw. Sorry.") -def RGBW(r,g,b,w, recalculate_w=True): - "Create new RGBW Color" - t = (r, g, b, w) - assert is_rgbw_tuple(t) - return(Color(rgb_to_hsv(t), recalculate_w)) -def RGB(r,g,b,w=0, recalculate_w=True): +def HSI(h,s,i): + "Create new HSI color" + t = (h,s,i) + assert is_hsi_hsl_tuple(t) + return RGB( hsi2rgb(h,s,i) ) + +def RGB(r,g,b, x=False): "Create a new RGB color" - t = (r, g, b, w) + t = (r,g,b) assert is_rgb_tuple(t) - return Color(rgb_to_hsv(t), recalculate_w) + return Color(rgb_to_hsv(t), x) -def HSV(h,s,v, w=0.0, recalculate_w=True): +def HSV(h,s,v, x=False): "Create a new HSV color" - return Color((h, s, v, w), recalculate_w) + return Color((h,s,v),x) + +def HSL(h,s,l): + "Create new HSL color" + t = (h,s,l) + assert is_hsi_hsl_tuple(t) + (h,s,v) = hsl2hsv(t[0], t[1], t[2]) + print(h,s,v) + return Color((constrain(h/360.0,0.0,1.0),s,v)) -def Hex(value, recalculate_w=True): +def Hex(value): "Create a new Color from a hex string" value = value.lstrip('#') lv = len(value) - rgb_t = (int(value[i:i+lv/3], 16) for i in range(0, lv, lv/3)) - r = next(rgb_t) - g = next(rgb_t) - b = next(rgb_t) - return RGB(r, g, b, 0, recalculate_w=True) #JEM -not sure what to do here + rgb_t = (int(value[i:i+int(lv/3)], 16) for i in range(0, lv, int(lv/3))) + return RGB(*rgb_t) -class Color(object): - - def __init__(self, hsv_tuple, recalculate_w=True): - self.recalculate_w = recalculate_w +class Color: + def __init__(self, hsv_tuple, only_rgb=False): self._set_hsv(hsv_tuple) - + self.only_rgb = only_rgb + + def __repr__(self): + return "rgb=%s hsv=%s" % (self.rgb, self.hsv) def copy(self): return deepcopy(self) - def _set_hsv(self, hsv_tuple, preserve_w=False): + def _set_hsv(self, hsv_tuple): assert is_hsv_tuple(hsv_tuple) + # convert to a list for component reassignment self.hsv_t = list(hsv_tuple) - #I'm trting to solve the problem of the c - if preserve_w is True: - pass - elif self.recalculate_w is True: - new_w = int(getWhiteColor(self)) - l = list(hsv_tuple) - l[-1] = new_w - hsv_tuple = tuple(l) - self.hsv_t = list(hsv_tuple) - else: - pass #all done - @property def rgbw(self): - "returns a rgbw[0-255] tuple" - return hsv_to_rgbw(self.hsv_t) - + "returns a tuple of 4 values each in the range of 0-255" + hsi = rgb2hsi(self.rgb[0], self.rgb[1], self.rgb[2]) + return hsi2rgbw( hsi[0], hsi[1], hsi[2] ) @property def rgb(self): @@ -236,6 +371,13 @@ def hsv(self): def hex(self): "returns a hexadecimal string" return '#%02x%02x%02x' % self.rgb + + @property + def hsl(self): + "returns HSL tuple" + (h,s,l) = hsv2hsl(self.hsv_t[0], self.hsv_t[1], self.hsv_t[2]) + h = constrain(h*360.0, 0.0,360.0) + return (h,s,l) """ Properties representing individual HSV compnents @@ -249,7 +391,6 @@ def h(self): @h.setter def h(self, val): - assert 0.0 <= val <= 1.0 v = clamp(val, 0.0, 1.0) self.hsv_t[0] = round(v, 8) @@ -259,7 +400,6 @@ def s(self): @s.setter def s(self, val): - assert 0.0 <= val <= 1.0 v = clamp(val, 0.0, 1.0) self.hsv_t[1] = round(v, 8) @@ -269,73 +409,84 @@ def v(self): @v.setter def v(self, val): - assert 0.0 <= val <= 1.0 v = clamp(val, 0.0, 1.0) - new_hsv = self.hsv_t - new_hsv[2] = round(v, 8) - self._set_hsv(new_hsv) + self.hsv_t[2] = round(v, 8) + - """ - Properties representing individual RGB components + + """ + Properties representing individual RGB components """ @property - def r(self): + def rgb_r(self): return self.rgb[0] - @r.setter - def r(self, val): + @rgb_r.setter + def rgb_r(self, val): assert 0 <= val <= 255 - r, g, b, w = self.rgb - new = (val, g, b, w) + r,g,b = self.rgb + new = (val, g, b) assert is_rgb_tuple(new) self._set_hsv(rgb_to_hsv(new)) @property - def g(self): + def rgb_g(self): return self.rgb[1] - @g.setter - def g(self, val): + @rgb_g.setter + def rgb_g(self, val): assert 0 <= val <= 255 - r, g, b, w = self.rgb - new = (r, val, b, w) + r,g,b = self.rgb + new = (r, val, b) assert is_rgb_tuple(new) self._set_hsv(rgb_to_hsv(new)) @property - def b(self): + def rgb_b(self): return self.rgb[2] - @b.setter - def b(self, val): + @rgb_b.setter + def rgb_b(self, val): assert 0 <= val <= 255 - r, g, b, w = self.rgb - new = (r, g, val, w) + r,g,b = self.rgb + new = (r, g, val) assert is_rgb_tuple(new) self._set_hsv(rgb_to_hsv(new)) + """ + Properties representing individual RGBW components + """ @property - def w(self): - return int(self.rgbw[-1]) + def r(self): + if self.only_rgb: + return self.rgb[0] + else: + return self.rgbw[0] - @w.setter - def w(self, val): - assert 0 <= val <= 255 - r, g, b, w = self.rgbw - new = (r, g, b, val) - self._set_hsv(rgb_to_hsv(new), preserve_w=True) + @property + def g(self): + if self.only_rgb: + return self.rgb[1] + else: + return self.rgbw[1] + + @property + def b(self): + if self.only_rgb: + return self.rgb[2] + else: + return self.rgbw[2] @property - def recalculate_w(self): - return self._recalculate_w + def w(self): + if self.only_rgb: + return 0 + else: + return self.rgbw[3] - @recalculate_w.setter - def recalculate_w(self, val=True): - self._recalculate_w = val - - - if __name__=='__main__': import doctest doctest.testmod() + + diff --git a/color2.py b/color2.py deleted file mode 100644 index f09d319..0000000 --- a/color2.py +++ /dev/null @@ -1,278 +0,0 @@ -"""JEM Playing Around With RGB<->HSI<->RGBW conversion formulas""" -""" Butchered the original color.py module to do this work, please do not consider this as production useful""" - -import colorsys -import math -import numpy as np -from copy import deepcopy - -__all__=['RGB', 'HSV', 'Hex', 'Color', 'RGBW'] - - -def constrain(val, min, max): - ret = val - if val <= min: - ret = min - if val >= max: - ret=max - return ret - - -def is_hsv_tuple(hsv): - "check that a tuple contains 3 values between 0.0 and 1.0" - return len(hsv) == 4 and all([(0.0 <= t <= 1.0) for t in hsv[0:3]]) - - -def is_rgb_tuple(rgb): - "check that a tuple contains 3 values between 0 and 255" - return len(rgb) == 4 and all([(0 <= t <= 255) for t in rgb]) - - -def is_rgbw_tuple(rgbw): - "check for valid RGBW tuple" - return len(rgbw) == 4 and all([(0 <= t <= 255) for t in rgbw]) - - -def test_hsi2rgbw(h,s,i): - (r,g,b,w) = hsi2rgbw(h,s,i) - print("ORIG-HSI: {0} / {1} / {2} ||| HSI: {3} / {4} / {5} ".format(h,s,i,r,g,b,w)) - - -def test_rgb2hsi2rgb(r,g,b): - (h,s,i) = rgb2hsi(r,g,b) - print("ORIGRGB: {0} / {1} / {2} ||| HSI: {3} / {4} / {5} ".format(r,g,b,h,s,i)) - (rr,gg,bb) = hsi2rgbA(h,s,i) - print("CONV RGB: {0} / {1} / {2} ||| HSI: {3} / {4} / {5} ".format(rr,gg,bb,h,s,i)) - - -def rgb_hsi_tests(): - colors = { - 'white' : ((1,1,1),(1.0,1.0,1.0)), #ok - 'olive' : ((.75,.75,0),(60.0,1.0,.5)), #ok - 'teal' : ((.5,1.0,1.0),(180.0,.4,.833)), #ok - 'purple' : ((.75,.25,.75),(300.0, .571, .583)), #ok - 'blue2' : ((.255,.104, .918),(251.1,.756,.426)), #ok - 'green2': ((.116,.675,.255),(134.9, .667, .349)), # ok - 'orange' : ((.931,.463,.316),(14.3, .446, .570)), #ok - 'yellow' : ((.998,.974,.532),(56.9, .363 ,.835)), #ok - 'dusty_blue' : ((.495,.493,.721),(240.5, .135, .570 )), #ok - 'dark' : ((0.0,0.0,0.0),(0.0,0.0,0.0)) #ok - } - - for i in colors: - print(i) - r,g,b = colors[i][0] - R = constrin(r*255.0,0,255) - G = constrain(g*255.0,0,255) - B = constrain(b*255.0,0,255) - - (h,s,i) = colors[i][1] - (H2,S2,I2) = rgb2hsi(R,G,B) - print("EXPECTED:: R: {0} / G: {1} / B: {2} || H: {3} / S: {4} / I: {5}".format(R,G,B,h,s,i)) - print("Calculated: ..........................................h= {0} / s= {1} / i {2}".format(round(H2,4),round(S2,4),round(I2,4))) - - -#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white -def hsi2rgb(H,S,I): - r = 0.0 - g = 0.0 - b = 0.0 - - H = math.fmod(H,360.0) - H = 3.14159*H/180.0 - S = constrain(S, 0.0,1.0) - I = constrain(I, 0.0,1.0) - - if H < 2.09439: - r = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) - g = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) - b = 255.0*I/3.0*(1.0-S) - elif H < 4.188787: - H = H - 2.09439 - g = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) - b = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) - r = 255.0*I/3.0*(1.0-S) - else: - H = H - 4.188787 - b = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) - r = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) - g = 255.0*I/3.0*(1.0-S) - - return ( constrain(int(r*3.0),0,255), constrain(int(g*3.0),0,255), constrain(int(b*3.0),0,255 )) #for some reason, the rgb numbers need to be X3... - - -#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white -def hsi2rgbw(H,S,I): - r = 0 - g = 0 - b = 0 - w = 0 - cos_h = 0.0 - cos_1047_h = 0.0 - - H = float(math.fmod(H,360)) # cycle H around to 0-360 degrees - H = 3.14159*H/180.0 # Convert to radians. - S = constrain(S,0.0,1.0) - I = constrain(I,0.0,1.0) - - if(H < 2.09439): - cos_h = math.cos(H) - cos_1047_h = math.cos(1.047196667-H) - r = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) - g = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) - b = 0.0 - w = 255.0*(1.0-S)*I - elif(H < 4.188787): - H = H - 2.09439 - cos_h = math.cos(H) - cos_1047_h = math.cos(1.047196667-H) - g = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) - b = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) - r = 0.0 - w = 255.0*(1.0-S)*I - else: - H = H - 4.188787 - cos_h = math.cos(H) - cos_1047_h = math.cos(1.047196667-H) - b = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) - r = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) - g = 0.0 - w = 255.0*(1.0-S)*I - - return (r*3,g*3,b*3,w) #for some reason, the rgb numbers need to be X3... - - - -def rgb2hsi(red,green,blue): - r = constrain(float(red)/255.0,0.0,1.0) - g = constrain(float(green)/255.0, 0.0,1.0) - b = constrain(float(blue)/255.0,0.0,1.0) - intensity = 0.33333*(r+g+b) - - M = max(r,g,b) - m = min(r,g,b) - C = M - m - - saturation = 0.0 - if intensity == 0.0: - saturation = 0.0 - else: - saturation = (1-(m/intensity)) - - hue = 0 - if M == m: - hue = 0 - if M == r: - print("A",M,m) - if M == m: - hue = 0.0 - else: - hue = 60.0* (0.0 + ((g-b) / (M-m))) - if M == g: - print("B") - if M == m: - hue = 0.0 - else: - hue = 60.0* (2.0 + ((b-r) / (M-m))) - if M == b: - print("C") - if M == m: - hue = 0.0 - else: - hue = 60.0 * (4.0 + ((r-g) / (M-m))) - if hue < 0.0: - print("D") - hue = hue + 360 - - return(hue,abs(saturation),intensity) - - - - -def HSI(h,s,i): - return Color(0,0,0,0) - - -def RGBW(r,g,b,w): - "Create new RGBW Color" - t = (r,g,b,w) - assert is_rgbw_tuple(t) - return(Color(t)) - -def RGB(r,g,b): - "Create a new RGB color" - return Color(hsi2rgbw(rgb2hsi(r,g,b))) - - - -def Hex(value): - "Create a new Color from a hex string" - value = value.lstrip('#') - lv = len(value) - rgb_t = (int(value[i:i+lv/3], 16) for i in range(0, lv, lv/3)) - r = rgb_t.next() - g = rgb_t.next() - b = rgb_t.next() - return (r,g,b) - - -class Color(object): - - def __init__(self, rgbw_tuple): -# from IPython import embed; embed() - print(rgbw_tuple) - self.rgbw = rgbw_tuple - - - def copy(self): - return deepcopy(self) - - - - def rgbw(self): - "returns a rgbw[0-255] tuple" - return self.rgbw - - - - """ - Properties representing individual RGBW components - """ - @property - def r(self): - return self.rgbw[0] - - @r.setter - def r(self, val): - self.rgbw[0] = val - - @property - def g(self): - return self.rgbw[1] - - @g.setter - def g(self, val): - self.rgbw[1] = val - - @property - def b(self): - return self.rgbw[2] - - @b.setter - def b(self, val): - self.rgbw[2] = val - - @property - def w(self): - return rgbw[3] - - @w.setter - def w(self,val): - self.rgbw[3] = val - - - - -if __name__=='__main__': - import doctest - doctest.testmod() diff --git a/data/pixel_map.json b/data/pixel_map.json deleted file mode 100644 index 79f6129..0000000 --- a/data/pixel_map.json +++ /dev/null @@ -1,258 +0,0 @@ -{ -"1": [1, 1], -"2": [1, 5], -"3": [1, 9], -"4": [1, 13], -"5": [1, 17], -"6": [1, 21], -"7": [1, 25], -"8": [1, 29], -"9": [1, 33], -"10": [1, 37], -"11": [1, 41], -"12": [1, 45], -"13": [1, 49], -"14": [1, 53], -"15": [1, 57], -"16": [1, 61], -"17": [1, 65], -"18": [1, 69], -"19": [1, 73], -"20": [1, 77], -"21": [1, 81], -"22": [1, 85], -"23": [1, 89], -"24": [1, 93], -"25": [1, 97], -"26": [1, 101], -"27": [1, 105], -"28": [1, 109], -"29": [1, 113], -"30": [1, 117], -"31": [1, 121], -"32": [1, 125], -"33": [1, 129], -"34": [1, 133], -"35": [1, 137], -"36": [1, 141], -"37": [1, 145], -"38": [1, 149], -"39": [1, 153], -"40": [1, 157], -"41": [1, 161], -"42": [1, 165], -"43": [1, 169], -"44": [1, 173], -"45": [1, 177], -"46": [1, 181], -"47": [1, 185], -"48": [1, 189], -"49": [1, 193], -"50": [1, 197], -"51": [1, 201], -"52": [1, 205], -"53": [1, 209], -"54": [1, 213], -"55": [1, 217], -"56": [1, 221], -"57": [1, 225], -"58": [1, 229], -"59": [1, 233], -"60": [1, 237], -"61": [1, 241], -"62": [1, 245], -"63": [1, 249], -"64": [1, 253], -"65": [1, 257], -"66": [1, 261], -"67": [1, 265], -"68": [1, 269], -"69": [1, 273], -"70": [1, 277], -"71": [1, 281], -"72": [1, 285], -"73": [1, 289], -"74": [1, 293], -"75": [1, 297], -"76": [1, 301], -"77": [1, 305], -"78": [1, 309], -"79": [1, 313], -"80": [1, 317], -"81": [1, 321], -"82": [1, 325], -"83": [1, 329], -"84": [1, 333], -"85": [1, 337], -"86": [1, 341], -"87": [1, 345], -"88": [1, 349], -"89": [1, 353], -"90": [1, 357], -"91": [1, 361], -"92": [1, 365], -"93": [1, 369], -"94": [1, 373], -"95": [1, 377], -"96": [1, 381], -"97": [1, 385], -"98": [1, 389], -"99": [1, 393], -"100": [1, 397], -"101": [1, 401], -"102": [1, 405], -"103": [1, 409], -"104": [1, 413], -"105": [1, 417], -"106": [1, 421], -"107": [1, 425], -"108": [1, 429], -"109": [1, 433], -"110": [1, 437], -"111": [1, 441], -"112": [1, 445], -"113": [1, 449], -"114": [1, 453], -"115": [1, 457], -"116": [1, 461], -"117": [1, 465], -"118": [1, 469], -"119": [1, 473], -"120": [1, 477], -"121": [1, 481], -"122": [1, 485], -"123": [1, 489], -"124": [1, 493], -"125": [1, 497], -"126": [1, 501], -"127": [1, 505], -"128": [1, 509], -"129": [2, 1 ], -"130": [2, 5], -"131": [2, 9], -"132": [2, 13], -"133": [2, 17], -"134": [2, 21], -"135": [2, 25], -"136": [2, 29], -"137": [2, 33], -"138": [2, 37], -"139": [2, 41], -"140": [2, 45], -"141": [2, 49], -"142": [2, 53], -"143": [2, 57], -"144": [2, 61], -"145": [2, 65], -"146": [2, 69], -"147": [2, 73], -"148": [2, 77], -"149": [2, 81], -"150": [2, 85], -"151": [2, 89], -"152": [2, 93], -"153": [2, 97], -"154": [2, 101], -"155": [2, 105], -"156": [2, 109], -"157": [2, 113], -"158": [2, 117], -"159": [2, 121], -"160": [2, 125], -"161": [2, 129], -"162": [2, 133], -"163": [2, 137], -"164": [2, 141], -"165": [2, 145], -"166": [2, 149], -"167": [2, 153], -"168": [2, 157], -"169": [2, 161], -"170": [2, 165], -"171": [2, 169], -"172": [2, 173], -"173": [2, 177], -"174": [2, 181], -"175": [2, 185], -"176": [2, 189], -"177": [2, 193], -"178": [2, 197], -"179": [2, 201], -"180": [2, 205], -"181": [2, 209], -"182": [2, 213], -"183": [2, 217], -"184": [2, 221], -"185": [2, 225], -"186": [2, 229], -"187": [2, 233], -"188": [2, 237], -"189": [2, 241], -"190": [2, 245], -"191": [2, 249], -"192": [2, 253], -"193": [2, 257], -"194": [2, 261], -"195": [2, 265], -"196": [2, 269], -"197": [2, 273], -"198": [2, 277], -"199": [2, 281], -"200": [2, 285], -"201": [2, 289], -"202": [2, 293], -"203": [2, 297], -"204": [2, 301], -"205": [2, 305], -"206": [2, 309], -"207": [2, 313], -"208": [2, 317], -"209": [2, 321], -"210": [2, 325], -"211": [2, 329], -"212": [2, 333], -"213": [2, 337], -"214": [2, 341], -"215": [2, 345], -"216": [2, 349], -"217": [2, 353], -"218": [2, 357], -"219": [2, 361], -"220": [2, 365], -"221": [2, 369], -"222": [2, 373], -"223": [2, 377], -"224": [2, 381], -"225": [2, 385], -"226": [2, 389], -"227": [2, 393], -"228": [2, 397], -"229": [2, 401], -"230": [2, 405], -"231": [2, 409], -"232": [2, 413], -"233": [2, 417], -"234": [2, 421], -"235": [2, 425], -"236": [2, 429], -"237": [2, 433], -"238": [2, 437], -"239": [2, 441], -"240": [2, 445], -"241": [2, 449], -"242": [2, 453], -"243": [2, 457], -"244": [2, 461], -"245": [2, 465], -"246": [2, 469], -"247": [2, 473], -"248": [2, 477], -"249": [2, 481], -"250": [2, 485], -"251": [2, 489], -"252": [2, 493], -"253": [2, 497], -"254": [2, 501], -"255": [2, 505], -"256": [2, 509] -} diff --git a/go_tri.py b/go_tri.py index 7bf1938..9472b2b 100644 --- a/go_tri.py +++ b/go_tri.py @@ -7,16 +7,19 @@ import threading import cherrypy -from model.simulator import SimulatorModel +from grid import Geometry, Grid +from model import sACN, SimulatorModel +import netifaces import osc_serve -import triangle_grid import shows import util -from web.web import TriangleWeb +from web import TriangleWeb # Prints stack trace on failure faulthandler.enable() +logger = logging.getLogger("pyramidtriangles") + def speed_interpolation(val): """ @@ -34,14 +37,15 @@ def speed_interpolation(val): else: return hi_interp(val) -low_interp = util.make_interpolater(0.0, 0.5, 2.0, 1.0) -hi_interp = util.make_interpolater(0.5, 1.0, 1.0, 0.5) + +low_interp = util.util.make_interpolater(0.0, 0.5, 2.0, 1.0) +hi_interp = util.util.make_interpolater(0.5, 1.0, 1.0, 0.5) class ShowRunner(threading.Thread): - def __init__(self, model, queue, max_showtime=240, fail_hard=True): + def __init__(self, grid, queue, max_showtime=240, fail_hard=True): super(ShowRunner, self).__init__(name="ShowRunner") - self.model = model + self.grid = grid self.queue = queue self.fail_hard = fail_hard @@ -91,7 +95,7 @@ def process_command(self, msg): if isinstance(msg, str): if msg == "shutdown": self.running = False - logging.info("ShowRunner shutting down") + logger.info("ShowRunner shutting down") elif msg == "clear": self.clear() time.sleep(2) @@ -103,7 +107,7 @@ def process_command(self, msg): self.max_show_time = int(msg.split(':')[1]) elif isinstance(msg, tuple): - logging.debug(f'OSC: {msg}') + logger.debug(f'OSC: {msg}') (addr, val) = msg addr = addr.split('/z')[0] @@ -131,8 +135,8 @@ def process_command(self, msg): print("ignoring unknown msg:", str(msg)) def clear(self): - """Clears contained model.""" - self.model.clear() + """Clears contained grid.""" + self.grid.clear() def next_show(self, name=None): show = None @@ -140,16 +144,16 @@ def next_show(self, name=None): if name in self.shows: show = self.shows[name] else: - logging.warning(f'unknown show: {name}') + logger.warning(f'unknown show: {name}') if not show: - logging.info("choosing random show") + logger.info("choosing random show") (name, show) = next(self.randseq) self.clear() self.prev_show = self.show - self.show = show(self.model) + self.show = show(self.grid) print(f'next show: {name}') self.framegen = self.show.next_frame() self.show_params = hasattr(self.show, 'set_param') @@ -174,7 +178,7 @@ def run(self): self.check_queue() d = self.get_next_frame() - self.model.go() + self.grid.go() if d: real_d = d * self.speed_x time.sleep(real_d) @@ -188,7 +192,7 @@ def run(self): self.next_show() except Exception: - logging.exception("unexpected exception in show loop!") + logger.exception("unexpected exception in show loop!") if self.fail_hard: raise else: @@ -199,14 +203,14 @@ def osc_listener(q, port=5700): """Create the OSC Listener thread""" listen_address = ('0.0.0.0', port) - logging.info(f'Starting OSC Listener on {listen_address}') + logger.info(f'Starting OSC Listener on {listen_address}') osc_serve.create_server(listen_address, q) class TriangleServer(object): - def __init__(self, tri_model, args): + def __init__(self, grid, args): self.args = args - self.tri_model = tri_model + self.grid = grid self.queue = queue.LifoQueue() @@ -225,24 +229,25 @@ def _create_services(self): try: osc_listener(self.queue) except Exception: - logging.warning("Can't create OSC listener", exc_info=True) + logger.warning("Can't create OSC listener", exc_info=True) # Show runner - self.runner = ShowRunner(self.tri_model, self.queue, args.max_time, fail_hard=args.fail_hard) + self.runner = ShowRunner( + self.grid, self.queue, args.max_time, fail_hard=args.fail_hard) if args.shows: print("setting show:", args.shows[0]) self.runner.next_show(args.shows[0]) def start(self): if self.running: - logging.warning("start() called, but tri_grid is already running!") + logger.warning("start() called, but tri_grid is already running!") return try: self.runner.start() self.running = True except Exception: - logging.exception("Exception starting tri_grid!!") + logger.exception("Exception starting tri_grid!!") def stop(self): if self.running: # should be safe to call multiple times @@ -254,11 +259,11 @@ def stop(self): self.running = False except Exception: - logging.exception("Exception stopping tri_grid!!") + logger.exception("Exception stopping tri_grid!!") def go_headless(self): """Run without the web interface""" - logging.info("Running without web interface") + logger.info("Running without web interface") try: while True: time.sleep(999) # control-c breaks out of time.sleep @@ -269,7 +274,7 @@ def go_headless(self): def go_web(self): """Run with the web interface""" - logging.info("Running with web interface") + logger.info("Running with web interface") show_names = [name for (name, cls) in shows.load_shows()] print(f'shows: {show_names}') @@ -293,16 +298,22 @@ def go_web(self): if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s') + console = logging.StreamHandler() + console.setFormatter(logging.Formatter('%(levelname)s - %(message)s')) + logger.addHandler(console) parser = argparse.ArgumentParser(description='Triangle Light Control') + parser.add_argument('-r', '--rows', type=int, + default=11, help='Rows per panel') parser.add_argument('--max-time', type=float, default=float(60), help='Maximum number of seconds a show will run (default 60)') + parser.add_argument('--bind', help='Local address to use for sACN') parser.add_argument('--simulator', dest='simulator', action='store_true') - parser.add_argument('--list', action='store_true', help='List available shows') + parser.add_argument('--list', action='store_true', + help='List available shows') parser.add_argument('shows', metavar='show_name', type=str, nargs='*', help='name of show (or shows) to run') parser.add_argument('--fail-hard', type=bool, default=True, @@ -311,28 +322,44 @@ def go_web(self): args = parser.parse_args() if args.list: - logging.info("Available shows: %s", ', '.join([name for (name, cls) in shows.load_shows()])) + logger.info("Available shows: %s", ', '.join( + [name for (name, cls) in shows.load_shows()])) sys.exit(0) if args.simulator: sim_host = "localhost" sim_port = 4444 - logging.info(f'Using TriSimulator at {sim_host}:{sim_port}') + logger.info(f'Using TriSimulator at {sim_host}:{sim_port}') - model = SimulatorModel(sim_host, port=sim_port, model_json='./data/pixel_map.json') - triangle_grid = triangle_grid.make_tri(model, 5) + model = SimulatorModel(sim_host, port=sim_port) else: - logging.info("Starting SACN") - from model.sacn_model import sACN - model = sACN(max_dmx=800, model_json="./data/pixel_map.json") + bind = args.bind + if not bind: + gateways = netifaces.gateways()[netifaces.AF_INET] + + for _, interface, _ in gateways: + for a in netifaces.ifaddresses(interface).get(netifaces.AF_INET, []): + if a['addr'].startswith('192.168.0'): + logger.info( + f"Auto-detected Pyramid local IP: {a['addr']}") + bind = a['addr'] + break + if bind: + break + + if not bind: + parser.error( + 'Failed to auto-detect local IP. Are you on Pyramid Scheme wifi or ethernet?') + + logger.info("Starting sACN") + model = sACN(bind, args.rows) - triangle_grid = triangle_grid.make_tri(model, 3) + app = TriangleServer(Grid(model, Geometry(args.rows)), args) - app = TriangleServer(triangle_grid, args) try: app.start() # start related service threads app.go_web() # enter main blocking event loop except Exception: - logging.exception("Unhandled exception running TRI!") + logger.exception("Unhandled exception running TRI!") finally: app.stop() diff --git a/grid/__init__.py b/grid/__init__.py new file mode 100644 index 0000000..0d2fa61 --- /dev/null +++ b/grid/__init__.py @@ -0,0 +1,7 @@ + +from .cell import Address, Cell, Direction, Position, Orientation, Coordinate +from .geom import Geometry +from .grid import Grid, Location, Pixel, Query, Selector +from .select import (every, on_edge, left_edge, right_edge, bottom_edge, inset, pointed, + pointed_up, pointed_down, edge_neighbors, vertex_neighbors, hexagon) +from .traversal import sweep, left_to_right, right_to_left diff --git a/grid/cell.py b/grid/cell.py new file mode 100644 index 0000000..708ca7e --- /dev/null +++ b/grid/cell.py @@ -0,0 +1,276 @@ +from enum import IntEnum +from functools import lru_cache +from itertools import chain +from typing import List, Mapping, Optional, NamedTuple + +from .geom import Geometry + + +class Orientation(IntEnum): + POINT_UP = 1 + POINT_DOWN = -1 + + def invert(self) -> "Orientation": + return Orientation.POINT_UP if self is Orientation.POINT_DOWN else Orientation.POINT_DOWN + + +class Direction(IntEnum): + LEFT_TO_RIGHT = 1 + NATURAL = 0 + RIGHT_TO_LEFT = -1 + + def natural_for(self, orientation: Orientation) -> bool: + """ + Returns true if the order of pixel addresses within the cell + matches the desired direction. + """ + + return self is Direction.NATURAL or self == orientation + + +class Cell(NamedTuple): + """ + A Cell stores the properties of a "cell" (mini-triangle) within one of + the panels. + """ + + position: "Position" + orientation: Orientation + addresses: List["Address"] + + row_count = 11 #why is this hard coded? (jem) + + + def pixel_addresses(self, direction: Direction = Direction.LEFT_TO_RIGHT) -> List["Address"]: + return (self.addresses + if direction.natural_for(self.orientation) + else reversed(self.addresses)) + + @property + def coordinate(self) -> "Coordinate": + return Coordinate.from_pos(self.position, self.geom) + + @property + def row(self) -> int: + return self.position.row + + @property + def col(self) -> int: + return self.position.col + + @property + def id(self) -> int: + return self.position.id + + @property + def above(self) -> Optional["Position"]: + return self.position.adjust(row=-1, col=-1) if self.row > 0 else None + + @property + def below(self) -> Optional["Position"]: + return self.position.adjust(row=1, col=1) if self.row + 1 < self.geom.rows else None + + @property + def left(self) -> Optional["Position"]: + return self.position.adjust(col=-1) if self.col > 0 else None + + @property + def right(self) -> Optional["Position"]: + return self.position.adjust(col=1) if self.col + 1 < self.geom.row_length(self.row) else None + + @property + def is_up(self) -> bool: + return self.orientation is Orientation.POINT_UP + + @property + def is_down(self) -> bool: + return self.orientation is Orientation.POINT_DOWN + + @property + def is_edge(self) -> bool: + return self.is_left_edge or self.is_right_edge or self.is_bottom_edge + + @property + def is_left_edge(self) -> bool: + """Returns True if cell is along the left edge of the greater triangle.""" + return self.col == 0 + + @property + def is_right_edge(self) -> bool: + """Returns True if cell is along the right edge of the greater triangle.""" + return self.col + 1 == self.geom.row_length(self.row) + + @property + def is_bottom_edge(self) -> bool: + """Returns True if cell is along the bottom edge of the greater triangle.""" + return self.row + 1 == self.geom.rows and self.is_up + + @property + def is_top_corner(self) -> bool: + """Returns True if cell is the top corner of the greater triangle.""" + return self.id == 0 + + @property + def is_right_corner(self) -> bool: + """Returns True if cell is the right corner of the greater triangle.""" + return self.is_bottom_edge and self.is_right_edge + + @property + def is_left_corner(self) -> bool: + """Returns True if cell is the left corner of the greater triangle.""" + return self.is_bottom_edge and self.is_left_edge + + def __hash__(self): + return hash((type(self), self.position)) + + +class Position(NamedTuple): + """ + Position is (row, column) where the top row is 0, and every row begins with column 0. + + Position(0, 0) is the apex of the triangle. + """ + row: int + col: int + + @classmethod + @lru_cache(maxsize=512) + def from_id(cls, id: int) -> "Position": + row_below = 1 + while Geometry.triangular_number(row_below) <= id: + row_below += 1 + + row = row_below - 1 + col = id - Geometry.triangular_number(row) + return cls(row, col) + + @property + def id(self) -> int: + return Geometry.triangular_number(self.row) + self.col + + def adjust(self, row: int = 0, col: int = 0) -> "Position": + return type(self)(self.row + row, self.col + col) + + +class Coordinate(NamedTuple): + """ + Coordinate is (x, y) such that the left-most, bottom triangle is (0, 0). + + For Coordinate(x, y), the triangle above is Coordinate(x, y + 1), left is Coordinate(x - 1, y), below is + Coordinate(x, y - 1), and right is Coordinate(x + 1, y). + + Coordinate differs from `Position`. Position(0, 0) is the apex of the whole triangle, whereas Coordinate(0, 0) + refers to the left corner. + + Coordinates are a different way to spatially reason than Position. + """ + x: int + y: int + + @classmethod + def from_pos(cls, pos: Position, geom: Geometry) -> "Coordinate": + y = geom.rows - 1 - pos.row + x = pos.col + y + return cls(x, y) + + def pos(self, geom: Geometry): + return Position(geom.rows - 1 - self.y, self.x - self.y) + + +class Address(NamedTuple): + """ + Address refers to a cell's DMX address within one of the + triangle panels. + """ + + universe: int + offset: int + + @property + def next(self) -> "Address": + next_offset = self.offset + 4 + if next_offset >= universe_size(self.universe): + return Address(self.universe + 1, 0) + + return Address(self.universe, next_offset) + + def skip(self, n: int) -> "Address": + addr = self + for _ in range(n): + addr = addr.next + + return addr + + def range(self, len: int) -> List["Address"]: + addrs = [self] + for _ in range(len - 1): + addrs.append(addrs[-1].next) + + return addrs + + +def universe_count(row_count: int, start: Address = Address(1, 4)) -> int: + return row_count # XXX(lyra): I don't think this is real + + +def universe_size(universe_id: int) -> int: + """ + Gives the number of connected channels in each DMX universe. + + Every third universe is shorter. (Also, not all channels are + visible; some correspond to the strip segments between rows.) + """ + + return 512 if universe_id % 3 != 0 else (44 * 4) + + +def generate(geom: Geometry, start: Address = Address(1, 4)) -> Mapping[Position, Cell]: + cells = {} + for row in range(geom.rows - 1, -1, -1): + row_mapping = mouth(geom, row, start) + cells.update(row_mapping) + + end_address = max(chain.from_iterable( + cell.addresses for cell in row_mapping.values())) + start = end_address.next + + return cells + + +def mouth(geom: Geometry, row: int, start: Address) -> Mapping[Position, Cell]: + up = up_teeth(geom, row, start, row + 1) + last_up_address = up[max(up)].addresses[-1] + first_after_gap = last_up_address.range(11)[-1] + down = down_teeth(geom, row, first_after_gap, row) + + return {**up, **down} + + +def up_teeth(geom: Geometry, row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, Cell]: + cells = {} + addr = start + + for i in range(length): + pos = Position(row, i * 2) + addrs = addr.range(pixels_per_cell) + cells[pos] = Cell(pos, Orientation.POINT_UP, addrs, geom) + + addr = addrs[-1].next + + return cells + + +def down_teeth(geom: Geometry, row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, Cell]: + cells = {} + addr = start + + col = length * 2 - 1 + for _ in range(length): + pos = Position(row, col) + addrs = addr.range(pixels_per_cell) + cells[pos] = Cell(pos, Orientation.POINT_DOWN, addrs, geom) + + col -= 2 + addr = addrs[-1].next + + return cells diff --git a/grid/geom.py b/grid/geom.py new file mode 100644 index 0000000..aa17eeb --- /dev/null +++ b/grid/geom.py @@ -0,0 +1,29 @@ +from typing import NamedTuple + + +class Geometry(NamedTuple): + """ + Geometry represents the dimensions of a panel or side of the car. + """ + + rows: int + + def row_length(self, n: int) -> int: + if n < 0 or n >= self.rows: + raise IndexError(f'row {n} out of range ({self.rows} rows total)') + + return (n + 1) * 2 - 1 + + def midpoint(self, row: int) -> int: + length = self.row_length(row) + return int(length - (length / 2)) + + @property + def cell_count(self) -> int: + return sum(self.row_length(i) for i in range(self.rows)) + + @staticmethod + def triangular_number(n: int) -> int: + """Returns the number of elements in an equilateral triangle of n rows.""" + # Typically the triangle number is (n(n+1))/2 but our triangle has rows of 1, 3, 5... + return n ** 2 diff --git a/grid/grid.py b/grid/grid.py new file mode 100644 index 0000000..8536073 --- /dev/null +++ b/grid/grid.py @@ -0,0 +1,126 @@ +import logging +from typing import Callable, Iterator, Iterable, List, Mapping, NamedTuple, Union, Type + +from color import Color, RGB +from model import ModelBase +from .cell import generate, Address, Cell, Direction, Position, Coordinate +from .geom import Geometry + +logger = logging.getLogger('pyramidtriangles') + +Location = Union[Coordinate, Position, int] + +Query = Callable[['Grid'], Iterable[Cell]] +Selector = Union[Location, + Cell, + Iterable[Cell], + Query] + + +class Pixel(NamedTuple): + cell: Cell + address: Address + model: Type[ModelBase] + + def set(self, color: Color): + self.model.set(self.cell, self.address, color) + + def __call__(self, color: Color): + self.set(color) + + +class Grid(Mapping[Location, Cell]): + geom: Geometry + _model: Type[ModelBase] + _cells: List[Cell] + + def __init__(self, model: Type[ModelBase], geom: Geometry = Geometry(rows=11)): + if geom.rows < 1: + raise ValueError(f'Geometry(rows={geom.rows}) is invalid') + + self.geom = geom + self._model = model + + cells_by_id = {cell.id: cell + for cell in generate(geom).values()} + self._cells = [cells_by_id[i] for i in range(len(cells_by_id))] + + @property + def row_count(self) -> int: + return self.geom.rows + + @property + def cells(self) -> List[Cell]: + return list(self._cells) + + def select(self, sel: Selector) -> Iterable[Cell]: + if isinstance(sel, (int, Coordinate, Position)): + cells = [self[sel]] + elif isinstance(sel, Cell): + cells = [sel] + elif isinstance(sel, Iterable) and not isinstance(self, str): + cells = sel + elif callable(sel): + cells = sel(self) + else: + raise TypeError(f'invalid Cell selector {sel}') + + return cells + + def pixels(self, sel: Selector, direction: Direction = Direction.NATURAL) -> Iterator[Pixel]: + """ + Yield the settable pixels of one or more cells. + """ + + for cell in self.select(sel): + for addr in cell.pixel_addresses(direction): + yield Pixel(cell, addr, self._model) + + def set(self, sel: Selector, color: Color): + for pixel in self.pixels(sel): + pixel.set(color) + + def go(self): + """ + Flush the underlying model (render its current state). + """ + self._model.go() + + def clear(self, color: Color = RGB(0, 0, 0)): + self.set(self.cells, color) + self.go() + + def __getitem__(self, loc: Location) -> Cell: + if isinstance(loc, Position): + cell_id = loc.id + elif isinstance(loc, Coordinate): + cell_id = loc.pos(self.geom).id + else: + cell_id = loc + + if cell_id < 0: + raise KeyError(cell_id) + + try: + cell = self._cells[cell_id] + except IndexError: + raise KeyError(cell_id) + else: + if isinstance(loc, Position) and loc != cell.position: + logger.warning('got wrong cell: expected %r, got %r', + loc, cell.position) + raise KeyError(loc) + elif isinstance(loc, Coordinate) and loc != cell.coordinate: + logger.warning('got wrong cell: expected %r, got %r', + loc, cell.coordinate) + raise KeyError(loc) + return cell + + def __iter__(self): + return (cell.position for cell in self._cells) + + def __len__(self) -> int: + return len(self._cells) + + def __repr__(self): + return f'<{type(self).__name__} rows={self.row_count} {self._model}>' diff --git a/grid/select.py b/grid/select.py new file mode 100644 index 0000000..7e88fa5 --- /dev/null +++ b/grid/select.py @@ -0,0 +1,207 @@ +from typing import Iterable, List, NamedTuple, Sequence + +from .cell import Cell, Orientation +from .grid import Grid, Position, Query, Location + + +def query(grid: Grid, q: Query) -> Iterable[Cell]: + return q(grid) + + +def every(grid: Grid) -> List[Cell]: + return grid.cells + + +def on_edge(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_edge] + + +def left_edge(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_left_edge] + + +def right_edge(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_right_edge] + + +def bottom_edge(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_bottom_edge] + + +def inset(distance: int) -> Query: + """ + Selects an inner triangle, `distance` cells away from the edges. + """ + + def query(grid: Grid) -> List[Cell]: + # find the top point + top_row = distance * 2 + top_col = grid.geom.midpoint(top_row) + cells = {grid[Position(top_row, top_col)]} + edge_cells = set() + + bottom_row = grid.row_count - distance - 1 + for prev_row in range(top_row, bottom_row): + prev_cells = [c for c in cells if c.row == prev_row] + edge_cells = {min(prev_cells), max(prev_cells)} + midpoint = grid.geom.midpoint(prev_row) + + for cell in edge_cells: + below = cell.below + if below is None: + continue + + # TODO(lyra): grid[grid[]] ugh + if cell.col <= midpoint: + cells.add(grid[below]) + cells.add(grid[grid[below].left]) + if cell.col >= midpoint: + cells.add(grid[below]) + cells.add(grid[grid[below].right]) + + if not edge_cells: + return [] + + for col in range(min(edge_cells).col, max(edge_cells).col + 1): + cells.add(grid[Position(bottom_row, col)]) + + return list(cells) + + return query + + +def pointed(orientation: Orientation) -> Query: + def query(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.orientation is orientation] + + return query + + +def pointed_up(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_up] + + +def pointed_down(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_down] + + +class Neighbors(NamedTuple): + left: Cell + middle: Cell + right: Cell + + +def edge_neighbors(loc: Location) -> Query: + """ + Queries a tuple of (left, middle, right) cells that share an edge with the given (row, column) cell. + + Left neighbor is the edge directly to the left of the cell, regardless of up/down orientation. + Middle neighbor is either the top or bottom neighbor depending where the edge is. + Right neighbor is the cell immediately to the right. + """ + + def query(grid: Grid) -> Neighbors: + cell = grid.get(loc) + if cell is None: + return Neighbors(None, None, None) + + pos = cell.position + + left = grid.get(pos.adjust(0, -1)) + middle = (grid.get(pos.adjust(1, 1)) + if cell.is_up + else grid.get(pos.adjust(-1, -1))) + right = grid.get(pos.adjust(0, 1)) + + return Neighbors(left, middle, right) + + return query + + +def vertex_neighbors(loc: Location) -> Query: + """ + Queries a tuple of (left, middle, right) cells that share a vertex with the given (row, column) cell. + + Left neighbor is the cell opposite the left vertex of the given cell. + Middle neighbor is the cell opposite the middle (up or down) vertex of the given cell. + Right neighbor is the cell opposite the right vertex of the given cell. + """ + + def query(grid: Grid) -> Neighbors: + cell = grid.get(loc) + if cell is None: + return Neighbors(None, None, None) + + pos = cell.position + + if cell.is_up: + left = grid.get(pos.adjust(1, -1)) + middle = grid.get(pos.adjust(-1, -1)) + right = grid.get(pos.adjust(1, 3)) + else: + left = grid.get(pos.adjust(-1, -2)) + middle = grid.get(pos.adjust(1, 1)) + right = grid.get(pos.adjust(-1, 1)) + + return Neighbors(left, middle, right) + + return query + + +def hexagon(base_loc: Location) -> Query: + """ + Selector for hexagon pattern of cells surrounding a starting cell. + + Given a starting location, returns a function + func(Grid) -> [Neighbor, Neighbor, Neighbor, Neighbor, Neighbor, Neighbor] + The neighbors, surrounding cells, make a hexagon with the starting location cell as the base. For neighbor cells + off the grid, 'None' is returned. + + For example, starting with an up-facing cell, numbered with 1's here, there would be a total of 6 neighbors in the + hexagon (including cell 1), in a clock-wise order. Here's an attempt at a drawing of neighbors 1, 2, 3, 4, 5, and 6: + + 5 444 3 + 555 4 333 + 666 1 222 + 6 111 2 + """ + def hexagon_query(grid: Grid) -> Sequence[Cell]: + # Helper function that returns edge neighbors (sharing a wall with) of a given cell. + def neighbors(cell) -> Neighbors: + return Neighbors(*query(grid, edge_neighbors(cell.id))) + + btm_cell = grid[base_loc] + a, b, c, d, e, f = btm_cell, None, None, None, None, None + + if a.is_left_edge: + b = neighbors(a).right + if b is not None: + c = neighbors(b).middle + if c is not None: + d = neighbors(c).left + if d is not None: + e = neighbors(d).left + if e is not None: + f = neighbors(e).middle + + elif a.is_right_edge: + f = neighbors(a).left + if f is not None: + e = neighbors(f).middle + if e is not None: + d = neighbors(e).right + if d is not None: + c = neighbors(d).right + if c is not None: + b = neighbors(c).middle + else: + b = neighbors(a).right + c = neighbors(b).middle + d = neighbors(c).left + e = neighbors(d).left + f = neighbors(e).middle + + hexa = list(filter(None, [a, b, c, d, e, f])) + return hexa + + return hexagon_query diff --git a/grid/traversal.py b/grid/traversal.py new file mode 100644 index 0000000..3a14bbc --- /dev/null +++ b/grid/traversal.py @@ -0,0 +1,45 @@ + +from typing import Iterator, Sequence + +from .cell import Direction, Position +from .grid import Geometry + + +def sweep(direction: Direction, row_count: int) -> Iterator[Sequence[Position]]: + """ + Generates a left-to-right or right-to-left vertical sequence of coordinates. + """ + + if direction is Direction.NATURAL: + raise ValueError('Direction.NATURAL is invalid with traversal.sweep()') + if not row_count > 0: + raise ValueError(f'traversal requires row_count({row_count}) > 0') + + geom = Geometry(rows=row_count) + + row_lengths = range(geom.row_length(row_count - 1)) + if direction == Direction.RIGHT_TO_LEFT: + row_lengths = reversed(row_lengths) + + for bottom_column in row_lengths: + row_to_start = row_count - 1 - bottom_column + + coordinates = [] + for curr_column in range(bottom_column + 1): + curr_row = row_to_start + curr_column + if not 0 <= curr_row < row_count: + continue + if curr_column >= geom.row_length(curr_row): + continue + + coordinates.append(Position(curr_row, curr_column)) + + yield coordinates + + +def left_to_right(row_count: int) -> Iterator[Sequence[Position]]: + return sweep(Direction.LEFT_TO_RIGHT, row_count) + + +def right_to_left(row_count: int) -> Iterator[Sequence[Position]]: + return sweep(Direction.RIGHT_TO_LEFT, row_count) diff --git a/investigate.py b/investigate.py new file mode 100644 index 0000000..8e5328f --- /dev/null +++ b/investigate.py @@ -0,0 +1,40 @@ +from itertools import cycle +import pprint +import time + +from model import sACN, demo_triangle_mapping +import grid +from grid.cells import CELLS, Position + +model = sACN(model_json="./data/pixel_map.json", + pixelmap=demo_triangle_mapping()) +tri = grid.make_triangle(model, 2) + +tri.go() + +colors = cycle(range(3)) +for cell in sorted(CELLS): + print(cell) + co = next(colors) + + for addr in CELLS[cell]: + model.leds[addr.universe][addr.offset + co] = 128 + model.leds[addr.universe][addr.offset + 3] = 64 + + tri.go() + time.sleep(0.1) + + +# for i in range(511, 0, -4): +# model.leds[3][i] = 200 +# tri.go() +# time.sleep(0.02) + +# for d in range(1, 13): +# u = model.leds[d] +# u[3] = 200 +# if d > 1: +# model.leds[d - 1][510] = 200 +# model.leds[d - 1][505] = 200 +# model.leds[d - 1][500] = 200 +# tri.go() diff --git a/model/__init__.py b/model/__init__.py new file mode 100644 index 0000000..69e194e --- /dev/null +++ b/model/__init__.py @@ -0,0 +1,6 @@ +# These imports include submodules under the `model` namespace (e.g. model.SimulatorModel is available). +from .base import ModelBase +from .mirror import MirrorModel +# from .ola_model import OLAModel +from .sacn_model import sACN +from .simulator import SimulatorModel diff --git a/model/base.py b/model/base.py new file mode 100644 index 0000000..44225e3 --- /dev/null +++ b/model/base.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod +from color import Color +from grid.cell import Address, universe_count, universe_size, Cell +from typing import List, Mapping + + +class ModelBase(ABC): + """Abstract base class for simulators.""" + + @abstractmethod + def set(self, cell: Cell, addr: Address, color: Color): + """ + Set one pixel to a particular color. + + addr is an Address, except in the case of the simulator it is a cell ID. + """ + + @abstractmethod + def go(self): + """Flush all buffered data out to devices.""" + + +def map_leds(row_count: int) -> Mapping[int, List[int]]: + count = universe_count(row_count) + + return {i + 1: [0] * universe_size(i + 1) for i in range(count)} diff --git a/model/mirror.py b/model/mirror.py index 43ede90..1b6dddc 100644 --- a/model/mirror.py +++ b/model/mirror.py @@ -1,25 +1,23 @@ -""" -Proof of concept that dealing with mirroring across multiple backend models is best done at this layer. -Completely agnostic as to what the cell ids look like, they are just passed through to the underlying model. -""" -from .modelbase import ModelBase +from color import Color +from grid.cell import Address, Cell +from .base import ModelBase class MirrorModel(ModelBase): + """ + Proof of concept that dealing with mirroring across multiple backend models is best done at this layer. + """ + def __init__(self, *models): - self.models = [] - if models: - for m in models: - self.add_model(m) + self.models = list(models) def add_model(self, model): self.models.append(model) - # Model basics - def set_pixel(self, pixel, color, cellid=None): + def set(self, cell: Cell, addr: Address, color: Color): for m in self.models: - m.set_pixel(pixel, color, cellid) + m.set(addr, color) def go(self): for m in self.models: diff --git a/model/modelbase.py b/model/modelbase.py deleted file mode 100644 index 47d59d6..0000000 --- a/model/modelbase.py +++ /dev/null @@ -1,13 +0,0 @@ -from abc import ABC, abstractmethod - - -class ModelBase(ABC): - """Abstract base class for simulators.""" - - @abstractmethod - def set_pixel(self, pixel, color, cell_id): - """Set the color for a pixel.""" - - @abstractmethod - def go(self): - """Flush all buffered data out to devices.""" diff --git a/model/ola_model.py b/model/ola_model.py index fed3f39..5ece519 100644 --- a/model/ola_model.py +++ b/model/ola_model.py @@ -7,17 +7,24 @@ """ import array import json +import logging +from typing import Iterator + import ola +from .base import ModelBase +from .mapping import PixelMap -from .modelbase import ModelBase +logger = logging.getLogger("pyramidtriangles") +# XXX(lyra): this is no longer a valid ModelBase class OLAModel(ModelBase): - def __init__(self, max_dmx, model_json=None): + def __init__(self, max_dmx, model_json: str, pixelmap: PixelMap): # XXX any way to check if this is a valid connection? self.PIXEL_MAP = None self._map_leds(model_json) + self._pixelmap = pixelmap self.wrapper = ola.ClientWrapper() self.client = self.wrapper.Client() # Keys for LEDs are integers representing universes, each universe has an array of possible DMX channels @@ -41,17 +48,21 @@ def _map_leds(self, f): self.PIXEL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) # Model basics - def set_pixel(self, pixel, color, cellid=None): - if pixel in self.PIXEL_MAP: - ux = self.PIXEL_MAP[pixel][0] + def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: + for pixel in self._pixelmap[cell_id]: + if pixel not in self.PIXEL_MAP: + logger.warning(f'{pixel} not in sACN pixel ID map') + + ux = self.PIXEL_MAP[pixel][0] ix = self.PIXEL_MAP[pixel][1] - 1 # dmx is 1-based, python lists are 0-based - self.leds[ux][ix] = color.g - self.leds[ux][ix+1] = color.r - self.leds[ux][ix+2] = color.b - self.leds[ux][ix+3] = color.w - else: - print(f'WARNING: {pixel} not in pixel ID MAP') + def set_color(color): + self.leds[ux][ix] = color.r + self.leds[ux][ix + 1] = color.g + self.leds[ux][ix + 2] = color.b + self.leds[ux][ix + 3] = color.w + + yield set_color def go(self): data_to_send = {} diff --git a/model/sacn_model.py b/model/sacn_model.py index e9282b9..b83feb7 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -4,60 +4,48 @@ Pixels are representations of the addressable unit in your object. Cells can have multiple pixels in this model only have one LED each. """ -import json +import logging +from typing import Union + import sacn +from color import Color +from grid.cell import Address, Cell +from .base import ModelBase, map_leds -from .modelbase import ModelBase +logger = logging.getLogger("pyramidtriangles") class sACN(ModelBase): - def __init__(self, max_dmx, model_json=None): - # XXX any way to check if this is a valid connection? - - self.sender = sacn.sACNsender(bind_address="192.168.1.210", universeDiscovery=False) # Must supply an IP address to bind to that is in the same subnet as the devices routiung the universes. Might haave to assign a second IP to the eth adapter to get this to work (this is what I had to do on my mac) + def __init__(self, bind_address: str, row_count: int): + self.sender = sacn.sACNsender( + bind_address=bind_address, + universeDiscovery=False, + ) self.sender.start() - self.PIXEL_MAP = None - self.leds = {} # dictionary which will hold an array of 512 int's for each universe, universes are keys to the arrays. - self._map_leds(model_json) - # Keys for LEDs are integers representing universes, each universe has an array of possible DMX channels - # Pixels are an LED represented by 4 DMX addresses + # dictionary which will hold an array of 512 int's for each universe, universes are keys to the arrays. + self.leds = map_leds(row_count) + for universe_index in self.leds: + self.sender.activate_output(universe_index) + self.sender[universe_index].multicast = True def __del__(self): self.sender.stop() # If the object is destructing, close the sender connection - def _map_leds(self, f): - # Loads a json file with mapping info describing your leds. - # The json file is formatted as a dictionary of numbers (as strings sadly, b/c json is weird - # each key in the dict is a fixtureUID. - # each array that fixtureUID returns is of the format [universeUID, DMXstart#] - # initializing just 4 universes!!! Need to make this more configurable. - with open(f, 'r') as json_file: - self.PIXEL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) - - for i in self.PIXEL_MAP: - universe = int(self.PIXEL_MAP[i][0]) - if universe not in self.leds.keys(): - self.sender.activate_output(universe) - self.sender[universe].multicast = True - self.leds[universe] = [0] * 512 - - # Model basics - def set_pixel(self, pixel, color, cellid=None): - if pixel in self.PIXEL_MAP: - ux = self.PIXEL_MAP[pixel][0] - ix = self.PIXEL_MAP[pixel][1] - 1 # dmx is 1-based, python lists are 0-based - - self.leds[ux][ix] = color.g - self.leds[ux][ix+1] = color.r - self.leds[ux][ix+2] = color.b - self.leds[ux][ix+3] = color.w - else: - print(f'WARNING: {pixel} not in pixel ID MAP') - - def set_pixels(self, pixels, color): - for pixel in pixels: - self.set_pixel(pixel, color) + def set(self, cell: Cell, addr: Address, color: Color): + try: + channels = self.leds[addr.universe] + except KeyError: + raise IndexError( + f'attempt to set channel in undefined universe {addr.universe}') + + # our Color tuples have their channels in the same order as sACN + for i, c in enumerate(color.rgbw): + try: + channels[addr.offset + i] = c + except IndexError: + raise IndexError( + f'internal error in sACN model; failed to assign to universe {addr.universe}, address {addr.offset}') def go(self): for ux in self.leds: diff --git a/model/simulator.py b/model/simulator.py index e10cefc..97cdb3d 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -1,74 +1,39 @@ """ Model to communicate with a Simulator over a TCP socket. - -Panels are numbered as strings of the form '12b', indicating 'business' or 'party' side of the sheep. - -XXX Should this class be able to do range checks on cell ids? """ +import logging +import queue import socket -import json -from .modelbase import ModelBase +from color import Color +from grid import Address, Cell +from .base import ModelBase SIM_DEFAULT = (188, 210, 229) # BCD2E5, "off" color for simulator +logger = logging.getLogger("pyramidtriangles") class SimulatorModel(ModelBase): - def __init__(self, hostname, port=4444, debug=False, model_json=None): - self.CELL_MAP = None - self._map_leds(model_json) - - self.server = (hostname, port) - self.debug = debug - self.sock = None - - # map of cells to be set on the next call to go - self.dirty = {} - - self.connect() + def __init__(self, hostname: str, port: int): + self.hostname = hostname + self.port = port - def connect(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.connect(self.server) - # XXX throw an exception if the socket isn't available + self.sock.connect((self.hostname, self.port)) - def __repr__(self): - return "SimulatorModel(%s, port=%d, debug=%s)" % (self.server[0], self.server[1], self.debug) - - # Loaders - def _map_leds(self, f): - # Loads a json file with mapping info describing your leds. - # The json file is formatted as a dictionary of numbers (as strings sadly, b/c json is weird - # each key in the dict is a fixtureUID. - # each array that fixtureUID returns is of the format [universeUID, DMXstart#] - with open(f, 'r') as json_file: - self.CELL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) - - # Model basics - def set_cell(self, cell, color): - cell = cell + 1 # Simulator cells not 0 based - try: - if cell in self.CELL_MAP: - ux = self.CELL_MAP[cell][0] - ix = self.CELL_MAP[cell][1] - 1 - sim_key = cell - # The simulator does not care about universes, but does care about UIDs. I'm manufacturing one by joinng the Universe and fixture ID into the key. - self.dirty[sim_key] = color - else: - print("WARNING: {0} not in cell ID MAP".format(cell)) + # queue of 'dirty' messages to send + self.message_queue = queue.SimpleQueue() - except: - pass + def __repr__(self): + return f'{__class__.__name__} (hostname={self.hostname}, port={self.port})' - def set_pixel(self, pixel, color, cellid): - self.set_cell(cellid, color) + def set(self, cell: Cell, addr: Address, color: Color): + # Enqueue a message to simulator, sets address + msg = f"{str(cell.id)} {','.join(map(str, color.rgb))}\n" + self.message_queue.put(msg) def go(self): - for num in self.dirty: - color = self.dirty[num] - msg = f'b {num} {color.r},{color.g},{color.b}\n' - if self.debug: - print(msg) + while not self.message_queue.empty(): + msg = self.message_queue.get() + logger.debug(msg) self.sock.send(msg.encode()) - - self.dirty = {} diff --git a/osc_serve.py b/osc_serve.py index d24e9e0..4bf7288 100644 --- a/osc_serve.py +++ b/osc_serve.py @@ -7,16 +7,18 @@ THROTTLE_TIME = 0.1 # seconds +logger = logging.getLogger("pyramidtriangles") + def server_test(): - logging.info("Instantiating OSCServer:") + logger.info("Instantiating OSCServer:") osc.osc_startup() osc.osc_udp_server('0.0.0.0', 5700, "main") def printing_handler(addr, tags, stuff, source): msg_string = "%s [%s] %s" % (addr, tags, str(stuff)) - logging.info("OSCServer Got: '%s' from %s", msg_string, source) + logger.info("OSCServer Got: '%s' from %s", msg_string, source) # send a reply to the client. msg = oscbuildparse.OSCMessage("/printed", None, msg_string) @@ -25,13 +27,13 @@ def printing_handler(addr, tags, stuff, source): osc.osc_method("/print", printing_handler, argscheme=oscmethod.OSCARG_ADDRESS + oscmethod.OSCARG_DATAUNPACK) - logging.info("Starting OSC server. Use ctrl-C to quit.") + logger.info("Starting OSC server. Use ctrl-C to quit.") try: while True: osc.osc_process() except KeyboardInterrupt: - logging.info("Closing OSC server") + logger.info("Closing OSC server") osc.osc_terminate() @@ -43,7 +45,7 @@ def handler(addr, tags, data, source): sincelast = now - last_msg[addr] if sincelast >= THROTTLE_TIME: - logging.debug("%s [%s] %s", addr, tags, str(data)) + logger.debug("%s [%s] %s", addr, tags, str(data)) last_msg[addr] = now queue.put((addr, data)) @@ -53,6 +55,4 @@ def handler(addr, tags, data, source): if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s') - server_test() diff --git a/poetry.lock b/poetry.lock index 9b743e8..39dc9e1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -76,6 +76,18 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.4.1" +[[package]] +category = "main" +description = "Color math and conversion library." +name = "colormath" +optional = false +python-versions = "*" +version = "3.0.0" + +[package.dependencies] +networkx = ">=2.0" +numpy = "*" + [[package]] category = "main" description = "Better living through Python with decorators" @@ -171,7 +183,26 @@ description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false python-versions = ">=3.4" -version = "7.1.0" +version = "7.2.0" + +[[package]] +category = "main" +description = "Portable network interface information." +name = "netifaces" +optional = false +python-versions = "*" +version = "0.10.9" + +[[package]] +category = "main" +description = "Python package for creating and manipulating graphs and networks" +name = "networkx" +optional = false +python-versions = ">=3.5" +version = "2.3" + +[package.dependencies] +decorator = ">=4.3.0" [[package]] category = "main" @@ -350,6 +381,17 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" version = "1.12.0" +[[package]] +category = "main" +description = "Color scales and color conversion made easy for Python." +name = "spectra" +optional = false +python-versions = "*" +version = "0.0.11" + +[package.dependencies] +colormath = ">=3.0.0" + [[package]] category = "main" description = "Objects and routines pertaining to date and time (tempora)" @@ -423,7 +465,7 @@ python-versions = ">=2.7" version = "0.5.2" [metadata] -content-hash = "56bbf34095ab39c12e98a9c830cae130df03478dce540375e3cd83f127c3432d" +content-hash = "2fd2a0a06749304d2ee2d6f808d1d2c33573b5059aab4896e635faa500bc38aa" python-versions = "^3.7" [metadata.hashes] @@ -435,6 +477,7 @@ backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", cheroot = ["1593fa2a42b18744ac485aadf5fec4a29ebfee00ba3937a2269b8ffc94447879", "f6a85e005adb5bc5f3a92b998ff0e48795d4d98a0fbb7edde47a7513d4100601"] cherrypy = ["48de31ba3db04c5354a0fcf8acf21a9c5190380013afca746d50237c9ebe70f0", "641b51570158dd301da5d569085c04fe7f2cc98474d103f8137d8dbe36b2f6e7"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] +colormath = ["3d4605af344527da0e4f9f504fad7ddbebda35322c566a6c72e28edb1ff31217"] decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"] importlib-metadata = ["6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", "cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db"] ipython = ["11067ab11d98b1e6c7f0993506f7a5f8a91af420f7e82be6575fcb7a6ca372a0", "60bc55c2c1d287161191cc2469e73c116d9b634cff25fe214a43cba7cec94c79"] @@ -443,7 +486,9 @@ ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c6 jedi = ["53c850f1a7d3cfcd306cc513e2450a54bdf5cacd7604b74e42dd1f0758eaaf36", "e07457174ef7cb2342ff94fa56484fe41cec7ef69b0059f01d3f812379cb6f7c"] jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] -more-itertools = ["3ad685ff8512bf6dc5a8b82ebf73543999b657eded8c11803d9ba6b648986f4d", "8bb43d1f51ecef60d81854af61a3a880555a14643691cc4b64a6ee269c78f09a"] +more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] +netifaces = ["078986caf4d6a602a4257d3686afe4544ea74362b8928e9f4389b5cd262bc215", "0c4304c6d5b33fbd9b20fdc369f3a2fef1a8bbacfb6fd05b9708db01333e9e7b", "2dee9ffdd16292878336a58d04a20f0ffe95555465fee7c9bd23b3490ef2abf3", "3095218b66d359092b82f07c5422293c2f6559cf8d36b96b379cc4cdc26eeffa", "30ed89ab8aff715caf9a9d827aa69cd02ad9f6b1896fd3fb4beb998466ed9a3c", "4921ed406386246b84465950d15a4f63480c1458b0979c272364054b29d73084", "563a1a366ee0fb3d96caab79b7ac7abd2c0a0577b157cc5a40301373a0501f89", "5b3167f923f67924b356c1338eb9ba275b2ba8d64c7c2c47cf5b5db49d574994", "6d84e50ec28e5d766c9911dce945412dc5b1ce760757c224c71e1a9759fa80c2", "755050799b5d5aedb1396046f270abfc4befca9ccba3074f3dbbb3cb34f13aae", "75d3a4ec5035db7478520ac547f7c176e9fd438269e795819b67223c486e5cbe", "7a25a8e28281504f0e23e181d7a9ed699c72f061ca6bdfcd96c423c2a89e75fc", "7cc6fd1eca65be588f001005446a47981cbe0b2909f5be8feafef3bf351a4e24", "86b8a140e891bb23c8b9cb1804f1475eb13eea3dbbebef01fcbbf10fbafbee42", "ad10acab2ef691eb29a1cc52c3be5ad1423700e993cc035066049fa72999d0dc", "b2ff3a0a4f991d2da5376efd3365064a43909877e9fabfa801df970771161d29", "b47e8f9ff6846756be3dc3fb242ca8e86752cd35a08e06d54ffc2e2a2aca70ea", "da298241d87bcf468aa0f0705ba14572ad296f24c4fda5055d6988701d6fd8e1", "db881478f1170c6dd524175ba1c83b99d3a6f992a35eca756de0ddc4690a1940", "f0427755c68571df37dc58835e53a4307884a48dec76f3c01e33eb0d4a3a81d7", "f8885cc48c8c7ad51f36c175e462840f163cb4687eeb6c6d7dfaf7197308e36b", "f911b7f0083d445c8d24cfa5b42ad4996e33250400492080f5018a28c026db2b"] +networkx = ["8311ddef63cf5c5c5e7c1d0212dd141d9a1fe3f474915281b73597ed5f1d4e3d"] numpy = ["0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", "141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", "14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", "27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", "2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", "3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", "52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", "6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", "7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", "7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", "94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", "a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", "ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", "b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", "b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", "cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", "d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", "dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", "dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", "e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", "ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", "f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", "f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7"] ola = ["60d263f99e95da94bbc9baa5737625070c016def4aad52cbaeced9ed9913eab2", "fc96b689000812cf7a2af44cf6c7b4cf1b37944c1ee1860d97eba06aa0cbcf66"] osc4py3 = ["f45364b793270dfec860b399af6b5b361e54d440f14bf43d33b83a0b615a714c"] @@ -462,6 +507,7 @@ pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d74 pywin32 = ["22e218832a54ed206452c8f3ca9eff07ef327f8e597569a4c2828be5eaa09a77", "32b37abafbfeddb0fe718008d6aada5a71efa2874f068bee1f9e703983dcc49a", "35451edb44162d2f603b5b18bd427bc88fcbc74849eaa7a7e7cfe0f507e5c0c8", "4eda2e1e50faa706ff8226195b84fbcbd542b08c842a9b15e303589f85bfb41c", "5f265d72588806e134c8e1ede8561739071626ea4cc25c12d526aa7b82416ae5", "6852ceac5fdd7a146b570655c37d9eacd520ed1eaeec051ff41c6fc94243d8bf", "6dbc4219fe45ece6a0cc6baafe0105604fdee551b5e876dc475d3955b77190ec", "9bd07746ce7f2198021a9fa187fa80df7b221ec5e4c234ab6f00ea355a3baf99"] sacn = ["764d891e90fabf32161d636375558a57d9a830ec53f9e168f464dd5bd8564d51"] six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] +spectra = ["8eb362a5187cb63cee13cd01186799c0c791a3ad3bec57b279132e12521762b8"] tempora = ["cb60b1d2b1664104e307f8e5269d7f4acdb077c82e35cd57246ae14a3427d2d6", "d28a03d2f64ee81aec6e6bff374127ef306fe00c1b7e27c7ff1618344221a699"] traitlets = ["9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"] typing = ["38566c558a0a94d6531012c8e917b1b8518a41e418f7f15f00e129cc80162ad3", "53765ec4f83a2b720214727e319607879fec4acde22c4fbb54fa2604e79e44ce", "84698954b4e6719e912ef9a42a2431407fe3755590831699debda6fba92aac55"] diff --git a/ponzicolor/__init__.py b/ponzicolor/__init__.py new file mode 100644 index 0000000..8ca7e60 --- /dev/null +++ b/ponzicolor/__init__.py @@ -0,0 +1,14 @@ +""" +Color implements RGBW color values, with the RGB portion able to be modeled +in CIE perceptual color spaces. +""" + +# +# a Python port of go-colorful +# Copyright © 2013 Lucas Beyer +# https://github.com/lucasb-eyer/go-colorful +# + +from .color import Color, color, white +from .scale import Scale +from .space import RGB, Lab, HCL diff --git a/ponzicolor/blend.py b/ponzicolor/blend.py new file mode 100644 index 0000000..9b14a33 --- /dev/null +++ b/ponzicolor/blend.py @@ -0,0 +1,23 @@ +from .space import HCL, Lab + +# Blending colors depends on what color space the colors are represented in. + + +def hcl(a: HCL, b: HCL, t: float) -> HCL: + h = _interpolate_angle(a.h, b.h, t) + c = scale(a.c, b.c, t) + l = scale(a.l, b.l, t) + return HCL(h, c, l) + + +def lab(a: Lab, b: Lab, t: float) -> Lab: + return Lab(l=scale(a.l, b.l, t), a=scale(a.a, b.a, t), b=scale(a.b, b.b, t)) + + +def scale(a: float, b: float, t: float) -> float: + return a + t * (b - a) + + +def _interpolate_angle(a: float, b: float, t: float) -> float: + delta = ((b - a) % 360.0 + 540.0) % 360.0 - 180 + return (a + t * delta + 360.0) % 360.0 diff --git a/ponzicolor/color.py b/ponzicolor/color.py new file mode 100644 index 0000000..45170dc --- /dev/null +++ b/ponzicolor/color.py @@ -0,0 +1,124 @@ +from typing import NamedTuple, Optional, Tuple, Union + +from . import blend +from .space import ( + RGB, + Lab, + HCL, + XYZ, + LinearRGB, + hcl_to_lab, + lab_to_hcl, + lab_to_xyz, + xyz_to_lab, + xyz_to_linear_rgb, + linear_rgb_to_xyz, + srgb_to_linear_rgb, + linear_rgb_to_srgb, +) + + +class Color(NamedTuple): + r: float # [0-1] + g: float # [0-1] + b: float # [0-1] + w: float # [0-1] + + @classmethod + def white(cls, w: float) -> "Color": + return cls(0.0, 0.0, 0.0, w) + + @classmethod + def from_hex(cls, h: str) -> "Color": + if h.startswith("#"): + h = h[1:] + if len(h) == 6: + h += "00" + return Color(*tuple(int(h[i : i + 2], 16) / 255 for i in range(0, 8, 2))) + + @classmethod + def from_rgb(cls, rgb: RGB, w: float = 0.0) -> "Color": + return cls(rgb.r, rgb.g, rgb.b, w) + + @classmethod + def from_linear_rgb(cls, rgb: LinearRGB, w: float = 0.0) -> "Color": + return cls.from_rgb(linear_rgb_to_srgb(rgb), w) + + @classmethod + def from_xyz(cls, xyz: XYZ, w: float = 0.0) -> "Color": + return cls.from_linear_rgb(xyz_to_linear_rgb(xyz), w) + + @classmethod + def from_lab(cls, lab: Lab, w: float = 0.0) -> "Color": + return cls.from_xyz(lab_to_xyz(lab), w) + + @classmethod + def from_hcl(cls, hcl: HCL, w: float = 0.0) -> "Color": + return cls.from_lab(hcl_to_lab(hcl), w) + + @property + def valid(self) -> bool: + return all(0.0 <= c <= 1.0 for c in self) + + def clamp(self) -> "Color": + def c(v: float) -> float: + return max(0.0, min(v, 1.0)) + + return Color(c(self.r), c(self.g), c(self.b), c(self.w)) + + def blend(self, other: "Color", bias: float) -> "Color": + return Color.from_hcl( + blend.hcl(self.hcl, other.hcl, bias), w=blend.scale(self.w, other.w, bias) + ) + + @property + def dmx(self) -> Tuple[int, int, int, int]: + def c(v: float) -> int: + return int(v * 255.0 + 0.5) + + return (c(self.r), c(self.g), c(self.b), c(self.w)) + + @property + def hex(self) -> str: + return "#%02X%02X%02X%02X" % self.dmx + + @property + def rgb(self) -> RGB: + return RGB(self.r, self.g, self.b) + + @property + def linear_rgb(self) -> LinearRGB: + return srgb_to_linear_rgb(self.rgb) + + @property + def xyz(self) -> XYZ: + return linear_rgb_to_xyz(self.linear_rgb) + + @property + def lab(self) -> Lab: + return xyz_to_lab(self.xyz) + + @property + def hcl(self) -> HCL: + return lab_to_hcl(self.lab) + + @property + def hue(self) -> float: + return self.hcl.h + + +def color(chroma: Union[str, RGB, Lab, HCL], white: float = 0.0) -> Color: + if isinstance(chroma, str): + return Color.from_hex(chroma) + if isinstance(chroma, HCL): + return Color.from_hcl(chroma, white) + elif isinstance(chroma, Lab): + return Color.from_lab(chroma, white) + elif isinstance(chroma, RGB): + return Color.from_rgb(chroma, white) + else: + raise TypeError("%r is not an RGB, Lab, or HCL color" % (chroma,)) + + +def white(w: float) -> Color: + return Color.white(w) diff --git a/ponzicolor/linear.py b/ponzicolor/linear.py new file mode 100644 index 0000000..e548bf1 --- /dev/null +++ b/ponzicolor/linear.py @@ -0,0 +1,66 @@ +def linearize(v: float) -> float: + return ((v + 0.055) / 1.055) ** 2.4 if v > 0.04045 else v / 12.92 + + +def linearize_fast(v: float) -> float: + v1 = v - 0.5 + v2 = v1 * v1 + v3 = v2 * v1 + v4 = v2 * v2 + + return ( + -0.248750514614486 + + 0.925583310193438 * v + + 1.16740237321695 * v2 + + 0.280457026598666 * v3 + - 0.0757991963780179 * v4 + ) + + +def delinearize(v: float) -> float: + return 1.055 * (v ** (1.0 / 2.4)) - 0.055 if v > 0.0031308 else 12.92 * v + + +def delinearize_fast(v: float) -> float: + if v > 0.2: + v1 = v - 0.6 + v2 = v1 * v1 + v3 = v2 * v1 + v4 = v2 * v2 + v5 = v3 * v2 + return ( + 0.442430344268235 + + 0.592178981271708 * v + - 0.287864782562636 * v2 + + 0.253214392068985 * v3 + - 0.272557158129811 * v4 + + 0.325554383321718 * v5 + ) + elif v > 0.03: + v1 = v - 0.115 + v2 = v1 * v1 + v3 = v2 * v1 + v4 = v2 * v2 + v5 = v3 * v2 + return ( + 0.194915592891669 + + 1.55227076330229 * v + - 3.93691860257828 * v2 + + 18.0679839248761 * v3 + - 101.468750302746 * v4 + + 632.341487393927 * v5 + ) + else: + v1 = v - 0.015 + v2 = v1 * v1 + v3 = v2 * v1 + v4 = v2 * v2 + v5 = v3 * v2 + return ( + 0.0519565234928877 + + 5.09316778537561 * v + - 99.0338180489702 * v2 + + 3484.52322764895 * v3 + - 150028.083412663 * v4 + + 7168008.42971613 * v5 + ) diff --git a/ponzicolor/scale.py b/ponzicolor/scale.py new file mode 100644 index 0000000..7cdbf54 --- /dev/null +++ b/ponzicolor/scale.py @@ -0,0 +1,35 @@ +from itertools import islice + +from typing import List, Sequence, Tuple +from .color import Color + + +class Scale: + """ + Scale is a linear color gradient. + """ + + points: List[Tuple[Color, float]] + + @classmethod + def of(cls, *colors: Color) -> "Scale": + return cls.linear(colors) + + @classmethod + def linear(cls, colors: Sequence[Color]) -> "Scale": + s = 1.0 / len(colors) + return cls([(color, i * s) for i, color in enumerate(colors)]) + + def __init__(self, points: Sequence[Tuple[Color, float]]): + self.points = list(points) + + def __call__(self, t: float) -> Color: + if t < 0.0 or t > 1.0: + raise ValueError(f"Scale must be called with 0 ≤ t ≤ 1, not {t}.") + + for (c1, p1), (c2, p2) in zip(self.points, islice(self.points, 1, None)): + if p1 <= t <= p2: + bias = (t - p1) / (p2 - p1) + return c1.blend(c2, bias).clamp() + else: + return self.points[-1][0] diff --git a/ponzicolor/space.py b/ponzicolor/space.py new file mode 100644 index 0000000..60354ea --- /dev/null +++ b/ponzicolor/space.py @@ -0,0 +1,151 @@ +from math import atan2, cos, degrees, sin, sqrt, radians +from typing import Callable, NamedTuple + +from . import linear + + +class RGB(NamedTuple): + """ + RGB is an sRGB color. + """ + + r: float # [0-1] + g: float # [0-1] + b: float # [0-1] + + @property + def valid(self) -> bool: + return 0.0 <= self.r <= 1.0 and 0.0 <= self.g <= 1.0 and 0.0 <= self.b <= 1.0 + + def clamp(self) -> "RGB": + def c(v: float) -> float: + return max(0.0, min(v, 1.0)) + + return RGB(c(self.r), c(self.g), c(self.b)) + + +class Lab(NamedTuple): + """ + Lab is a color in the CIE L*a*b* perceptually-uniform color space. + """ + + l: float + a: float + b: float + + +class HCL(NamedTuple): + """ + HCL is a color in the CIE L*C*h° color space, a polar projection of L*a*b*. + It's basically a superior HSV. + """ + + h: float # hue [0-360) + c: float # chroma [0-1] + l: float # luminance [0-1] + + +class XYZ(NamedTuple): + """ + XYZ is a color in CIE's standard color space. + """ + + x: float + y: float + z: float + + +class LinearRGB(NamedTuple): + """ + RGB is a linear color. + """ + + r: float # [0-1] + g: float # [0-1] + b: float # [0-1] + + +# Reference white points + +D50 = XYZ(0.96422, 1.00000, 0.82521) +D65 = XYZ(0.95047, 1.00000, 1.08883) + + +def hcl_to_lab(hcl: HCL) -> Lab: + h_rad = radians(hcl.h) + a = hcl.c * cos(h_rad) + b = hcl.c * sin(h_rad) + return Lab(hcl.l, a, b) + + +def lab_to_hcl(lab: Lab) -> HCL: + t = 1.0e-4 + h = ( + degrees(atan2(lab.b, lab.a)) % 360.0 + if abs(lab.b - lab.a) > t and abs(lab.a) > t + else 0.0 + ) + c = sqrt(lab.a ** 2 + lab.b ** 2) + l = lab.l + + return HCL(h, c, l) + + +def lab_to_xyz(lab: Lab, white_ref: XYZ = D65) -> XYZ: + def finv(t: float) -> float: + return ( + t ** 3 + if t > 6.0 / 29.0 + else 3.0 * 6.0 / 29.0 * 6.0 / 29.0 * (t - 4.0 / 29.0) + ) + + l2 = (lab.l + 0.16) / 1.16 + return XYZ( + white_ref.x * finv(l2 + lab.a / 5.0), + white_ref.y * finv(l2), + white_ref.z * finv(l2 - lab.b / 2.0), + ) + + +def xyz_to_lab(xyz: XYZ, white_ref: XYZ = D65) -> Lab: + def f(t: float) -> float: + return ( + t ** (1 / 3) + if t > 6.0 / 29.0 * 6.0 / 29.0 * 6.0 / 29.0 + else t / 3.0 * 29.0 / 6.0 * 29.0 / 6.0 + 4.0 / 29.0 + ) + + fy = f(xyz.y / white_ref.y) + return Lab( + 1.16 * fy - 0.16, + 5.0 * (f(xyz.x / white_ref.x) - fy), + 2.0 * (fy - f(xyz.z / white_ref.z)), + ) + + +def xyz_to_linear_rgb(xyz: XYZ) -> LinearRGB: + return LinearRGB( + 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, + -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, + 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, + ) + + +def linear_rgb_to_xyz(rgb: LinearRGB) -> XYZ: + return XYZ( + 0.4124564 * rgb.r + 0.3575761 * rgb.g + 0.1804375 * rgb.b, + 0.2126729 * rgb.r + 0.7151522 * rgb.g + 0.0721750 * rgb.b, + 0.0193339 * rgb.r + 0.1191920 * rgb.g + 0.9503041 * rgb.b, + ) + + +def linear_rgb_to_srgb( + rgb: LinearRGB, delinearize: Callable[[float], float] = linear.delinearize +) -> RGB: + return RGB(delinearize(rgb.r), delinearize(rgb.g), delinearize(rgb.b)) + + +def srgb_to_linear_rgb( + rgb: RGB, linearize: Callable[[float], float] = linear.linearize +) -> LinearRGB: + return LinearRGB(linearize(rgb.r), linearize(rgb.g), linearize(rgb.b)) diff --git a/pyproject.toml b/pyproject.toml index bfd6655..0a30dd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,8 @@ numpy = "^1.16.4" sacn = "^1.3" ola = "^0.10.7" jinja2 = "^2.10" +spectra = "^0.0.11" +netifaces = "^0.10.9" [tool.poetry.dev-dependencies] pytest = "^3.0" diff --git a/scripts/build_pixel_map_json.py b/scripts/build_pixel_map_json.py new file mode 100644 index 0000000..651f507 --- /dev/null +++ b/scripts/build_pixel_map_json.py @@ -0,0 +1,22 @@ +import os +import sys + + +num_ux = int(sys.argv[1]) + +pixel_num = 1 +print("{") +for ux in range(1, num_ux+1): + dmx_start = 1 + if ux in (3,6,9,12): + while dmx_start < 176: + print('"{0}": [{1}, {2}],'.format(pixel_num,ux,dmx_start)) + pixel_num += 1 + dmx_start += 4 + else: + while dmx_start < 512: + print('"{0}": [{1}, {2}],'.format(pixel_num,ux,dmx_start)) + pixel_num += 1 + dmx_start += 4 + +print("}") diff --git a/scripts/build_tri_cell_mapping.py b/scripts/build_tri_cell_mapping.py new file mode 100644 index 0000000..50ec0f1 --- /dev/null +++ b/scripts/build_tri_cell_mapping.py @@ -0,0 +1,38 @@ +import os +import sys + +curr_cell = 0 + +#Built from https://docs.google.com/spreadsheets/d/16Ys242V437N968W5UU2WQdonFJ6SwZJeYmEXGbC9cYo/edit#gid=0 +ds = {1: [1, 1051,0], + 2: [3, 1018,1050+8], + 3: [5, 969, 1017+8], + 4: [7, 904, 968+8], + 5: [9, 823, 903+8], + 6: [11, 726,822+8], + 7: [13, 613, 725+8], + 8: [15, 484,612+8], + 9: [17, 339, 483+8], + 10: [19, 178,338+8], + 11: [21, 1, 177+8] + } + +for row in ds: + n_cells = ds[row][0] # ncells in row + u = ds[row][1] # up cell counter + d = ds[row][2] # down cell counter + + is_up = True + for cell in range (1, n_cells+1): + if is_up: + print('{0}: [{1},{2},{3},{4},{5},{6},{7},{8}],'.format(curr_cell, u, u+1, u+2 , u+3, u+4, u+5, u+6, u+7)) + is_up = False + u += 8 + + else: + print('{0}: [{1},{2},{3},{4},{5},{6},{7},{8}],'.format(curr_cell, d ,d+1, d+2, d+3, d+4,d+5,d+6, d+7)) + d += 8 + is_up = True + + curr_cell += 1 + diff --git a/shows/__init__.py b/shows/__init__.py index c7e6475..4cbb899 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -1,7 +1,19 @@ # These imports include submodules under the `shows` namespace (e.g. shows.UpDown is available). +from .cycle_hsv import CycleHSV from .left_to_right import LeftToRight from .left_to_right_and_back import LeftToRightAndBack +from .marching_hexes import MarchingHexes from .one_by_one import OneByOne from .random_cells import Random from .showbase import ShowBase, load_shows, random_shows -from .up_down import UpDown +from .stargate import Stargate +from .strobe import Strobe +from .marching_hexes import MarchingHexes +from .top_down import TopDown +from .two_hexes import TwoHexes +from .tendrils import Tendrils +from .warp import Warp +from .tendrils import Tendrils + +from .index_debug import IndexDebug +from .universe_debug import UniverseDebug diff --git a/shows/cycle_hsv.py b/shows/cycle_hsv.py new file mode 100644 index 0000000..0e79bcf --- /dev/null +++ b/shows/cycle_hsv.py @@ -0,0 +1,87 @@ +import time + +from color import HSV as hsv +from grid import Grid, every +from .showbase import ShowBase + + +class CycleHSV(ShowBase): + def __init__(self, grid: Grid, frame_delay: float = 0.1): + self.grid = grid + self.frame_delay = frame_delay + self.n_cells = len(self.grid) + + def next_frame(self): + while True: + ca = hsv(0.0, 0.0, 0.0, True) + while ca.v < 1.0: + ca.v += 0.0008 + self.grid.set(every, ca) + self.grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + while ca.s < 1.0: + ca.s += 0.0008 + self.grid.set(every, ca) + self.grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + while ca.h < 1.0: + ca.h += 0.0008 + self.grid.set(every, ca) + self.grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + self.grid.clear() + time.sleep(3) + + while True: + ca = hsv(0.0, 0.0, 0.0, False) + while ca.v < 1.0: + ca.v += 0.0008 + self.grid.set(every, ca) + self.grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + while ca.s < 1.0: + ca.s += 0.0008 + self.grid.set(every, ca) + self.grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + while ca.h < 1.0: + ca.h += 0.0008 + self.grid.set(every, ca) + self.grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + self.grid.clear() + time.sleep(3) + + while True: + ca = hsv(0.0, 0.0, 1.0, False) + + while ca.s < 1.0: + ca.s += 0.0008 + self.grid.set(every, ca) + self.grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + while ca.h < 1.0: + ca.h += 0.0008 + self.grid.set(every, ca) + self.grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + self.grid.clear() + time.sleep(3) + + yield self.frame_delay diff --git a/shows/index_debug.py b/shows/index_debug.py new file mode 100644 index 0000000..af7b95a --- /dev/null +++ b/shows/index_debug.py @@ -0,0 +1,29 @@ +from color import HSV as hsv +from grid import Grid, every +from .showbase import ShowBase + + +class IndexDebug(ShowBase): + def __init__(self, grid: Grid, frame_delay: float = 0.05): + self.grid = grid + self.frame_delay = frame_delay + self.n_cells = len(self.grid) + + def next_frame(self): + while True: + for cell in sorted(self.grid.cells): + universe = max(a.universe for a in cell.addresses) + hue = 1.0 - (cell.position.row / self.grid.row_count) * 0.9 + + self.grid.clear() + self.grid.set(cell.position, hsv(hue, 0.8, 0.9)) + self.grid.go() + yield self.frame_delay + + for cell in sorted(self.grid.cells): + universe = max(a.universe for a in cell.addresses) + hue = min(0.9, (universe - 1) * 0.1) + + self.grid.set(cell, hsv(hue, 0.8, 0.9)) + self.grid.go() + yield self.frame_delay diff --git a/shows/left_to_right.py b/shows/left_to_right.py index 5b90461..0b147b0 100644 --- a/shows/left_to_right.py +++ b/shows/left_to_right.py @@ -1,34 +1,44 @@ +from color import HSV from .showbase import ShowBase -from color import RGBW +from grid import Grid, left_to_right +import time class LeftToRight(ShowBase): - def __init__(self, tri_grid, frame_delay=1.0): - self.tri_grid = tri_grid + def __init__(self, grid: Grid, frame_delay: float = 0.2): + self.grid = grid self.frame_delay = frame_delay def next_frame(self): - xlen = len(self.tri_grid._triangle_grid) - ylen = len(self.tri_grid._triangle_grid[0]) - x = 0 - y = 0 - while True: - self.tri_grid.clear() + row_count = self.grid.row_count + + hsv = HSV(0.5, 0.2, .75) +# from IPython import embed; embed() + pix_arr = [] + a_ctr = 0 + for points in left_to_right(row_count): - print(f"x={x} y={y}") - if y < ylen: - for rows in self.tri_grid._triangle_grid: - cell = self.tri_grid._triangle_grid[x][y] - print("AAA", x, y, rows) - if cell is None: - pass - else: - self.tri_grid.set_cell_by_cellid(cell.get_id(), RGBW(255, 255, 25, 25)) - x += 1 - x = 0 - y += 1 - else: - x = 0 - y = 0 + for pos in points: + cell = self.grid[pos] + b_ctr = 0 + for pixel in list(self.grid.pixels(cell.id)): + if len(pix_arr) <= a_ctr+b_ctr: + pix_arr.append([]) + pix_arr[a_ctr+b_ctr].append(pixel) + pixel(hsv) + self.grid.go() + b_ctr += 1 + a_ctr += 4 + + while True: + for i in pix_arr: + for ii in i: + print(ii) + ii(hsv) + self.grid.go() + time.sleep(0.2) - yield self.frame_delay + hsv.h += .1 + if hsv.h >= 1.0: + hsv.h = 0.0 + yield self.frame_delay diff --git a/shows/left_to_right_and_back.py b/shows/left_to_right_and_back.py index b6ae5c2..074da20 100644 --- a/shows/left_to_right_and_back.py +++ b/shows/left_to_right_and_back.py @@ -1,73 +1,51 @@ +from color import HSV from .showbase import ShowBase -from color import RGBW +from grid import Grid, Direction, sweep +from grid.cell import Direction, Position, row_length +from grid import traversal import time - class LeftToRightAndBack(ShowBase): - def __init__(self, tri_grid, frame_delay=1.0): - self.tri_grid = tri_grid + def __init__(self, grid: Grid, frame_delay: float = 1.0): + self.grid = grid self.frame_delay = frame_delay def next_frame(self): - xlen = len(self.tri_grid._triangle_grid) - ylen = len(self.tri_grid._triangle_grid[0]) - x = 0 - y = 0 - fwd = True - pix = 0 - while True: - - if fwd is True: - self.tri_grid.clear() + n_rows = self.grid.row_count + hsv = HSV(0.0,0.9,.5) - if y < ylen: - for rows in self.tri_grid._triangle_grid: - cell = self.tri_grid._triangle_grid[x][y] + pix_arr = [] + a_ctr = 0 + for points in traversal.left_to_right(n_rows): + for (row, col) in points: + cell = self.grid.select(Position(row=row,col=col)) + b_ctr = 0 + for pixel_address in cell[0].pixel_addresses(): + if len(pix_arr) <= a_ctr+b_ctr: + pix_arr.append([]) + pix_arr[a_ctr+b_ctr].append(pixel_address) + b_ctr += 1 + a_ctr += 4 + self.grid.clear() - if cell is None: - pass - else: - r=255 - g = 0 - for pix in range(6): - self.tri_grid.set_pixel(cell.get_pixels()[pix], RGBW(r, g, 0, 1), cell.get_id()) - time.sleep(.2) - self.tri_grid.go() - g += 40 - r -= 3 - x += 1 - x = 0 - y += 1 - else: - x = 0 - y = ylen-1 - fwd = False - else: - if y >= 0: - for rows in self.tri_grid._triangle_grid: - - cell = self.tri_grid._triangle_grid[x][y] - - if cell is None: - pass - else: - g = 255 - b = 0 - for pix in range(6): - self.tri_grid.set_pixel(cell.get_pixels()[5-pix], RGBW(0, g, b, 1), cell.get_id()) - time.sleep(.2) - self.tri_grid.go() - g -= 40 - b += 40 - x += 1 - - y -= 1 - x = 0 - else: - y = 0 - x = 0 - fwd = True + while True: - yield self.frame_delay + for i in pix_arr: #yes, i + for ii in i: #yes, ii! + self.grid._model.set(ii, hsv) + self.grid.go() + hsv.h += .09 + if hsv.h >= 1.0: + hsv.h = 0.0 + time.sleep(0.8) + + for i in reversed(pix_arr): + for ii in reversed(i): + self.grid._model.set(ii, hsv) + self.grid.go() + hsv.h += .09 + if hsv.h >= 1.0: + hsv.h = 0.0 + time.sleep(0.8) diff --git a/shows/marching_hexes.py b/shows/marching_hexes.py new file mode 100644 index 0000000..286dbca --- /dev/null +++ b/shows/marching_hexes.py @@ -0,0 +1,28 @@ +from .showbase import ShowBase +from color import HSV +from grid import hexagon, pointed_up +import random as rnd +import time + + +class MarchingHexes(ShowBase): + def __init__(self, grid, frame_delay=0.1): + self.grid = grid + self.frame_delay = frame_delay + + self.n_cells = len(self.grid.cells) + + def next_frame(self): + hsv = HSV(1.0, 1, 1) + + while True: + self.grid.clear() + + for cell in self.grid.select(pointed_up): + self.grid.set(hexagon(cell.position), hsv) + self.grid.go() + time.sleep(1) + + hsv.h = 0.0 if hsv.h >= 1.0 else hsv.h + 0.2 + + yield self.frame_delay diff --git a/shows/one_by_one.py b/shows/one_by_one.py index 62eb76f..232b712 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -1,24 +1,20 @@ +from color import RGB +from grid import Grid from .showbase import ShowBase -from color import RGBW class OneByOne(ShowBase): - def __init__(self, tri_grid, frame_delay=1.5): - self.tri_grid = tri_grid + def __init__(self, grid: Grid, frame_delay: float = 0.9): + self.grid = grid self.frame_delay = frame_delay def next_frame(self): - ncells = len(self.tri_grid.get_cells())-1 - self.tri_grid.clear() - cell_n = 0 + ncells = len(self.grid) while True: - self.tri_grid.clear() - print(cell_n) - self.tri_grid.set_cell_by_cellid(self.tri_grid.get_cells()[cell_n].get_id(), RGBW(255, 255, 25, 25)) + for cell in range(ncells): + self.grid.clear() - if cell_n >= ncells: - cell_n = -1 - cell_n += 1 - - yield self.frame_delay + for pixel in self.grid.pixels(cell): + pixel.set(RGB(255, 255, 25)) + yield self.frame_delay diff --git a/shows/random_cells.py b/shows/random_cells.py index 7f639dd..1298277 100644 --- a/shows/random_cells.py +++ b/shows/random_cells.py @@ -1,19 +1,31 @@ +from typing import Deque +import random + from .showbase import ShowBase -from color import RGBW -import random as rnd +from color import RGB +from grid.cell import Cell class Random(ShowBase): - def __init__(self, tri_grid, frame_delay = 0.1): - self.tri_grid = tri_grid + def __init__(self, grid, frame_delay=0.1): + self.grid = grid self.frame_delay = frame_delay - self.n_cells = len(self.tri_grid.get_cells()) + from IPython import embed; embed() + + def shuffle(self) -> Deque[Cell]: + cells = self.grid.cells + random.shuffle(cells) + + return Deque(cells) def next_frame(self): + cells = self.shuffle() while True: - self.tri_grid.clear() - self.tri_grid.set_cell_by_cellid(rnd.randint(1, self.n_cells-2), RGBW(200, 255, 25, 25)) - self.tri_grid.set_cell_by_cellid(rnd.randint(1, self.n_cells-2), RGBW(200, 10, 25, 25)) + if len(cells) == 0: + cells = self.shuffle() + + self.grid.clear() + self.grid.set(cells.popleft(), RGB(200, 10, 25)) yield self.frame_delay diff --git a/shows/showbase.py b/shows/showbase.py index 9d48ae8..3028c34 100644 --- a/shows/showbase.py +++ b/shows/showbase.py @@ -38,7 +38,7 @@ def random_shows(no_repeat: float = 1/3) -> Iterator[Tuple[str, Type[ShowBase]]] while True: show = choice(seq) - while show[0] in seen: + while show[0] in seen or 'Debug' in show[0]: show = choice(seq) seen.append(show[0]) diff --git a/shows/stargate.py b/shows/stargate.py new file mode 100644 index 0000000..b7c4595 --- /dev/null +++ b/shows/stargate.py @@ -0,0 +1,37 @@ +import random + +from color import Color, HSV +from grid import Grid, inset +from .showbase import ShowBase + + +class Stargate(ShowBase): + def __init__(self, grid: Grid, frame_delay: float = 0.25): + self.grid = grid + self.frame_delay = frame_delay + self.hue = 0 + + def generate_color(self) -> Color: + hue = self.hue + self.hue += random.uniform(0.1, 0.2) + if self.hue >= 1: + self.hue -= 1 + + return HSV(hue, random.uniform(0.7, 0.9), 0.7) + + def next_frame(self): + self.grid.clear() + yield self.frame_delay + + colors = [self.generate_color()] + + while True: + for distance, color in enumerate(colors): + self.grid.set(inset(distance), color) + + self.grid.go() + yield self.frame_delay + + colors.insert(0, self.generate_color()) + if len(colors) > 4: + colors.pop() diff --git a/shows/strobe.py b/shows/strobe.py new file mode 100644 index 0000000..bcae18b --- /dev/null +++ b/shows/strobe.py @@ -0,0 +1,30 @@ +import time + +from .showbase import ShowBase +from color import RGB +from grid import every + + +class Strobe(ShowBase): + def __init__(self, grid, frame_delay=0.02): + self.grid = grid + self.frame_delay = frame_delay + self.n_cells = len(self.grid._cells) + + def next_frame(self): + while True: + self.grid.clear() + self.grid.set(every, RGB(200, 5, 5)) + self.grid.go() + time.sleep(0.02) + self.grid.set(every, RGB(100, 100, 100)) + self.grid.go() + time.sleep(0.02) + self.grid.set(every, RGB(5, 5, 255)) + self.grid.go() + time.sleep(0.02) + self.grid.set(every, RGB(100, 100, 100)) + self.grid.go() + time.sleep(0.02) + + yield self.frame_delay diff --git a/shows/top_down.py b/shows/top_down.py new file mode 100644 index 0000000..47d3452 --- /dev/null +++ b/shows/top_down.py @@ -0,0 +1,34 @@ +"""Simpe Demo Show. Move from Top of Triangle To Bottom, lighting each row at a time""" + +from .showbase import ShowBase +from color import HSV +from grid import hexagon, pointed_up +import random as rnd +import time +from grid.cell import Direction, Position, row_length + + +class TopDown(ShowBase): + def __init__(self, grid, frame_delay=0.1): + self.grid = grid + self.frame_delay = frame_delay + + self.n_cells = len(self.grid.cells) +# from IPython import embed; embed() + + def next_frame(self): + + self.grid.clear() + + while True: + hsv = HSV(1.0, 1, 1) + + for row in range (0,11): + for col in range (0, row_length(row+1)): ### row+1 b/c the row_length function expects 1 indexed row nums + self.grid.set(self.grid.select(Position(row=row, col=col)), hsv) + self.grid.go() + time.sleep(1.5) + + hsv.h -= 0.08 + + yield self.frame_delay diff --git a/shows/universe_debug.py b/shows/universe_debug.py new file mode 100644 index 0000000..4ca4720 --- /dev/null +++ b/shows/universe_debug.py @@ -0,0 +1,22 @@ +from color import HSV as hsv +from grid import Grid, every +from .showbase import ShowBase + + +class UniverseDebug(ShowBase): + def __init__(self, grid: Grid, frame_delay: float = 0.1): + self.grid = grid + self.frame_delay = frame_delay + self.n_cells = len(self.grid) + + def next_frame(self): + self.grid.clear() + + while True: + for cell in self.grid.cells: + universe = max(a.universe for a in cell.addresses) + hue = min(0.9, (universe - 1) * 0.1) + self.grid.set(cell, hsv(hue, 0.8, 0.9)) + + self.grid.go() + yield self.frame_delay diff --git a/shows/up_down.py b/shows/up_down.py index 3b97563..aee9d6f 100644 --- a/shows/up_down.py +++ b/shows/up_down.py @@ -1,32 +1,23 @@ +from color import RGB +from grid import Grid, Orientation, pointed from .showbase import ShowBase -from color import RGBW class UpDown(ShowBase): - def __init__(self, tri_grid, frame_delay=2): - self.cells = tri_grid + def __init__(self, grid: Grid, frame_delay: float = 2.0): + self.grid = grid self.frame_delay = frame_delay def next_frame(self): - a = "up" + orientation = Orientation.POINT_UP while True: - self.cells.clear() - - if a == "up": - print('up') - for i in self.cells.get_up_cells(): - print("Up", i.get_id()) - self.cells.set_cell_by_cellid(i.get_id(), RGBW(255, 255, 255, 255)) - else: - print('down') - for i in self.cells.get_down_cells(): - print("down", i.get_id()) - self.cells.set_cell_by_cellid(i.get_id(), RGBW(255, 0, 0, 0)) - - if a == "up": - a = "down" - else: - a = "up" + self.grid.clear() + color = (RGB(0, 255, 255) + if orientation is Orientation.POINT_UP + else RGB(255, 0, 200)) + self.grid.set(pointed(orientation), color) + self.grid.go() + orientation = orientation.invert() yield self.frame_delay diff --git a/shows/warp.py b/shows/warp.py new file mode 100644 index 0000000..c0eb898 --- /dev/null +++ b/shows/warp.py @@ -0,0 +1,24 @@ +from randomcolor import random_color +from .showbase import ShowBase +from grid import Grid, inset + + +class Warp(ShowBase): + def __init__(self, grid: Grid, frame_delay: float = 0.2): + self.grid = grid + self.frame_delay = frame_delay + + # Not sure of the proper formula for this, but allows running on normal or mega triangle. + self.max_distance = 0 + while grid.select(inset(self.max_distance)): + self.max_distance += 1 + + def next_frame(self): + color = random_color(hue='purple') + + while True: + for distance in range(self.max_distance): + self.grid.clear() + self.grid.set(inset(distance), color) + self.grid.go() + yield self.frame_delay diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cell.py b/tests/test_cell.py new file mode 100644 index 0000000..f0934b2 --- /dev/null +++ b/tests/test_cell.py @@ -0,0 +1,59 @@ +from color import Color +from grid import Grid, Position, Geometry, Cell, Address, Coordinate +from model import ModelBase + + +class FakeModel(ModelBase): + def set(self, cell: Cell, addr: Address, color: Color): + pass + + def go(self): + pass + + +def test_position_symmetry(): + for curr_id in range(256): + assert Position.from_id(curr_id).id == curr_id + + +def test_coordinate_symmetry(): + geom = Geometry(rows=3) + values = ( + ((0, 0), (2, 2)), + ((1, 0), (1, 1)), + ((1, 1), (2, 1)), + ((1, 2), (3, 1)), + ((2, 0), (0, 0)), + ((2, 1), (1, 0)), + ((2, 2), (2, 0)), + ((2, 3), (3, 0)), + ((2, 4), (4, 0)), + ) + + for ((row, col), (x, y)) in values: + pos = Position(row, col) + coord = Coordinate(x, y) + assert Coordinate.from_pos(pos, geom) == coord + assert coord.pos(geom) == pos + + +def test_cell_attributes(): + triangle = Grid(model=FakeModel(), geom=Geometry(rows=3)) + assert len(triangle.cells) == 9 + + top = triangle[Position(0, 0)] + assert top.is_top_corner and top.is_left_edge and top.is_right_edge and top.is_up and top.is_edge + assert not top.is_bottom_edge + + left_corner = triangle[Position(2, 0)] + assert left_corner.is_left_corner and left_corner.is_left_edge and left_corner.is_bottom_edge and left_corner.is_up + assert not left_corner.is_right_edge + + right_corner = triangle[Position(2, 4)] + assert right_corner.is_right_corner and right_corner.is_right_edge and right_corner.is_bottom_edge + assert right_corner.is_up + assert not right_corner.is_left_edge + + inner = triangle[Position(1, 1)] + assert inner.is_down + assert not inner.is_left_edge and not inner.is_right_edge and not inner.is_bottom_edge and not inner.is_edge diff --git a/tests/test_color.py b/tests/test_color.py new file mode 100644 index 0000000..abd9d9b --- /dev/null +++ b/tests/test_color.py @@ -0,0 +1,292 @@ +from typing import NamedTuple + +from pytest import approx + +from ponzicolor.color import color +from ponzicolor.space import ( + RGB, + Lab, + HCL, + XYZ, + LinearRGB, + hcl_to_lab, + lab_to_hcl, + lab_to_xyz, + xyz_to_lab, + xyz_to_linear_rgb, + linear_rgb_to_xyz, + srgb_to_linear_rgb, + linear_rgb_to_srgb, +) +from ponzicolor.linear import delinearize, linearize + + +class Case(NamedTuple): + hex: str + rgb: RGB + lrgb: LinearRGB + xyz: XYZ + lab: Lab + hcl: HCL + + +cases = [ + Case( + "#ffffff", + RGB(1.0, 1.0, 1.0), + LinearRGB(1, 1, 1), + XYZ(0.950470, 1.000000, 1.088830), + Lab(1.000000, 0.000000, 0.000000), + HCL(0.0000, 0.000000, 1.000000), + ), + Case( + "#80ffff", + RGB(0.5, 1.0, 1.0), + LinearRGB(0.21404114048223255, 1, 1), + XYZ(0.626296, 0.832848, 1.073634), + Lab(0.931390, -0.353319, -0.108946), + HCL(197.1371, 0.369735, 0.931390), + ), + Case( + "#ff80ff", + RGB(1.0, 0.5, 1.0), + LinearRGB(1, 0.21404114048223255, 1), + XYZ(0.669430, 0.437920, 0.995150), + Lab(0.720892, 0.651673, -0.422133), + HCL(327.0661, 0.776450, 0.720892), + ), + Case( + "#ffff80", + RGB(1.0, 1.0, 0.5), + LinearRGB(1, 1, 0.21404114048223255), + XYZ(0.808654, 0.943273, 0.341930), + Lab(0.977637, -0.165795, 0.602017), + HCL(105.3975, 0.624430, 0.977637), + ), + Case( + "#8080ff", + RGB(0.5, 0.5, 1.0), + LinearRGB(0.21404114048223255, 0.21404114048223255, 1), + XYZ(0.345256, 0.270768, 0.979954), + Lab(0.590453, 0.332846, -0.637099), + HCL(297.5843, 0.718805, 0.590453), + ), + Case( + "#ff8080", + RGB(1.0, 0.5, 0.5), + LinearRGB(1, 0.21404114048223255, 0.21404114048223255), + XYZ(0.527613, 0.381193, 0.248250), + Lab(0.681085, 0.483884, 0.228328), + HCL(25.2610, 0.535049, 0.681085), + ), + Case( + "#80ff80", + RGB(0.5, 1.0, 0.5), + LinearRGB(0.21404114048223255, 1, 0.21404114048223255), + XYZ(0.484480, 0.776121, 0.326734), + Lab(0.906026, -0.600870, 0.498993), + HCL(140.2920, 0.781050, 0.906026), + ), + Case( + "#808080", + RGB(0.5, 0.5, 0.5), + LinearRGB(0.21404114048223255, 0.21404114048223255, 0.21404114048223255), + XYZ(0.203440, 0.214041, 0.233054), + Lab(0.533890, 0.000000, 0.000000), + HCL(0.0000, 0.000000, 0.533890), + ), + Case( + "#00ffff", + RGB(0.0, 1.0, 1.0), + LinearRGB(0.0, 1.0, 1.0), + XYZ(0.538014, 0.787327, 1.069496), + Lab(0.911132, -0.480875, -0.141312), + HCL(196.3762, 0.501209, 0.911132), + ), + Case( + "#ff00ff", + RGB(1.0, 0.0, 1.0), + LinearRGB(1.0, 0.0, 1.0), + XYZ(0.592894, 0.284848, 0.969638), + Lab(0.603242, 0.982343, -0.608249), + HCL(328.2350, 1.155407, 0.603242), + ), + Case( + "#ffff00", + RGB(1.0, 1.0, 0.0), + LinearRGB(1.0, 1.0, 0.0), + XYZ(0.770033, 0.927825, 0.138526), + Lab(0.971393, -0.215537, 0.944780), + HCL(102.8512, 0.969054, 0.971393), + ), + Case( + "#0000ff", + RGB(0.0, 0.0, 1.0), + LinearRGB(0.0, 0.0, 1.0), + XYZ(0.180437, 0.072175, 0.950304), + Lab(0.322970, 0.791875, -1.078602), + HCL(306.2849, 1.338076, 0.322970), + ), + Case( + "#00ff00", + RGB(0.0, 1.0, 0.0), + LinearRGB(0.0, 1.0, 0.0), + XYZ(0.357576, 0.715152, 0.119192), + Lab(0.877347, -0.861827, 0.831793), + HCL(136.0160, 1.197759, 0.877347), + ), + Case( + "#ff0000", + RGB(1.0, 0.0, 0.0), + LinearRGB(1.0, 0.0, 0.0), + XYZ(0.412456, 0.212673, 0.019334), + Lab(0.532408, 0.800925, 0.672032), + HCL(39.9990, 1.045518, 0.532408), + ), + Case( + "#000000", + RGB(0.0, 0.0, 0.0), + LinearRGB(0.0, 0.0, 0.0), + XYZ(0.000000, 0.000000, 0.000000), + Lab(0.000000, 0.000000, 0.000000), + HCL(0.0000, 0.000000, 0.000000), + ), +] + + +def test_linear_rgb(): + for c in cases: + actual = srgb_to_linear_rgb(c.rgb) + assert actual.r == approx(c.lrgb.r, 0.0001) + assert actual.g == approx(c.lrgb.g, 0.0001) + assert actual.b == approx(c.lrgb.b, 0.0001) + for c in cases: + actual = linear_rgb_to_srgb(c.lrgb) + assert actual.r == approx(c.rgb.r, 0.0001) + assert actual.g == approx(c.rgb.g, 0.0001) + assert actual.b == approx(c.rgb.b, 0.0001) + + +def test_xyz(): + for c in cases: + actual = linear_rgb_to_xyz(srgb_to_linear_rgb(c.rgb)) + assert actual.x == approx(c.xyz.x, 0.0001) + assert actual.y == approx(c.xyz.y, 0.0001) + assert actual.z == approx(c.xyz.z, 0.0001) + for c in cases: + actual = linear_rgb_to_srgb(xyz_to_linear_rgb(c.xyz)) + assert actual.r == approx(c.rgb.r, 0.0001, 0.0001) + assert actual.g == approx(c.rgb.g, 0.0001, 0.0001) + assert actual.b == approx(c.rgb.b, 0.0001, 0.0001) + + +def test_lab(): + for c in cases: + actual = xyz_to_lab(linear_rgb_to_xyz(srgb_to_linear_rgb(c.rgb))) + assert actual.l == approx(c.lab.l, 0.0001, 0.0001) + assert actual.a == approx(c.lab.a, 0.0001, 0.0001) + assert actual.b == approx(c.lab.b, 0.0001, 0.0001) + for c in cases: + actual = linear_rgb_to_srgb(xyz_to_linear_rgb(lab_to_xyz(c.lab))) + assert actual.r == approx(c.rgb.r, 0.0001, 0.0001) + assert actual.g == approx(c.rgb.g, 0.0001, 0.0001) + assert actual.b == approx(c.rgb.b, 0.0001, 0.0001) + + +def test_hcl(): + for c in cases: + actual = lab_to_hcl(xyz_to_lab(linear_rgb_to_xyz(srgb_to_linear_rgb(c.rgb)))) + assert actual.h == approx(c.hcl.h, 0.0001, 0.0001) + assert actual.c == approx(c.hcl.c, 0.0001, 0.0001) + assert actual.l == approx(c.hcl.l, 0.0001, 0.0001) + for c in cases: + actual = linear_rgb_to_srgb(xyz_to_linear_rgb(lab_to_xyz(hcl_to_lab(c.hcl)))) + assert actual.r == approx(c.rgb.r, 0.0001, 0.0001) + assert actual.g == approx(c.rgb.g, 0.0001, 0.0001) + assert actual.b == approx(c.rgb.b, 0.0001, 0.0001) + + +def test_blend(): + assert color("#2E4057").blend(color("#048BA8"), 0.1).hex == "#2F476000" + assert color("#2E4057").blend(color("#048BA8"), 0.2).hex == "#2F4E6900" + assert color("#2E4057").blend(color("#048BA8"), 0.3).hex == "#2F557100" + assert color("#2E4057").blend(color("#048BA8"), 0.4).hex == "#2E5C7A00" + assert color("#2E4057").blend(color("#048BA8"), 0.5).hex == "#2B648200" + assert color("#2E4057").blend(color("#048BA8"), 0.6).hex == "#286B8A00" + assert color("#2E4057").blend(color("#048BA8"), 0.7).hex == "#23739200" + assert color("#2E4057").blend(color("#048BA8"), 0.8).hex == "#1D7B9A00" + assert color("#2E4057").blend(color("#048BA8"), 0.9).hex == "#1483A100" + + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.1).hex == "#DCD8E600" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.2).hex == "#D8D3E300" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.3).hex == "#D3CDE000" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.4).hex == "#CFC8DD00" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.5).hex == "#CBC2DA00" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.6).hex == "#C6BCD700" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.7).hex == "#C2B7D400" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.8).hex == "#BEB1D000" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.9).hex == "#BAACCD00" + + assert color("#DD403A").blend(color("#B8B42D"), 0.1).hex == "#DE4E3200" + assert color("#DD403A").blend(color("#B8B42D"), 0.2).hex == "#DD5B2A00" + assert color("#DD403A").blend(color("#B8B42D"), 0.3).hex == "#DB682300" + assert color("#DD403A").blend(color("#B8B42D"), 0.4).hex == "#D9741C00" + assert color("#DD403A").blend(color("#B8B42D"), 0.5).hex == "#D5801600" + assert color("#DD403A").blend(color("#B8B42D"), 0.6).hex == "#D18B1200" + assert color("#DD403A").blend(color("#B8B42D"), 0.7).hex == "#CC961300" + assert color("#DD403A").blend(color("#B8B42D"), 0.8).hex == "#C6A01900" + assert color("#DD403A").blend(color("#B8B42D"), 0.9).hex == "#BFAA2200" + + assert color("#3D348B").blend(color("#E6AF2E"), 0.1).hex == "#6A328B00" + assert color("#3D348B").blend(color("#E6AF2E"), 0.2).hex == "#8E2F8700" + assert color("#3D348B").blend(color("#E6AF2E"), 0.3).hex == "#AC2F7F00" + assert color("#3D348B").blend(color("#E6AF2E"), 0.4).hex == "#C5347300" + assert color("#3D348B").blend(color("#E6AF2E"), 0.5).hex == "#D8416600" + assert color("#3D348B").blend(color("#E6AF2E"), 0.6).hex == "#E6535800" + assert color("#3D348B").blend(color("#E6AF2E"), 0.7).hex == "#EE694A00" + assert color("#3D348B").blend(color("#E6AF2E"), 0.8).hex == "#F0803D00" + assert color("#3D348B").blend(color("#E6AF2E"), 0.9).hex == "#EE973200" + + assert color("#191716").blend(color("#E6AF2E"), 0.1).hex == "#2E221C00" + assert color("#191716").blend(color("#E6AF2E"), 0.2).hex == "#432F2100" + assert color("#191716").blend(color("#E6AF2E"), 0.3).hex == "#593C2500" + assert color("#191716").blend(color("#E6AF2E"), 0.4).hex == "#6E4A2800" + assert color("#191716").blend(color("#E6AF2E"), 0.5).hex == "#83582B00" + assert color("#191716").blend(color("#E6AF2E"), 0.6).hex == "#98682D00" + assert color("#191716").blend(color("#E6AF2E"), 0.7).hex == "#AC782F00" + assert color("#191716").blend(color("#E6AF2E"), 0.8).hex == "#C08A2F00" + assert color("#191716").blend(color("#E6AF2E"), 0.9).hex == "#D49C2F00" + + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.1).hex == "#BEE9C100" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.2).hex == "#BDE6C000" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.3).hex == "#BBE2C000" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.4).hex == "#BADFC000" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.5).hex == "#B9DBBF00" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.6).hex == "#B9D8BE00" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.7).hex == "#B8D4BE00" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.8).hex == "#B7D0BD00" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.9).hex == "#B7CDBC00" + + +def test_linearize(): + assert linearize(0.000000) == approx(0.000000) + assert linearize(0.040000) == approx(0.003096, 0.0001) + assert linearize(0.100000) == approx(0.010023, 0.0001) + assert linearize(0.200000) == approx(0.033105, 0.0001) + assert linearize(0.250000) == approx(0.050876, 0.0001) + assert linearize(0.500000) == approx(0.214041, 0.0001) + assert linearize(0.750000) == approx(0.522522, 0.0001) + assert linearize(1.000000) == approx(1.000000, 0.0001) + + +def test_delinearize(): + assert delinearize(0.000000) == approx(0.000000, 0.0001, 0.0001) + assert delinearize(0.003000) == approx(0.038760, 0.0001, 0.0001) + assert delinearize(0.010000) == approx(0.099853, 0.0001, 0.0001) + assert delinearize(0.050000) == approx(0.247801, 0.0001, 0.0001) + assert delinearize(0.100000) == approx(0.349190, 0.0001, 0.0001) + assert delinearize(0.200000) == approx(0.484529, 0.0001, 0.0001) + assert delinearize(0.250000) == approx(0.537099, 0.0001, 0.0001) + assert delinearize(0.500000) == approx(0.735357, 0.0001, 0.0001) + assert delinearize(0.750000) == approx(0.880825, 0.0001, 0.0001) + assert delinearize(1.000000) == approx(1.000000, 0.0001, 0.0001) diff --git a/tests/test_geom.py b/tests/test_geom.py new file mode 100644 index 0000000..33a3c82 --- /dev/null +++ b/tests/test_geom.py @@ -0,0 +1,19 @@ +from grid import Geometry + + +def test_row_len(): + # (1, 1), (2, 3), (3, 5), (4, 7)... + for (row, length) in enumerate(range(1, 16, 2), start=1): + assert Geometry(rows=row).row_length(row - 1) == length + + +def test_triangle_number(): + for (n, number) in [ + (1, 1), + (2, 4), + (3, 9), + (4, 16), + (5, 25), + (6, 36) + ]: + assert Geometry.triangular_number(n) == number diff --git a/tests/test_grid.py b/tests/test_grid.py new file mode 100644 index 0000000..023f2d5 --- /dev/null +++ b/tests/test_grid.py @@ -0,0 +1,104 @@ +from pytest import raises + +from color import Color +from grid import ( + Position, Geometry, Grid, Cell, Address, bottom_edge, left_edge, right_edge, vertex_neighbors, edge_neighbors +) +from model import ModelBase + + +class FakeModel(ModelBase): + def set(self, cell: Cell, addr: Address, color: Color): + pass + + def go(self): + pass + + +def test_triangle_counts(): + for row_count in range(1, 15): + triangle = Grid(model=FakeModel(), geom=Geometry(rows=row_count)) + assert triangle.row_count == row_count + + expected_cell_count = Geometry.triangular_number(row_count) + assert len(triangle) == len(triangle.cells) == expected_cell_count,\ + f'cell count {len(triangle.cells)} != expected {expected_cell_count} with rows {row_count}' + + # Each edge has the same number of elements are the number or total rows. + assert row_count == len(bottom_edge(triangle)) == len(left_edge(triangle)) == len(right_edge(triangle)) + + +def test_cells_out_of_bounds(): + triangle = Grid(model=FakeModel(), geom=Geometry(rows=2)) + + with raises(KeyError): + triangle[Position(-1, 0)] + with raises(KeyError): + triangle[Position(0, -1)] + with raises(KeyError): + triangle[Position(0, 1)] # Only (0, 0) in first row. + with raises(KeyError): + triangle[Position(1, 3)] + with raises(KeyError): + triangle[Position(2, 0)] # Row 2 does not exist. + + with raises(KeyError): + triangle[-1] + with raises(KeyError): + triangle[len(triangle)] + + assert triangle[2] is not None + + assert not any(triangle.select(vertex_neighbors(2))) + + +def test_cell_attributes(): + triangle = Grid(model=FakeModel(), geom=Geometry(rows=3)) + assert len(triangle.cells) == 9 + + top = triangle[Position(0, 0)] + assert top.is_top_corner and top.is_left_edge and top.is_right_edge and top.is_up and top.is_edge + assert not top.is_bottom_edge + + left_corner = triangle[Position(2, 0)] + assert left_corner.is_left_corner and left_corner.is_left_edge and left_corner.is_bottom_edge and left_corner.is_up + assert not left_corner.is_right_edge + + right_corner = triangle[Position(2, 4)] + assert right_corner.is_right_corner and right_corner.is_right_edge and right_corner.is_bottom_edge + assert right_corner.is_up + assert not right_corner.is_left_edge + + inner = triangle[Position(1, 1)] + assert inner.is_down + assert not inner.is_left_edge and not inner.is_right_edge and not inner.is_bottom_edge and not inner.is_edge + + +def test_cell_neighbors(): + triangle = Grid(model=FakeModel(), geom=Geometry(rows=5)) + + # Upward facing cell + (left, middle, right) = triangle.select(edge_neighbors(6)) + assert left.id == 5 + assert middle.id == 12 + assert right.id == 7 + + (left, middle, right) = triangle.select(vertex_neighbors(6)) + assert left.id == 10 + assert middle.id == 2 + assert right.id == 14 + + # Downward facing cell + (left, middle, right) = triangle.select(edge_neighbors(5)) + assert left.id == 4 + assert middle.id == 1 + assert right.id == 6 + + (left, middle, right) = triangle.select(vertex_neighbors(5)) + assert left is None + assert middle.id == 11 + assert right.id == 3 + + # Invalid cell + assert not any(triangle.select(edge_neighbors(Position(1, -1)))) + assert not any(triangle.select(vertex_neighbors(Position(1, -1)))) diff --git a/tests/shows_test.py b/tests/test_shows.py similarity index 100% rename from tests/shows_test.py rename to tests/test_shows.py diff --git a/tests/test_traversal.py b/tests/test_traversal.py new file mode 100644 index 0000000..678c411 --- /dev/null +++ b/tests/test_traversal.py @@ -0,0 +1,19 @@ +from grid import traversal + + +def test_left_to_right(): + sequence = list(traversal.left_to_right(1)) + assert sequence == [[(0, 0)]] + + sequence = list(traversal.left_to_right(2)) + assert sequence == [[(1, 0)], [(0, 0), (1, 1)], [(1, 2)]] + + sequence = list(traversal.left_to_right(3)) + assert sequence == [[(2, 0)], [(1, 0), (2, 1)], [(0, 0), (1, 1), (2, 2)], [(1, 2), (2, 3)], [(2, 4)]] + + +def test_right_to_left(): + for rows in range(1, 12): + sequence = list(traversal.right_to_left(rows)) + expected = list(reversed(list(traversal.left_to_right(rows)))) + assert sequence == expected diff --git a/tests/test_web.py b/tests/test_web.py index 7297f10..67041e4 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -3,7 +3,7 @@ from jinja2 import escape from cherrypy.test import helper -from web.web import TriangleWeb +from web import TriangleWeb command_queue = queue.LifoQueue() diff --git a/triangle_grid.py b/triangle_grid.py deleted file mode 100644 index c907325..0000000 --- a/triangle_grid.py +++ /dev/null @@ -1,408 +0,0 @@ -from color import RGBW - - -def make_tri(model, n_rows): - return TriangleGrid(model, n_rows) - - -def calc_last_row_len(max_row): - row_len = 1 - curr_row = 1 - - while curr_row < max_row: - if max_row == 1: - row_len = 1 - else: - row_len += 2 - curr_row += 1 - return row_len - -## -## Triangle Grid class to represent one strip -## - - -# Some reading on grids http://www-cs-students.stanford.edu/~amitp/game-programming/grids/ -class TriangleGrid(object): - def __init__(self, model, n_rows): - self._model = model - self._n_rows = n_rows - self._len_of_last_row = calc_last_row_len(n_rows) - - self._triangle_grid = [[None for i in range(self._len_of_last_row)] for j in range(self._n_rows)] #B/C this way of building a 2d array give you buggy garbage where setting [0][0] assigns all rows at posn [0] the value you are seetting[[None]*self.len_of_last_row]*self.n_rows - self._build_triangle_array_and_grid(n_rows) - - def __str__(self): - return str("ME") - - def __repr__(self): - print("what is this for?") - - #OMG, this is such a nighmare... but it works... - def _build_triangle_array_and_grid(self, n_rows): - self._cells = [] - if n_rows < 1: - raise Exception('row num must be 1+', n_rows) - if n_rows > 16: - raise Exception('row num must be <15') - if len(self._cells) > 0: - raise Exception('You have already built a triangle grid, clear this one first') - - #By 'Y' I mean column..... sorry - grid_y_start_pos = (int(self._len_of_last_row)-1)//2 - grid_y_curr_pos = grid_y_start_pos - - row_len = 0 - end_cell_id = 0 - cell_id = 0 - end_cell_id = 0 - top_pixel = 73 #pointy top - btm_pixel = 67 #flat btm - curr_row = 0 - while curr_row < n_rows: -# print "Curr Row:", curr_row - left_set = False - cells_added = 0 - up_down = 'up' - l_corner = False - r_corner = False - top = False - if curr_row == n_rows: - l_corner = True - else: - l_corner = False - - is_l_edge = False - is_r_edge = False - is_btm_edge = False - - loop_start = True - row_pos = 1 - while cell_id <= end_cell_id: - if curr_row == n_rows and cell_id == end_cell_id+1: - r_corner = True - else: - r_corner = False - - if curr_row == 0: - top = True - else: - top = False - if loop_start is True: - is_l_edge = True - else: - is_l_edge = False - - if cell_id == end_cell_id+1: - is_r_edge = True - else: - is_r_edge = False - if curr_row == n_rows: - if up_down in 'up': - is_btm_edge = True - else: - is_btm_edge = False - else: - is_btm_edge = False - cells_added += 1 - - tco = None #triagnel cell object - if up_down == 'up': - tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [top_pixel, top_pixel-1, top_pixel-2, top_pixel-3, top_pixel-4, top_pixel-5]) - self._cells.append(tco) - top_pixel -= 6 - up_down = 'down' - else: - if curr_row >1: - tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [btm_pixel, btm_pixel+1, btm_pixel+2, btm_pixel+3, btm_pixel+4, btm_pixel+5]) - self._cells.append(tco) - btm_pixel += 6 - else: - tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [btm_pixel, btm_pixel-1, btm_pixel-2, btm_pixel-3, btm_pixel-4, btm_pixel-5]) - self._cells.append(tco) - btm_pixel -= 6 - up_down = 'up' - - #Add Cell To Triangle Grid! - tco.add_row_col(curr_row, grid_y_curr_pos) - self._triangle_grid[curr_row][grid_y_curr_pos] = tco - grid_y_curr_pos += 1 - - if l_corner is True: - l_corner = False - loop_start = False - - row_pos += 1 - - cell_id +=1 - - if cell_id == end_cell_id: - up_down = 'up' - end_cell_id += 2 + cells_added - - grid_y_curr_pos = grid_y_start_pos - curr_row-1 - - curr_row += 1 - btm_pixel -= 2 #(curr_row*6)+7 - if curr_row > 1: - btm_pixel -= (curr_row*6)+9+9 - - top_pixel -= (curr_row*6)+9 - - def go(self): - self._model.go() - - def clear(self): - self.set_all_cells(RGBW(0, 0, 0, 0)) - self.go() - - def get_cells(self): - return self._cells - - def get_triangle_grid(self): - return self._triangle_grid - - def get_cell_by_grid_coords(self, rown, coln): - "RETURNS cell object or None if no cell is mapped to the coord" - - if rown > self._n_rows: - return None - elif coln > self._len_of_last_row: - return None - else: - return self._triangle_grid[rown][coln] - - def get_cell_by_id(self, cell_id): - return self._cells[cell_id+1] - - def get_cell_by_array_posn(self, arr_pos): - return self._cells[arr_pos] - - def set_cell(self, cell, color): - if cell is None: - print("WARNING: Skipping 'Nonetype' cell") - else: - for pixel in cell.get_pixels(): - self._model.set_pixel(pixel, color, cell.get_id()) - - def set_cell_by_cellid(self, cell, color): - for pixel in self._cells[cell].get_pixels(): - self._model.set_pixel(pixel, color, cell) - - def get_all_cells(self): - "Return the list of valid cell IDs" - return self._cells - - def set_cells_by_cellid(self, cells, color): - for cell in cells: - for pixel in self._cells[cell].get_pixels(): - self._model.set_pixel(pixel, color, cell.get_id()) - - def set_cells(self, cells, color): - for cell in cells: - if cell is None: - print("WARNING: Skipping 'Nonetype' cell") - else: - for pixel in cell.get_pixels(): - self._model.set_pixel(pixel, color, cell.get_id()) - - def set_all_cells(self, color): - for cell in self._cells: - for pixel in cell.get_pixels(): - self._model.set_pixel(pixel, color, cell.get_id()) - - def set_pixel(self, pixel, color, cellid): - self._model.set_pixel(pixel, color, cellid) ###Have to pass the cellid through b/c the simulator does not understand pixels - - def clear(self): - self.set_all_cells(RGBW(0, 0, 0, 0)) - self.go() - - def go(self): - self._model.go() - - # convenience methods for grabbing useful parts of the triangle grid - - def get_left_side_cells(self): - cells = [] - for i in self._cells: - if i.is_left_edge(): - cells.append(i) - return cells - - def get_right_side_cells(self): - cells = [] - for i in self._cells: - if i.is_right_edge(): - cells.append(i) - return cells - - def get_bottom_side_cells(self): - cells = [] - for i in self._cells: - if i.is_bottom_edge(): - cells.append(i) - return cells - - def get_up_cells(self): - cells = [] - for i in self._cells: - if i.is_up(): - cells.append(i) - return cells - - def get_down_cells(self): - cells = [] - for i in self._cells: - if i.is_down(): - cells.append(i) - return cells - - def is_edge(self, cell): - if cell._l_edge: - return True - elif cell._r_edge: - return True - elif cell._btm_side: - return True - else: - return False - - # Triangle Grid Helper Functions - - def get_edge_neighbors_by_coord(self, row, col): - "returned in a tuple of (left neighbor, middle neighbor, right neighbor)" - "Where left is the edge directly to the left of the cell, regardless of up/down orientation" - "Where middle is either the top or bottom neighbor depending where the edge is. the cell knows its up/down orientation" - "Right neighbor is the cell immediately to the right." - cell = self.get_cell_by_grid_coords(row, col) - if cell is None: - return None - else: - l = None - m = None - r = None - if cell.is_up(): - l = self.get_cell_by_grid_coords(row, col-1) - m = self.get_cell_by_grid_coords(row+1, col) - r = self.get_cell_by_grid_coords(row, col+1) - else: - l = self.get_cell_by_grid_coords(row, col-1) - m = self.get_cell_by_grid_coords(row-1, col) - r = self.get_cell_by_grid_coords(row, col+1) - return (l, m, r) - - def get_edge_neighbors_by_cell(self, cell): - return self.get_edge_neighbors_by_coord(cell.get_row_num(), cell.get_col_pos()) - - def get_vertex_neighbors_by_coord(self, row, col): - "Return the neighbors whose vertexes are point to point" - "Uses same logic as edge_neighbors, left, middle, right" - cell = self.get_cell_by_grid_coords(row, col) - if cell is None: - return None - else: - l = None - m = None - r = None - if cell.is_up(): - l = self.get_cell_by_grid_coords(row+1, col-1) - m = self.get_cell_by_grid_coords(row-1, col) - r = self.get_cell_by_grid_coords(row+1, col+1) - else: - l = self.get_cell_by_grid_coords(row-1, col-1) - m = self.get_cell_by_grid_coords(row+1, col) - r = self.get_cell_by_grid_coords(row-1, col+1) - - return (l, m, r) - - def get_vertex_neighbors_by_cell(self, cell): - return self.get_vertex_neighbors_by_coord(cell.get_row_num(), cell.get_col_pos()) - - def get_hexagon_from_btm_cell_by_coords(self, row, col): - "Given the coordinates of an up facing cell, return the surrounding cells which will make " - "A hexagon with the specified cell as the base" - "the tuple will begin with the upper left cell, then it's upper neighbor, then its upper right" - "neighbor.... and so on" - - cell = self.get_cell_by_grid_coords(row, col) - if cell is None: - return None - else: - a = self.get_cell_by_grid_coords(row, col-1) - b =self.get_cell_by_grid_coords(row-1, col-1) - c =self.get_cell_by_grid_coords(row-1, col) - d =self.get_cell_by_grid_coords(row-1, col+1) - e =self.get_cell_by_grid_coords(row, col+1) - return (cell, a, b, c, d, e) - - def get_hexagon_from_btm_cell_by_cell(self, cell): - return self.get_hexagon_from_btm_cell_by_coords(cell.get_row_num(), cell.get_col_pos()) - - -class TriangleCell(object): - def __init__(self, cell_id, row, up_down, is_top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, pixels=[]): - - self._id = cell_id - self._row_n = row - self._row_pos = row_pos - self._l_edge = is_l_edge - if cell_id in [1, 3]: - self._l_edge = True - self._r_edge = is_r_edge - if cell_id in [1, 3]: - self._r_edge= True - self._top_cell = is_top - self._btm_right = r_corner - self._btm_left = l_corner - self._pointy_side = up_down - self._btm_side = is_btm_edge - self._up_down = up_down - self._pixels = pixels #do we really need a pixel class? - - def add_row_col(self, r, c): - self._grid_row_n = r - self._grid_col_n = c - - def get_id(self): - return self._id - - def get_pixels(self,oriented=True): - if oriented: - if self._up_down is 'up': - return self._pixels - else: - if self._row_n == 2: ####FIXME Something wrrong with bottom cells in row 2 and maybe deeper - return self._pixels - else: - return self._pixels[::-1] - else: - return self._pixels - - def get_row_num(self): - return self._grid_row_n - def get_col_pos(self): - return self._grid_col_n - def is_left_edge(self): - return self._l_edge - def is_right_edge(self): - return self._r_edge - def is_bottom_edge(self): - return self._btm_side - def is_top(self): - return self._top_cell - def is_right_btm_corner(self): - return self._btm_right - def is_left_btm_corner(self): - return self._btm_left - def row_num(self): - return self._row_n - def is_up(self): - return self._up_down in "up" - def is_down(self): - return self._up_down in "down" - - -class TriangleCellPixel(object): - def __init__(self): - pass diff --git a/morph.py b/util/morph.py similarity index 100% rename from morph.py rename to util/morph.py diff --git a/util.py b/util/util.py similarity index 51% rename from util.py rename to util/util.py index 7018eaf..b389b7f 100644 --- a/util.py +++ b/util/util.py @@ -1,11 +1,19 @@ -# http://stackoverflow.com/questions/1969240/mapping-a-range-of-values-to-another +from color import HSV +import random + +def choose_random_hsv(): + + return(HSV(random.uniform(0.0,1.0), random.uniform(0.0,1.0), random.uniform(0.0,1.0))) + + +# http://stackoverflow.com/questions/1969240/mapping-a-range-of-values-to-another def make_interpolater(in_min, in_max, out_min, out_max): """Return a function that translates from one range to another""" - # Figure out how wide each range is + # Figure out how wide each range is inSpan = in_max - in_min outSpan = out_max - out_min - # Compute the scale factor between left and right values + # Compute the scale factor between left and right values scaleFactor = float(outSpan) / float(inSpan) return lambda value: out_min + (value - in_min) * scaleFactor diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..7011911 --- /dev/null +++ b/web/__init__.py @@ -0,0 +1 @@ +from .web import TriangleWeb