node.jsを使ってflashにも描き込むお絵描きチャット

2012/02/6

こんにちは。きんくまです。

先日の土曜日にF-siteで話す予定でした。
が、金曜日に父が亡くなりまして、急遽行けなくなってしまいました。
当日、会場にこられた方や、スタッフの皆様にはご迷惑をおかけしました。
すみませんでした。

ただ、せっかく作った発表用のものがありますので、アップします。

今回お題はjsflを使って何かをするというものでした。
それで、仕事の効率化みたいなものは、他の方々がすばらしいものを作ってくるだろうなと思ったので、
私はネタ担当と勝手に思い、「jsfl使ってこんなこともできたよ!」というものを作ろうと思いました。

なので、ブラウザからflashのステージ上や同時接続している他のブラウザにも描き込む
お絵描きチャットを作ってみました。

node_canvas_cap1 node_canvas_cap2 node_canvas_cap3

flashとサーバーの通信は、WindowSWF(拡張パネル)からSocket通信をしています。
ブラウザとサーバーはWebosocketを使って通信します。
サーバーはnode.jsを使って立てます。

それで、各フレームに描いたら、swiffyでパブリッシュをします。
ブラウザから描き込んでいるので、iPhoneからも描き込めて、swiffyでパブリッシュなので、変換されたアニメーションも表示することができました。

発表の資料はこちらです。

PDF版
node_canvas.pdf

ソースはnode.jssocket.ioを使っているので、かなり短くすみました。

ソース一式です。

server.js
node.jsのサーバーたちです。

var app = require('http').createServer(handler),
    io = require('socket.io').listen(app),
    fs = require('fs'),
    net = require('net'),
    url = require('url'),
    canvasdata = require('./canvasdata'),
    tlframes = [],
    flashSock;

for(var i = 0, len = 6; i < len; i++){
    tlframes[i] = new canvasdata.TLFrame();
}

var responseFile = function(path, res){
    fs.readFile(__dirname + path, function (err, data) {
        if (err) {
            res.writeHead(500);
            return res.end('Error loading index.html');
        }

        res.writeHead(200);
        res.end(data);
    });
};

function handler(req, res) {
    var path = url.parse(req.url);
    if(path.href === '/'){
        responseFile('/index.html', res);
    }else if(path.href === '/canvasdata.js'){
        responseFile('/canvasdata.js', res);
    }else if(path.href === '/ui_image.png'){
        responseFile('/ui_image.png', res);
    }else if(path.href === '/canvas.swf.html'){
        responseFile('/../flash/flash/canvas.swf.html', res);
    }else{
        res.writeHead(404);
        res.end('not found');
    }
};

app.listen(4000);

io.set('log level', 2);

io.sockets.on('connection', function (socket) {
    //clone
    var frame,
        stroke,
        flashJSON;
    socket.emit('clone', { type:'start' });
    for(var i = 0, len = tlframes.length; i < len; i += 1){
        frame = tlframes[i];
        for(var j = 0, len2 = frame.getStrokeLength(); j < len2; j += 1){
            stroke = frame.getStroke(j);
            socket.emit('clone', {
                type:'add',
                frame:i,
                stroke:j,
                width:stroke.width,
                color:stroke.color,
                points:stroke.points        
            });
        }
    }
    socket.emit('clone', { type:'end' });

    //touch

    socket.on('touch', function(data){
        if(data.type === 'start'){
            frame = tlframes[data.frame];
            data.stroke = frame.getStrokeLength();
            stroke = new canvasdata.Stroke(data.width, data.color);
            stroke.points.push(data.x, data.y);
            frame.addStroke(stroke);
            io.sockets.emit('touch', data);
            
        }else if(data.type === 'move'){
            frame = tlframes[data.frame];
            stroke = frame.getStroke(data.stroke);
            stroke.points.push(data.x, data.y);
            io.sockets.emit('touch', data);

        }else if(data.type === 'end'){
            if(flashSock){
                frame = tlframes[data.frame];
                stroke = frame.getStroke(data.stroke);
                flashJSON = JSON.stringify({
                    "frame":data.frame,
                    "stroke":stroke
                });
                flashSock.write(flashJSON); 
                //JSON.parse(text);
            }
        }
    });
    socket.on('publish', function(data){
        flashJSON = JSON.stringify({"type":"publish"});
        flashSock.write(flashJSON);
    });
});


