Skip to content

Ecce Signum

Immanentize the Empathy

  • Home
  • About Me
  • Published Works and Literary Matters
  • Indexes
  • Laboratory
  • Notebooks
  • RSS Feed

Tag: procedural art

IWSG, February 2023: Indie Book Covers

2023-02-012023-02-02 John Winkelman

Lindemayer system experiment

Happy February, O my pixel pushers and ink rearrangers!  And for those who observe the Lunar New Year, happy Year of the Water Rabbit! I am feeling re-energized after a weekend at ConFusion 2023 where I volunteered to help run the ‘con, participated on a couple of panels, and hung out with and talked to many many many great writers and artists. ConFusion is my favorite event, and a great way to start the year.

The Insecure Writer’s Support Group question for February 2023 is:

If you are an Indie author, do you make your own covers or purchase them? If you publish trad, how much input do you have about what goes on your cover?

I have played around with making covers for a couple of chapbooks, but never for an actual book, on account of I don’t have any actual books for which to create or commission cover art.

That being said, were I to commission a cover, I would make sure it was created by a human artist, with minimal or no use of any of the AI [sic] image generators (Midjourney, DALL-E 2, etc.) which have been recently in the news for, among other things, copyright infringement of the artwork on which the image generator’s base neural network was trained. Call me old-fashioned, but any art I pay for will be created by actual artist. And I say that as someone who has played around with generative art for over two decades.

More information on this issue here:
https://www.theverge.com/2023/1/16/23557098/generative-ai-art-copyright-legal-lawsuit-stable-diffusion-midjourney-deviantart
https://www.polygon.com/23558946/ai-art-lawsuit-stability-stable-diffusion-deviantart-midjourney
https://petapixel.com/2022/12/21/midjourny-founder-admits-to-using-a-hundred-million-images-without-consent/
https://www.digitalcameraworld.com/news/midjourney-founder-basically-admits-to-copyright-breaching-and-artists-are-angry

Speaking of computer-generated art, I created the above image with my Lindenmayer System Explorer. If you want to duplicate this and play around with it, enter the following bolded text into the “import/export” field, click “Import Data”, then click “render.”

{“iterations”:”4″,”lineLength”:”4″,”initialAngle”:”270″,”angle”:”30″,”angleTaper”:”0″,”lineWidth”:”2″,”lineScale”:”1″,”lineTaper”:”-1″,”lineColors”:”222222,000099,990000,999900″,”backgroundColor”:”000000″,”axiom”:”[F]+[F]+[F]+[F]+[F]+[F]+[F]+[F]+[F]+[F]+[F]+[F]+”,”grammar”:”F:[FF[F+F]F]F!fF[F-F]”}

Happy writing, everyone!

Insecure Writer's Support Group BadgeThe Insecure Writer’s Support Group
is a community dedicated to encouraging
and supporting insecure writers
in all phases of their careers.

 

Posted in Literary MattersTagged art, IWSG, procedural art, publishing 3 Comments on IWSG, February 2023: Indie Book Covers

Yet Another New Old Project : Procedural Terrain

2020-10-12 John Winkelman

[CLICK HERE to see the Procedural Terrain Explorer]

From 2004 to about 2009 I spent a lot of time playing around with procedural generation, generative art, and game design and development. Naturally at one point all of those interests came together and I began to research ways that game maps could be built using procedural generation techniques. I started several games but they never went beyond the planning or rudimentary prototype stage. I learned a lot about programming and wrote a lot of code, but never really had much to show for it.

In November 2007 I successfully created a 3D-ish height map using Perlin Noise as generated by the built-in Actionscript functionality. Since a grayscale image is made up of 256 possible colors, it is simple to interpret the values as heights. And with 256 values to choose from it is simple to come up with color substitutions so that instead of something which looks like clouds you have patterns which look like lakes, plains and mountains.

Now I have successfully recreated and expanded upon that experiment, using plain Javascript. Instead of Perlin Noise, this version uses Simplex Noise, though if I move toward turning this into a full game I will switch to OpenSimplex or something, because Perlin Noise and Simplex Noise are copyrighted.

With the basic generator in place you can add whatever colors you want, to make the height map look like whatever you want.

More blog posts and detailed instructions will follow, but for now head over to the labs and enjoy the experiment!

 

 

Posted in ProgrammingTagged game development, procedural art comment on Yet Another New Old Project : Procedural Terrain

Another New Old Project: Cellular Automata

2020-09-25 John Winkelman

[CLICK HERE to see the 2d Cellular Automata project in the Labs]

Being over twenty years into my career, and having a little downtime, coupled with the back-to-school energy of September, I have been having the time of my life revisiting old visual and math experiments from the early 2000s. This is one of my favorite – a tool/toy for visualizing 2D cellular automata as described by Stephen Wolfram in his book A New Kind of Science.

I first created this tool in Macromedia Flash back in January of 2003. This was about the time that Actionscript became a useable development language, and it was close enough to Javascript syntax that it was easy to pick up. This was fortunate, as my career for the next seven years depended entirely on my Actionscript programming skills. Then I took a sharp left turn into Drupal, about which the less said, the better.

The patterns are created by iterating through an array of ones and zeroes, and choosing the pattern of the next line by applying simple rules to the cells of the previous iteration. As with the earlier Javascript projects, you can save interesting patterns as a .png by right-clicking on the canvas.

I put this together in my spare moments on Codepen.io. You can see the pen with fully-editable code here.

Posted in ProgrammingTagged cellular automata, Javascript, procedural art comment on Another New Old Project: Cellular Automata

Links and Notes for the Week of March 24, 2019

2019-03-29 John Winkelman

  • Fifty of the Best Poetry Books from Modern Authors. I have, or have read, only five of these. I obviously have work to do.
  • Fun Twitter thread UNPOPULAR OPINIONS: EPIC FANTASY.
  • Tor.com’s list of genre books coming out in April 2019
    • Fantasy
    • Science Fiction
    • Genre Benders

Over the past couple of weeks I have resurrected an old Flash experiment from back around, oh, 2007: The Lindenmayer Explorer. The image at the top of this post was created in the Explorer. Head over and check it out! If you create anything interesting, post it online and add a link in the comments here. Basic instructions are on the page. More detailed instructions and notes to follow.

Posted in Links and NotesTagged books, Lindenmayer, poetry, procedural art, reading comment on Links and Notes for the Week of March 24, 2019

Kohonen Map in Actionscript 3

2012-05-01 John Winkelman

Kohonen Map in Actionscript 3

This image is the output of a Kohonen Map, also called a self-organizing map, which is a type of simple neural network, mostly used for sorting and grouping large data sets. I am fairly happy with the results.

This is something I have wanted to do for about four years, starting with a project I worked on back in 2008. Click the image to launch the app. Once launched, click “PLAY/PAUSE” to begin; click “RESET” to restart the sorting process, and change the value in the “grid size” input field to change the dimensions of the color grid. Be careful; anything above 100 (e.g., 10,000 squares) will begin to run slowly.

To create the app, I started with Processing source code I found at jjguy.com. Translating the code to Actionscript 3 took about four hours, and another four to tweak and test and modify to accommodate Flash-specific functionality. All in all, it was surprisingly easy.

The code follows. There are three parts; Main.as, which is contains the UI, global variables, and initialization code; SOM.as, which is the code for the map, and Node.as, which contains the code for the individual blocks of color.

All of the source code, including compiled .swf, can be downloaded here.

 

Main.as

