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;
}
}
}