Такой вот незамысловатый эффект. Под катом исходники, местами комментарии и пояснения.
Обдумывая как работает тот или иной спецэффект в голову приходят много мыслей, но до реализации не всегда доходит, в виду малого опыта именно со спецэффектами. «Пора исправлять ситуацию» — подумал я. Немного поразмыслив в голову пришла простая идея по поводу того, чего бы такого накодить. Задачей себе поставил сделать эффект появления текста, как будто его печатают попиксельной сваркой, и из точки сварки рассыпаются частицы. И, заодно, описать это в виде статьи. Писатель правда из меня никудышный, но надеюсь, эта статья принесет кому-нибудь пользу.
Описание алгоритма
Освнова эффекта это вырезанный, по заданному текстом контуру, кусок шума Перлина (док). Над текстом расположена маска, которая двигаеться вдоль оси 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);
}
}
Автор: KeeReal