package {
    import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
    import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.text.TextField;
	import flash.text.TextFieldType;
	import flash.text.TextFormat;
	import flash.utils.Timer;
    [SWF(width=720,height=480,frameRate=32,backgroundColor=0x000000)]
    public class Main extends Sprite {
		[Embed(
			source='C:/WINDOWS/Fonts/ARIAL.TTF', 
			fontName='ArialEmbed', 
			unicodeRange='U+0020-U+002F,U+0030-U+0039,U+003A-U+0040,U+0041-U+005A,U+005B-U+0060,U+0061-U+007A,U+007B-U+007E', 
			mimeType="application/x-font-truetype", embedAsCFF="false"
		)]
		private static var _arialEmbed:Class;
		
		internal var _timer:Timer;

		private var isPlaying:Boolean = false;
		
		private var btnPlayPause:Sprite;
		private var btnReset:Sprite;
		private var txtIterations:TextField;
		private var txtGridSize:TextField;
		private var colorTextBG:int = 0xcccccc;
		private var btnTextFormat:TextFormat = new TextFormat("ArialEmbed",12,0x333333,null,null,null,null,null,"center");
		private var labelTextFormat:TextFormat = new TextFormat("ArialEmbed",12,0xededed,null,null,null,null,null,"right");
		private var inputTextFormat:TextFormat = new TextFormat("ArialEmbed",12,0x000000,null,null,null,null,null,"left");

		private var som:SOM;
		private var iter:int;
		private var maxIters:int = 4000;
		public var screenW:int=480;
		public var screenH:int=480;
		private var gridSize:int = 40;

		private var rgb:Array = [
			[1,1,1],
			[0,0,0],
			[1,0,1],
			[1,0,0],
			[0,1,0],
			[0,0,1],
			[1,1,0],
			[0,1,1],
			[1,.4,.4],
			[.25,.25,.25]
		];
		
        public function Main():void {
            addEventListener(Event.ADDED_TO_STAGE,onAddedToStage);
        }
        private function onAddedToStage(e:Event):void {
            removeEventListener(Event.ADDED_TO_STAGE,onAddedToStage);
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
            init();
        }
        private function init():void {
			_timer = new Timer(10);
			_timer.addEventListener(TimerEvent.TIMER,onTimer);
			initInterface();
			initMap();
			_timer.start();
        }
		
		/*	create and populate the UI elements	*/
		private function initInterface():void {
			var txtIterationsLabel:TextField = getTextField("Iterations",10,160,80,20,labelTextFormat);
			addChild(txtIterationsLabel);
			
			txtIterations = getTextField("0",100,160,50,20,labelTextFormat);
			txtIterations.selectable = false;
			addChild(txtIterations);
			
			var txtGridSizeLabel:TextField = getTextField("GRID SIZE",10,200,80,20,labelTextFormat);
			addChild(txtGridSizeLabel);
			
			txtGridSize = getTextField(gridSize.toString(),100,200,80,20,inputTextFormat);
			txtGridSize.background = true;
			txtGridSize.backgroundColor = 0xffffff;
			txtGridSize.border = true;
			txtGridSize.borderColor=0xcccccc;
			txtGridSize.type = TextFieldType.INPUT;
			txtGridSize.restrict = "0-9";
			addChild(txtGridSize);
			
			var playPauseButton:Sprite = getTextButton("PLAY/PAUSE",20,300,80,20);
			playPauseButton.addEventListener(MouseEvent.CLICK,function(e:Event):void {
				isPlaying = !isPlaying;
			});
			addChild(playPauseButton);
			
			var resetButton:Sprite = getTextButton("RESET",120,300,80,20);
			resetButton.addEventListener(MouseEvent.CLICK,function(e:Event):void {
				iter=1;
				gridSize = parseInt(txtGridSize.text);
				maxIters = gridSize*100;
				som.init(maxIters,gridSize,gridSize);
				updateMap();
			});
			addChild(resetButton);
		
		}
		
		/*	create and initialize an instance of the map 	*/
		private function initMap():void {
			som = new SOM(gridSize,gridSize, 3,screenW,screenH);
			som.x = 240;
			som.y = 0;
			addChild(som);
			iter = 1;	
			som.init(maxIters,gridSize,gridSize);
			updateMap();
		}
		
		/*	called on every tick of the timer	*/
		private function onTimer(e:TimerEvent):void {
			if(isPlaying) updateMap();
			e.updateAfterEvent();
		}
		
		/*	tell the map to make another iterations through the data, then render it to the screen	*/
		private function updateMap():void {
			var t:int = Math.floor(Math.random()*rgb.length);
			if (iter < maxIters){
				som.train(iter, rgb[t]);
				som.render();
				txtIterations.text = iter.toString();
				iter++;
			}
		}
		
		/*	functions for building interface elements	*/
		private function getTextField(txt:String,x:int,y:int,w:int,h:int,format:TextFormat):TextField {
			var tf:TextField = new TextField();
			tf.x = x;
			tf.y = y;
			tf.width = w;
			tf.height = h;
			tf.embedFonts = true;
			tf.text = txt;
			tf.setTextFormat(format);
			tf.defaultTextFormat = format;
			return tf;
		}
		private function getTextButton(txt:String,x:int,y:int,w:int,h:int):Sprite {
			var s:Sprite = new Sprite();
			s.x = x;
			s.y = y;
			s.graphics.lineStyle(1,0x808080,1,true);
			s.graphics.beginFill(colorTextBG,1);
			s.graphics.drawRect(0,0,w,h);
			s.graphics.endFill();
			s.buttonMode=true;
			s.mouseChildren = false;
			s.useHandCursor=true;
			var t:TextField = new TextField();
			t.width=w;
			t.height=h;
			t.selectable = false;
			t.embedFonts = true;
			t.text = txt;
			t.setTextFormat(btnTextFormat);
			t.defaultTextFormat = btnTextFormat;
			t.wordWrap = false;
			t.multiline=false;
			s.addChild(t);
			return s;
		}
    }
}

 

SOM.as

package {
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	public class SOM extends Sprite {
		public var mapWidth:int;
		public var mapHeight:int;
		public var nodes:Array;
		public var radius:Number;
		public var timeConstant:Number;
		public var learnRate:Number = 0.05;
		public var inputDimension:int;
		private var pixPerNodeW:Number;
		private var pixPerNodeH:Number;
		
		private var canvasWidth:int;
		private var canvasHeight:int;
		private var canvasData:BitmapData;
		private var canvas:Bitmap;
		
		public var learnDecay:Number;
		public var radiusDecay:Number;
		
		/*	constructor	*/
		public function SOM(w:int,h:int,n:int,mapW:int,mapH:int):void {
			mapWidth = w;
			mapHeight = h;
			radius = (h + w) / 2;
			inputDimension = n;
			canvasWidth = mapW;
			canvasHeight = mapH;
			canvasData = new BitmapData(canvasWidth,canvasHeight,false,0x000000);
			canvas = new Bitmap(canvasData);
			addChild(canvas);
			
		}
		/*	initialize the map	*/
		public function init(iterations:int,w:int,h:int):void {
			mapWidth = w;
			mapHeight = h;
			radius = (h + w) / 2;
			pixPerNodeW = canvasWidth/mapWidth;
			pixPerNodeH = canvasHeight/mapHeight;
			nodes = [];
			for(var i:int = 0; i < mapHeight; i++){
				nodes[i] = [];
				for(var j:int = 0; j < mapWidth; j++) {
					nodes[i][j] = new Node(inputDimension, mapHeight, mapWidth);
					nodes[i][j].x = i;
					nodes[i][j].y = j;
				}//for j
			}//for i
			timeConstant = iterations/Math.log(radius);
			learnDecay = learnRate;
			radiusDecay = (mapWidth + mapHeight) / 2;
		}
		/*	iterate through and update the weights of each node	*/
		public function train(i:int,w:Array):void {  
			radiusDecay = radius*Math.exp(-(i/timeConstant));
			learnDecay = learnRate*Math.exp(-(i/timeConstant));
			//get best matching unit
			var ndxComposite:int = bestMatch(w);
			var x:int = ndxComposite >> 16;
			var y:int = ndxComposite & 0x0000FFFF;
			//scale best match and neighbors...
			for(var a:int = 0; a < mapHeight; a++) {
				for(var b:int = 0; b < mapWidth; b++) {
					var d:Number = dist(nodes[x][y].x, nodes[x][y].y, nodes[a][b].x, nodes[a][b].y);
					var influence:Number = Math.exp((-1 * Math.pow(d,2)) / (2*radiusDecay*i));
					if (d < radiusDecay) {      
						for(var k:int = 0; k < inputDimension; k++) {
							nodes[a][b].w[k] += influence * learnDecay * (w[k] - nodes[a][b].w[k]);
						}//for k
					}	//if d
				} //for j
			} // for i
		} // train()
		
		
		/*	functions used by training method, for calculating node weights and distances	*/
		public function dist(x1:Number,y1:Number,x2:Number,y2:Number):Number {
			return Math.sqrt( Math.pow(x2 - x1,2) + Math.pow(y2 - y1,2) );
		}
		public function distance(node1:Node, node2:Node):Number {
			return Math.sqrt( Math.pow(node2.x - node1.x,2) + Math.pow(node2.y - node1.y,2) );	
		}
		public function bestMatch(w:Array):int {
			var minDist:Number = Math.sqrt(inputDimension);
			var minIndex:int = 0;
			for (var i:int = 0; i < mapHeight; i++) {
				for (var j:int = 0; j < mapWidth; j++) {
				var tmp:Number = weight_distance(nodes[i][j].w, w);
					if (tmp < minDist) {
						minDist = tmp;
						minIndex = (i << 16) + j;
					}  //if
				} //for j
			} //for i
			return minIndex;
		}
		public function weight_distance(x:Array, y:Array):Number {
			if (x.length != y.length) {
				//	trace("Error in SOM::distance(): array lengths don't match");
			}
			var tmp:Number = 0.0;
			for(var i:int = 0; i < x.length; i++) {
				tmp += Math.pow( (x[i] - y[i]),2);
			}
			tmp = Math.sqrt(tmp);
			return tmp;
		}
		
		/*	render node information to the screen	*/
		public function render():void {
			for(var i:int = 0; i < mapWidth; i++) {
				for(var j:int = 0; j < mapHeight; j++) {
					var r:Number = (nodes[i][j].w[0]*255);
					var g:Number = (nodes[i][j].w[1]*255);
					var b:Number = (nodes[i][j].w[2]*255);
					var c:Number = r << 16 ^ g << 8 ^ b;
					canvasData.fillRect(new Rectangle(i*pixPerNodeW, j*pixPerNodeH, pixPerNodeW, pixPerNodeH),c);
				} // for j
			} // for i
		} // render()
	}
}

 

