
Такой вот незамысловатый эффект. Под катом исходники, местами комментарии и пояснения.
Обдумывая, как работает тот или иной спецэффект, в голову приходит много мыслей, но не всегда доходит до реализации, в виду малого опыта именно со спецэффектами. «Пора исправлять ситуацию» — подумал я. Немного поразмыслив появилась простая идея по поводу того, чего бы такого накодить. Задачей себе поставил сделать эффект появления текста, как будто его печатают попиксельной сваркой, и из точки сварки рассыпаются частицы. И, заодно, описать это в виде статьи. Писатель правда из меня никудышный, но надеюсь, что эта статья принесет кому-нибудь пользу.
Описание алгоритма
Освнова эффекта это вырезанный, по заданному текстом контуру, кусок шума Перлина (док). Над текстом расположена маска, которая двигаеться вдоль оси OХ вправо приоткрывая текст. В главном цикле определяем где сейчас находится правый край маски, и для каждого Y, по высоте битмапы с текстом, достаем непрозрачный пиксель и начинаем его анимировать. Плюс здесь же показываем где сечас находится точка «сварки».
Реализация
Текст вырезан с помощью BlendMode.ERASE, который стирает пиксели фонового объекта на основе значения альфа канала подмешиваемого объекта. То есть при альфе равной 0xFF значение альфа канала фона будет равно 0x00.

Метод подготовки битмапы с текстом.
код
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); } }
