RGB/HSV/HSL Color Cube
An improved version of my RGB cube (which I forked by accident, causing me to create this).
Interaction:
A-D toggles through rgb,hsv and hsl color models (D toggles right, A toggles left, starting at rgb).
Space stops the automatic rotation.
Left/Right spins the cube around manually.
The red, green and blue lines represent the three color channels (rgb, hsv, hsl) respectively.
My friends and I had some discussions about mixing color theory and linear algebra. One of the things we talked about was having color components as base vectors in a coordinate system, and this is pretty much just a visualization of that.
Rendered in some faux-3d kind of way, nothing too interesting there.
// Beware of sloppy coding. I'm full of it!
// Do you like descriptive, or at least reasonably
// logical variable names? Good for you, but you won't
// find such things here. ;)
package {
import flash.events.KeyboardEvent;
import flash.geom.Point;
import flash.filters.ColorMatrixFilter;
import flash.events.Event;
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.display.BitmapData;
[SWF(width = "465", height = "465", backgroundColor = "0x000000")]
public class FlashTest extends Sprite {
public function FlashTest() {
bmd = new BitmapData(BMD_HEIGHT,BMD_WIDTH,true,0xff000000);
stage.addChild(new Bitmap(bmd));
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDown);
stage.addEventListener(Event.ENTER_FRAME, doStuff);
}
private function keyDown(e:KeyboardEvent):void {
if(e.keyCode == 68) {
mode = (mode+1) % 3;
} else if (e.keyCode == 65) {
mode = (mode+2) % 3;
}
if(e.keyCode == 32) {
if(spinning) {
forcedTheta = time;
} else {
time = forcedTheta;
}
spinning = !spinning;
}
if(e.keyCode == 37) {
dT += 0.01;
} else if (e.keyCode == 39) {
dT -= 0.01;
}
}
private var mode:int = 0;
private var spinning:Boolean = true;
private var dT:Number = 0;
private var forcedTheta:Number = 0;
private var bmd:BitmapData;
private var time:Number = 0;
private const TIMESTEP:Number = 0.01;
private const FRAMES_BETWEEN_ACTIONS:int = 0;
private var count:int = FRAMES_BETWEEN_ACTIONS;
private const BG_COLOR:uint = 0xff222222;
private const BMD_HEIGHT:int = 464;
private const BMD_WIDTH:int = 464;
private const BMD_NUMPIX:int = BMD_HEIGHT*BMD_WIDTH;
private var pixels:Vector.<uint> = new Vector.<uint>(BMD_HEIGHT*BMD_WIDTH, true);
private var cmf:ColorMatrixFilter = new ColorMatrixFilter(
[0.9, 0, 0, 0, 0,
0, 0.9, 0, 0, 0,
0, 0, 0.9, 0, 0,
0, 0, 0, 0.97, 0]);
private function doStuff(e:Event):void {
if(spinning)
time += TIMESTEP;
else {
forcedTheta += dT;
dT *= 0.95;
dT = (dT<0 ? -dT : dT) < 0.0001 ? 0 : dT;
}
if(++count > FRAMES_BETWEEN_ACTIONS) {
count = 0;
if(spinning)
drawStuff(time);
else
drawStuff(forcedTheta);
}
}
private function drawStuff(theta:Number):void {
// A density constant, fun to play around with!
var D:int = 11;
// Half width height and depth
var hW:int, hH:int, hD:int;
hW = hH = hD = D;
var X:Number, Y:Number, Z:Number;
var index:int, color:uint;
var sc:Number = 110/D;
// Precalculate trig values, because calculating cos and sin of theta
// (2D+1)^3 [or in this case 12167] times per frame instead of one time
// is just plain stupid!
var sinTheta:Number = Math.sin(theta);
var cosTheta:Number = Math.cos(theta);
var x:Number, y:Number, z:Number;
// Clear vector, or use trails?
if(true) {
var l:int = pixels.length;
while(--l >= 0) {
pixels[l] = BG_COLOR;
}
} else {
bmd.applyFilter(bmd, bmd.rect, new Point(), cmf);
pixels = bmd.getVector(bmd.rect);
}
for (X = -hW; X<=hW; ++X) {
for (Y = -hH; Y<=hH; ++Y) {
for (Z = -hD; Z<=hD; ++Z) {
// Set color based on coordinate
if(mode == 0)
color = rgbaToHex((X+hW)/(2*hW), (Y+hH)/(2*hH), (Z+hD)/(2*hD), 1);
else if(mode == 1)
color = hsvaToHex((X+hW)/(2*hW), (Y+hH)/(2*hH), (Z+hD)/(2*hD), 1);
else
color = hslaToHex((X+hW)/(2*hW), (Y+hH)/(2*hH), (Z+hD)/(2*hD), 1);
// Get transformed coordinate index
index = transPtIndex(X,Y,Z,sinTheta,cosTheta,sc);
// Color a 2x2 square
if(index+BMD_WIDTH+1 < BMD_NUMPIX && index > 0) {
pixels[index] = color;
pixels[index+1] = color;
pixels[index+BMD_WIDTH] = color;
pixels[index+BMD_WIDTH+1] = color;
}
}
}
}
// Time to actually draw stuff
bmd.lock();
// Draw all colored points
bmd.setVector(bmd.rect, pixels);
// Define corners
var p1:Point = transPt(new Point(-hW, -hH), sinTheta, cosTheta, sc, -hD);
var p2:Point = transPt(new Point(-hW, hH), sinTheta, cosTheta, sc, -hD);
var p3:Point = transPt(new Point( hW, z hH), sinTheta, cosTheta, sc, -hD);
var p4:Point = transPt(new Point( hW, -hH), sinTheta, cosTheta, sc, -hD);
var p5:Point = transPt(new Point(-hW, -hH), sinTheta, cosTheta, sc, hD);
var p6:Point = transPt(new Point(-hW, hH), sinTheta, cosTheta, sc, hD);
var p7:Point = transPt(new Point( hW, hH), sinTheta, cosTheta, sc, hD);
var p8:Point = transPt(new Point( hW, -hH), sinTheta, cosTheta, sc, hD);
// Draw lines between corners to create a boundary cube
var red:uint = 0xaa0000;
var gre:uint = 0x00aa00;
var blu:uint = 0x0000aa;
var col:uint = 0xaaaaaa;
efla(p1,p2,gre,bmd);
efla(p2,p3,col,bmd);
efla(p3,p4,col,bmd);
efla(p4,p1,red,bmd);
efla(p5,p6,col,bmd);
efla(p6,p7,col,bmd);
efla(p7,p8,col,bmd);
efla(p8,p5,col,bmd);
efla(p1,p5,blu,bmd);
efla(p2,p6,col,bmd);
efla(p3,p7,col,bmd);
efla(p4,p8,col,bmd);
bmd.unlock();
}
private function hslaToHex(h:Number,s:Number,l:Number,a:Number):uint {
var C:Number, X:Number, m:Number, H:Number;
m = 2*l-1;
m = m<0 ? -m : m;
C = (1-m)*s;
H = 6*h;
m = H%2 - 1;
m = m<0 ? -m : m;
X = C*(1-m);
m = l - C/2;
if(H <= 1) {
return rgbaToHex(C+m,X+m,m, a);
} else if (H <= 2) {
return rgbaToHex(X+m,C+m,m, a);
} else if (H <= 3) {
return rgbaToHex(m, C+m,X+m,a);
} else if (H <= 4) {
return rgbaToHex(m, X+m,C+m,a);
} else if (H <= 5) {
return rgbaToHex(X+m,m, C+m,a);
} else {
return rgbaToHex(C+m,m, X+m,a);
}
return 0x00000000;
}
private function hsvaToHex(h:Number,s:Number,v:Number,a:Number):uint {
var C:Number, X:Number, m:Number, H:Number;
C = v*s;
H = 6*h;
m = H%2 - 1;
m = m<0 ? -m : m;
X = C*(1-m);
m = v-C;
if(H <= 1) {
return rgbaToHex(C+m,X+m,m, a);
} else if (H <= 2) {
return rgbaToHex(X+m,C+m,m, a);
} else if (H <= 3) {
return rgbaToHex(m, C+m,X+m,a);
} else if (H <= 4) {
return rgbaToHex(m, X+m,C+m,a);
} else if (H <= 5) {
return rgbaToHex(X+m,m, C+m,a);
} else {
return rgbaToHex(C+m,m, X+m,a);
}
return 0x00000000;
}
private function rgbaToHex(r:Number,g:Number,b:Number,a:Number):uint {
return (0xff*a << 24) | (0xff*r << 16) | (0xff*g << 8) | 0xff*b;
}
// Function to transform a single point
private function transPt(p:Point, stheta:Number, ctheta:Number, sc:Number, z:Number):Point {
var x:Number = p.x*ctheta + p.y*stheta;
var y:Number = p.y*ctheta - p.x*stheta;
z = -z;
p.x = BMD_WIDTH/2+(x+y)*sc;
p.y = BMD_HEIGHT/2+((x-y)/3 + 1.2*z)*sc;
return p;
}
private function transPtIndex(x:Number, y:Number, z:Number, stheta:Number, ctheta:Number, sc:Number):int {
var p:Point = transPt(new Point(x,y), stheta, ctheta, sc, z);
return int(p.x) + BMD_WIDTH*int(p.y);
}
// An implementation of a "Extremely Fast Line Algoritm"
// Just google it and you'll find the code I based it off
private function efla(p1:Point, p2:Point, color:uint, bmd:BitmapData): void
{
var x:int = p1.x; var y:int = p1.y;
var x2:int = p2.x; var y2:int = p2.y;
var shortLen:int = y2-y;
var longLen:int = x2-x;
if ((shortLen ^ (shortLen >> 31)) - (shortLen >> 31) > (longLen ^ (longLen >> 31)) - (longLen >> 31)) {
shortLen ^= longLen;
longLen ^= shortLen;
shortLen ^= longLen;
var yLonger:Boolean = true;
} else
yLonger = false;
var inc:int = longLen < 0 ? -1 : 1;
var multDiff:Number = longLen == 0 ? shortLen : shortLen / longLen;
if (yLonger)
for (var i:int = 0; i != longLen; i += inc)
bmd.setPixel(x + i*multDiff, y+i, color);
else
for (i = 0; i != longLen; i += inc)
bmd.setPixel(x+i, y+i*multDiff, color);
}
}
}