Пиксельный эффект на Actionscript 3.0

в 22:39, , рубрики: actionscript 3.0, effects, Flash-платформа, Песочница, метки: ,
preview

Такой вот незамысловатый эффект. Под катом исходники, местами комментарии и пояснения.

Обдумывая как работает тот или иной спецэффект в голову приходят много мыслей, но до реализации не всегда доходит, в виду малого опыта именно со спецэффектами. «Пора исправлять ситуацию» — подумал я. Немного поразмыслив в голову пришла простая идея по поводу того, чего бы такого накодить. Задачей себе поставил сделать эффект появления текста, как будто его печатают попиксельной сваркой, и из точки сварки рассыпаются частицы. И, заодно, описать это в виде статьи. Писатель правда из меня никудышный, но надеюсь, эта статья принесет кому-нибудь пользу.

Описание алгоритма

Освнова эффекта это вырезанный, по заданному текстом контуру, кусок шума Перлина (док). Над текстом расположена маска, которая двигаеться вдоль оси OХ вправо приоткрывая текст. В главном цикле определяем где сейчас находится правый край маски, и для каждого Y, по высоте битмапы с текстом, достаем непрозрачный пиксель и начинаем его анимировать. Плюс здесь же показываем где сечас находится точка «сварки».

Реализация

Текст вырезан с помощью BlendMode.ERASE, который стирает пиксели фонового объекта на основе значения альфа канала подмешиваемого объекта. То есть при альфе равной 0xFF значение альфа канала фона будет равно 0x00.

noised text
Метод подготовки битмапы с текстом.

код

private function GetMaskedText(text:String):BitmapData
{
    var tf:TextField = new TextField();
    var format:TextFormat = new TextFormat("Arial", 60, 0xFFFFFF, true);
    tf.defaultTextFormat = format;
    tf.text = text;
    tf.width = tf.textWidth + 4.0;
    tf.height = tf.textHeight + 4.0;
    tf.filters = [new GlowFilter(0xFFFFFF, 1.0, 2, 2, 4, 3)];
    var w:int = tf.width;
    var h:int = tf.height;

    var noiseBdata:BitmapData = new BitmapData(w, h, true, 0xFFFFFFFF);
    // используем красный и зеленый канал для генерации шума
    var channels:int = BitmapDataChannel.GREEN | BitmapDataChannel.RED; 
    // собственно сам шум
    noiseBdata.perlinNoise(
		w / 6
		, h / 4
		, 6
		, int(Math.random() * 1000)
		, false
		, false
		, channels
		, false);
    // слегка осветляю шум
    noiseBdata.colorTransform(noiseBdata.rect, new ColorTransform(1.4, 1.4, 1.4)); 
    var noiseBmp:Bitmap = new Bitmap(noiseBdata);

    // тут будет текст
    var textBdata:BitmapData = new BitmapData(w, h, true, 0x00000000); 
    textBdata.draw(tf);
    var textBmp:Bitmap = new Bitmap(textBdata);

    // а это для битмапы из которой будет вырезан текст
    var eraseTextBdata:BitmapData = noiseBdata.clone(); 
    eraseTextBdata.draw(noiseBmp);
    eraseTextBdata.draw(textBmp, null, null, BlendMode.ERASE);
    var eraseTextBmp:Bitmap = new Bitmap(eraseTextBdata);

    // и результат, из шума вырезаем вырезанный текст
    var eraseBackByTextBdata:BitmapData = noiseBdata.clone();
    eraseBackByTextBdata.draw(eraseTextBmp, null, null, BlendMode.ERASE);
    eraseBackByTextBdata.applyFilter(
		eraseBackByTextBdata
		, eraseBackByTextBdata.rect
		, new Point()
		, new GlowFilter(0xFFFFFF, 1.0, 3, 3)
    );

    noiseBdata.dispose();
    textBdata.dispose();
    eraseTextBdata.dispose();

    head = new BitmapData(6, 6, true, 0xffFFFFFF);
    return eraseBackByTextBdata;
}

Теперь надо добавить текст на сцену, поверх положить маску.

код

