我不是做美術設計的料,但是又希望成品可以美觀一點,這時還是借助一些方法來讓介面好看一點。
自從twitter發佈了bootstrap,看起來真的不少人做過嘗試。利用他,可以做出簡潔美觀的介面,而不需要花太多的功夫,不過嘗試以後,也發現一些缺點。
首先不管三七二十一,先把視訊以及白板放上去好了。希望的layout是有一個header,上面有login的輸入框,然後下面使用tab來把各個功能區隔開來。
嘗試了一下,介面的html:
<html lang='en'>
<meta charset='utf-8'>
<link rel="stylesheet" type="text/css" href="/css/normalize.css">
<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="js/colorPicker.css" type="text/css" />
<style>
video {
border: solid 1px #6699cc;
border-radius: 10px;
padding: 15px 15px 15px 15px;
}
.header {
background: #223344;
color: #FFFFFF;
padding: 5px 5px 5px 5px;
text-align: right;
height: 32px;
vertical-align: middle;
}
#loginpanel {
text-align: right;
padding: 0 0 0 0;
margin: 0 0 0 0;
}
#accountpanel {
font-size: 16px;
}
.lineup {
display: inline-block;
}
#canvas {
border: solid 1px gray;
}
#preview {
border: solid 1px gray;
position:absolute;
z-index:100;
left: 479px;
top: 120px;
}
.panel {
margin: 5px 5px 5px 5px;
padding: 5px 5px 5px 5px;
border: solid 2px #336699;
width: 778px;
}
.tools {
margin-right: 5px;
padding: 2px 2px 2px 2px;
border: solid 2px #6699CC;
width: 240px;
height: 474px;
float:left;
line-height: 14px;
}
label {
font-size: 10px;
}
span {
font-size: 9px;
color: red;
font-weight: bold;
}
</style>
<script src='http://code.jquery.com/jquery-1.8.2.js'></script>
<script src='js/jquery.colorPicker.js'></script>
<script src="/js/bootstrap.min.js"></script>
<script src='amd.js'></script>
<script src='EventEmitter.js'></script>
<script src='PaintTools.js'></script>
<script src='PaintTools.tools.js'></script>
<script src='/ws.io/ws.io.js'></script>
<script src="PaintTools.wstool.js"></script>
<script>
var socket = io.connect('ws://127.0.0.1:8443');
$(document).ready(function() {
$('#myTab a').click(function (e) {
e.preventDefault();
$(this).tab('show');
});
require(['test848a'], function(conference) {
conference(socket);
});
// global variables
var preview = $('#preview');
var canvas = $('#canvas');
preview.offset({left:canvas.offset().left, top:canvas.offset().top});
var context = canvas[0].getContext('2d');
var context1 = preview[0].getContext('2d');
var tool = $C(context, context1);
// drawing tool setup
tool.on('drawType', function(v) {$('#drawtypestatus').html(v);});
tool.lineCap = 'round';
tool.lineJoin = 'round';
tool.lineWidth = 1;
// color picker setup
$('#color1').colorPicker({
pickerDefault: 'ffffff',
onColorChange: function(id, val) {
tool.fillStyle = val;
}
});
$('#color2').colorPicker({
pickerDefault: 'ffffff',
onColorChange: function(id, val) {
tool.strokeStyle = val;
}
});
// change fillStyle and font of PaintTools will also change those of text input
tool.on('fillStyle', function(val) {
$('#keyin').css('color', val);
});
tool.on('font', function(val) {
$('#keyin').css('font', val);
});
function wsCallBack(obj) {
switch(obj.type) {
case 'drawText':
obj.type = 'wsDrawText';
break;
case 'freestyle':
obj.type = 'wsDrawLine';
break;
case 'strokeRect':
obj.type = 'wsStrokeRect';
break;
case 'fillRect':
obj.type = 'wsFillRect';
break;
case 'strokeCircle':
obj.type = 'wsStrokeCircle';
break;
case 'fillCircle':
obj.type = 'wsFillCircle';
break;
case 'strokeEclipse':
obj.type = 'wsStrokeEclipse';
break;
case 'fillEclipse':
obj.type = 'wsFillEclipse';
break;
case 'eraser':
obj.type = 'wsEraser';
break;
}
socket.emit('wsdraw', obj);
}
tool.registCallback('drawText', 'keypress', wsCallBack);
socket.on('wsdraw', function(m) {
var _old = tool.drawType;
tool.drawType = m.type;
tool.handle('wsdraw', m);
tool.drawType = _old;
});
//drawing dom event
preview.bind('mousedown', function(e) {
e.preventDefault();
tool.handle('mousedown', e, wsCallBack);
});
preview.bind('mousemove', function(e) {
e.preventDefault();
tool.handle('mousemove', e, wsCallBack);
});
preview.bind('mouseup', function(e) {
tool.handle('mouseup', e, wsCallBack);
});
preview.bind('mouseleave', function(e) {
tool.handle('mouseleave', e, wsCallBack);
});
// tool panel event
$('#linewidth').bind('change', function() {
tool.lineWidth = $(this).val();
});
$('#freestyle').bind('click', function() {
tool.drawType = 'freestyle';
});
$('#strokerect').bind('click', function() {
tool.drawType = 'strokeRect';
});
$('#fillrect').bind('click', function() {
tool.drawType = 'fillRect';
});
$('#strokecircle').bind('click', function() {
tool.drawType = 'strokeCircle';
});
$('#fillcircle').bind('click', function() {
tool.drawType = 'fillCircle';
});
$('#strokeeclipse').bind('click', function() {
tool.drawType = 'strokeEclipse';
});
$('#filleclipse').bind('click', function() {
tool.drawType = 'fillEclipse';
});
$('#drawtext').bind('click', function() {
tool.drawType = 'drawText';
});
$('#fontface').bind('change', function() {
tool.fontFace = $(this).val();
});
$('#fontsize').bind('change', function() {
tool.fontSize = $(this).val();
});
$('#fontweight').bind('change', function() {
tool.fontWeight = $(this).val();
});
$('#fontstyle').bind('change', function() {
tool.fontStyle = $(this).val();
});
$('#eraser').bind('click', function() {
tool.drawType = 'eraser';
});
});
</script>
<div class='header'>
<div id='accountpanel' style='display:none'></div>
<div id='loginpanel'><form id='form1' class='form-inline' method='get'>
<input type='text' id='account' placeholder='Account' class='input-small'>
<input type='password' id='password' placeholder='Password' class='input-small'>
<button type='submit' class='btn'>Signin</button>
</form></div>
</div>
<br>
<div class="container" style="text-align: center">
<ul class='nav nav-tabs' id='myTab'>
<li class='active'><a href='#conference'>Video Conference</a></li>
<li><a href='#whiteboard'>White Board</a></li>
</ul>
<div class='tab-content'>
<div class='tab-pane active' id='conference'>
<div class='lineup'>
<video id="local" width="320" autoplay></video><br>
<form class='form-inline'>
<button class="btn-small" id="btnStart" disabled>start</button>
<select id='users'>
</select>
<button class="btn-small" id="btnCall" disabled>call</button>
</form>
</div>
<div id='remote'></div>
</div>
<div class='tab-pane' id='whiteboard'>
<div>
<div class='tools' id='tools'>
<div>
<label for='drawtypestatus'>Draw Type: </label><span id='drawtypestatus'></span><br>
<hr size='1' width='100%'>
</div>
<div>
<label for='color1'>Fill: </label><div style='display:inline-block'><input type='text' id='color1' name='color1' value='#000000'></div><br>
<label for='color2'>Stroke: </label><div style='display:inline-block'><input type='text' id='color2' name='color2' value='#000000'></div><br>
<label for='linewidth'>Line Width: </label><select id='linewidth'>
<option value="1">1</option>
<option value="3">3</option>
<option value="5">5</option>
<option value="7">7</option>
</select>
<hr size='1' width='100%'>
</div>
<div>
<button id="freestyle" class='btn btn-mini'>freesytle</button>
<button id='strokerect' class='btn btn-mini'>Stroke Rectangle</button>
<button id='fillrect' class='btn btn-mini'>Fill Rectangle</button>
<button id='strokecircle' class='btn btn-mini'>Stroke Circle</button>
<button id='fillcircle' class='btn btn-mini'>Fill Circle</button>
<button id='strokeeclipse' class='btn btn-mini'>Stroke Eclipse</button>
<button id='filleclipse' class='btn btn-mini'>Fill Eclipse</button>
<button id='drawtext' class='btn btn-mini'>Draw Text</button>
<button id='eraser' class='btn btn-mini'>Eraser</button>
<hr size='1' width='100%'>
</div>
<div>
<label for='fontface'>Font: </label><select id='fontface'>
<option value='sans-serif'>sans-serif</option>
<option value='serif'>serif</option>
<option value='cursive'>cursive</option>
<option value='fantasy'>fantasy</option>
<option value='monospace'>monospace</option>
</select>
<label>Size: </label><select id='fontsize'>
<option value='10px'>10px</option>
<option value='12px'>12px</option>
<option value='14px'>14px</option>
<option value='16px'>16px</option>
<option value='18px'>18px</option>
<option value='20px'>20px</option>
<option value='24px'>24px</option>
</select>
<label>Weight: </label><select id='fontweight'>
<option value='400'>default</option>
<option value='700'>bold</option>
</select>
<label>Style: </label><select id='fontstyle'>
<option value='normal'>normal</option>
<option value='italic'>italic</option>
</select>
</div>
</div>
<canvas id="canvas" width='640' height='480'></canvas>
<canvas id="preview" width='640' height='480'></canvas>
</div>
</div>
</div>
</div>
其中也常是利用AMD的方式,逐步把操作獨立成模組。例如先把視訊需要用的的操作,放入獨立的檔案中:
define('test848a', [], function() {
return function(socket) {
var localStream,remoteStream;
var connections = {};
$('#btnStart').click(function() {
navigator.webkitGetUserMedia({video:true,audio:true}, function(stream) {
localStream = stream;
$('#local').attr('src', webkitURL.createObjectURL(stream));
}, function(info) {
console.log('getUserMedia Error:' + info);
});
this.disabled = true;
$('#btnCall').attr('disabled', false);
});
$('#btnCall').click(function(e) {
e.preventDefault;
e.stopPropagation();
console.log('btnCall pressed');
var to = $('#users').val();
connections[to] = {connection:null, localStream:null, remoteStream:null};
this.disable = true;
connections[to].connection = new webkitPeerConnection00('STUN stun.1.google.com:19302', function(candidate, more) {
if(candidate) {
console.log(candidate);
socket.emit('ice', {from:id, to:to, sdp:candidate.toSdp(), label:candidate.label});
}
});
connections[to].connection.onaddstream = function(e) {
var video = document.createElement('video');
var contain = document.createElement('div');
var btn = document.createElement('button');
$(btn).html('Hang up');
$(contain).attr('class', 'lineup')
$(video).attr('autoplay', true);
$(video).attr('id', to);
$(video).attr('width', 320);
connections[to].remoteStream = e.stream;
$(video).attr('src', webkitURL.createObjectURL(e.stream));
$(contain).append(video);
$(contain).append(btn);
$('#remote').append(contain);
//$('#btnHangup').attr('disabled', false);
};
connections[to].connection.addStream(localStream);
var offer = connections[to].connection.createOffer(null);
connections[to].connection.setLocalDescription(connections[to].connection.SDP_OFFER, offer);
socket.emit('offer', {to:to, from:id, sdp:offer.toSdp()});
console.log('')
return false;
});
$('#form1').bind('submit', function(e) {
console.log('submit triggered');
e.preventDefault();
e.stopPropagation();
socket.emit('login', {account:$('#account').val(),password:$('#password').val()});
return false;
});
socket.on('ice', function(data) {
console.log('ice triggered.');
connections[data.from].connection.processIceMessage(new IceCandidate(data.label,data.sdp));
console.log('socket on ice: ', getIceStateDesc(connections[data.from].connection.iceState));
});
socket.on('offer', function(data) {
console.log('offer triggered');
if(!connections[data.from]) {
connections[data.from] = {connection:null,remoteStream:null};
}
connections[data.from].connection = new webkitPeerConnection00(null, function(candidate, more) {
if(candidate) {
console.log(candidate);
socket.emit('ice', {to: data.from, from: data.to, sdp:candidate.toSdp(), label:candidate.label});
}
});
connections[data.from].connection.onaddstream = function(e) {
var video = document.createElement('video');
var contain = document.createElement('div');
var btn = document.createElement('button');
$(btn).html('Hang up');
$(contain).attr('class', 'lineup')
$(video).attr('autoplay', true);
$(video).attr('id', data.from);
$(video).attr('width', 320);
connections[to].remoteStream = e.stream;
$(video).attr('src', webkitURL.createObjectURL(e.stream));
$(contain).append(video);
$(contain).append(btn);
$('#remote').append(contain);
//$('#btnHangup').attr('disabled', false);
};
connections[data.from].connection.addStream(localStream);
connections[data.from].connection.setRemoteDescription(connections[data.from].connection.SDP_OFFER, new SessionDescription(data.sdp));
var answer = connections[data.from].connection.createAnswer(data.sdp, {has_video:true,has_audio:true});
connections[data.from].connection.setLocalDescription(connections[data.from].connection.SDP_ANSWER, answer);
socket.emit('answer', {to:data.from, from:data.to, sdp:answer.toSdp()});
});
socket.on('answer', function(data) {
console.log('answer triggered');
connections[data.from]['connection'].setRemoteDescription(connections[data.from].connection.SDP_ANSWER, new SessionDescription(data.sdp));
socket.emit('startice', {to: data.from, from: data.to});
connections[data.from].connection.startIce();
});
socket.on('startice', function(data) {
console.log('startice triggered');
connections[data.from].connection.startIce();
});
/*socket.on('hangup', function() {
$('#btnHangup').attr('disabled', true);
$('#btnCall').attr('disabled', false);
connections[id].connection.close();
delete connections[id];
});*/
var id,account;
socket.on('loginsuccess', function(data) {
$('#loginpanel').css('display', 'none');
$('#accountpanel').css('display', 'block');
$('#accountpanel').html('Welcome '+data.account+' ');
id = data.id;
account = data.account;
$('#btnStart').attr('disabled', false);
});
socket.on('loginfail', function(data) {
$('#account').val('');
$('#password').val('');
});
socket.on('updatelist', function(data) {
$('#users').html('');
for(var i in data) {
var opt = document.createElement('option');
$(opt).html(data[i])
$(opt).attr('value', i);
$('#users').append(opt);
}
});
function getIceStateDesc(state) {
switch(state) {
case 0x100:
return 'ICE_GATHERING';
break;
case 0x200:
return 'ICE_WAITING';
break;
case 0x300:
return 'ICE_CHECKING';
break;
case 0x400:
return 'ICE_CONNECTED';
break;
case 0x500:
return 'ICE_COMPLETED';
break;
case 0x600:
return 'ICE_FAILED';
break;
case 0x700:
return 'ICE_CLOSED';
break;
default:
return '';
}
}
};
});
然後透過幾行程式碼載入執行:
require(['test848a'], function(conference) {
conference(socket);
});
伺服器部分,也只是把需要的部份組合進去:
var fs = require('fs'),
url = require('url'),
mime = require('mime'),
index = '/test848.html',
cache = {},
app = require('http').createServer(function(req, res) {
var resource = url.parse(req.url).pathname;
if(typeof cache[resource] === 'undefined') {
fs.stat(__dirname + resource, function(err, stat) {
if(err) {
if(typeof cache[index] === 'undefined') {
fs.readFile(__dirname+index, function(err, data) {
if(err) {
console.log('cond 1');
res.writeHead(500);
res.end();
} else {
//cache[index] = data;
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(data);
}
})
} else {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(cache[index]);
}
} else {
fs.readFile(__dirname + resource, function(err, data) {
if(err) {
if(typeof cache[index] === 'undefined') {
fs.readFile(__dirname+index, function(err, data) {
if(err) {
res.writeHead(500);
res.end();
} else {
//cache[index] = data;
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(data);
}
});
} else {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(cache[index]);
}
} else {
//cache[resource] = data;
var type = mime.lookup(resource);
res.setHeader('Content-Type', type);
res.writeHead(200);
res.end(data);
}
})
}
})
} else {
var type = mime.lookup(resource);
res.setHeader('Content-Type', type);
res.writeHead(200);
res.end(cache[resource]);
}
}),
io = require('./ws.io').listen(app);
io.sockets.on('connection', function (socket) {
socket.on('offer', function(data) {
socket.to(data.to).emit('offer', {to:data.to, from:data.from, sdp: data.sdp});
});
socket.on('answer', function(data) {
socket.to(data.to).emit('answer', {to:data.to, from:data.from, sdp: data.sdp});
});
socket.on('ice', function(data) {
socket.to(data.to).emit('ice', {to:data.to, from:data.from, sdp:data.sdp,label:data.label});
});
socket.on('startice', function(data) {
socket.to(data.to).emit('startice', {to:data.to, from:data.from});
});
socket.on('hangup', function() {
socket.to(data.to).emit('hangup', {});
});
var users = {
'fillano': 'abcd',
'hildegard': 'efgh',
'wolfram': 'ijkl'
};
socket.on('login', function(data) {
if(!!users[data.account]) {
if(data.password===users[data.account]) {
socket.emit('loginsuccess', {account:data.account,id:socket.id});
if(!socket.manager.list) {
socket.manager.list = {};
}
socket.manager.list[socket.id] = data.account;
for(var i in this.manager.namespaces[this.namespace]['all']) {
this.manager.namespaces[this.namespace]['all'][i].emit('updatelist', socket.manager.list);
}
} else {
socket.emit('loginfail', {})
}
} else {
socket.emit('loginfail',{})
}
});
socket.on('wsdraw', function(m) {
console.log('user provided wsdraw handler');
socket.broadcast.emit('wsdraw', m);
console.log('wsdraw broadcasted');
});
});
app.listen(8443);
(為了測試,把file cache機制關掉了)
接下來就啟動伺服器程式來看看(html是透過程式中的http傳送到瀏覽器,WebSocket與http共享一個port)...
畫面乍看之下還好:
登入看起來也還ok:
打開視訊看看:
看起來沒太大問題。但是打開白板:
問題看起來還不小,這已經是調整過button大小的了,但是select元素實在大的很誇張...看起來還需要花一些時間調整介面。不過白板是介面上比較複雜的,其他部分調整應該就沒那麼複雜。
網路上是有一些select的解決方法,不過還是想辦法從CSS條比較徹底。但是...差不多要截稿了,就暫時停下腳步吧,今天是最後一天。
目前看起來一些還沒做完的事情有:
這三十天,除了嘗試這些HTML5應用,也整理了一些東西,包含:
這裡面最複雜的是ws.io,比預計花了多了幾天才包好。AMD其實也有點複雜,他需要用非同步的方式載入模組,由於是一層一層透過依賴關係找,這樣的遞迴需要用特別方式處理。然後處理require時,依照語法,需要把多個在define定義的函數,每個有不定數量的參數,組合起來。想的時候覺得有點難,不過寫的時候倒是幾行就結束了,因為這裡是使用同步的方式遞迴,沒有非同步遞迴那麼複雜。
就這樣啦,雖然沒有在三十天完成,不過之後還會持續調整。(不會像現在這麼趕就是了)
(2012-11-11 21:33 補充)
ㄝ,Chrome改版到23後,PeerConnection的介面全改了,連物件的名字都改了...現在叫做RTCPeerConnection(在chrome要加上webkit前綴),所以之前寫的視訊會議不會動了。可能得花幾天測試一下新的API...
iT邦幫忙MVPfillano提到:
謝謝,勉強撐完了...
最後這兩天真是精彩絕倫,因為費公自己當男模粉墨登場耶!!!
恭喜費大鐵人鍊成
fillano提到:
看起來沒太大問題。
問題大了怎麼沒問題
這是在廚房還是浴室拍的呀
在廚房,這裡比較能專心...在浴室會有困難,沒桌子
iT邦幫忙MVPfillano提到:
不過這樣之前寫的視訊會議程式就要全翻掉。
不管如何,盡善盡美是一定要的啦~~
費公改好後,若要大家在浴室開視訊會議再通知大家一下(廚房就不用了)...
iT邦幫忙MVPfillano提到:
門關了一半,概念驗證程式改在 且戰且走HTML5(28) 建立視訊會議 。
哇,好厲駭喔!!
費公不能再這樣威猛下去了,不然小囉囉們可能就要為費公建生祠了...
又發現新的問題XD,想要在ws.io加上真正的router,讓不同的websoket路徑有不同的namespace來handle,卻發現我把ws實體化與onconnection處理等都放在Namespace了,應該放在Manager才對,我不需要為不同的namespace啟動不同的server阿
找個時間改一改......果然處處碰壁阿
iT邦幫忙MVPfillano提到:
果然處處碰壁阿
這鐵文果然徹底絕對百分百符合觀眾期待......看費公處處碰壁~~~
這樣我應該改姓繆....不過繆拉也不姓繆....
發現ws沒有提供在runtime時的path資訊...原來他是可以一個path一個server,然後共享一個http伺服器,那部就是我之前做的調一下就可以...做完了才發現,只好再改回來...這樣又要花幾天了
我該改名叫鐵壁繆拉
「部」->「不」
iT邦幫忙MVPfillano提到:
我該改名叫鐵壁繆拉
其實修煉之路是暢通無壁的,只是這路有十萬八千里,法顯與玄奘都有走過....
蛤?這條路本身就是壁?....
ok,趁中午休息把ws.io改好了,晚上來貼一貼。
原本的完整程式就放在:且戰且走HTML5(26) 使用ws.io完成資源共享,所以把改過的程式碼也放在那裡。有空時再把它放到github上。
就知道發漏費公的文最爽了,因為可以跟海綿寶寶分出高下....
fillano 大大實在太精實了
喜酒都吃完散場了
他還在改新人戀愛史投影片
antijava提到:
他還在改“新人戀愛”史投影片
當然要改啊:先買票,還是先上車,這粉重要....
蛤?已經結婚啦....