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;
            });
            
            // uncomment to enable performance timing
//            releasePerformanceTestSparks();
        }
        
        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;
            
            // unlike Processing, Flash stores a record of calls to graphics.draw* as vector data. It is therefore
            // necessary for performance to draw the vector data to an image beffer every frame and clear the vectors
            
            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;



// CONFIGURATION CONSTANTS
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 {
        // Create a random fully saturated colour. Colours are fully saturated if they have
        // one RGB  value at 255, one at 0, and the third at some  arbitrary value.
        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; // extract red byte
        var vy:Number = (((value >> 8) & 0xFF) / 128 - 1) * MAX_INITIAL_V; // extract green byte
        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);
    }
}