private var textBdata:BitmapData;
private var textBmp:Bitmap;
private var txtMask:Shape;
private var btnRestart:MiniButton;
private function InitalizeLayout():void
{
    screen = new BitmapData(W, H, true, 0x00000000);
    addChild(new Bitmap(screen));
 
    textBdata = GetMaskedText("..Hello world..");
    textBmp = new Bitmap(textBdata);
    textBmp.x = (W - textBmp.width) * 0.5;
    textBmp.y = (H - textBmp.height) * 0.5;
    addChild(textBmp);
 
    txtMask = new Shape();
    txtMask.graphics.beginFill(0xFFFFFF);
    txtMask.graphics.drawRect(0, 0, 4, textBmp.height);
    txtMask.graphics.endFill();
 
    txtMask.x = textBmp.x;
    txtMask.y = textBmp.y;
    textBmp.mask = txtMask;
    addChild(txtMask);

    btnRestart = new MiniButton("restart");
    btnRestart.x = (W - MiniButton.W) * 0.5;
    btnRestart.y = 10.0;
    addChild(btnRestart);
}

Переменная screen — это большая BitmapData в которой будут отрисовываться частицы. Сразу небольшая оговорка, для храниния частиц решил использовать односвязный список вместо Vector, в виду того, что придется часто удалять частицы и делать vector.splice. А из списка удалить элемент проще — нужно просто исключить элемент и поменять ссылки у соседей.
Класс Particle

код
class Particle
{
    private static const GRAVITY:Point = new Point(0, 0.2);
    private var speed:Point;
    public var position:Point;
    public var color:int;
    public var next:Particle;
 
    public function Particle(x:Number, y:Number, color:int)
    {    
        position= new Point(x, y);
        // случайная начальная скорость
        speed = new Point();
        speed.x = Math.random() * 10 - 2;
        speed.y = Math.random() * 1 - 4;
        this.color = color;
    }

    public function Update():void
    {
        speed = speed.add(GRAVITY);
        pos = pos.add(speed);
    }
}

Для перезапуска анимации нужна кнопка рестарт. У кнопки два состояния — подсвеченное при наведение и обычное.

код

class MiniButton extends Sprite
{
    private var tf:TextField;
    public static const W:int = 100;
    public static const H:int = 24;
	
    public function MiniButton(text:String)
    {
        tf = new TextField();
        var format:TextFormat = new TextFormat(
                "Arial"
                , 16
                , 0x676767
                , true
                , null, null, null, null
                , TextFormatAlign.CENTER);
        tf.defaultTextFormat = format;
        tf.mouseEnabled = false;
        tf.text = text;
        tf.width = W;
        tf.height = H;
		
        Redraw(0xB3F7B6);
		
        this.filters = [new GlowFilter(0xFFFFFF, 0.5, 14, 14, 3, 3)];
		
        mouseChildren = false;
        buttonMode = true;
		
        addEventListener(MouseEvent.ROLL_OVER, HandleRollOver);
        addEventListener(MouseEvent.ROLL_OUT, HandleRollOut);
    }
	
    private function Redraw(color:int):void
    {
        graphics.clear();
        graphics.beginFill(0xB3F7B6);
        graphics.drawRoundRect(0, 0, W, H, 16);
        graphics.endFill();
    }
	
    private function HandleRollOut(e:MouseEvent):void 
    {
        Redraw(0xB3F7B6);
    }
	
    private function HandleRollOver(e:MouseEvent):void 
    {
        Redraw(0x37EC41);
    }
}

Методы перезапуска и остановки анимации.

код

private function Reset():void
{
    btnRestart.visible = false;
    txtMask.width = 4;
    isRunning = true;
	
    addEventListener(Event.ENTER_FRAME, HandleEnterFrame);
    }	
		
    private function Stop():void
    {
    btnRestart.visible = true;
    isRunning = false;
			
    removeEventListener(Event.ENTER_FRAME, HandleEnterFrame);
}

Необходимый функционал готов. В конструкторе вызываем InitializeLayout и запускаем анимацию.

код
public function Main() 
{	
    InitalizeLayout();
    Reset();	
    btnRestart.addEventListener(MouseEvent.CLICK, HandleResetClick);
}

Вот и главный цикл, который, правда, получился слегка громоздким.

код

