こんにちは。きんくまです。
先日の土曜日にF-siteで話す予定でした。
が、金曜日に父が亡くなりまして、急遽行けなくなってしまいました。
当日、会場にこられた方や、スタッフの皆様にはご迷惑をおかけしました。
すみませんでした。
ただ、せっかく作った発表用のものがありますので、アップします。
今回お題はjsflを使って何かをするというものでした。
それで、仕事の効率化みたいなものは、他の方々がすばらしいものを作ってくるだろうなと思ったので、
私はネタ担当と勝手に思い、「jsfl使ってこんなこともできたよ!」というものを作ろうと思いました。
なので、ブラウザからflashのステージ上や同時接続している他のブラウザにも描き込む
お絵描きチャットを作ってみました。

flashとサーバーの通信は、WindowSWF(拡張パネル)からSocket通信をしています。
ブラウザとサーバーはWebosocketを使って通信します。
サーバーはnode.jsを使って立てます。
それで、各フレームに描いたら、swiffyでパブリッシュをします。
ブラウザから描き込んでいるので、iPhoneからも描き込めて、swiffyでパブリッシュなので、変換されたアニメーションも表示することができました。
発表の資料はこちらです。
PDF版
node_canvas.pdf
ソースはnode.jsとsocket.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();
}
}
}