var sockServer = net.createServer(function(socket){
    console.log('client connected');
    flashSock = socket;

    socket.on('end', function(){
        console.log('client disconnected');
    });
    socket.on('data', function(buf){
        var msg = buf.toString();
        if(msg.indexOf('swiffy complete') !== -1){
            io.sockets.emit('location', {href:'canvas.swf.html'});
            //console.log('complete');
        }
    });
});

sockServer.listen(3000, function(){
    console.log('server listen');
});

canvasdata.js
クライアント、サーバー共通で使います。

var TLFrame = function(){
    this.strokes = [];  
};
TLFrame.prototype.addStroke = function(stroke){
    this.strokes.push(stroke);
};
TLFrame.prototype.getStroke = function(index){
    return this.strokes[index];
};
TLFrame.prototype.getStrokeLength = function(){
    return this.strokes.length;
};
var Stroke = function(width, color){
    this.width = width;
    this.color = color;
    this.points = [];
};

if(exports){
	exports.TLFrame = TLFrame;
	exports.Stroke = Stroke;	
}

index.html
クライアント側です。

<html>
<head>
<meta name="apple-mobile-web-app-capable" content="no">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
<script src="/socket.io/socket.io.js"></script>
<script src="canvasdata.js"></script>
<script>

var Painter = function(){
    this.canvas = document.getElementById('mycanvas');
    this.ctx = this.canvas.getContext('2d');
    this.CW = this.canvas.width;
    this.CH = this.canvas.height;
    this.tlframes;
    this.currentFrameIdx = 0;
    this.currentStrokeIdx = 0;
    this.initFrames();

    this.clear();
};
Painter.prototype = {
    initFrames:function(){
        this.tlframes = [];
        for(var i = 0, len = 6; i < len; i++){
            this.tlframes[i] = new TLFrame();
        }
        this.currentFrameIdx = 0;
        this.currentStrokeIdx = 0;
    },
    clear:function(){
        this.ctx.fillStyle = '#ffffff';
        this.ctx.fillRect(0,0,this.CW, this.CH);
    },
    draw:function(frame){
        var stroke,
            points;
        this.clear();
        for(var i = 0, len = frame.getStrokeLength(); i < len; i += 1){
            stroke = frame.getStroke(i);
            points = stroke.points;
            this.ctx.strokeStyle = stroke.color;
            this.ctx.lineWidth = stroke.width;
            this.ctx.beginPath();
            this.ctx.moveTo(points[0], points[1]);
            for(var j = 2, len2 = points.length; j < len2; j += 2){
                this.ctx.lineTo(points[j], points[j+1]);
            }
            this.ctx.stroke();
        }
    },
    onSocketTouchData:function(data){
        var frame,
            stroke;
        if(data.type === 'start'){
            this.currentStrokeIdx = data.stroke;
            frame = this.tlframes[data.frame];
            stroke = new Stroke(data.width, data.color);
            stroke.points.push(data.x, data.y);
            frame.addStroke(stroke);

        }else if(data.type === 'move'){
            frame = this.tlframes[data.frame];
            stroke = frame.getStroke(data.stroke);
            stroke.points.push(data.x, data.y);

            if(data.frame === this.currentFrameIdx){
                this.draw(frame);   
            }
            

        }else if(data.type === 'end'){
            /*
            frame = this.tlframes[data.frame];
            stroke = frame.getStroke(data.stroke);
            stroke.points.push(data.x, data.y);
            this.draw(frame);
            */
        }
    },
    onSocketCloneData:function(data){
        var frame,
            stroke;
        if(data.type === 'start'){
            this.initFrames();

        }else if(data.type === 'add'){
            frame = this.tlframes[data.frame];
            stroke = new Stroke(data.width, data.color);
            stroke.points = data.points;
            frame.addStroke(stroke);

        }else if(data.type === 'end'){
            frame = this.tlframes[this.currentFrameIdx];
            this.draw(frame);
        }
    },
    setCurrentFrameIdx:function(idx){
        this.currentFrameIdx = idx;
        this.draw(this.tlframes[idx]);
    }
};