private var firstParticle:Particle;
private function HandleEnterFrame(e:Event):void 
{
    if (!isRunning) return;

    // лочим и очищаем 
    screen.lock();
    screen.fillRect(screen.rect, 0x00000000);
 
    // X координата точки печати
    var currentX:int = (txtMask.x + txtMask.width) - textBmp.x;
 
    if (currentX >= 0 && currentX < textBmp.width)
    {
        // проходим по битмапе с текстом 
        var color:int;
        var alpha:int;
        while (currentY < txtMask.height)
        {
            // и достаем пиксель
            color = textBdata.getPixel32(currentX, currentY);
            alpha = (color >> 24) & 0xFF;
            // отбрасываем слабовидимые пиксели
            if (alpha > 0x7f) 
            {
                // потому что и так будем делать их более прозрачными
                alpha /= 1.4;
                color = alpha << 24 | (color & 0xFFFFFF);
    
                for (var i:int = 0; i < 8; ++i)
                {
                    var pp:Particle = new Particle(txtMask.x + txtMask.width, txtMask.y + currentY + i, color);
                    if (firstParticle == null)
                    {
                        firstParticle = pp;
                    }
                    else 
                    {
                        pp.next = firstParticle;
                        firstParticle = pp;
                    }
                }
                
                currentY += 6;
                screen.copyPixels(head, head.rect, new Point(
                        txtMask.x + txtMask.width
                        , txtMask.y + currentY - head.height / 2 )
                );
                screen.applyFilter(
                        screen
                        , screen.rect
                        , new Point()
                        , new BlurFilter(2, 2)
                );
                break;
            }
            currentY += 2;
         }
    }
 
    var p:Particle = firstParticle;
    var prev:Particle;
    while (p != null)
    {
        p.Update();
        
        // проверяем не вышла ли частица за границы экрана
        if (p.pos.x < 0 || p.pos.y < 0 || p.pos.x > W || p.pos.y > H)
        {
            // удаление частицы из списка
            if (prev == null)
            {
                p = p.next;
                firstParticle = p;
                continue;
            }
            else
            {
                prev.next = p.next;
            }
        }
 
        // частицу сделаем пожирнее
        var clr:int = p.c;
        screen.setPixel32(p.pos.x, p.pos.y, clr);
        screen.setPixel32(p.pos.x-1, p.pos.y, clr);
        screen.setPixel32(p.pos.x+1, p.pos.y, clr);
        screen.setPixel32(p.pos.x, p.pos.y-1, clr);
        screen.setPixel32(p.pos.x, p.pos.y + 1, clr);
 
        prev = p;
        p = p.next;
    }
 
    // и добавим "веса" частице
    screen.applyFilter(screen, screen.rect, new Point(), new GlowFilter(0xFFFF00, 0.8, 10, 10));
    screen.applyFilter(screen, screen.rect, new Point(), new BlurFilter(2, 2));
    screen.unlock();
 
    if (currentY >= txtMask.height) 
    {
        currentY = 0;
        txtMask.width += 2;
    }
    if (txtMask.width >= textBmp.width)
    {
        if (firstParticle == null)
        {
            Stop();
        }
    }
}

PS. На всякий случай полный исходник

код