Node.as

package {
	public class Node {
		public var x:int;
		public var y:int;
		public var weightCount:int;
		public var w:Array;
		public function Node(n:int,X:int,Y:int):void {
			x = X;
			y = Y;
			weightCount = n;
			w = [];
			for(var i:int = 0;i<weightCount;i++) {
				w.push(Math.random()*.5+.25);
			}
		}
	}
}

 

Posted in ProgrammingTagged Flash, procedural art comment on Kohonen Map in Actionscript 3

3D Langton’s Ant, in Actionscript 3 Using Away3d

2012-04-12 John Winkelman

Fast on the heels of the 3D Langton’s Ants in Javascript  using Three.js, here is a version done in Actionscript 3 using Away3d. This will look better on faster computers. Click the image to launch the experiment.

Other than some additional rotation around the main axis, it is identical to the Javascript version, including a glitch that kicks in somewhere around 1200 cubes. In the Javascript version, Chrome would crash at around 700 cubes. In this version, it starts to get a little glitchy at 1600, then progressively more glitchy until it eventually stops updating the screen completely. Oddly, the script continues to run; you will be able to see the number of cubes increment in the upper left corner. I am not sure if this is a hard limit built into Away3d, or the Flash 3D API, or if there is a memory limit of some kind being reached. I suspect – based on the occasional warnings which popped up during development – that it is a hard-coded polygon limit within Away3d. There is probably some way around it, but I don’t (yet) have the know-how to go in and fix it.

Anyway, here is the code for the experiment. Comment out any lines which use the “org.eccesignum.*” files; they assume you have the code for my custom InfoPanel in your library path.

package {
	import away3d.cameras.Camera3D;
	import away3d.containers.ObjectContainer3D;
	import away3d.containers.Scene3D;
	import away3d.containers.View3D;
	import away3d.entities.Mesh;
	import away3d.lights.DirectionalLight;
	import away3d.lights.PointLight;
	import away3d.materials.ColorMaterial;
	import away3d.materials.lightpickers.*;
	import away3d.primitives.SphereGeometry;
	import away3d.primitives.CubeGeometry;
	
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.geom.Vector3D;
	import flash.utils.Timer;
	
	import org.eccesignum.utilities.InfoPanel;
	
	[SWF(width=640,height=480,frameRate=32,backgroundColor=0x000000)]
	
	public class Main extends Sprite {
		internal var _info:InfoPanel;
		private var view:View3D;
		private var cubeContainer:ObjectContainer3D;
		private var scene:Scene3D;
		private var camera:Camera3D;
		private var directionalLight:DirectionalLight;
		private var lightPicker:StaticLightPicker
		private var cMaterial:ColorMaterial;
		private var antX:Number = 32,
			antY:Number = 32,
			antZ:Number = 32,
			nextX:Number,
			nextY:Number,
			nextZ:Number,
			cellsX:int = 64,
			cellsY:int = 64,
			cellsZ:int = 64,
			cellWidth:int = 8,
			cellHeight:int = 8,
			cellDepth:int = 8,
			antSize:int = 7,
			maxDirections:Number = 8,
			colorMultiplier:Number = Math.round(256/cellsX),
			xOff:Number = cellsX/2*cellWidth,
			yOff:Number = cellsY/2*cellHeight,
			zOff:Number = cellsZ/2*cellDepth,
			objects:Array,
			antDirection:Number = 1,
			filledCells:int = 0;
		
		public function Main():void {
			addEventListener(Event.ADDED_TO_STAGE,onAddedToStage);
		}
		private function onAddedToStage(e:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE,onAddedToStage);
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			init();
		}
		private function init():void {
			_info = new InfoPanel(this,100,50);
			scene = new Scene3D();
			camera = new Camera3D();
			view = new View3D(null,camera);
			view.antiAlias = 2;
			camera.x = 0;
			camera.z = 150;
			camera.y = -300;
			cubeContainer = new ObjectContainer3D();
			cubeContainer.rotationY=0;
			cubeContainer.rotationZ=45;
			
			directionalLight = new DirectionalLight(0,150,-300);
			directionalLight.diffuse = 1;
			directionalLight.specular = 0.3;
			directionalLight.color=0xffffff;
			scene.addChild(directionalLight);
			lightPicker = new StaticLightPicker([directionalLight]);
			
			cMaterial = new ColorMaterial(0x999999);
			cMaterial.lightPicker  = lightPicker;
			
			scene.addChild(cubeContainer);
			camera.lookAt(new Vector3D(0,0,0));
			view.scene = scene;
			addChild(view);

			objects = new Array(cellsX);
			for(var i:int=0;i<objects.length;i++) {
				objects[i] = new Array(cellsY);
				for(var j:int=0;j<objects[i].length;j++) {
					objects[i][j] = new Array(cellsZ);
				}
			}
			view.render();
			addEventListener(Event.ENTER_FRAME,onEnterFrame);
		}
		private function onEnterFrame(e:Event):void {
			if(!objects[antX][antY][antZ]) {
				antDirection++;
				if(antDirection == maxDirections) antDirection = 0;
				addObject(antX,antY,antZ);
			} else {
				removeObject(antX,antY,antZ);
				antDirection--;
				if(antDirection == -1) antDirection = maxDirections-1;
			}
			switch(antDirection) {
				case 0:
					antZ--;
					break;
				case 1:
					antX++;
					break;
				case 2:
					antY++;
					break;
				case 3:
					antX--;
					break;
				case 4:
					antZ++;
					break;
				case 5:
					antX++;
					break;
				case 6:
					antY--;
					break;
				case 7:
					antX--;
					break;
				default:
					break;
			}
			if(antY < 0) antY += cellsY;
			if(antY >= cellsY) antY -= cellsY;
			if(antX < 0) antX += cellsX;
			if(antX >= cellsX) antX -= cellsX;
			if(antZ < 0) antZ += cellsZ;
			if(antZ >= cellsZ) antZ -= cellsZ;

			cubeContainer.rotationZ+=.5;
			cubeContainer.rotationY+=.5;
			cubeContainer.rotationX+=.5;
			_info.update(filledCells.toString(),true);
			view.render();
		}
		private function addObject(x:int,y:int,z:int):void {
			var cGeometry:CubeGeometry = new CubeGeometry();
				cGeometry.width = antSize;
				cGeometry.height = antSize;
				cGeometry.depth = antSize;
			var cMesh:Mesh = new Mesh(cGeometry,cMaterial);
				cMesh.x = x*cellWidth-xOff;
				cMesh.y = y*cellHeight-yOff;
				cMesh.z = z*cellDepth-zOff;
			cubeContainer.addChild(cMesh);
			objects[x][y][z] = cMesh;
			filledCells++;
		}
		private function removeObject(x:int,y:int,z:int):void {
			cubeContainer.removeChild(objects[x][y][z]);
			objects[x][y][z].material.dispose();
			objects[x][y][z].dispose();
			objects[x][y][z] = null;
			filledCells--;
		}
		private function getRGB(r:int,g:int,b:int):int {
			var rgb:int = parseInt((r*colorMultiplier).toString(16) + (g*colorMultiplier).toString(16) + (b*colorMultiplier).toString(16),16);
			return rgb;
		}
			
	}
}

