package { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Loader; import flash.display.Sprite; import flash.events.Event; import flash.filesystem.File; import flash.filesystem.FileMode; import flash.filesystem.FileStream; import flash.geom.ColorTransform; import flash.geom.Matrix; import flash.geom.Rectangle; import flash.net.URLRequest; import flash.text.TextField; import flash.text.TextFormat; import flash.utils.ByteArray; import com.adobe.images.PNGEncoder; import com.controul.math.prng.ParkMiller; public class PNGSequenceRenderer extends Sprite { //////// //////// //////// Resources. //// State. private var hh : Number; private var vv : Number; private var ff : Number; //// Renderer resources. private var loader : Loader; private var repository : File; private var canvas : BitmapData; private var matrix : Matrix = new Matrix; private var rectangle : Rectangle = new Rectangle; private var colors : ColorTransform = new ColorTransform; private var countG : uint = 0; private var countL : uint = 0; private var prng : ParkMiller = new ParkMiller; private var current : MovieScene; //// Movie resources. private var queue : MovieScene; private var inputWidth : Number = 853; private var inputHeight : Number = 480; private var showAll : Boolean = true; private var shake : Number = 0; private var flicker : Number = 0; private var transparent : Boolean = false; private var background : uint = 0; //// Interface. private var output : TextField = new TextField; private var viewport : Bitmap = new Bitmap; //// Constructor. public function PNGSequenceRenderer () : void { //// Stage stuff. if ( !stage ) addEventListener ( Event.ADDED_TO_STAGE, __stage ); else __stage ( null ); //// Viewport. addChild ( viewport ); viewport.smoothing = false; viewport.pixelSnapping = 'always'; //// Output window. output.width = 1; output.height = 1; output.wordWrap = false; output.autoSize = 'left'; output.setTextFormat ( output.defaultTextFormat = new TextFormat ( 'Lucida Console', 9, 0, true ) ); output.blendMode = 'invert'; addChild ( output ); } private function __stage ( e : Event ) : void { removeEventListener ( Event.ADDED_TO_STAGE, __stage ); //// Setup PNGSequenceRenderer. stage.quality = 'best'; stage.align = 'TL'; stage.scaleMode = 'noScale'; stage.frameRate = 80; } //////// //////// //////// Public API. /** * Sets the dimensions of the input swf material. ( Default 853/480. ) * @param width (pixels) * @param height (pixels) * @return The PNGSequenceRenderer itself for lambda chaining. */ public function setInputSize ( width : Number, height : Number ) : PNGSequenceRenderer { inputWidth = width; inputHeight = height; log ( "Input set to " + width + " by " + height + " pixels" ); return this; } /** * Sets the dimensions of the outputed png sequence. ( Default: match input. ) * @param width (pixels) * @param height (pixels) * @return The PNGSequenceRenderer itself for lambda chaining. */ public function setOutputSize ( width : uint, height : uint ) : PNGSequenceRenderer { if ( canvas ) canvas.dispose (); canvas = new BitmapData ( width, height, transparent, background ); log ( "Output set to " + width + " by " + height + " pixels" ); return this; } /** * Sets the scale mode for the output, show-all or false for no-border. Both preserve aspect ratio. * @param showAll true for show-all, false for no-border. ( Default: true ) * @return The PNGSequenceRenderer itself for lambda chaining. */ public function setScaleMode ( showAll : Boolean ) : PNGSequenceRenderer { this.showAll = showAll; log ( "Scale mode is set to " + ( showAll ? "'show all'" : "'no border'" ) ); return this; } /** * Controls camera shake. ( Examples: 0 - disabled, 1 - moderate, 2 - heavy. Default 0. ) * @param factor * @return The PNGSequenceRenderer itself for lambda chaining. */ public function setCameraShake ( factor : Number = 0 ) : PNGSequenceRenderer { shake = factor; log ( "Camera shake set to " + shake ); return this; } /** * Controls flicker. ( Examples: 0 - disabled, 1 - moderate, 2 - heavy. Default 0. ) * @param factor * @return The PNGSequenceRenderer itself for lambda chaining. */ public function setFlicker ( factor : Number = 0 ) : PNGSequenceRenderer { flicker = factor; log ( "Flicker set to " + flicker ); return this; } /** * Sets the output background transparency and fill color. * @param transparency Defaults to false. * @param color Defaults to 0. * @return The PNGSequenceRenderer itself for lambda chaining. */ public function setBackground ( transparency : Boolean = false, color : uint = 0 ) : PNGSequenceRenderer { transparent = transparency; background = color; if ( canvas ) { var c : BitmapData = new BitmapData ( canvas.width, canvas.height, transparent, background ); canvas.dispose (); canvas = c; } log ( "Background transparency set to " + transparent + ", color set to " + background ); return this; } /** * Enqueues a scene for rendering. * @param url The location of the movie to render. * @param cameraShakeMultiplier Local camera shake adjustment. Camera shake for this scene = global * local shake factors. * @param flickerMultiplier Local flicker adjustment. Flicker for this scene = global * local flicker factors. * @param fromFrame The index (zero-based) of the first frame to render. * @param toFrame The index (zero-based) of the frame upon which to end the scene. If a pink screen ( solid #ff00ff ) is reached earlier, the scene PNGSequenceRenderering ends anyway. * @return The PNGSequenceRenderer itself for lambda chaining. */ public function enqueueScene ( url : String, cameraShakeMultiplier : Number = 1, flickerMultiplier : Number = 1, fromFrame : uint = 0, toFrame : uint = 0xffffff ) : PNGSequenceRenderer { var s : MovieScene = queue; if ( !s ) s = queue = new MovieScene; else { while ( s.next ) s = s.next; s = s.next = new MovieScene; } s.url = url; s.shake = cameraShakeMultiplier; s.flicker = flickerMultiplier; s.from = fromFrame; s.to = toFrame; log ( "Enqueuing " + url ); if ( s.shake != 1 ) log ( " - shake * " + s.shake ); if ( s.flicker != 1 ) log ( " - flicker * " + s.flicker ); if ( s.from != 0 ) log ( " - skip first " + s.from + " frames" ); if ( s.to != 0xffffff ) log ( " - end early on frame " + s.to ); return this; } /** * Begins loading scenes and outputing frames in the specified folder. * @param outputFolder The folder where all outputed files are to be put. */ public function render ( outputFolder : String ) : void { //// Lookup output folder. repository = new File ( outputFolder ); //// Empty output folder. var files : Array = repository.getDirectoryListing (), i : int = 0, n : int = files.length; for ( i = 0; i < n; i ++ ) { var file : File = files [ i ]; if ( file.extension.toLowerCase () == 'png' && file.name.match ( /frame\d{4}.*/i ) ) file.deleteFile (); } //// Setup canvas. if ( !canvas ) canvas = new BitmapData ( 853, 380, true, 0 ); countG = 0; //// Load movie. if ( !loader ) loader = new Loader; else loader.unloadAndStop (); //// Begin rendering. processNext (); //// Connect viewport. viewport.bitmapData = canvas; } private function processNext () : void { //// Stop rendering. removeEventListener ( Event.ENTER_FRAME, __enter ); //// Unload current scene. if ( loader.content ) loader.unloadAndStop (); //// Fetch next scene. current = queue; if ( current ) queue = current.next; else { log ( 'Rendering complete.' ); print ( 'Find all files in:\n' + repository.nativePath ); return; } //// Load next scene. print ( 'Loading ' + current.url + ' ...' ); loader.load ( new URLRequest ( current.url ) ); loader.contentLoaderInfo.addEventListener ( Event.COMPLETE, __loaded ); //// Reset counter and trailers. countL = 0; hh = 0; vv = 0; ff = 0; } private function __loaded ( e : Event ) : void { //// Start rendering frames. log ( 'Rendering ' + current.url + ' ...' ); addEventListener ( Event.ENTER_FRAME, __enter ); } private function __enter ( e : Event ) : void { var h : Number, v : Number, scale : Number, mmm : Matrix, ccc : ColorTransform, rrr : Rectangle; //// //// //// Skip. countL ++; if ( countL < current.from ) return print ( 'Skipping ...' ); if ( countL > current.to ) return processNext (); //// //// //// Draw. //// Reset the transformation matrix. matrix.identity (); if ( shake ) { //// Matrix is used. mmm = matrix; //// Shake the matrix. scale = Math.max ( inputWidth, inputHeight ) / 150 * shake * current.shake; h = prng.standardNormal () * scale; v = prng.standardNormal () * scale; h = ( 29 * hh + h ) / 30; v = ( 29 * vv + v ) / 30; hh = h; vv = v; scale = 1 + 2 * Math.max ( Math.abs ( h / inputWidth ), Math.abs ( v / inputHeight ) ); h -= inputWidth * ( scale - 1 ) / 2; v -= inputHeight * ( scale - 1 ) / 2; matrix.translate ( h, v ); matrix.scale ( scale, scale ); } //// Scale the matrix. h = canvas.width / inputWidth; v = canvas.height / inputHeight; scale = 1; if ( h != 1 || v != 1 ) { //// Matrix is used. mmm = matrix; //// Scale and center. if ( h > v || ( h < v && !showAll ) ) { scale = v; matrix.translate ( inputWidth * 0.5 * ( h / v - 1 ), 0 ); if ( showAll ) { //// Rectangle is used. rrr = rectangle; rectangle.x = inputWidth * scale * 0.5 * ( h / v - 1 ); rectangle.y = 0; } } else { scale = h; matrix.translate ( 0, inputHeight * 0.5 * ( v / h - 1 ) ); if ( showAll && h != v ) { //// Rectangle is used. rrr = rectangle; rectangle.x = 0; rectangle.y = inputHeight * scale * 0.5 * ( v / h - 1 ); } } matrix.scale ( scale, scale ); rectangle.width = inputWidth * scale; rectangle.height = inputHeight * scale; } if ( flicker ) { //// Color transform is used. ccc = colors; //// Apply flicker. var value : Number = prng.uniform (); value *= value; value *= value; value *= current.flicker; value = ( 4 * ff + value ) / 5; ff = value * flicker; colors.redOffset = - 40 * value; colors.greenOffset = - 40 * value; colors.blueOffset = - 40 * value; colors.redMultiplier = 1 + value; colors.greenMultiplier = 1 + value; colors.blueMultiplier = 1 + value; } //// Reset canvas and draw. canvas.fillRect ( new Rectangle ( 0, 0, canvas.width, canvas.height ), 0 ); canvas.draw ( loader, mmm, ccc, null, rrr ); //// //// //// End scene upon pink frame. if ( canvas.getPixel ( canvas.width / 2, canvas.height / 2 ) == 0xff00ff ) return processNext (); //// //// //// Encode. var ba : ByteArray = PNGEncoder.encode ( canvas ); //// Flush. var cpadded : String = String ( countG ); while ( cpadded.length < 5 ) cpadded = '0' + cpadded; var file : File = repository.resolvePath ( 'frame' + cpadded + '.png' ); var stream : FileStream = new FileStream; stream.open ( file, FileMode.WRITE ); stream.truncate (); stream.writeBytes ( ba ); stream.close (); //// //// //// Advance countG. countG ++; print ( 'Writing to ' + file.nativePath + ' ...' ); //// //// //// Cleanup. ba.length = 0; } //////// //////// //////// Output. private var outstr : String = ''; private function print ( str : String ) : void { output.text = outstr + '\n\n' + str; output.scrollV = output.maxScrollV; } private function log ( str : String ) : void { outstr += '\n' + str; output.text = outstr; output.scrollV = output.maxScrollV; } } } class MovieScene { public var url : String; public var from : uint; public var to : uint; public var shake : Number; public var flicker : Number; public var next : MovieScene; }