var Socketio = function(){
    this.socket;
    this.main;
    this.delegate;
};
Socketio.prototype = {
    connect:function(){
        this.socket = io.connect('http://localhost');
        this.registerEvents();
    },
    registerEvents:function(){
        var self = this;
        this.socket.on('touch', function(data){
            if(self.delegate){
                self.delegate.onSocketTouchData(data);
            }
            if(data.type === 'start'){
                self.main.setTimelineBg(data.frame, 'keyframe_element');
            }
        });
        this.socket.on('clone',function(data){
            if(self.delegate){
                self.delegate.onSocketCloneData(data);
            }
            if(data.type === 'add'){
                self.main.setTimelineBg(data.frame, 'keyframe_element');
            }
        });
        this.socket.on('location',function(data){
            location.href = data.href;
        });
    },
    emitTouchStart:function(frameIdx, strokeWidth, strokeColor, point){
        this.socket.emit('touch', {
           type:'start',
           frame:frameIdx,
           x:point.x,
           y:point.y,
           width:strokeWidth,
           color:strokeColor
        });
    },
    emitTouchMove:function(frameIdx, strokeIdx, point){
        this.socket.emit('touch', {
           type:'move',
           frame:frameIdx,
           stroke:strokeIdx,
           x:point.x,
           y:point.y
        });
    },
    emitTouchEnd:function(frameIdx, strokeIdx, point){
        this.socket.emit('touch', {
           type:'end',
           frame:frameIdx,
           stroke:strokeIdx
           //x:point.x,
           //y:point.y
        });
    },
    emitPublish:function(){
        this.socket.emit('publish', {type:'publish'});
    }
};

var TouchUtil = function(){
};
TouchUtil.addListener = function(element, type, listener){
    if('ontouchstart' in window){
        switch(type){
            case 'start':
                element.addEventListener('touchstart', listener);
                break;
            case 'move':
                element.addEventListener('touchmove', listener);
                break;
            case 'end':
                element.addEventListener('touchend', listener);
                break;
            defalt:
                break;
        }
    }else{
        switch(type){
            case 'start':
                element.addEventListener('mousedown', listener);
                break;
            case 'move':
                element.addEventListener('mousemove', listener);
                break;
            case 'end':
                element.addEventListener('mouseup', listener);
                break;
            defalt:
                break;
        }
    }
};
TouchUtil.removeListener = function(element, type, listener){
    if('ontouchstart' in window){
        switch(type){
            case 'start':
                element.removeEventListener('touchstart', listener);
                break;
            case 'move':
                element.removeEventListener('touchmove', listener);
                break;
            case 'end':
                element.removeEventListener('touchend', listener);
                break;
            defalt:
                break;
        }
    }else{
        switch(type){
            case 'start':
                element.removeEventListener('mousedown', listener);
                break;
            case 'move':
                element.removeEventListener('mousemove', listener);
                break;
            case 'end':
                element.removeEventListener('mouseup', listener);
                break;
            defalt:
                break;
        }
    }
};
TouchUtil.getPositionByEvent = function(e){
    if(e.touches && e.touches[0]){
        return {x:e.touches[0].clientX, y:e.touches[0].clientY};
    }else{
        return {x:e.clientX, y:e.clientY};
    }
};

