package
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.display.StageQuality;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.utils.getTimer;
[SWF("1000", height="800")]
public class Imagine extends Sprite
{
private var sparks:Array = [];
private var data:ImageData;
private var timeStart:Number;
private var timeLastFrame:Number;
private var frame:int;
private var mousePressed:Boolean;
private var buffer:BitmapData;
private var canvas:Sprite = new Sprite();
private var debugText:TextField;
[Embed("logo.jpg")]
private static var LOGO:Class;
public function Imagine()
{
data = new ImageData((new LOGO()).bitmapData, this);
buffer = new BitmapData(stage.stageWidth, stage.stageHeight, false, 0);
addChild(new Bitmap(buffer));
stage.quality = StageQuality.LOW;
stage.frameRate = 200;
addEventListener(Event.ENTER_FRAME, handleEnterFrame);
stage.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {
mousePressed = true;
});
stage.addEventListener(MouseEvent.MOUSE_UP, function(e:MouseEvent):void {
mousePressed = false;
});
}
public function releasePerformanceTestSparks():void {
const grid:int = 10;
frame = 0;
timeStart = getTimer();
for (var i:int=0; i<width; i+= grid) {
for (var j:int=0; j<height; j+= grid) {
sparks.push(new Spark(i, j, data.isTextAt(i, j), data));
}
}
}
private function handleEnterFrame(e:Event):void {
if (mousePressed) {
addSparks(mouseX, mouseY);
}
var finishedUpTo:int = -1;
canvas.graphics.clear();
for (var i:int=0; i<sparks.length; i++) {
var spark:Spark = sparks[i];
if (!spark.isFinished) {
spark.update(canvas.graphics);
} else if (finishedUpTo == i-1) {
finishedUpTo = i;
}
}
buffer.draw(canvas);
if (finishedUpTo != -1) {
sparks.splice(0, finishedUpTo+1);
}
if (timeStart > 0 && ++frame == 20) {
if (debugText == null) {
debugText = new TextField();
debugText.textColor = 0xFFFFFF;
debugText.autoSize = TextFieldAutoSize.LEFT;
addChild(debugText);
}
debugText.text = "20 frames with " + sparks.length + " sparks in " + (getTimer() - timeStart) + " ms";
}
trace((getTimer() - timeLastFrame) + " ms this frame with " + sparks.length + " sparks");
timeLastFrame = getTimer();
}
private function addSparks(centerX:int, centerY:int):void {
for (var i:int=0; i<SPARKS_PER_FRAME; i++) {
var offset:Number = Math.pow(Math.random(), SPARK_RELEASE_POWER) * SPARK_RELEASE_RADIUS;
var angle:Number = Math.random() * Math.PI * 2;
var x:int = centerX + Math.sin(angle) * offset;
var y:int = centerY + Math.cos(angle) * offset;
var isText:Boolean = data.isTextAt(x, y);
if (!isText && Math.random() > BG_RELEASE_PROBABILITY) {
continue;
}
sparks.push(new Spark(x, y, isText, data));
}
}
}
}
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.display.Graphics;
import flash.display.Stage;
import flash.geom.Point;
const RADIUS_DECAY:Range = new Range(0.95, 0.98);
const MAX_INITIAL_V:Number = 1;
const INITIAL_RADIUS:Range = new Range(5, 20);
const FORCE_AMPLITUDE:Range = new Range(-0.05, 0.05);
const FORCE_PERIOD:Range = new Range(10, 50);
const GREY_VALUE:Range = new Range(0, 128);
const TRANSPARENCY:Number = 0.1;
const SPARKS_PER_FRAME:int = 50;
const SPARK_RELEASE_RADIUS:Number = 200;
const SPARK_RELEASE_POWER:Number = 2;
const BG_RELEASE_PROBABILITY:Number = 0.3;
const DARKENING_RADIUS:Range = new Range(3, 7);
const DARKENING_BEGIN_DELAY:int = 5;
const DARKENING_ALPHA:Number = 0.1;
class Spark {
private var location:Point;
private var initialLocation:Point;
private var velocity:Point;
private var radius:Number;
private var darkeningRadius:Number;
private var radiusDecay:Number;
private var color:uint;
private var xForceAmplitude:Number;
private var xForcePeriod:Number;
private var yForceAmplitude:Number;
private var yForcePeriod:Number;
private var forceCounter:int = 0;
private var isText:Boolean;
private var finished:Boolean = false;
private var data:ImageData;
public function Spark(x:int, y:int, isText:Boolean, data:ImageData) {
this.location = new Point(x, y);
this.data = data;
this.initialLocation = new Point(x, y);
this.isText = isText;
this.velocity = data.velocityAt(x, y);
this.radius = INITIAL_RADIUS.get();
this.radiusDecay = RADIUS_DECAY.get();
this.color = isText ? randomColor() : randomGrey();
this.darkeningRadius = DARKENING_RADIUS.get();
this.xForceAmplitude = FORCE_AMPLITUDE.get();
this.xForcePeriod = FORCE_PERIOD.get();
this.yForceAmplitude = FORCE_AMPLITUDE.get();
this.yForcePeriod = FORCE_PERIOD.get();
}
public function update(graphics:Graphics):void {
graphics.beginFill(color, TRANSPARENCY);
graphics.drawEllipse(location.x, location.y, radius, radius);
forceCounter++;
velocity.x += Math.sin(Math.PI * 2 * forceCounter / xForcePeriod) * xForceAmplitude;
velocity.y += Math.sin(Math.PI * 2 * forceCounter / yForcePeriod) * yForceAmplitude;
if (isText) {
graphics.beginFill(0, DARKENING_ALPHA);
graphics.drawEllipse(initialLocation.x + random(-1, 1), initialLocation.y + random(-1, 1), darkeningRadius, darkeningRadius);
}
if (radius < 1) {
finished = true;
}
location.x += velocity.x;
location.y += velocity.y;
radius *= radiusDecay;
}
private function randomColor():uint {
var parts:Array = [255, (int) (Math.random() * 255), 0];
for (var from:int=0; from<parts.length; from++) {
var to:int = Math.random() * parts.length;
var tmp:int = parts[from];
parts[from] = parts[to];
parts[to] = tmp;
}
return (parts[0] << 16) | (parts[1] << 8) | parts[2];
}
private function randomGrey():uint {
var shade:int = GREY_VALUE.get();
return (shade << 16) | (shade << 8) | shade;
}
public function get isFinished():Boolean {
return finished;
}
}
function random(from:Number, to:Number):Number {
return from + (Math.random() * (to - from));
}
class Range {
private var from:Number;
private var to:Number;
public function Range(from:Number, to:Number) {
this.from = from;
this.to = to;
}
public function get():Number {
return random(from, to);
}
}
/**
* An image file containing a special kind of image used to drive the
* animation, where for each pixel, the red and green values encode the
* x and y velocity vector. The blue is a text mask, where values over
* 128 indicate text, and equal to or less than 128 indicate background
*/
class ImageData {
private var image:BitmapData;
private var parent:DisplayObject;
public function ImageData(image:BitmapData, parent:DisplayObject) {
this.image = image;
this.parent = parent;
}
public function isTextAt(x:Number, y:Number):Boolean {
var value:uint = image.getPixel(dataX(x), dataY(y));
return (value & 0xFF) > 128;
}
public function velocityAt(x:Number, y:Number):Point {
var value:uint = image.getPixel(dataX(x), dataY(y));
var vx:Number = (((value >> 16) & 0xFF) / 128 - 1) * MAX_INITIAL_V; var vy:Number = (((value >> 8) & 0xFF) / 128 - 1) * MAX_INITIAL_V; return new Point(vx, vy);
}
private function dataX(windowX:Number):int {
return (int) (windowX / parent.width * image.width);
}
private function dataY(windowY:Number):int {
return (int) (windowY / parent.height * image.height);
}
}