package  
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.BitmapDataChannel;
	import flash.display.BlendMode;
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.filters.BlurFilter;
	import flash.filters.GlowFilter;
	import flash.geom.ColorTransform;
	import flash.geom.Point;
	import flash.text.TextField;
	import flash.text.TextFormat;
	
	
	/**
	 * ...
	 * @author KeeReal
	 */
	
	public class Main extends Sprite 
	{
//- PRIVATE & PROTECTED VARIABLES -------------------------------------------------------------------------

		private var textBdata:BitmapData;
		private var textBmp:Bitmap;

		private var screen:BitmapData;
		private var head:BitmapData;
		private var txtMask:Shape;
		
		private var firstParticle:Particle;
		private var btnRestart:MiniButton;
		
		private var isRunning:Boolean;
		private var currentY:int = 0;

//- PUBLIC & INTERNAL VARIABLES ---------------------------------------------------------------------------

		public static const W:int = 460;
		public static const H:int = 240;

//- CONSTRUCTOR -------------------------------------------------------------------------------------------
	
		public function Main() 
		{	
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP;
			
			
			InitalizeLayout();
			Reset();
			
			btnRestart.addEventListener(MouseEvent.CLICK, HandleResetClick);
		}
       
//- PRIVATE & PROTECTED METHODS ---------------------------------------------------------------------------

		private function Reset():void
		{
			btnRestart.visible = false;
			txtMask.width = 4;
			isRunning = true;
			
			addEventListener(Event.ENTER_FRAME, HandleEnterFrame);
		}
		
		
		private function Stop():void
		{
			btnRestart.visible = true;
			isRunning = false;
			
			removeEventListener(Event.ENTER_FRAME, HandleEnterFrame);
		}
		 


		private function InitalizeLayout():void 
		{
			screen = new BitmapData(W, H, true, 0x00000000);
			addChild(new Bitmap(screen));
			
			textBdata = GetMaskedText("..Hello habr..");
			textBmp = new Bitmap(textBdata);
			textBmp.x = (W - textBmp.width) * 0.5;
			textBmp.y = (H - textBmp.height) * 0.5;
			addChild(textBmp);
			
			txtMask = new Shape();
			txtMask.graphics.beginFill(0xFFFFFF);
			txtMask.graphics.drawRect(0, 0, 4, textBmp.height);
			txtMask.graphics.endFill();
			
			txtMask.x = textBmp.x;
			txtMask.y = textBmp.y;
			textBmp.mask = txtMask;
			addChild(txtMask);
			
			
			btnRestart = new MiniButton("restart");
			btnRestart.x = (W - MiniButton.W) * 0.5;
			btnRestart.y = 10.0;
			addChild(btnRestart);
		}


		private function GetMaskedText(text:String):BitmapData
		{
			var tf:TextField = new TextField();
			var format:TextFormat = new TextFormat("Arial", 60, 0xFFFFFF, true);
			tf.defaultTextFormat = format;
			tf.text = text;
			tf.width = tf.textWidth + 4.0;
			tf.height = tf.textHeight + 4.0;
			tf.filters = [new GlowFilter(0xFFFFFF, 1.0, 2, 2, 4, 3)];
			
			var w:int = tf.width;
			var h:int = tf.height;
			
			var noiseBdata:BitmapData = new BitmapData(w, h, true, 0xFFFFFFFF);
			
			var channels:int = BitmapDataChannel.GREEN | BitmapDataChannel.RED;
			noiseBdata.perlinNoise(w / 6, h / 4, 6, int(Math.random() * 1000), false, false, channels, false);
			noiseBdata.colorTransform(noiseBdata.rect, new ColorTransform(1.4, 1.4, 1.4));
			
			var noiseBmp:Bitmap = new Bitmap(noiseBdata);
			
			var textBdata:BitmapData = new BitmapData(w, h, true, 0x00000000);
			textBdata.draw(tf);
			var textBmp:Bitmap = new Bitmap(textBdata);
			
			var eraseTextBdata:BitmapData = noiseBdata.clone();
			eraseTextBdata.draw(noiseBmp);
			eraseTextBdata.draw(textBmp, null, null, BlendMode.ERASE);
			var eraseTextBmp:Bitmap = new Bitmap(eraseTextBdata);
			
			var eraseBackByTextBdata:BitmapData = noiseBdata.clone();
			eraseBackByTextBdata.draw(eraseTextBmp, null, null, BlendMode.ERASE);
			eraseBackByTextBdata.applyFilter(
					eraseBackByTextBdata
					, eraseBackByTextBdata.rect
					, new Point()
					, new GlowFilter(0xFFFFFF, 1.0, 3, 3)
			);
			
			noiseBdata.dispose();
			textBdata.dispose();
			eraseTextBdata.dispose();
			
			head = new BitmapData(6, 6, true, 0xffFFFFFF);
			
			return eraseBackByTextBdata;
		}

//- PUBLIC & INTERNAL METHODS -----------------------------------------------------------------------------
//- EVENT HANDLERS ----------------------------------------------------------------------------------------

		private function HandleResetClick(e:MouseEvent):void 
		{
			Reset();
		}
		

		private function HandleEnterFrame(e:Event):void 
		{
			if (!isRunning) return;
			
			screen.lock();
			screen.fillRect(screen.rect, 0x00000000);
			
			var currentX:int = (txtMask.x + txtMask.width) - textBmp.x;
			
			if (currentX >= 0 && currentX < textBmp.width)
			{
				var color:int;
				var alpha:int;
				while (currentY < txtMask.height)
				{
					color = textBdata.getPixel32(currentX, currentY);
					alpha = (color >> 24) & 0xFF;
					if (alpha > 0x7f) 
					{
						alpha /= 1.4;
						color = alpha << 24 | (color & 0xFFFFFF);
						
						for (var i:int = 0; i < 8; ++i)
						{
							var pp:Particle = new Particle(txtMask.x + txtMask.width, txtMask.y + currentY + i, color);
							if (firstParticle == null)
							{
								firstParticle = pp;
							}
							else 
							{
								pp.next = firstParticle;
								firstParticle = pp;
							}
						}
						
						currentY += 6;
						screen.copyPixels(head, head.rect, new Point(
								txtMask.x + txtMask.width
								, txtMask.y + currentY - head.height / 2 )
						);
						screen.applyFilter(
								screen
								, screen.rect
								, new Point()
								, new BlurFilter(2, 2)
						);
						break;
					}
					
					currentY += 2;
				}
			}
			
			
			var p:Particle = firstParticle;
			var prev:Particle;
			while (p != null)
			{
				p.Update();
				
				if (p.position.x < 0 || p.position.y < 0 || p.position.x > W || p.position.y > Main.H)
				{
					if (prev == null) 
					{
						p = p.next;
						firstParticle = p;
						continue;
					}
					else
					{
						prev.next = p.next;
					}
				}
				
				var clr:int = p.color;
				screen.setPixel32(p.position.x, p.position.y, clr);
				screen.setPixel32(p.position.x - 1, p.position.y, clr);
				screen.setPixel32(p.position.x + 1, p.position.y, clr);
				screen.setPixel32(p.position.x, p.position.y - 1, clr);
				screen.setPixel32(p.position.x, p.position.y + 1, clr);
				
				prev = p;
				p = p.next;
			}
			
			screen.applyFilter(screen, screen.rect, new Point(), new GlowFilter(0xFFFF00, 0.8, 10, 10));
			screen.applyFilter(screen, screen.rect, new Point(), new BlurFilter(2, 2));
			screen.unlock();
			
			if (currentY >= txtMask.height) 
			{
				currentY = 0;
				txtMask.width += 2;
			}
			
			
			if (txtMask.width >= textBmp.width)
			{
				if (firstParticle == null)
				{
					Stop();
				}
			}
		}

//- GETTERS & SETTERS -------------------------------------------------------------------------------------
//- HELPERS ----------------------------------------------------------------------------------------------- 
	}
}

