I did some nice spiros ;^).
I can't resiste to show you my wheeling 'guiguitrochoid': http://www.openprocessing.org/visuals/?visualID=760
fullscreen public class ColorFunction
{
// 1: Minimum, 2: Maximum, 3: rotMulipiler, 4: rotAdder
float[] rData = new float[4];
float[] gData = new float[4];
float[] bData = new float[4];
public ColorFunction(float[] _rData, float[] _gData, float[] _bData)
{
rData = _rData;
gData = _gData;
bData = _bData;
}
public float getR(float rot)
{
return (rData[0] + rData[1]*0.5f*(sin(rot*rData[2] + rData[3])+1));
}
public float getB(float rot)
{
return (bData[0] + bData[1]*0.5f*(sin(rot*bData[2] + bData[3])+1));
}
public float getG(float rot)
{
return (gData[0] + gData[1]*0.5f*(sin(rot*gData[2] + gData[3])+1));
}
public void SetStroke(float rot)
{
stroke(getR(rot), getG(rot), getB(rot));
}
public float[] GetColor(float rot)
{
return new float[]{getR(rot), getG(rot), getB(rot)};
}
}
// A cycloid that points inwards but travel on the INSIDE of the main circle
public class Entocycloid extends Spirograph
{
public Entocycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
}
public Entocycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety, float _rot, float _rotSpeed)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
rot = _rot;
rotSpeed = _rotSpeed;
}
public Entocycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety, float _rot, float _rotSpeed, ColorFunction _colorFunction)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
rot = _rot;
rotSpeed = _rotSpeed;
colorFunction = _colorFunction;
}
public void draw()
{
float currx = offsetx + (mainRadius - smallRadius) * cos(rot) + positionRadius * cos(rot*(mainRadius - smallRadius)/smallRadius);
float curry = offsety + (mainRadius - smallRadius) * sin(rot) + positionRadius * sin(rot*(mainRadius - smallRadius)/smallRadius);
if(prevx != Float.MIN_VALUE)
{
colorFunction.SetStroke(rot);
line(prevx, prevy, currx, curry);
}
prevx = currx;
prevy = curry;
rot += rotSpeed;
}
}
// A cycloid that points inwards but travel on the OUTSIDE of the main circle
public class Epicycloid extends Spirograph
{
public Epicycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
}
public Epicycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety, float _rot, float _rotSpeed)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
rot = _rot;
rotSpeed = _rotSpeed;
}
public Epicycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety, float _rot, float _rotSpeed, ColorFunction _colorFunction)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
rot = _rot;
rotSpeed = _rotSpeed;
colorFunction = _colorFunction;
}
public void draw()
{
float currx = offsetx + (mainRadius + smallRadius) * cos(rot) - positionRadius * cos(rot*(mainRadius + smallRadius)/smallRadius);
float curry = offsety + (mainRadius + smallRadius) * sin(rot) - positionRadius * sin(rot*(mainRadius + smallRadius)/smallRadius);
if(prevx != Float.MIN_VALUE)
{
colorFunction.SetStroke(rot);
line(prevx, prevy, currx, curry);
}
prevx = currx;
prevy = curry;
rot += rotSpeed;
}
}
// A cycloid that points outwards but travel on the OUTSIDE of the main circle
public class Hypercycloid extends Spirograph
{
public Hypercycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
}
public Hypercycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety, float _rot, float _rotSpeed)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
rot = _rot;
rotSpeed = _rotSpeed;
}
public Hypercycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety, float _rot, float _rotSpeed, ColorFunction _colorFunction)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
rot = _rot;
rotSpeed = _rotSpeed;
colorFunction = _colorFunction;
}
public void draw()
{
float currx = offsetx + (mainRadius + smallRadius) * cos(rot) - positionRadius * cos(rot*(mainRadius + smallRadius)/smallRadius);
float curry = offsety + (mainRadius + smallRadius) * sin(rot) + positionRadius * sin(rot*(mainRadius + smallRadius)/smallRadius);
if(prevx != Float.MIN_VALUE)
{
colorFunction.SetStroke(rot);
line(prevx, prevy, currx, curry);
}
prevx = currx;
prevy = curry;
rot += rotSpeed;
}
}
// A cycloid that points outwards but travel on the INSIDE of the main circle
public class Hypocycloid extends Spirograph
{
public Hypocycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
}
public Hypocycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety, float _rot, float _rotSpeed)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
rot = _rot;
rotSpeed = _rotSpeed;
}
public Hypocycloid(float _mainRadius, float _smallRadius, float _positionRadius, float _offsetx, float _offsety, float _rot, float _rotSpeed, ColorFunction _colorFunction)
{
mainRadius = _mainRadius;
smallRadius = _smallRadius;
positionRadius = _positionRadius;
offsetx = _offsetx;
offsety = _offsety;
rot = _rot;
rotSpeed = _rotSpeed;
colorFunction = _colorFunction;
}
public void draw()
{
float currx = offsetx + (mainRadius - smallRadius) * cos(rot) + positionRadius * cos(rot*(mainRadius - smallRadius)/smallRadius);
float curry = offsety + (mainRadius - smallRadius) * sin(rot) - positionRadius * sin(rot*(mainRadius - smallRadius)/smallRadius);
if(prevx != Float.MIN_VALUE)
{
colorFunction.SetStroke(rot);
line(prevx, prevy, currx, curry);
}
prevx = currx;
prevy = curry;
rot += rotSpeed;
}
}
public abstract class Spirograph
{
float rot = 0f;
float rotSpeed = PI / 30;
float mainRadius;
float smallRadius;
float positionRadius;
float offsetx, offsety;
float prevx = Float.MIN_VALUE;
float prevy = Float.MIN_VALUE;
ColorFunction colorFunction = new ColorFunction(new float[]{ 0, 240, 1f/5f, 0 }, new float[]{ 0, 210, 1f/11f, PI }, new float[]{ 0, 230, 1f/9f, PI/3 });
public abstract void draw();
}
/* SPIROGRAPH CREATOR by CALLUM ROGERS
*
* This code is licensed under the GNU GPL, the terms of which can be found here: http://creativecommons.org/licenses/GPL/2.0/
* You are free to modify, distrbute and share this source code and binaries as long as you abide by the terms of the GPL
*
*/
Spirograph s;
boolean paused = false;
boolean speededUp = false;
boolean superSpeedUp = false;
boolean mouseDown = false;
PFont font;
boolean createOwn = false;
int createOwnStage = -1;
float[] createOwnData = new float[8];
int ct = -1;
int[] lastMousePos = new int[]{ Integer.MIN_VALUE, Integer.MIN_VALUE };
public static final int CYCLOID_ENTO = 0;
public static final int CYCLOID_EPI = 1;
public static final int CYCLOID_HYPO = 2;
public static final int CYCLOID_HYPER = 3;
boolean cfDesigner = false;
float[] lastStroke = new float[3];
ColorFunction colorFunction = new ColorFunction(new float[]{ 0, 240, 1f/5f, 0 }, new float[]{ 0, 210, 1f/11f, PI }, new float[]{ 0, 230, 1f/9f, PI/3 });
float crot = 0f;
boolean infoScreen = true;
void setup()
{
size(600,600);
smooth();
frameRate(60);
noFill();
stroke(100);
background(255);
ellipseMode(CENTER);
font = loadFont("Calibri-16.vlw");
textFont(font, 16);
textAlign(LEFT);
s = CreateRandomSpirograph();
}
Spirograph CreateRandomSpirograph()
{
Spirograph sp = s;
switch((int)random(4))
{
case 0: sp = new Entocycloid(width/2 - 20 - random(20), random(width/9,width/5) + random(-7,7), random(width/9, width/5), width/2, height/2, 0f, PI/30, colorFunction); break;
case 1: sp = new Epicycloid(width/5 - random(20), random(width/9,width/5) + random(-7,7), random(width/9, width/5), width/2, height/2, 0f, PI/30, colorFunction); break;
case 2: sp = new Hypocycloid(width/2 - 20 - random(20), random(width/9,width/5) + random(-7,7), random(width/9, width/5), width/2, height/2, 0f, PI/30, colorFunction); break;
case 3: sp = new Hypercycloid(width/5 - random(20), random(width/9,width/5) + random(-7,7), random(width/9, width/5), width/2, height/2, 0f, PI/30, colorFunction); break;
}
return sp;
}
ColorFunction CreateRandomColorFunction()
{
return new ColorFunction(new float[]{ random(0,100), random(160,230), 1f/random(3,11), random(0,TWO_PI) }, new float[]{ random(0,100), random(160,230), 1f/random(3,11), random(0,TWO_PI) }, new float[]{ random(0,100), random(160,230), 1f/random(3,11), random(0,TWO_PI) });
}
void draw()
{
if(infoScreen)
{
if(mouseDown)
{
background(255);
infoScreen = false;
}
else
{
background(255);
fill(0);
text("Spirograph creator - made by Callum Rogers",10,20);
text("Buttons: ",20,60);
text("N: Create your own custom spirograph!",120,60);
text("R: Create a random spirograph",120,80);
text("F: Randomize the colours",120,100);
text("C: Create your own custom colours!",120,120);
text("P: Pause the drawing",120,140);
text("S: Speed up drawing 2x",120,160);
text("Z: Speed up drawing 4x",120,180);
text("I: Show this screen",120,200);
textAlign(CENTER);
text("Click the mouse or press a button to continue!",width/2,height/2);
textAlign(LEFT);
noFill();
}
}
else if(createOwn)
{
background(255);
if(createOwnStage == -1)
{
if(ct != -1)
{
createOwnStage++;
}
else
{
fill(0);
text("Please select the type of spirography you want: ",10,20);
text("Outward Pointing (Hypercycloid) - Press H",20,40);
text("Inward Pointing (Epicycloid) - Press E",20,60);
noFill();
}
}
else if(createOwnStage == 0)
{
if(mouseDown)
{
mouseDown = false;
createOwnData[0] = mouseX;
createOwnData[1] = mouseY;
createOwnStage++;
if(lastMousePos[0] != Integer.MIN_VALUE)
{
mouseX = lastMousePos[0];
mouseY = lastMousePos[1];
}
lastMousePos = new int[]{ Integer.MIN_VALUE, Integer.MIN_VALUE };
}
else
{
fill(0);
text("Select the center of the main circle (Press m to select the exact middle)",10,20);
noFill();
ellipse(mouseX, mouseY, 2, 2);
}
}
else if(createOwnStage == 1)
{
if(mouseDown)
{
mouseDown = false;
float radius = dist(createOwnData[0],createOwnData[1],mouseX,mouseY);
createOwnData[2] = radius;
createOwnStage++;
}
else
{
fill(0);
text("Select the size of the main circle (move your mouse first, don't click it straight away!)",10,20);
noFill();
float diameter = dist(createOwnData[0],createOwnData[1],mouseX,mouseY)*2;
ellipse(createOwnData[0], createOwnData[1], diameter, diameter);
}
}
else if(createOwnStage == 2)
{
if(mouseDown)
{
mouseDown = false;
createOwnData[3] = mouseX;
createOwnData[4] = mouseY;
createOwnData[5] = abs(createOwnData[2]-dist(mouseX, mouseY, createOwnData[0],createOwnData[1]));
if(dist(mouseX, mouseY, createOwnData[0],createOwnData[1]) > createOwnData[2])
{
if(ct == CYCLOID_ENTO) ct = CYCLOID_EPI;
else if(ct == CYCLOID_HYPO) ct = CYCLOID_HYPER;
}
createOwnStage++;
}
else
{
fill(0);
text("Select the center of the moving circle (it can be on the outside of the main circle)",10,20);
noFill();
ellipse(createOwnData[0], createOwnData[1], createOwnData[2]*2, createOwnData[2]*2);
float diameter = (createOwnData[2]-dist(mouseX, mouseY, createOwnData[0],createOwnData[1]))*2;
ellipse(mouseX, mouseY, diameter, diameter);
}
}
else if(createOwnStage == 3)
{
float distFromCircle = dist(mouseX, mouseY, createOwnData[3], createOwnData[4]);
if(mouseDown)
{
mouseDown = false;
createOwnData[6] = distFromCircle;
createOwnData[7] = atan((mouseY - createOwnData[4])/(mouseX - createOwnData[3]));
createOwnStage++;
}
else
{
fill(0);
text("Select the point to draw from on (or off!) the moving circle",10,20);
noFill();
ellipse(createOwnData[0], createOwnData[1], createOwnData[2]*2, createOwnData[2]*2);
ellipse(createOwnData[3], createOwnData[4], createOwnData[5]*2, createOwnData[5]*2);
stroke(150);
line(createOwnData[3],createOwnData[4],mouseX,mouseY);
stroke(lastStroke[0], lastStroke[1], lastStroke[2]);
ellipse(mouseX, mouseY, 2, 2);
}
}
else if(createOwnStage >= 4)
{
createOwn = false;
infoScreen = false;
//PrintArray(createOwnData);
switch(ct)
{
case CYCLOID_ENTO: s = new Entocycloid(createOwnData[2], createOwnData[5], createOwnData[6], createOwnData[0], createOwnData[1], createOwnData[7], PI / 30, colorFunction); break;
case CYCLOID_EPI: s = new Epicycloid(createOwnData[2], createOwnData[5], createOwnData[6], createOwnData[0], createOwnData[1], createOwnData[7], PI / 30, colorFunction); break;
case CYCLOID_HYPO: s = new Hypocycloid(createOwnData[2], createOwnData[5], createOwnData[6], createOwnData[0], createOwnData[1], createOwnData[7], PI / 30, colorFunction); break;
case CYCLOID_HYPER: s = new Hypercycloid(createOwnData[2], createOwnData[5], createOwnData[6], createOwnData[0], createOwnData[1], createOwnData[7], PI / 30, colorFunction); break;
}
}
}
else if(cfDesigner)
{
background(255);
float[] sections = new float[]{ 40, 40+(width-80)/6, 40+(width-80)*2/6, 40+(width-80)*3/6, 40+(width-80)*4/6 };
fill(0);
text("Red:",10,sections[0]);
text("Green:",10,sections[1]);
text("Blue:",10,sections[2]);
text("Range:",60,sections[0]);
text("Range:",60,sections[1]);
text("Range:",60,sections[2]);
DrawGradient((int)140, (int)sections[0]-15, width-170, 30, color(0,0,0), color(255,0,0), X_AXIS);
DrawGradient((int)140, (int)sections[1]-15, width-170, 30, color(0,0,0), color(0,255,0), X_AXIS);
DrawGradient((int)140, (int)sections[2]-15, width-170, 30, color(0,0,0), color(0,0,255), X_AXIS);
stroke(0);
line(140+colorFunction.rData[0]/255*(width-170), sections[0]-19, 140+colorFunction.rData[0]/255*(width-170), sections[0]+19);
line(140+colorFunction.rData[1]/255*(width-170), sections[0]-19, 140+colorFunction.rData[1]/255*(width-170), sections[0]+19);
text("Min",130+colorFunction.rData[0]/255*(width-170), sections[0]-21);
text("Max",130+colorFunction.rData[1]/255*(width-170), sections[0]-21);
line(140+colorFunction.gData[0]/255*(width-170), sections[1]-19, 140+colorFunction.gData[0]/255*(width-170), sections[1]+19);
line(140+colorFunction.gData[1]/255*(width-170), sections[1]-19, 140+colorFunction.gData[1]/255*(width-170), sections[1]+19);
text("Min",130+colorFunction.gData[0]/255*(width-170), sections[1]-21);
text("Max",130+colorFunction.gData[1]/255*(width-170), sections[1]-21);
line(140+colorFunction.bData[0]/255*(width-170), sections[2]-19, 140+colorFunction.bData[0]/255*(width-170), sections[2]+19);
line(140+colorFunction.bData[1]/255*(width-170), sections[2]-19, 140+colorFunction.bData[1]/255*(width-170), sections[2]+19);
text("Min",130+colorFunction.bData[0]/255*(width-170), sections[2]-21);
text("Max",130+colorFunction.bData[1]/255*(width-170), sections[2]-21);
text("Drag the sliders to change the range for each component of the colour.",10,sections[3]-20);
text("Press C to leave. Press F to randomise the colours.",10,sections[3]);
text("Below shows how the colour function changes with time:",10,sections[3]+20);
fill(colorFunction.getR(crot), colorFunction.getG(crot), colorFunction.getB(crot));
crot += PI / 30;
rect(20,height-20-sections[2]-20,width-40,height-sections[4]+20);
noFill();
if(mouseDown)
{ // Sliders:
if((mouseX > 140) && (mouseX < width-30))
{ // Red
if((mouseY > sections[0]-15)&&(mouseY < sections[0]+15))
{
if(dist1(140+colorFunction.rData[0]/255*(width-170),mouseX) < 20)
{
colorFunction.rData[0] = (mouseX-140)/(float)(width-170)*255f;
if(colorFunction.rData[0] >= colorFunction.rData[1]) colorFunction.rData[1]++;
}
else if(dist1(140+colorFunction.rData[1]/255*(width-170),mouseX) < 20)
{
colorFunction.rData[1] = (mouseX-140)/(float)(width-170)*255f;
if(colorFunction.rData[1] <= colorFunction.rData[0]) colorFunction.rData[0] = colorFunction.rData[1]-1;
}
} // Green
else if((mouseY > sections[1]-15)&&(mouseY < sections[1]+15))
{
if(dist1(140+colorFunction.gData[0]/255*(width-170),mouseX) < 20)
{
colorFunction.gData[0] = (mouseX-140)/(float)(width-170)*255f;
if(colorFunction.gData[0] >= colorFunction.gData[1]) colorFunction.gData[1]++;
}
else if(dist1(140+colorFunction.gData[1]/255*(width-170),mouseX) < 20)
{
colorFunction.gData[1] = (mouseX-140)/(float)(width-170)*255f;
if(colorFunction.gData[1] <= colorFunction.gData[0]) colorFunction.gData[0] = colorFunction.gData[1]-1;
}
} // Blue
if((mouseY > sections[2]-15)&&(mouseY < sections[2]+15))
{
if(dist1(140+colorFunction.bData[0]/255*(width-170),mouseX) < 20)
{
colorFunction.bData[0] = (mouseX-140)/(float)(width-170)*255f;
if(colorFunction.bData[0] >= colorFunction.bData[1]) colorFunction.bData[1]++;
}
else if(dist1(140+colorFunction.bData[1]/255*(width-170),mouseX) < 20)
{
colorFunction.bData[1] = (mouseX-140)/(float)(width-170)*255f;
if(colorFunction.bData[1] <= colorFunction.bData[0]) colorFunction.bData[0] = colorFunction.bData[1]-1;
}
}
}
}
}
else if(!paused)
{
s.draw();
if(speededUp || superSpeedUp)
s.draw();
if(superSpeedUp)
{
s.draw();
s.draw();
}
}
}
float dist1(float point1, float point2)
{
return abs(point1 - point2);
}
static final int Y_AXIS = 0;
static final int X_AXIS = 1;
void DrawGradient(int x, int y, float w, float h, color c1, color c2, int axis ){
// calculate differences between color components
float deltaR = red(c2)-red(c1);
float deltaG = green(c2)-green(c1);
float deltaB = blue(c2)-blue(c1);
// choose axis
if(axis == Y_AXIS){
/*nested for loops set pixels
in a basic table structure */
// column
for (int i=x; i<=(x+w); i++){
// row
for (int j = y; j<=(y+h); j++){
color c = color(
(red(c1)+(j-y)*(deltaR/h)),
(green(c1)+(j-y)*(deltaG/h)),
(blue(c1)+(j-y)*(deltaB/h))
);
set(i, j, c);
}
}
}
else if(axis == X_AXIS){
// column
for (int i=y; i<=(y+h); i++){
// row
for (int j = x; j<=(x+w); j++){
color c = color(
(red(c1)+(j-x)*(deltaR/w)),
(green(c1)+(j-x)*(deltaG/w)),
(blue(c1)+(j-x)*(deltaB/w))
);
set(j, i, c);
}
}
}
}
void mousePressed()
{
mouseDown = true;
}
void mouseReleased()
{
mouseDown = false;
}
void keyPressed()
{
if(infoScreen)
{
background(255);
infoScreen = false;
}
else if(key != CODED)
{
switch(key)
{
case 'n': lastStroke = s.colorFunction.GetColor(s.rot); createOwnStage = -1; ct = -1; if(createOwn == false) createOwn = true; break;
case 'm': if(createOwn){ lastMousePos[0] = mouseX; lastMousePos[1] = mouseY; mouseX = width/2; mouseY = height/2; if(createOwnStage == 0) mouseDown = true; }; break;
case 'h': if(createOwn && (createOwnStage == -1)) ct = CYCLOID_HYPO; break;
case 'e': if(createOwn && (createOwnStage == -1)) ct = CYCLOID_ENTO; break;
case 'p': paused = !paused; break;
case 'r': background(255); s = CreateRandomSpirograph(); break;
case 'f': colorFunction = CreateRandomColorFunction(); break;
case 'c': background(255); if(cfDesigner) s.colorFunction = colorFunction; cfDesigner = !cfDesigner; infoScreen = false; break;
case 'i': infoScreen = true; break;
case 's': speededUp = true; break;
case 'z': superSpeedUp = true; break;
// Uncomment this to enable saving (doesn't work on the internet)
//case 's': Calendar cal = Calendar.getInstance(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss"); save("spirograph-save-" + sdf.format(cal.getTime()) + ".tiff"); break;
}
}
}
void keyReleased()
{
if(key != CODED)
{
switch(key)
{
case 's': speededUp = false; break;
case 'z': superSpeedUp = false; break;
}
}
}
void PrintArray(float[] arr)
{
for(int i = 0; i < arr.length; i++)
{
println(i + ": " + arr[i]);
}
}
This is a sketch that allows you to create spirograph-like cycloids.
Buttons:
N: Allows you to create your own custom spirograph!
R: Creates a random spirograph
F: Randomizes the colours
C: Bring up a screen that allows you to create your own custom colours!
P: Pauses the drawing of the spirograph
I: Shows the information screen.
When creating custom spirographs, you may have to move your mouse before clicking to continue.
Enjoy!
CgRobot