Feel free to use and modify the code to your heart’s content. If you come up with anything nifty, post a link to it in the comments.

Posted in ProgrammingTagged Flash, math, procedural art comment on 3D Langton’s Ant, in Actionscript 3 Using Away3d

Langton’s Ant in 3d, Using Javascript and Three.js

2012-03-02 John Winkelman

Langton's Ant in 3d, using Javascript and Three.js

Click the screenshot to launch the demo. You will need to use the latest version of Chrome, Firefox or Safari if you want to see it running.

This is a re-creation of a Flash experiment from several years ago. Back before I started playing around with 3d, I created a series of experiments exploring cellular automata, the majority of which were variations on the Langton’s Ant algorithm. The most advanced version was one in pseudo-3d, and that was the last for over three years. Now with the advent of easy-to-use 3d libraries (like Three.js, used here), and the option of hardware acceleration to allow for complex animations, I felt it was time to re-visit some of the old experiments.

Here is the source code for the 3d ant:

window.requestAnimFrame = (function(){
	return  window.requestAnimationFrame	|| 
		window.webkitRequestAnimationFrame || 
		window.mozRequestAnimationFrame    || 
		window.oRequestAnimationFrame      || 
		window.msRequestAnimationFrame     || 
		function( callback ){
			window.setTimeout(callback, 1000 / 60);
		};
})();

var canvas,
	stage,
	container,
	scene,
	camera,
	renderer,
	projector,
	light;
	
var WIDTH = 640,
	HEIGHT = 640;
	VIEW_ANGLE = 45,
	ASPECT = WIDTH/HEIGHT,
	NEAR = .1,
	FAR = 10000,
	centerX = 0,
	centerY = 0,
	centerZ = 0,
	cameraX = 0,
	cameraY = 200,
	cameraZ = 400;
	
var antX = 32,
	antY = 32,
	antZ = 32,
	nextX,
	nextY,
	nextZ,
	cellsX = 64,
	cellsY = 64,
	cellsZ = 64,
	cellWidth = 8,
	cellHeight = 8,
	cellDepth = 8,
	antSize = 7,
	maxDirections = 8,
	colorMultiplier = Math.round(256/cellsX),
	xOff = cellsX/2*cellWidth,
	yOff = cellsY/2*cellHeight,
	zOff = cellsZ/2*cellDepth,
	base,
	objects,
	antDirection = 1,
	filledCells=0,
	
	isRotating=true,
	isPaused=false;
	
	function init() {
		canvas = document.getElementById("myCanvas");
		scene = new THREE.Scene();
		camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR );
		camera.position.x = cameraX;
		camera.position.y = cameraY;
		camera.position.z = cameraZ;
		camera.lookAt(scene.position);
		scene.add( camera );
		projector = new THREE.Projector();
		renderer = new THREE.WebGLRenderer( { antialias: true } );
		renderer.setSize( WIDTH, HEIGHT );
		light = new THREE.SpotLight();
		light.position.set( cameraX, cameraY, cameraZ );
		scene.add(light);
		canvas.appendChild( renderer.domElement );
		renderer.render(scene,camera);
		
		base = new THREE.Object3D();
		base.position.x = centerX;
		base.position.y = centerY;
		base.position.x = centerZ;
		base.rotation.y = 45;
		scene.add(base);
		camera.lookAt(base.position);
		objects = new Array(cellsX);
		for(var i=0;i<objects.length;i++) {
			objects[i] = new Array(cellsY);
			for(var j=0;j<objects[i].length;j++) {
				objects[i][j] = new Array(cellsZ);
			}
		}
		onEnterFrame();
	}
	
	function onEnterFrame() {
		requestAnimFrame(onEnterFrame);
		renderer.render(scene,camera);
		if(isPaused==true) return;
		if(!objects[antX][antY][antZ]) {
			antDirection++;
			if(antDirection == maxDirections) antDirection = 0;
			addObject(antX,antY,antZ);
		} else {
			removeObject(antX,antY,antZ);
			antDirection--;
			if(antDirection == -1) antDirection = maxDirections-1;
		}
		switch(antDirection) {
			case 0:
				antZ--;
				break;
			case 1:
				antX++;
				break;
			case 2:
				antY++;
				break;
			case 3:
				antX--;
				break;
			case 4:
				antZ++;
				break;
			case 5:
				antX++;
				break;
			case 6:
				antY--;
				break;
			case 7:
				antX--;
				break;
			default:
				break;
		}
		if(antY < 0) antY += cellsY;
		if(antY >= cellsY) antY -= cellsY;
		if(antX < 0) antX += cellsX;
		if(antX >= cellsX) antX -= cellsX;
		if(antZ < 0) antZ += cellsZ;
		if(antZ >= cellsZ) antZ -= cellsZ;
		if(isRotating==true) base.rotation.y +=.01;
		document.getElementById("count").value = filledCells;
	}
	function addObject(x,y,z) {
		var sphereMaterial = new THREE.MeshPhongMaterial({
			color: getRGB(x,y,z),
			opacity:1
		});	
		var obj = new THREE.Mesh(
		   new THREE.CubeGeometry(antSize,antSize,antSize),
		   sphereMaterial);
		obj.position.x = x*cellWidth-xOff;
		obj.position.y = y*cellHeight-yOff;
		obj.position.z = z*cellDepth-zOff;
		obj.overdraw = true;
		base.add(obj);
		objects[x][y][z] = obj;
		filledCells++;
	}
	function removeObject(x,y,z) {
		base.remove(objects[x][y][z]);
		objects[x][y][z] = null;
		filledCells--;
	}
	function getRGB(r,g,b) {
		var rgb = parseInt((r*colorMultiplier).toString(16) + (g*colorMultiplier).toString(16) + (b*colorMultiplier).toString(16),16);
		return rgb;
	}
	onload=init;

The above code assumes you have downloaded a copy of Three.js and have a HTML page with appropriate elements set up. To see the entirety of the code I used, right-click on the screenshot and open it in a new tab or window, then view source. It’s all one file, other than the Three.js library. Feel free to copy it in its entirety and play around.

Posted in ProgrammingTagged math, procedural art comment on Langton’s Ant in 3d, Using Javascript and Three.js

Speaking of Three.js

2012-02-26 John Winkelman

Trigonometry experiment using Three.js

Click the above image to see a recreation of a previous Flash experiment (source code here), using Three.js. And see below for the source code used to create this thing.

// shim layer with setTimeout fallback set to 60 frames per second
window.requestAnimFrame = (function(){
	return  window.requestAnimationFrame	|| 
		window.webkitRequestAnimationFrame || 
		window.mozRequestAnimationFrame    || 
		window.oRequestAnimationFrame      || 
		window.msRequestAnimationFrame     || 
		function( callback ){
			window.setTimeout(callback, 1000 / 60);
		};
})();

var canvas,
	stage,
	container,
	scene,
	camera,
	renderer,
	projector,
	light;
	
var w = 800;
var h = 600;
var VIEW_ANGLE = 45;
var ASPECT = w/h;
var NEAR = .1;
var FAR = 10000;
var centerX = 0;
var centerY = 0;
var centerZ = 0;
var radius = 175;
var cameraRadius = 1000;
var cameraX = cameraRadius;
var cameraY = cameraRadius;
var cameraZ = cameraRadius;
var theta = 0;
var group;
var objects = [];
var numObjects = 0;
var orbitSteps = 1000;
var orbitSpeed = Math.PI*2/orbitSteps;
var objectInterval;
var objectPosition;
var direction = 1;
var index = 0;
var startingObjects = 400;

