メタボールにチャンレジしてみたよ(2)

前回に引き続き、メタボールを使った表現にチャレンジしてみました。

今回は、壁面で反射するパーティクルをメタボールレンダリングしてみました。多少見栄えを良くするためにBlurFilterとGlowFilterをかけています。

ソースコードこちらから。

メタボールにチャレンジしてみたよ

Tweenerを使って動きを作っていくとシャキーンという感じの動きを簡単に作れるのですが、
慣れてくると少し有機的な動きを取り入れてみたいという衝動に駆られます。

ということで、今回は「メタボール」に挑戦してみました。

メタボールって言葉自体はあまり馴染みがないかもしれませんが、ボールが滑らかにくっついたり離れたりするデモを見た事ある人は結構いるのではないかと思います。
あのレンダリング手法をメタボールと言います。

今回チャレンジしたのは2Dメタボールです。3Dは実装が大変そうなのでやめました。
参考にしたのは、mysketch.blog 炎の作成(1):メタボールの作成という記事です。
というか実装の方針は、ほとんどそのままです。

作成したサンプルではマウスを追跡するボールを時間差で動かしています。ボール自体は中心から外側に向かってアルファ値を滑らかに変化させており、このボールをメタボールとして描画しています。
描画の方法はいたって単純で、アルファが閾値以下の場合は白、閾値以上の場合は黒に色の置き換えをするだけです。この置き換えは、BitmapDataのpaletteMap()で実現できます。

今回は、メタボールの基本的な実装方法を確認しただけで終わりましたが次回以降では、メタボールを使って少し凝った表現を何か作ってみたいと思います。

ソースコード

BitmapDataを使ったモザイクエフェクト

最近仕事で忙殺されていて、趣味のプログラムを全然書いてなかったので久しぶりに書いてみました。

作ってみたのは、スライドショーに使えそうなモザイクエフェクトです。

モザイクは、画像の拡大縮小によって実現しています。

まず、画像を縮小コピーします。縮小コピーするとその縮小率に応じて元画像より画素が減ります。これを元の大きさに拡大すると減った分の画素が縮小した時に残った画素で埋められるためモザイク状の画像になります。

ソースコード

以下、コードです。Main.asがスライドショー本体で、MozaicEffect.asがモザイクエフェクトのコードです。