var Main = function(){
    var self = this,
    anoBodyTouchMove = function(e){
        e.preventDefault();
        self.onBodyTouchMove(e);
    },
    anoBodyTouchEnd = function(e){
        e.preventDefault();
        self.onBodyTouchEnd(e);
        TouchUtil.removeListener(self.body, 'move', anoBodyTouchMove);
        TouchUtil.removeListener(self.body, 'end', anoBodyTouchEnd);
    },
    anoMenuTouchMove = function(e){
        self.onMenuTouch(e);
    },
    anoMenuTouchEnd = function(e){
        TouchUtil.removeListener(self.body, 'move', anoMenuTouchMove);
        TouchUtil.removeListener(self.body, 'end', anoMenuTouchEnd);
    };

    this.painter = new Painter();
    this.socket = new Socketio();
    this.socket.main = this;
    this.socket.delegate = this.painter;
    this.socket.connect();

    this.body = document.getElementsByTagName('body')[0];

    TouchUtil.addListener(this.painter.canvas, 'start', function(e){
        e.preventDefault();
        self.onCanvasTouchStart(e);
        TouchUtil.addListener(self.body, 'move', anoBodyTouchMove);
        TouchUtil.addListener(self.body, 'end', anoBodyTouchEnd);
    });

    var timeline = document.getElementById('menu');
    TouchUtil.addListener(timeline, 'start', function(e){
        e.preventDefault();
        TouchUtil.addListener(self.body, 'move', anoMenuTouchMove);
        TouchUtil.addListener(self.body, 'end', anoMenuTouchEnd);
        self.onMenuTouch(e);
    });

    var publishBtn = document.getElementById('publish_link');
    publishBtn.addEventListener('click', function(e){
        e.preventDefault();
        self.socket.emitPublish();
    });
};
Main.prototype = {
    onBodyTouchMove:function(e){
        var point = TouchUtil.getPositionByEvent(e);
        point.y -= 55;
        this.socket.emitTouchMove(this.painter.currentFrameIdx, this.painter.currentStrokeIdx, point);
    },
    onBodyTouchEnd:function(e){
        var point = TouchUtil.getPositionByEvent(e);
        point.y -= 55;
        this.socket.emitTouchEnd(this.painter.currentFrameIdx, this.painter.currentStrokeIdx, point);
    },
    onCanvasTouchStart:function(e){
        var point = TouchUtil.getPositionByEvent(e);
        point.y -= 55;
        this.socket.emitTouchStart(this.painter.currentFrameIdx, 1, '#000000', point);
    },
    setTimelineBg:function(index, type){
        var timeline = document.getElementById('tl_frame'+index);
        if(type === 'empty'){
            
        }else if(type === 'keyframe'){
            timeline.style['background-position'] = '0px -25px';

        }else if(type === 'keyframe_element'){
            timeline.style['background-position'] = '0px -55px';    
        }
    },
    setTimelineCurrentPosition:function(frameInex){
        var tlcurrent = document.getElementById('timeline_current');
        tlcurrent.style['left'] = (frameInex * 50 + 1) + 'px';
    },
    onMenuTouch:function(e){
        var point = TouchUtil.getPositionByEvent(e);
        var tlframeIdx = Math.floor(point.x / 50);
        if(tlframeIdx > 5){
            return;
        }
        if(tlframeIdx !== this.painter.currentFrameIdx){
            this.painter.setCurrentFrameIdx(tlframeIdx);
            this.setTimelineCurrentPosition(tlframeIdx);
        }
    }
};


window.onload = function(){
  var main = new Main();
};


</script>
<style>
body{
	margin: 0px;
	padding: 0px;
	background-color:#000;
}
ul,li{
    margin:0px;
    padding:0px;
}
#wrapper{
    width:320px;
    height:460px;
    text-align:left;
    background-color:#D4D4D4;
}
#menu{
    height:55px;
    position: relative;
    overflow: hidden;
}
#timeline{
    width:350px;
    padding-top:25px;
    margin-left:0px;
}
#timeline li{
    width:50px;
    height:30px;
    display:block;
    float:left;
    background-image:url(ui_image.png);
    background-repeat:no-repeat;
    background-position:0px -25px;
}
#timeline li.empty{
    background:url(ui_image.png) no-repeat -200px -25px;
}
#timeline_ruler{
    width:320px;
    height:25px;
    background:url(ui_image.png) no-repeat 0px 0px;
    position: absolute;
    top:0px;
    left:0px;
}
#timeline_current{
    width:48px;
    height:53px;
    background:url(ui_image.png) no-repeat -260px -100px;
    position: absolute;
    top:2px;
    left:1px;
}
/*
canvas{
    border:1px solid #999;
}
*/
footer{
    width:320px;
    height:45px;
    background:url(ui_image.png) no-repeat 0px -160px;
}
#publish{
    width:64px;
    height:20px;
    padding-top:8px;
    margin-left:6px;
}
#publish a{
    width:64px;
    height:30px;
    display:block;
}
</style>
</head>
<body>
<div id="wrapper">
<div id="menu">
<ul id="timeline">
<li id="tl_frame0"></li>
<li id="tl_frame1"></li>
<li id="tl_frame2"></li>
<li id="tl_frame3"></li>
<li id="tl_frame4"></li>
<li id="tl_frame5"></li>
<li id="tl_frame6" class="empty"></li>
</ul>
<div id="timeline_current"></div>
<div id="timeline_ruler"></div>
</div>
<canvas width="320" height="360" id="mycanvas"></canvas>
<footer>
<div id="publish"><a href="#" id="publish_link"></a></div>
</footer>
</div>
</body>
</html>

NodePainter.as
flash側の拡張パネルです。

