....


Metaballs
version in actionScript 3.0,
using alpha-channel of a precounted bitmap and ColorMatrixFilter
7244



/* 	Metaballs.as
*	Petri Leskinen
*	leskinen[dot]petri[at]luukku[dot]com
*  	Espoo, Finland
*	3.3 2008 
*/

 package
{
	
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.display.Stage;

import flash.events.Event;
import flash.events.MouseEvent;
import flash.events.TimerEvent;

import flash.filters.ColorMatrixFilter;

import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;

import flash.utils.Timer;

public class Metaballs extends Sprite
	{
	
	private var _sprite:Sprite;
	private var myTimer:Timer;
	private var balls:Array;
	
	private var renderedImage:Bitmap;
	private var renderedBmp:BitmapData;
	
	private var ballImage:BitmapData;
	private var lightPoint:Point = new Point();
	
	
	function Metaballs() {
		// some background
		this.graphics.beginFill(0x104050,1.0);
		this.graphics.drawRect(0.0, 0.0, stage.stageWidth, stage.stageHeight); // 500.0, 500.0);
		
		// initializing the balls with some random location to start with
		balls = new Array;
		for (var i:int=0; i<12; i++) {
			// here a ball has just a position and size
			balls[i]= new Object();	
			balls[i]= {	x: 100+300*Math.random(),
						y: 200+100*Math.random(),
						size: 1.0-0.05*i } ;
		}
		
		// create a Bitmap of one ball
		ballImage = regerateBallImage(150,150);
		
		// 'renderedImage' is what we see in the player 
		renderedImage = new Bitmap;
		this.addChild(renderedImage);
		upDate();
		
		// adding some animation
		myTimer = new Timer(1000/20);
		myTimer.addEventListener(TimerEvent.TIMER, newPhase);
		myTimer.start();
		
		// ... and interactivity
		stage.addEventListener(MouseEvent.MOUSE_MOVE, followLight);
	}
	
	

	
	private function newPhase(e:Event):void {
		// timer animation
		var sina:Number;
		var cosa:Number;
		var po:Point = new Point();
		
		// animation by rotating the balls
		for (var i:int =0; i<balls.length; i++){
			cosa = Math.cos(0.01*(i+1) );
			sina = Math.sin(0.01*(i+1) );
			po.x = balls[i].x -renderedImage.width/2;
			po.y = balls[i].y -renderedImage.height/2;
			
			balls[i].x = po.x*cosa - po.y*sina +renderedImage.width/2;
			balls[i].y = po.x*sina + po.y*cosa +renderedImage.height/2;
		}
		
		upDate();
	}
	
	
	private function followLight(e:MouseEvent):void {
		/// mouse interactivity
		lightPoint.x = mouseX;
		lightPoint.y = mouseY;
	}
	
	// bitmap-operations in upDate() use these constants:
	private const destPoint:Point = new Point(0,0);	
	private const alphaAdjustFilter:ColorMatrixFilter = new ColorMatrixFilter(
							[	1.0, 0.0, 0.0, 0.0, 0.0,
								0.0, 1.0, 0.0, 0.0, 0.0,
								0.0, 0.0, 1.0, 0.0, 0.0,
								0.0, 0.0, 0.0, 16.0, -1920.0	] ); // if 50% transparency alpha=128, resulting alpha = 16*128-1920 = 128 !

								
	
	public function upDate():void {
		// rendering the scene
		renderedBmp = new BitmapData( stage.stageWidth, stage.stageHeight,true,0x0);
		var mtrx:Matrix;
		var xx:Number;
		var yy:Number;
		
		for (var i:int=0; i<balls.length; i++) {
			// translation to the center of bitmap
			mtrx= new Matrix(1.0, 0.0, 0.0, 1.0, -ballImage.width/2, -ballImage.height/2);
			
			// rotate towards the light source
			xx =  lightPoint.x - balls[i].x;
			yy =  lightPoint.y - balls[i].y;
			mtrx.rotate(Math.atan2(yy,xx));
			
			// ball's size
			mtrx.scale( balls[i].size, balls[i].size );
			
			// ball's position
			mtrx.translate( balls[i].x, balls[i].y);
			
			// draw ball to the bitmap
			renderedBmp.draw(ballImage, mtrx);
		}
		
		// var sourceRect:Rectangle = new Rectangle(0, 0, renderedBmp.width, renderedBmp.height);
		
		// adjusting the alpha-value, values 0 - 120 put to zero, 
		// values 120-136 make a nice blur on the edges
		// values 136-255 put to 255
		renderedBmp.applyFilter(renderedBmp, 
							new Rectangle(0, 0, renderedBmp.width, renderedBmp.height),
							destPoint, 
							alphaAdjustFilter);
		
		
/*		My first try was a version using .threshold, it left the edges pixelated in a ugly way
* 
		var operation:String = "<";
		var threshold:uint = 0x80000000;
		var color:uint = 0x00000000;
		var mask:uint = 0xFF000000;
		var copySource:Boolean = true;
		
		renderedBmp.threshold(renderedBmp,
							 sourceRect,
							 destPoint,
							 operation,
							 threshold,
							 color,
							 mask,
							 copySource); */
		
		renderedImage.bitmapData = renderedBmp;
	}
	
	
	private function regerateBallImage(width:int, height:int):BitmapData {
		// regerating a bitmapdata of one ball
		var yy:Number;
		var xx:Number;
		var zz:Number;
		var pxl:uint;
		
		// direction of light
		var lightSrc:Object = new Object();
		lightSrc= {	x: 1.0, y: 0.0,	z: 1.0 };
		//	turned into a unit vector
		xx = Math.sqrt(lightSrc.x*lightSrc.x +lightSrc.y*lightSrc.y +lightSrc.z*lightSrc.z);
		lightSrc.x /= xx;
		lightSrc.y /= xx;
		lightSrc.z /= xx;
		
		var lightAngle:Number;
		
		// generating the bitmap pixel by pixel
		var bmp:BitmapData = new BitmapData(width, height, true,0x0);
		for (var y:int =0; y<height; y++) {
			
			yy = 2.0*y/height -1.0 ; // in range -1 ... +1

			for (var x:int =0; x<width; x++) {
				
				xx = 2.0*x/width -1.0; // in range -1 ... +1
				zz = 1.0-xx*xx-yy*yy;  // sphere's squared z-coordinate, in range +1 ... -1
				
				if (zz>0.0) {
					// lightAngle, in fact the cosine of the angle between surface normal and light direction,
					// values between -1.0 ... 1.0
					//
					lightAngle = xx*lightSrc.x + yy*lightSrc.y + Math.sqrt(zz)*lightSrc.z;
					lightAngle *= (lightAngle>0.0) ? lightAngle*lightAngle : 0.0;
					
					// rendered pixel, alpha by sphere's z-component, color mixed by light angle
					pxl = (Math.floor(zz*0xFF)<<24) | (Math.floor(0xFF*lightAngle*lightAngle)<<16) | (Math.floor(0xFF*lightAngle)<<8) | 0x40-0x3F*lightAngle;
					
				} else {
					pxl = 0x0;
				}
				
				bmp.setPixel32(x,y,pxl);
			}
		}
		return bmp;
	}
	
	}
}
		
		

..