//Main.as
package {
	import flash.display.*;
	import flash.geom.*;
	import flash.events.*;
	import flash.utils.*;
	import flash.ui.*;

	[SWF(width="600", height="600", backgroundColor="#000000", frameRate="30")]
	public class Main extends Sprite {
		[Embed(source="sample1.png")]
		public var Sample1:Class;

		[Embed(source="sample2.png")]
		public var Sample2:Class;

		private var currentBitmap:Bitmap;
		private var imageList:Array;
		private var currentIndex:int;
		private var showEffect:MozaicEffect;
		private var hideEffect:MozaicEffect;
		public function Main() {
			stage.addEventListener(KeyboardEvent.KEY_DOWN, handlerKeydown);
			stage.addEventListener(MouseEvent.CLICK,  handlerClick);

			initImageList();
			currentBitmap = new Bitmap();
			addChild(currentBitmap);

			// init effects
			hideEffect = new MozaicEffect();
			hideEffect.useTimer = false;
			showEffect = new MozaicEffect();
			showEffect.reverse = true;
			showEffect.useTimer = false;

			// show the first image
			showNextImage();
		}

		private function initImageList():void {
			imageList = [new Sample1(), new Sample2()];
			currentIndex = -1;
		}

		private function showNextImage():void {
			var prev:BitmapData = currentImageData;
			currentIndex = (currentIndex + 1) % imageList.length;
			var next:BitmapData = currentImageData;

			var self:Main = this;
			showEffect.source = next;
			showEffect.addEventListener(Event.COMPLETE, function(e:Event):void {
				self.currentBitmap.bitmapData = next;
				if ( self.contains(hideEffect) ) {
					self.removeChild(hideEffect);
				}
				if ( self.contains(showEffect) ) {
					self.removeChild(showEffect);
				}
			});

			if ( prev != null ) {
				hideEffect.source = prev;
				addChild(hideEffect);
				hideEffect.addEventListener(Event.COMPLETE, function(e:Event):void {
					self.addChild(showEffect);
					showEffect.play();
				});
				hideEffect.play();
			}
			else {
				addChild(showEffect);
				showEffect.play();
			}
		}

		private function get currentImageData():BitmapData {
			if ( 0 <= currentIndex && currentIndex < imageList.length ) {
				return imageList[currentIndex].bitmapData;
			}
			return null;
		}

		private function handlerKeydown(e:KeyboardEvent):void {
			if ( e.keyCode == Keyboard.SPACE ) {
				showNextImage();
			}
		}

		private function handlerClick(e:MouseEvent):void {
			showNextImage();
		}
	}
}
// MozaicEffect.as
package {
	import flash.events.*;
	import flash.display.*;
	import flash.geom.*;
	import flash.utils.*;

	public class MozaicEffect extends Sprite {
		public var endCount:int;
		public var reverse:Boolean;
		public var interval:uint;
		public var useTimer:Boolean;

		private var counter:int;
		private var _bgColor:uint;
		private var _source:BitmapData;
		private var buffer:Bitmap;
		private var timer:Timer;
		public function MozaicEffect() {
			endCount = 12;
			counter = 0;
			reverse = false;
			bgColor = 0x000000;

			useTimer = true;
			interval = 100;
		}

		public function play():void {
			if ( source != null ) {
				counter = 0;
				var bmpData:BitmapData = new BitmapData(source.width, source.height, false, 0x000000);
				if ( !reverse ) {
					bmpData.copyPixels(source, new Rectangle(0, 0, source.width, source.height), new Point(0, 0));
				}
				buffer = new Bitmap(bmpData);
				addChild(buffer);
				if ( useTimer ) {
					timer = new Timer(interval, 0);
					timer.addEventListener(TimerEvent.TIMER, updateDisplay);
					timer.start();
				}
				else {
					addEventListener(Event.ENTER_FRAME, updateDisplay);
				}
			}
		}

		public function stop():void {
			if ( hasEventListener(Event.ENTER_FRAME) ) {
				removeEventListener(Event.ENTER_FRAME, updateDisplay);
			}
			if ( useTimer ) {
				timer.removeEventListener(TimerEvent.TIMER, updateDisplay);
				timer.stop();
			}
		}

		public function get bgColor():uint {
			return _bgColor;
		}

		public function set bgColor(color:uint):void {
			color = _bgColor;
		}

		public function get source():BitmapData {
			return _source;
		}

		public function set source(data:BitmapData):void {
			_source = data;
		}

		private function mozaic(src:BitmapData, scale:uint):void {
			var mtrx:Matrix = new Matrix();
			mtrx.scale(1.0 / (scale * scale), 1.0 / (scale * scale));
			var half:BitmapData = new BitmapData(src.width / scale, src.height / scale, false, 0x000000);
			half.draw(src, mtrx);
			var double:uint = scale * scale;
			mtrx.scale(double * double, double * double);
			src.draw(half, mtrx);
			half.dispose();
		}

		private function updateDisplay(e:Event):void {
			var bmpData:BitmapData = buffer.bitmapData;
			if ( counter > endCount ) {
				stop();
				if ( !reverse ) {
					bmpData.fillRect(new Rectangle(0, 0, source.width, source.height), bgColor);
				}
				var event:Event = new Event(Event.COMPLETE);
				dispatchEvent(event);
			}
			else {
				bmpData.copyPixels(source, new Rectangle(0, 0, source.width, source.height), new Point(0, 0));
				if ( reverse ) {
					mozaic(bmpData, endCount - counter + 1);
				}
				else {
					mozaic(bmpData, counter + 1);
				}
			}
			counter++;
		}
	}
}

AdvancedDataGridのヘッダに表示される縦線を消す方法

AdvancedDataGridのデフォルト設定だと、ヘッダがイケテなくて困った人は多いと思います。

とくに、並び替え機能が不要なときにsorter領域の縦線が消したくなった人は多いと思います。

普通に考えると、sortableColumnsというプロパティをfalseに設定する、もしくはAdvancedDataGridColumnのsortableプロパティをfalseにすれば並び替えができなくなる代わりに縦線は消えるだろうと思います。しかし、この縦線は並び替えを無効にしても残ります。

では、どうすれば良いのか?

ここでのやり取りを追ってみると、AdvancedDataGridのsortExpertModeプロパティを「true」にすれば良いらしいことがわかります。確かに、これを設定すると、

という感じで縦線が消えます。ちなみに、このプロパティをtrueにするとctrl(macではcommand)キーで複数列の並び替えが出来るようになります。つまりAdobe的には、(ctrl押しながらクリック出来る人) = (エキスパート)ということなんでしょうね。

以下余談です。