import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.filters.GlowFilter;
import flash.geom.Point;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.text.TextFormatAlign;

class Particle
{
	private static const GRAVITY:Point = new Point(0.0, 0.2);
	private var speed:Point;
	
	public var position:Point;
	public var color:int;
	public var next:Particle;
	
	
	public function Particle(x:Number, y:Number, color:int)
	{
		position = new Point(x, y);
		speed = new Point();
		speed.x = Math.random() * 10 - 2;
		speed.y = Math.random() * 1 - 4;
		this.color = color;
	}
	
	
	public function Update():void
	{
		speed = speed.add(GRAVITY);
		position = position.add(speed);
	}
}



class MiniButton extends Sprite
{
	private var tf:TextField;
	
	public static const W:int = 100;
	public static const H:int = 24;
	
	
	public function MiniButton(text:String)
	{
		tf = new TextField();
		var format:TextFormat = new TextFormat(
				"Arial"
				, 16
				, 0x676767
				, true
				, null, null, null, null
				, TextFormatAlign.CENTER);
		tf.defaultTextFormat = format;
		tf.mouseEnabled = false;
		tf.text = text;
		tf.width = W;
		tf.height = H;
		
		Redraw(0xB3F7B6);
		
		this.filters = [new GlowFilter(0xFFFFFF, 0.5, 14, 14, 3, 3)];
		
		mouseChildren = false;
		buttonMode = true;
		
		addEventListener(MouseEvent.ROLL_OVER, HandleRollOver);
		addEventListener(MouseEvent.ROLL_OUT, HandleRollOut);
	}
	
	
	private function Redraw(color:int):void
	{
		graphics.clear();
		graphics.beginFill(0xB3F7B6);
		graphics.drawRoundRect(0, 0, W, H, 16);
		graphics.endFill();
	}
	
	private function HandleRollOut(e:MouseEvent):void 
	{
		Redraw(0xB3F7B6);
	}
	
	private function HandleRollOver(e:MouseEvent):void 
	{
		Redraw(0x37EC41);
	}
}

Автор: KeeReal

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js