Prosto z Commodore 64: efekt plazmy

Od pewnego czasu wracam wspomnieniami do czasów demosceny, a od kilku dni odświeżam stare umiejętności i przekładam to, co kiedyś robiło się w assemblerze, na Flasha i Actionscript. Jednym z wynalazków koderów z demosceny, był efekt plazmy.

Opisowo efekt ten polegał na uzyskaniu interesujących graficznie animacji “rozlewających” się kolorów. Ponieważ jednak ówczesne komputery były zbyt wolne na obliczanie co ramke wartości każdego pixela na ekranie, koderzy użyli jednej ze swoich sztuczek: tylko raz na starcie rysowało się obraz pixeli na ekranie, a potem podmieniano jedynie paletę kolorów używaną przez układ graficzny. W ten sposób nawet ZX-Spectrum dawało radę:

W takim razie, skoro ZX-Spectrum ze swoim Zilogiem Z80 dawało radę, jak poradzi sobie flash na procesorach taktowanych gigaherzami? :) Nie możemy niestety operować bezpośrednio na palecie ( zabawa z ColorTransform również odpada), więc jednak musimy za każdym razem odrysować wszystkie pixele na ekranie. Spróbujmy:

package {
	import flash.display.Sprite;
	import flash.display.StageScaleMode;
	import flash.display.StageAlign;
	import flash.events.Event;
	import flash.display.BitmapData;
 
	[SWF(width="300", height="300", backgroundColor="0x000000")]
	public class PlasmaEffect extends Sprite
	{
		private var w:int = 150;
		private var h:int = 150;
		private var colors:Array = new Array();
		private var plasma:Array = [w];
		private var paletteShift:int;
 
		public function PlasmaEffect()
		{
			this.stage.scaleMode = StageScaleMode.NO_SCALE;
			this.stage.align = StageAlign.TOP_LEFT;
			this.stage.frameRate = 10;
			this.stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
 
			generatePalette();
			generatePlasma();
		}
 
		private function draw():void
		{
			this.graphics.clear();
 
	        for(var x:int = 0; x < w; x++)
	        for(var y:int = 0; y < h; y++)
	        {
	            this.graphics.lineStyle(1, colors[(plasma[x][y] + paletteShift) % 256]);
	            this.graphics.moveTo(x * 2, y * 2);
	            this.graphics.lineTo(x * 2, y * 2 + 1);  
	        }			
		}
 
 
		private function generatePlasma():void
		{
			var colorIndex:int;
 
		    for(var x:int = 0; x < w; x++)
		    {
		    	plasma[x] = [h];
 
			    for(var y:int = 0; y < h; y++)
			    {		    	
			        colorIndex =
			        (
			              128.0 + (128.0 * Math.sin(x / 16.0))
			            + 128.0 + (128.0 * Math.sin(y / 32.0))
			            + 128.0 + (128.0 * Math.sin(Math.sqrt(((x - w / 2.0)* (x - w / 2.0) + (y - h / 2.0) * (y - h / 2.0))) / 8.0))
			            + 128.0 + (128.0 * Math.sin(Math.sqrt((x * x + y * y)) / 8.0))
			        ) / 4;			        
 
			        plasma[x][y] = colorIndex;       
			    }			
		    }
		}
 
		private function generatePalette():void
		{
			var r:int;
			var g:int;
			var b:int;
 
			for(var x:int = 0; x < 256; x++)
			{
			    r = 128.0 + 128 * Math.sin(3.1415 * x / 16.0 );
			    g = 128.0 + 128 * Math.sin(3.1415 * x / 128.0 );
			    b = 0;
			    colors.push(r * 256 * 256 + g * 256 + b);
			} 			
		}
 
		private function onEnterFrame(e:Event):void
		{
			draw();	
			paletteShift += 1;
		}
	}
}

No cóż, na moim, całkiem współczesnym komputerze nie daje rady – ledwo 5 klatek na sekundę, i macierz pixeli o rozmiarach 150×150. Winowajcą jest metoda lineTo – zbyt wolna jak na nasze potrzeby.
Ale, drobna zmiana kodu, zastąpienie rysowanie poprzez graphics.lineTo na bezpośrednie ustawianie pixelów bitmapy:

package {
	import flash.display.Sprite;
	import flash.display.StageScaleMode;
	import flash.display.StageAlign;
	import flash.events.Event;
 
	import flash.display.BitmapData;
	import flash.display.Bitmap;
 
	[SWF(width="300", height="300", backgroundColor="0x000000")]
	public class PlasmaEffect extends Sprite
	{
		private var w:int = 300;
		private var h:int = 300;
		private var colors:Array = new Array();
		private var plasma:Array = [w];
		private var paletteShift:int;
 
		private var bitmap:BitmapData = new BitmapData(w, h, false);
 
		public function PlasmaEffect()
		{
			this.stage.scaleMode = StageScaleMode.NO_SCALE;
			this.stage.align = StageAlign.TOP_LEFT;
			this.stage.frameRate = 20;
			this.stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
 
			this.addChild(new Bitmap(bitmap));
 
			generatePalette();
			generatePlasma();
		}
 
		private function draw():void
		{
			this.graphics.clear();
 
	        for(var x:int = 0; x < w; x++)
	        for(var y:int = 0; y < h; y++)
	        {
	        	bitmap.setPixel(x, y, colors[(plasma[x][y] + paletteShift) % 256]);
	            //this.graphics.lineStyle(1, colors[(plasma[x][y] + paletteShift) % 256]);
	            //this.graphics.moveTo(x * 2, y * 2);
	            //this.graphics.lineTo(x * 2, y * 2 + 1);  
	        }			
		}
 
 
		private function generatePlasma():void
		{
			var colorIndex:int;
 
		    for(var x:int = 0; x < w; x++)
		    {
		    	plasma[x] = [h];
 
			    for(var y:int = 0; y < h; y++)
			    {		    	
			        colorIndex =
			        (
			              128.0 + (128.0 * Math.sin(x / 16.0))
			            + 128.0 + (128.0 * Math.sin(y / 32.0))
			            + 128.0 + (128.0 * Math.sin(Math.sqrt(((x - w / 2.0)* (x - w / 2.0) + (y - h / 2.0) * (y - h / 2.0))) / 8.0))
			            + 128.0 + (128.0 * Math.sin(Math.sqrt((x * x + y * y)) / 8.0))
			        ) / 4;			        
 
			        plasma[x][y] = colorIndex;       
			    }			
		    }
		}
 
		private function generatePalette():void
		{
			var r:int;
			var g:int;
			var b:int;
 
			for(var x:int = 0; x < 256; x++)
			{
			    r = 128.0 + 128 * Math.sin(3.1415 * x / 16.0 );
			    g = 128.0 + 128 * Math.sin(3.1415 * x / 128.0 );
			    b = 0;
			    colors.push(r * 256 * 256 + g * 256 + b);
			} 			
		}
 
		private function onEnterFrame(e:Event):void
		{
			draw();	
			paletteShift += 1;
		}
	}
}

i… sukces! :) Tym razem macierz 300×300 i 20 klatek na sekundę:

daje radę :)