package
{
	import adobe.utils.MMExecute;
	
	import com.adobe.serialization.json.JSON;
	import com.bit101.components.PushButton;
	
	import flash.display.Graphics;
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.ProgressEvent;
	import flash.net.Socket;
	
	[SWF(width="250",height="250",frameRate="24",backgroundColor="#aaaaaa")]
	public class NodePainter extends Sprite
	{
		public static const CONNECT_COLOR_RED:int = 0;
		public static const CONNECT_COLOR_GREEN:int = 1;
		
		private var _socket:Socket;
		private var _connectButton:PushButton;
		private var _isconnected:Boolean = false;
		private var _connectCircle:Sprite;
		public const HOST:String = "localhost";
		public const PORT:int = 3000;
		
		public function NodePainter()
		{
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			createConnectCircle();
			_connectButton = new PushButton(this, 10, 10, "CONNECT", onConnectClick);
		}
		
		private function onConnectClick(e:MouseEvent):void
		{
			if(_isconnected === false){
				_socket = new Socket();
				_socket.addEventListener(Event.CONNECT, onSocketConnect);
				_socket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
				_socket.connect(HOST, PORT);
				_connectButton.label = "DISCONNECT";
			}else{
				_isconnected = false;
				drawConnectCircle(CONNECT_COLOR_RED);
				_socket.removeEventListener(Event.CONNECT, onSocketConnect);
				_socket.removeEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
				_socket.close();
				_socket = null;
				_connectButton.label = "CONNECT";
			}
		}
		
		protected function onSocketData(event:ProgressEvent):void
		{
			var chars:Array = [],
				char:String,
				msg:String;
			while(_socket.bytesAvailable){
				char = _socket.readUTFBytes(1);
				chars.push(char);
			}
			msg = chars.join('');
			var obj:Object = JSON.decode(msg);
			
			if(obj.type === 'publish'){
				this.publishSwiffy();
				_socket.writeUTF('swiffy complete');
				_socket.flush();
				
			}else{
				this.changeFrame(obj.frame);
				this.changeStroke(obj.stroke.width, obj.stroke.color);
				this.drawStagePath(obj.stroke);
			}
			//trace(obj.width, obj.color, obj.points.length);
		}
		
		private function publishSwiffy():void
		{
			var commands:Array = [];
			commands.push('var path = fl.configURI + "Commands/" + "Export as HTML5 (Swiffy).jsfl";');
			commands.push('fl.runScript(path);');
			MMExecute(commands.join(''));
		}
		
		private function changeStroke(strokeWidth:int, strokeColor:String):void
		{
			var commands:Array = [];
			commands.push('var myStroke = fl.getDocumentDOM().getCustomStroke("toolbar");');
			commands.push('myStroke.color = "'+strokeColor+'";');
			commands.push('fl.getDocumentDOM().setCustomStroke(myStroke);');
			MMExecute(commands.join(''));
		}
		
		private function changeFrame(frameIndex:int):void
		{
			var jsfl:String = "fl.getDocumentDOM().getTimeline().currentFrame = "+frameIndex;
			MMExecute(jsfl);
		}
		
		private function drawStagePath(obj:Object):void
		{
			var commands:Array = [],
				i:int = 0,
				len:int = obj.points.length;
			commands.push('var fill = fl.getDocumentDOM().getCustomFill();');
			commands.push('fill.style= "noFill";');
			commands.push('fl.getDocumentDOM().setCustomFill( fill );');
			commands.push('var myPath = fl.drawingLayer.newPath();');
			
			for(i = 0; i < len; i += 2){
				commands.push('myPath.addPoint('+obj.points[i]+', '+obj.points[i+1]+');');
			}
			commands.push('myPath.makeShape();');
			MMExecute(commands.join(''));
		}
		
		protected function onSocketConnect(event:Event):void
		{
			_isconnected = true;
			drawConnectCircle(CONNECT_COLOR_GREEN);
		}
		
		private function createConnectCircle():void
		{
			_connectCircle = new Sprite();
			_connectCircle.x = 120;
			_connectCircle.y = 15;
			addChild(_connectCircle);
			drawConnectCircle(CONNECT_COLOR_RED);
		}
		
		private function drawConnectCircle(colorType:int):void
		{
			var color:int, 
				g:Graphics = _connectCircle.graphics;
			
			if(colorType === CONNECT_COLOR_RED){
				color = 0xFF0022;
				
			}else if(colorType === CONNECT_COLOR_GREEN){
				color = 0x74D062;
			}
			g.clear();
			g.lineStyle(1,0x888888,0.5);
			g.beginFill(color,1);
			g.drawCircle(5, 5, 5);
			g.endFill();
		}
	}
}
LINEで送る
Pocket

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

ページトップへ戻る