onload = init;
function init() {
	canvas = document.getElementById("myCanvas");
	scene = new THREE.Scene();
	camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR );
	camera.position.x = cameraX;
	camera.position.y = cameraY;
	camera.position.z = cameraZ;
	camera.lookAt(scene.position);
	scene.add( camera );
	projector = new THREE.Projector();
	renderer = new THREE.WebGLRenderer( { antialias: true } );
	renderer.setSize( w, h );
	light = new THREE.SpotLight();
	light.position.set( centerX, centerY, 160 );
	scene.add(light);
	canvas.appendChild( renderer.domElement );
	renderer.render(scene,camera);
	initObjects();
	onEnterFrame();
}
function initObjects() {
	group = new THREE.Object3D();
	group.position.x = centerX;
	group.position.y = centerY;
	group.position.x = centerZ;
	for(var i=0;i<startingObjects;i++) {
		addObject();
	}
	scene.add(group);
}
function addObject() {
	var sphereMaterial = new THREE.MeshLambertMaterial({
		color: Math.round(Math.random()*0xffffff)
	});	
	var obj = new THREE.Mesh(
	   new THREE.SphereGeometry(8,8,8),	//	radius,segments,rings
	   sphereMaterial);
	obj.position.x = 0;
	obj.position.y = 0;
	obj.position.z = 0;
	obj.overdraw = true;
	group.add(obj);
	objects.push(obj);
	numObjects = objects.length;
	objectInterval = orbitSteps/numObjects;
}
function onEnterFrame() {
	requestAnimFrame(onEnterFrame);
	renderer.render(scene,camera);
	for(var i = 0; i < numObjects; i++) {
		objectPosition = orbitSpeed*objectInterval*i;    //    each object is individually updated
		objects[i].position.x = radius * (Math.cos(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
		objects[i].position.y = radius * (Math.sin(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
		objects[i].position.z = centerZ + radius * Math.sin(theta + objectPosition) - radius*1*(Math.sin((radius/radius + 3) * (theta + objectPosition)));
	}
	group.rotation.x += .002;
	group.rotation.y += .004;
	group.rotation.z += .008;
	theta += (orbitSpeed*direction);
}

 

Posted in ProgrammingTagged math, procedural art comment on Speaking of Three.js

HTML5 Canvas Drawing Library Exploration: Three.js

2012-02-15 John Winkelman

This is the sixth in an ongoing series of posts exploring various Javascript drawing libraries.

Three.js is a powerful 3d drawing library, and currently the one to beat when it comes to leveraging the latest browser capabilities. When available, it can tie in to the WebGL rendering engine (current browsers on fairly new computers) for an experience which is very close to what you would get from a native application. This demo is not that ambitious, but it should give you a basic idea of how to get started building interactive 3d applications in Javascript.

Click here to launch the demo. Give it a couple of moments to load; the Three.js library is over 300kb.

HTML

<!DOCTYPE html>
<html>
	<head>
		<title>Trigonometron built using Three.js</title>
		<style>
			* {margin:0;padding:0;}
			body {background:#ffffff;}
			#myCanvas {width:800px;height:480px;position:absolute;left:0;top:0;background:#ffffff;}
		</style>
		<script type="text/javascript" src="Three.js"></script>
	</head>
	<body>
		<div id="myCanvas"></div>
		<script type="text/javascript">
			//	Javascript goes here
		</script>
	</body>
</html>

Fairly basic stuff here. Note that the container for the app is a <div/> element, not a <canvas/> element. Now on to the good bits.

Javascript

// shim layer with setTimeout fallback set to 60 frames per second
window.requestAnimFrame = (function(){
	return  window.requestAnimationFrame	|| 
		window.webkitRequestAnimationFrame || 
		window.mozRequestAnimationFrame    || 
		window.oRequestAnimationFrame      || 
		window.msRequestAnimationFrame     || 
		function( callback ){
			window.setTimeout(callback, 1000 / 60);
		};
})();

var canvas,
	stage,
	container,
	scene,
	camera,
	renderer,
	projector,
	light,
	
	currentShape="circle",
	isPaused = true,
	isInitialized = false,
	currentHoverTarget = null;


	
var shapes = ["circle","tricuspoid","tetracuspoid","epicycloid","epicycloid 2","epicycloid 3","lissajous","lemniscate","butterfly"];
var w = 800;
var h = 480;
var VIEW_ANGLE = 45;
var ASPECT = w/h;
var NEAR = .1;
var FAR = 10000;
var centerX = -100;
var centerY = 0;
var centerZ = -150;
var radius_x = 175;
var radius_y = 175;
var radius_z = 175;
var theta = 0;
var objects = [];
var buttons = [];
var numObjects = 0;
var r2d = 180/Math.PI;
var d2r = Math.PI/180;
var orbitSteps = 240;
var orbitSpeed = Math.PI*2/orbitSteps;
var objectInterval;
var objectPosition;
var direction = 1;
var index = 0;
var xVar1 = 0;
var xVar2;
var xVar3;
var xVar4;
var startingObjects = 100;
var newX;
var newY;
var newZ = centerZ;


function init() {
	canvas = document.getElementById("myCanvas");
	scene = new THREE.Scene();
	camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR );
	camera.position.y = 0;
	camera.position.z = 500;
	scene.add( camera );
	projector = new THREE.Projector();
	renderer = new THREE.WebGLRenderer( { antialias: true } );
	renderer.setSize( w, h );
	light = new THREE.SpotLight();
	light.position.set( centerX, centerY, 160 );
	scene.add(light);
	canvas.appendChild( renderer.domElement );
	renderer.render(scene,camera);
	canvas.addEventListener( 'mousedown', onDocumentMouseDown, false );
	canvas.addEventListener( 'mousemove', onDocumentMouseMove, false );
	
	initInterface();
	initObjects();
	onEnterFrame();
	isInitialized = true;
}
onload = init;


function initInterface() {
	var xOff = 310;
	var yOff = 150;
	var zOff = -100;
	for(var i=0;i<shapes.length;i++) {
		buildTextButton(shapes[i],xOff,yOff,zOff,120,20,15);
		yOff -= 23;
	}
	yOff -= 23;
	buildTextButton("play/pause",xOff,yOff,zOff,120,20,15);

}
function buildTextButton(text,bX,bY,bZ,bW,bH,bD) {
	var materials = [];
	var co = 0xededed;
	for ( var j = 0; j < 6; j ++ ) {
		materials.push(new THREE.MeshLambertMaterial({color:co,opacity:1}));
	}
	var ctx = document.createElement('canvas');
	ctx.getContext('2d').font = '12px Courier,monospace';
	ctx.getContext('2d').fillStyle = '#000000';
	ctx.getContext('2d').textAlign="center";
	ctx.getContext('2d').fillText(text, 125,10);
	var tex = new THREE.Texture(ctx);
	tex.needsUpdate = true;
	var mat = new THREE.MeshBasicMaterial({map: tex});
	mat.transparent = true;
	var textRender = new THREE.Mesh(
		new THREE.PlaneGeometry(ctx.width, ctx.height),
		mat
	);
	textRender.doubleSided = true;
	textRender.position.x = bX+25;
	textRender.position.y = bY-70;
	textRender.position.z = bZ+8;
	scene.add(textRender);
	var c = new THREE.Mesh(
		new THREE.CubeGeometry(bW,bH,bD, 1, 1, 1,materials ),
		new THREE.MeshFaceMaterial()
	);

	c.position.x = bX;
	c.position.y = bY;
	c.position.z = bZ;
	c.overdraw = true;
	c.name = text;
	scene.add(c);
	buttons.push(c);
}
function initObjects() {
	for(var i=0;i<startingObjects;i++) {
		addObject();
	}
}
function addObject() {
	var sphereMaterial = new THREE.MeshLambertMaterial({
		color: Math.round(Math.random()*0xffffff)
	});	
	var obj = new THREE.Mesh(
	   new THREE.SphereGeometry(10,16,16),	//	radius,segments,rings
	   sphereMaterial);

	obj.position.x = centerX
	obj.position.y = centerY
	obj.position.z = centerZ;
	obj.overdraw = true;
	scene.add(obj);
	objects.push(obj);
	numObjects = objects.length;
	objectInterval = orbitSteps/numObjects;
}

function onEnterFrame() {
	requestAnimFrame(onEnterFrame);
	renderer.render(scene,camera);
	if(isPaused==true && isInitialized==true) return;
	for(var i = 0; i < numObjects; i++) {
		objectPosition = orbitSpeed*objectInterval*i;    //    each object is individually updated
		switch(currentShape) {
			case "circle":
				newX = centerX + radius_x * Math.cos(theta + objectPosition);
				newY = centerY + radius_y * Math.sin(theta + objectPosition);
				break;
			case "tricuspoid":
				newX = centerX + (radius_x*.5) * ((2 * Math.cos(theta + objectPosition)) + Math.cos(2 * (theta + objectPosition)));
				newY = centerY + (radius_y*.5) * ((2 * Math.sin(theta + objectPosition)) - Math.sin(2 * (theta + objectPosition)));
				break;
			case "tetracuspoid":
				newX = centerX + radius_x * Math.pow((Math.cos(theta + objectPosition)),3);
				newY = centerY + radius_y * Math.pow((Math.sin(theta + objectPosition)),3);
				break;
			case "epicycloid":
				newX = centerX + (radius_x*.4) * Math.cos(theta + objectPosition) - radius_x*1*(Math.cos((radius_x/radius_x + 1) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * Math.sin(theta + objectPosition) - radius_y*1*(Math.sin((radius_y/radius_y + 1) * (theta + objectPosition)));
				break;
			case "epicycloid 2":
				newX = centerX + (radius_x*.4) * Math.cos(theta + objectPosition) - radius_x*1*(Math.cos((radius_x/radius_x + 2) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * Math.sin(theta + objectPosition) - radius_y*1*(Math.sin((radius_y/radius_y + 2) * (theta + objectPosition)));
				break;
			case "epicycloid 3":
				newX = centerX + (radius_x*.4) * Math.cos(theta + objectPosition) - radius_x*1*(Math.cos((radius_x/radius_x + 3) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * Math.sin(theta + objectPosition) - radius_y*1*(Math.sin((radius_y/radius_y + 3) * (theta + objectPosition)));
				break;
			case "lissajous":
				newX = centerX + radius_x * (Math.sin(3 * (theta + objectPosition) + xVar1));
				newY = centerY + radius_y * Math.sin(theta + objectPosition);
				xVar1 += .0005;
				break;
			case "lemniscate":
				newX = centerX + (radius_x*1.2) * ((Math.cos(theta + objectPosition)/(1 + Math.pow(Math.sin(theta + objectPosition),2))));
				newY = centerY + (radius_y*1.2) * (Math.sin(theta + objectPosition) * (Math.cos(theta + objectPosition)/(1 + Math.pow(Math.sin(theta + objectPosition),2))));
				break;
			case "butterfly":
				newX = centerX + (radius_x*.4) * (Math.cos(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
				newY = centerY + (radius_y*.4) * (Math.sin(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
				break;
			default:
				break;
	
		}
		objects[i].position.x = newX;
		objects[i].position.y = newY;
	}
	theta += (orbitSpeed*direction);
}

function onDocumentMouseMove( event ) {
	var targetObj = getIntersectedObject(event);
	if(targetObj == null) {
		document.getElementById("myCanvas").style.cursor="default";
		if(currentHoverTarget != null) {
			setObjectColor(currentHoverTarget,0xededed);
			currentHoverTarget = null;
		}
	} else {
		document.getElementById("myCanvas").style.cursor="pointer";
		if(targetObj != currentHoverTarget && currentHoverTarget != null) {
			setObjectColor(currentHoverTarget,0xededed);
		}
		setObjectColor(targetObj,0xffffff);
		currentHoverTarget = targetObj;
	}
}

function setObjectColor(thing,c) {
	for(var i=0;i<6;i++) {
		thing.geometry.materials[i].color.setHex(c);
	}
}

function onDocumentMouseDown( event ) {
	var targetObj = getIntersectedObject(event);
	if(targetObj != null) {
		if(targetObj.name=="play/pause") {
			isPaused = !isPaused;
		} else {
			currentShape = targetObj.name;
		}
	}
}
function getIntersectedObject(event) {
	event.preventDefault();
	var mouseX, mouseY;
	if(event.offsetX) {
		mouseX = event.offsetX;
		mouseY = event.offsetY;
	}
	else if(event.layerX) {
		mouseX = event.layerX;
		mouseY = event.layerY;
	}
	var vector = new THREE.Vector3(
		(mouseX / w) * 2 - 1,
		-(mouseY / h) * 2 + 1,
		0.5
	);
	projector.unprojectVector( vector, camera );
	var ray = new THREE.Ray( camera.position, vector.subSelf( camera.position ).normalize() );
	var intersects = ray.intersectObjects( buttons );
	if ( intersects.length > 0 ) {
		return intersects[0].object;
	} else {
		return null;
	}
}

Here is a line-by-line breakdown of the code.

  • 1-11 – requestAnimFrame – this is a custom function which tries to take advantage of the built-in (for new browsers) requestAnimationFrame functionality, which is meant to be used for frame-based animations (like Adobe Flash uses). Each browser uses its own variations, so this function tries each one, and if it is in a browser which does not support the functionality, it defaults to a setTimeout call.
  • 13-25 – initialize global variables. The first eight – “canvas” to “light”, are used by the Three.js to create, control, and render the 3d scene to the canvas.
  • 29-61 – initialize variables for the animation. These are not specific to Three.js. Note, however, that there are now “z” variables in addition to “x” and “y”
  • 64-86 – init() – set up 3d environment and add all of the graphic elements
    • 65 – associate the app with an HTML element
    • 66 – create a new scene for display
    • 67-70 – initialize and position a camera, relative to the scene. The camera is the “screen” through which you look at the scene
    • 71 – create a new projector
    • 72-73 – initialize the renderer, which controls how the app will be drawn to the screen. In tthe his instance, we will use the WebGL renderer
    • 74-76 – create a new light source for the scene
    • 77 – add the newly created renderer to the HTML DOM
    • 78 – draw the app to the screen
    • 79-80 – add mouse listeners
    • 82 – create the UI buttons
    • 83 – create the graphics for the animations
    • 84 – call the animation once to display everything to the screen
    • 85 – we are now initialized and ready to go
  • 87 – tell the browser to call the init() method as soon as the page finishes loading
  • 90-101 – initInterface() – create the UI elements for the app
    • 91-93 – set the initial x, y, and z positions for the UI buttons
    • 94-97 – create one text button for each element in the shapes[] array
    • 98 – add some space between the shape buttons and the play/pause button
    • 99 – create the play/pause button
  • 102-138 – buildTextButton() – create a 3d rectangle button, add text to one face, and position it on the screen
    • 103 – initialize the materials array
    • 104 – set the default color of the buttons
    • 105-107 – create a new Lambert Material for each face of the cube. Lambert materials can be lit by spotlights and cast shadows. Normal materials cannot.
    • 108-112 – create a new canvas element, add text to it, and style the text
    • 113-114 – copy the canvas element to a Texture
    • 115-116 – draw the texture to a Material
    • 117-125 – create a new display object, fill it with the Material, and position it in 3d space. In this case, it will have the same x and y coordinates as the gray box behind it, but will hover one “pixel” from the front face of the box. I am sure there are ways to draw the text directly to the face of a cube, but I have not yet discovered it. This is a hack.
    • 126-135 – create a cube display object, color it in with the materials array, and position it in 3d space
    • 136 – add the cube display object to the display list
    • 137 – add the cube to the array of buttons. This is for determining mouse interactions
  • 139-143 – initObjects() – create a number of graphic element for the animations, equal to the value of startingObjects
  • 144-160 – addObject() – create a new display object for the animations
    • 145-147 – create a new material and give it a random color
    • 148-150 – create a new Sphere display object
    • 152-154 – position the object in 3d space
    • 155 – set the material on top of the wireframe of the model, so the “edges” don’t show through
    • 156 – add the Sphere to the display list
    • 157 – add a reference to the Sphere to the array of spheres. Used for animations
    • 158-159 – update global variables to correctly set spacing of objects within the animations
  • 162-214 – onEnterFrame() – update the animation by one step
    • 163 – advance everything by one frame
    • 164 – draw everything to the screen
    • 165 – if the app is finished initializing, but is paused, do nothing
    • 166-212 – cycle through each Sphere display object in the animation and update its position on the screen. Note that this app only updates the Spheres in the x and y planes; z remains constant.
    • 213 – update the progress of the animation as a whole
  • 216-232 – onDocumentMouseMove() – called whenever the mouse is moved in the app; used to determine which button is currently being moused over
    • 217 – call getIntersectedObject() to determan which (if any) of the buttons is currently being moused over
    • 218-223 – if there is nothing being moused over…
      • 219 – set the cursor to the default
      • 220-223 – if there was an element being moused over in the last frame iterations…
        • 221 – call setObjectColor() to return the color of the rectangle to its default
        • 222 – set the currentHoverTarget to null
    • 224-231 – else if one of the buttons IS being moused over…
      • 225 – set the cursor to a pointer
      • 226-228 – if the current hover target is not the same as the one hovered over in the previous frame, yet SOMETHING was hovered over in the previous frame, set the previous target’s color to the default
      • 229 – set the color of the current target to the hover highlight color
      • 230 – set the global currentHoverTarget to equal the button which is currently being hovered over
  • 234-238 – setObjectColor() – cycle through the six faces of a button and set each face to the appropriate color
  • 240-249 – onDocumentMouseDown() – called whenever the mouse button is pressed within the app
    • 241 – find out which button, if any, was clicked
    • 242-248 – if a button was clicked on…
      • 243-244 – if it was the play/pause button, toggle the pause variable
      • 245-247 – if it was any other button, set the currentShape variable to the sppropriate value
  • 250-275 – getIntersectedObject() – determine if a mouse event happened where it might intersect a target display object
    • 251 – stop the mouse event from cascading through the rest of the DOM
    • 252 – declare coordinate variables
    • 253-260 – based on how the browser handles mouse events, determine where on the app the event occurred
    • 261-265 – create a new vector object based on the x, y, and z coordinates of the event within the app
    • 266-267 – calculate where the objects in 3d space intersect the screen (the camera) in 2d space
    • 268 – build an array of every object in the buttons[] array which intersects the mouse event
    • 269-270 – if there is more than 0 elements in this array, return the first (top) display object
    • 271-273 – if nothing is intersected, do nothing

So there you have it: The trigonometron in 3d. Well, 3d-ish. It exists in 3d space, but everything is on the same plane, so I suppose it is de-facto 2d. I have a couple of 3d demos which I will post in the next few days. All in all, figuring out the 3d stuff was less difficult than I thought it would be. My experiments last fall with Away3d and Flash certainly helped.

This will be the last library exploration for a while. Further posts will must likely be experiments using Three.js, or one of the other libraries I explored in this series of posts.

In case you missed them the previous posts in the series are here:

  • Easel.js
  • oCanvas.js
  • Raphael.js
  • Paper.js
  • Processing.js
Posted in ProgrammingTagged math, procedural art comment on HTML5 Canvas Drawing Library Exploration: Three.js

HTML5 Canvas Drawing Library Exploration: Processing.js

2012-02-13 John Winkelman

This is the fifth in a loose series of articles exploring some of the Javascript drawing libraries currently freely available to developers.

Today I will be experimenting with Processing.js, which is a radical departure from the previous libraries in this series.

Processing.js started out as Processing, a Java graphics library/custom language which can trace its origins back to the year 2001 and the big brains of Casey Reas and Ben Fry at the MIT Media Lab. When I first discovered Flash, my quest for source code and examples repeatedly brought me in contact with Proce55ing (as it was called back then). Many of my early coding experiments revolved around trying to re-create in Flash that which had been so wonderfully demonstrated in Java.

And now almost everything which has been done in Java can now be done in Javascript, with little change to the original source code. That’s right – Processing.js can use the same Processing code that the Java version uses.

Click here to launch the demo. Give it a moment; the Processing.js library is over 200kb.

HTML

<!DOCTYPE html>
<html>
	<head>
		<title>Trigonometron in Processing.js</title>
		<style>
			* {margin:0;padding:0;}
			body {background:#ffffff;}
			#myCanvas {width:800px;height:480px;position:absolute;left:0;top:0;}
		</style>
		<script type="text/javascript" src="processing-1.3.6.min_.js"></script>
	</head>
	<body>
		<canvas id="myCanvas" data-processing-sources="/path/to/trigonometron.pde"></canvas>
	</body>
</html>

Nothing too exciting here. The Processing.js library is loaded in the <head/> element. The <canvas/> tag has a custom attribute, “data-processing-sources”. The value for this attribute should be the path – relative or absolute – to the Processing source file (*.pde) used in this app.

Processing code

String currentShape="circle";
boolean isPaused = false;
boolean isInitialized = false;


String[] shapes = {"circle","tricuspoid","tetracuspoid","epicycloid","epicycloid 2","epicycloid 3","lissajous","lemniscate","butterfly"};
int w = 800;
int h = 480;
int centerX = 240;
int centerY = 240;
int radius_x = 150;
int radius_y = 150;
int theta = 0;
ArrayList objects;
ArrayList buttons;
int numObjects = 0;
float r2d = 180/PI;
float d2r = PI/180;
int orbitSteps = 180;
float orbitSpeed = PI*2/orbitSteps;
float objectInterval;
float objectPosition;
int direction = 1;
int index = 0;
int xVar1 = 0;
int xVar2;
int xVar3;
int xVar4;
int startingObjects = 100;
float newX;
float newY;

PFont buttonFont;

void setup(){
	size(800,480);
	background(255,255,255);
	fill(30);
	PFont buttonFont = loadFont("courier");
	textFont(buttonFont,14);
	textAlign(CENTER);
	objects = new ArrayList();
	buttons = new ArrayList();
	initInterface();
	initObjects();
	frameRate(32);
}

void initObjects() {
	for(int i=0;i<startingObjects;i++) {
		addObject();
	}
}
void addObject() {
	objects.add(new CircleObject(centerX,centerY,10,10));
	numObjects = objects.size();
	objectInterval = orbitSteps/numObjects;
}

void initInterface() {
	int xOff = 625;
	int yOff = 25;
	for(int i=0;i<shapes.length;i++) {
		buttons.add(new TextButton(xOff,yOff,150,20,shapes[i]));
		yOff += 23;
	}
	yOff += 23;
	buttons.add(new TextButton(xOff,yOff,150,20,"PLAY/PAUSE"));
}
void draw() {
	if(isPaused==true) return;
	background(255,255,255);
	for(int i=0;i<numObjects;i++) {
		objectPosition = orbitSpeed*objectInterval*i;
		switch(currentShape) {
			case "circle":
				newX = centerX + radius_x * cos(theta + objectPosition);
				newY = centerY + radius_y * sin(theta + objectPosition);
				break;
			case "tricuspoid":
				newX = centerX + (radius_x*.5) * ((2 * cos(theta + objectPosition)) + cos(2 * (theta + objectPosition)));
				newY = centerY + (radius_y*.5) * ((2 * sin(theta + objectPosition)) - sin(2 * (theta + objectPosition)));
				break;
			case "tetracuspoid":
				newX = centerX + radius_x * pow((cos(theta + objectPosition)),3);
				newY = centerY + radius_y * pow((sin(theta + objectPosition)),3);
				break;
			case "epicycloid":
				newX = centerX + (radius_x*.4) * cos(theta + objectPosition) - radius_x*1*(cos((radius_x/radius_x + 1) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * sin(theta + objectPosition) - radius_y*1*(sin((radius_y/radius_y + 1) * (theta + objectPosition)));
				break;
			case "epicycloid 2":
				newX = centerX + (radius_x*.4) * cos(theta + objectPosition) - radius_x*1*(cos((radius_x/radius_x + 2) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * sin(theta + objectPosition) - radius_y*1*(sin((radius_y/radius_y + 2) * (theta + objectPosition)));
				break;
			case "epicycloid 3":
				newX = centerX + (radius_x*.4) * cos(theta + objectPosition) - radius_x*1*(cos((radius_x/radius_x + 3) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * sin(theta + objectPosition) - radius_y*1*(sin((radius_y/radius_y + 3) * (theta + objectPosition)));
				break;
			case "lissajous":
				newX = centerX + radius_x * (sin(3 * (theta + objectPosition) + xVar1));
				newY = centerY + radius_y * sin(theta + objectPosition);
				xVar1 += .002;
				break;
			case "lemniscate":
				newX = centerX + (radius_x*1.2) * ((cos(theta + objectPosition)/(1 + pow(sin(theta + objectPosition),2))));
				newY = centerY + (radius_y*1.2) * (sin(theta + objectPosition) * (cos(theta + objectPosition)/(1 + pow(sin(theta + objectPosition),2))));
				break;
			case "butterfly":
				newX = centerX + (radius_x*.4) * (cos(theta + objectPosition) * (pow(5,cos(theta+objectPosition)) - 2 * cos(4 * (theta+objectPosition)) - pow(sin((theta+objectPosition)/12),4)));
				newY = centerY + (radius_y*.4) * (sin(theta + objectPosition) * (pow(5,cos(theta+objectPosition)) - 2 * cos(4 * (theta+objectPosition)) - pow(sin((theta+objectPosition)/12),4)));
				break;
			default:
				break;
	
		}
		objects.get(i).updatePosition(newX,newY);
	}
	theta += (orbitSpeed*direction);
	
	for(int j = 0;j<buttons.size();j++) {
		buttons.get(j).drawMe();
	}
}

void mouseClicked() {
	for(int i=0;i<buttons.size();i++) {
		buttons.get(i).onClick(mouseX,mouseY);
	}
}
void mouseMoved() {
	for(int i=0;i<buttons.size();i++) {
		buttons.get(i).onHover(mouseX,mouseY);
	}
}

class CircleObject {
	float x,y,d;
	int colorR,colorG,colorB;
	CircleObject(cX,cY,cW,cH) {
		x = cX;
		y = cY;
		w = cW;
		h = cH;
		colorR = random(255);
		colorG = random(255);
		colorB = random(255);
		drawMe();
	}
	void updatePosition(nX,nY) {
		x = nX;
		y = nY;
		drawMe();
	}
	void drawMe() {
		fill(colorR,colorG,colorB);
		stroke(128,128,128);
		ellipse(x,y,w,h);
	}
}

class TextButton {
	int x, y, w, h;
	string btnTxt;
	int overGray = 255;
	int offGray = 238;
	int currentGray = offGray;
	TextButton(int cX,int cY,int cW,int cH,string tx) {
		x = cX;
		y = cY;
		w = cW;
		h = cH;
		btnTxt = tx;
		drawMe();
	}
	void onClick(int mX,int mY) {
		if(mX>x && mY>y && mX < (x+w) && mY < (y+h)) {
			if(btnTxt=="PLAY/PAUSE"){
				isPaused = !isPaused;
			} else {
				currentShape = btnTxt;
			} 
		}
	}
	void onHover(int mX,int mY) {
		if(mX>x && mY>y && mX < (x+w) && mY < (y+h)) {
			cursor(HAND);
			currentGray = overGray;
		} else {
			cursor(ARROW);
			currentGray = offGray;
		}
	}
	void drawMe() {
		stroke(128,128,128);
		fill(currentGray,currentGray,currentGray);
		rect(x,y,w,h);
		fill(0,0,0);
		text(btnTxt,x,y+5,w,h);
	}
}

And here is where things become radically different from the previous examples. This is Processing script, which is essentially a custom, simplified version of Java. There is a lot going on here; more than with any of the previous examples. I will be going into extra detail in the following section.

  • 1-3 – instantiate global variables
  • 6-31 – instantiate variables specific to the animation. Note the different syntax used here – int, float, ArrayList, String, rather than simply declaring “var x = y”.
  • 33 – declare a variable to hold the font for the buttons
  • 35-47 – setup() – built-in Processing method. This is where the environment is set up, variables are initialized, and any external assets are referenced
    • 36 – set the width and height of the app
    • 37-38 – set the color of the background and fill it in with an alpha value of 30% opaque
    • 39 – set the base font for the app
    • 40 – set the size of the base font
    • 41 – set the base text alignment
    • 42-43 instantiate the array lists which will hold the buttons for the UI and the objects for the animations
    • 44-45 call the methods which will create the UI buttons and the objects for the animations
    • 46 – set the frame rate of the app to 32 frames per second.
  • 49-53 – initObjects() – create a number of objects for the animations, equal to the value of the variable startingObjects
  • 54-58 – addObject() – create a circle graphic for the animations
    • 55 – create an instance of the CircleObject class, and add it to the objects ArrayList
    • 56 – update the numObjects variable to equal the length of the objects ArrayList
    • 57 – adjust the spacing of the circle graphics in the animations, based on the number of circles
  • 60-69 – initInterface() – create and position the user interface buttons
    • 61-62 set the base X and Y positions for the buttons in the UI
    • 63-66 – create one TextButton instance for each element in the shapes array
    • 67 – increment the Y position for the play/pause button
    • 68 – create a TextButton instance for the play/pause button
  • 70-124 draw() – built-in Processing method. This is where the various objects are drawn on the screen. If the frameRate is set, then this method is called frameRate() times per second (see line 46).
    • 71 – if the animation is paused, do nothing
    • 72 – fill in the canvas with white
    • 73-119 cycle through and update the positions of each object in the animation
      • 74 – get the base position of one of the instances of the circle graphics
      • 75-116 – based on the current value of currentShape, find the new x/y position for the object
      • 117 – call the updatePosition() method of the object, and feed it the new x/y coordinates
    • 119 – update the rotation of the animation
    • 121-123 – iterate through the UI buttons and re-draw them to the screen
  • 126-130 – mouseClicked() – built-in Processing method. Called whenever a mouse click is detected in the app
    • 127-129 – cycle through all of the UI buttons and call the onClick() method of each, using the mouse X and Y coordinates as arguments.
  • 131-135 – mouseMoved() – built-in Processing method. Called whenever the mouse changes position in the app
    • 132-134 – cycle through all of the UI buttons and call the onHover() method of each, using the mouse X and Y coordinates as arguments.
  • 137-160 – class CircleObject{} – contains properties and methods of the circle graphics used to display the animations
    • 138-139 – initialize internal variables for this class
    • 140-149 – CircleObject() – constructor for creating instances of the CircleObject class. Also see line 55.
      • 141-144 – set internal variables to the arguments fed into the constructor
      • 145-147 – set random values for the red, green, and blue components of the fill color for this object
      • 148 – call the drawMe() method of this class
    • 150-154 – updatePosition() – changes the x and y position of this object
      • 151-152 – update the x and y variables in this object
      • 153 – call the drawMe() method to draw this object to the screen
    • 155-159 – drawMe() – method which draws the circle graphic to the screen. Also see lines 148 and 153
      • 156 – set the fill color of the circle graphic using the red, green, and blue values which were defined in the constructor
      • 157 – set the stroke color to medium gray
      • 158 – draw the circle (ellipse) to the screen
  • 162-201 – class TextButton{} – contains properties and methods for displaying UI buttons and handling mouse interactions
    • 163-167 – initialize internal variables for this class
    • 168-175 – TextButton() – constructor method for creating instances of the TextButton class. Also see lines 64 and 68
      • 169-173 set the values of the internal variables
      • 174 – call the drawMe() method of this class to draw an instance of this button to the screen
    • 176-184 – onClick() – check to see if the mouse was over this instance when it was clicked
      • 177-183 – check to see if the mouse x and y coordinates fall within the x/y and width/height area of this button
        • 178-179 – if this is the play/pause button, toggle the “isPaused” global variable
        • 180-183 – otherwise, set the global “currentShape” property to equal the value of this button
    • 185-193 – onHover() – check to see of the mouse was over this button instance when it moved
      • 186-188 – if the mouse x and y position was within the bounds of this button, set its background color to the hover color, and change the cursor to a hand
      • 189-192 – otherwise, set the background color to the default gray and change the cursor to an arrow
    • 194-200 – drawMe() – draw this instance of the button to the screen
      • 195 – set the stroke color to a medium gray
      • 196 – set the fill color for the background of this button
      • 197 – draw a gray rectangle
      • 198 – set the text color to black
      • 199 – draw the text for this button to the screen

And there it is. Probably the most complex code in this series. This is why libraries like Easel and oCanvas are so nice for simpler applications – they take all of the hassle out of creating UI elements.

Processing.js is a very big gun as far as drawing libraries go. It is perhaps not the best one to use for basic UI work, but if you have something which requires serious visualization power, such as representations of complex mathematics or scientific data, then this is the tool to use. It can easily interface with vanilla Javascript, so if you don’t want to go through the trouble of coding your own buttons, you can create the UI in plain HTML and use, say, jQuery to hook everything together.

As with the other articles in this series, the math for the animations comes from my Simple Trigonometric Curves Tutorial over at Kongregate.

Previous articles: Easel.js, oCanvas.js, Raphael.js, Paper.js.

Posted in ProgrammingTagged math, procedural art comment on HTML5 Canvas Drawing Library Exploration: Processing.js

Posts navigation

Older posts

Personal website of
John Winkelman

John Winkelman in closeup

Archives

Categories

Posts By Month

May 2025
S M T W T F S
 123
45678910
11121314151617
18192021222324
25262728293031
« Apr    

Links of Note

Reading, Writing
Tor.com
Locus Online
The Believer
File 770
IWSG

Watching, Listening
Writing Excuses Podcast
Our Opinions Are Correct
The Naropa Poetics Audio Archive

News, Politics, Economics
Naked Capitalism
Crooked Timber

Meta

  • Log in
  • Entries feed
  • Comments feed
  • WordPress.org

© 2025 Ecce Signum

Proudly powered by WordPress | Theme: x-blog by wpthemespace.com