つい最近、Flex Builder3を入手したのでFlexを触り始めました。起動がもっさりするのが気になりますが概ね快適です。

Flexを触り始めて思ったのが、色々とできそうなんだけど具体的にどうやればいいのかを調べるのが非常に面倒だと感じました。
特に日本語の情報源が乏しく、嵌まった場合にWEBを検索してもあまり有益な情報を得られないのが初心者にはつらい気がします。(FxUGが頼みの綱って感じですね)

というわけで、これからはFlexのTips的な記事も少しずつ書いていきたいなと思います。

サンプルコード

<?xml version="1.0"?>
<!-- charts/AxisRendererStrokes.mxml -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
	<mx:Script>
		<![CDATA[
			[Bindable]
			private var dataList:Array = [
				{col1: 100, col2:200, col3:500},
				{col1: 100, col2:200, col3:400},
				{col1: 200, col2:200, col3:300},
				{col1: 200, col2:200, col3:200},
				{col1: 300, col2:200, col3:100}
			];
		]]>
	</mx:Script>
	<mx:AdvancedDataGrid designViewDataType="flat" sortableColumns="false" sortExpertMode="true" dataProvider="{dataList}" id="adg1">
		<mx:columns>
			<mx:AdvancedDataGridColumn headerText="列 1" dataField="col1"/>
			<mx:AdvancedDataGridColumn headerText="列 2" dataField="col2"/>
			<mx:AdvancedDataGridColumn headerText="列 3" dataField="col3"/>
		</mx:columns>
	</mx:AdvancedDataGrid>
</mx:Application>

AIRでパスワードを保存する時はEncryptedLocalStoreを使うべき

WebサービスにアクセスするAIRアプリを作成していると、ユーザーの利便性を考慮してWebサービスのユーザー名とパスワードを保存しておきたいと思う時があります。
そうした時のために、AIRにはEncryptedLocalStoreクラスが用意されています。このクラスはOS固有のデータ保護APIを使用してデータを保護する仕組みを提供しています。
このクラスを通して保存されたデータは、他のユーザーやアプリケーションが解釈できないように暗号化して保存されます。具体的には128 ビットの AES-CBC暗号 が使われています。

AES暗号のような共通鍵暗号を使う場合、アプリケーションに固定の鍵を組み込んでおくような実装をしがちですが、そういった実装だとアプリケーションを解析すれば鍵を取得出来るため、アプリケーションが保存しているWebサービスのユーザー名とパスワードを回収するような悪意のあるプログラムを作成することが可能になります。
せっかく暗号化しても暗号鍵が安全に保管されていなければ暗号化の意味がありません。ドアに鍵をかけたのに、鍵をさしっぱなしにしているようなものです。

この問題は、一般にキーマネージメントの問題として知られておりWindowsではDPAPIMacではKeyChainを使う事で回避できます。EncryptedLocalStoreはこれらのAPIを使用しているので、AIRアプリでは基本的にEncryptedLocalStoreを使うべきです。

使い方も比較的簡単です。

import flash.data.EncryptedLocalStore;

//データの保存
var passwordString:String = 'xxxxxx';
var bytes:ByteArray = new ByteArray();
bytes.writeUTFBytes(passwordString);
EncryptedLocalStore.setItem('password', bytes);

//データの取得
var storedValue:ByteArray = EncryptedLocalStore.getItem('password');
trace(storedValue.readUTFBytes(storedValue.length));

参考

[as3][flash] カメラのキャプチャ取り込みをする場合に注意すべきこと

カメラから取り込んだ画像を使うFlashを随所で見かけるようになりましたが、複数のカメラがインストールされている環境が考慮されていない場合が多く、サンプルを動かせないということがよくあります。

ユーザーに複数のカメラから使いたいものを選択させるための設定パネルを表示するためには、

Security.showSettings(SecurityPanel.CAMERA);
var cam:Camera = Camera.getCamera();
if ( cam != null ) {
	vid.attachCamera(cam);

}	

といった感じで、Camera.getCamera()を呼び出す前に一行追加するだけです。

ちなみに、この方法はマニュアルのCameraクラスのgetCamera()の使用方法にて解説されていますが記載されているコードに誤植があります。

System.showSettings(SecurityPanel.CAMERA);

は、誤植です。正しくは、

Security.showSettings(SecurityPanel.CAMERA);

です。

ちなみに私はMacbookを使っているのですが、標準で複数のカメラドライバがインストールされているのでデモFlashが動かないということが結構ありました。