├── joeold.jpg ├── svg ├── p5.jpg ├── svg_test.html └── svg.js ├── favicon.ico ├── clipper ├── icon.png ├── manifest.json ├── background.html ├── Readme.html ├── content_script.js └── popup.html ├── openchrome ├── Readme ├── Makefile ├── generic.html ├── addbookmark.erl ├── demo1.erl ├── README.md ├── demo2.erl ├── demo3.erl ├── MIT-LICENSE ├── test1.html ├── generic.js ├── notes.txt ├── gui1.erl ├── svg_lib.js ├── work └── dragdiv.html ├── raphael.erl ├── svg_test.html ├── drag4.svg ├── sebg.erl ├── index.html ├── raphael-min.js └── jquery-1.5.min.js /joeold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joearms/SEBG/HEAD/joeold.jpg -------------------------------------------------------------------------------- /svg/p5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joearms/SEBG/HEAD/svg/p5.jpg -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joearms/SEBG/HEAD/favicon.ico -------------------------------------------------------------------------------- /clipper/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joearms/SEBG/HEAD/clipper/icon.png -------------------------------------------------------------------------------- /openchrome: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # an OS independent way to open a URL in a browser 4 | 5 | os=`uname` 6 | 7 | echo "starting $1" 8 | 9 | case $os in 10 | Linux) 11 | firefox $1;; 12 | Darwin) 13 | open -a 'Google Chrome' $1;; 14 | *) 15 | echo unknown OS $os;; 16 | esac 17 | -------------------------------------------------------------------------------- /Readme: -------------------------------------------------------------------------------- 1 | SEBG stands for 2 | 3 | Simple Erlang Browser Graphics 4 | 5 | Read index.html for documentation 6 | 7 | To run type make 8 | 9 | Notes: 10 | 11 | 1) You need a web socket enabled browser 12 | 2) Only tested on mac os-x 13 | 14 | Author: Joe Armstrong 15 | 16 | Contributions: 17 | 18 | Sergio Veiga -- websocket hadshake code 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .SUFFIXES: .erl .beam .yrl 2 | 3 | MODS := $(wildcard *.erl) 4 | 5 | %.beam: %.erl 6 | erlc -W $< 7 | 8 | all: beam 9 | (sleep 1 && ./openchrome http://localhost:1234/index.html) & 10 | erl -s sebg start 1234 11 | 12 | 13 | 14 | 15 | 16 | beam: ${MODS:%.erl=%.beam} 17 | 18 | clean: 19 | rm -rf *.beam 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /generic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Once you have started the brower 9 | click on
10 | 11 | demo1 13 | 14 |
Do view source to see the code :-) 15 | -------------------------------------------------------------------------------- /clipper/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Clipper 1.2", 3 | "description": "Clipper release 1 - first production version", 4 | "version": "1.2", 5 | "background_page": "background.html", 6 | "permissions": [ 7 | "tabs", 8 | "http://localhost:*//*/*", 9 | "http://*/*", 10 | "https://*/*" 11 | ], 12 | "browser_action": { 13 | "default_title": "Clipper", 14 | "default_icon": "icon.png", 15 | "popup": "popup.html" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /addbookmark.erl: -------------------------------------------------------------------------------- 1 | -module(addbookmark). 2 | -compile(export_all). 3 | 4 | start(Str) -> 5 | %% io:format("Str=~p~n",[Str]), 6 | T = sebg:parse_uri_args(Str), 7 | %% io:format("add bookmark:~p~n",[T]), 8 | Time = proplists:get_value("time", T), 9 | File = time_to_file(Time), 10 | file:write_file(File, term_to_binary(T)). 11 | 12 | time_to_file([$\s|T]) -> [$_|time_to_file(T)]; 13 | time_to_file([$(|T]) -> [$_|time_to_file(T)]; 14 | time_to_file([$)|T]) -> [$_|time_to_file(T)]; 15 | time_to_file([H|T]) -> [H|time_to_file(T)]; 16 | time_to_file([]) -> ".snip". 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo1.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2011 Joe Armstrong 2 | %% See MIT-LICENSE for licensing information. 3 | %% Time-stamp: <2011-02-12 11:57:32 joe> 4 | 5 | -module(demo1). 6 | -export([start/1]). 7 | 8 | %% simple demo 9 | %% once started Pid is a channel that csn be iused to talk 10 | %% to the browser. Pid ! {cmd,Javascript} 11 | 12 | start(Pid) -> 13 | Pid ! {eval, "document.body.innerHTML='';"}, 14 | Pid ! {eval, "document.body.style.backgroundColor='#eeffaa';"}, 15 | Pid ! {eval, "document.body.innerHTML+='
This is a chrome unpacked extension. 4 | 5 |
To install goto: 6 |
7 | Chrome -> Window -> Extensions 8 | 9 | click on "Load unpacked extension" 10 | and select the directory where this extension is contained 11 |12 | 13 |
This is just a beginning. Better analysis of the selection 16 | is highly desirable. 17 | 18 |
The code in this extension is a modified version of 21 | a chrome extension that I found on the net (and a blog) 22 | article. The name of the extension was: 23 | 24 |
25 | "name": "Linx Bookmark 1.1", 26 | "description": "Adds the current page to the Linx bookmarking system.", 27 | "version": "1.1", 28 |29 | 30 | 31 |
Unfortunately I cannot give a more precise reference, and 32 | have not been able to find the original. Mail my if you stumble over 33 | the origonal. 34 | 35 | 36 |
Some the code in content_script.js 37 | came from 38 | http://dev.day.com/content/ddc/blog/2010/06.html 39 | 40 | 41 | -------------------------------------------------------------------------------- /generic.js: -------------------------------------------------------------------------------- 1 | var websocket; 2 | var output; 3 | var c; 4 | var logdiv; 5 | 6 | window.onload = 7 | function() { 8 | //console.log('
started'); 9 | } 10 | 11 | function loadScript(File) { 12 | var s = document.createElement('script'); 13 | s.type = 'text/javascript'; 14 | s.src = File; 15 | s.onload = function(){send('loaded')}; 16 | document.body.appendChild(s); 17 | } 18 | 19 | function onClose(evt) { 20 | console.log('
closed');
21 | document.body.style.backgroundColor='#aabbcc';
22 | }
23 |
24 | function onMessage(evt) {
25 | try
26 | {
27 | eval(evt.data);
28 | }
29 | catch(e)
30 | {
31 | alert("oops:"+evt.data);
32 | }
33 | }
34 |
35 | function onError(evt) {
36 | document.body.style.backgroundColor='orange';
37 | }
38 |
39 | function send(msg) {
40 | websocket.send(msg);
41 | }
42 |
43 | function log(x) {
44 | logdiv.innerHTML += x;
45 | }
46 |
47 | function start_session(wsUri){
48 | console.log("
try to connect");
49 | document.body.style.backgroundColor='#ccaabb';
50 | websocket = new WebSocket(wsUri);
51 | websocket.onopen = onOpen;
52 | websocket.onclose = onClose;
53 | websocket.onmessage = onMessage;
54 | websocket.onerror = onError;
55 | return(false);
56 | }
57 |
58 | function onOpen(evt) {
59 | console.log("connected");
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/clipper/content_script.js:
--------------------------------------------------------------------------------
1 | // code cut-and-paste from
2 | // http://dev.day.com/content/ddc/blog/2010/06.html
3 |
4 | // console.log('injecting code');
5 |
6 | function getContent() {
7 | // console.log('getting selection');
8 | var selection = window.getSelection( );
9 | var markup = serializeSelection( selection );
10 | // var finalMarkup = formatPage( markup );
11 | // return finalMarkup;
12 | return markup;
13 | }
14 |
15 | function serializeSelection( selection ) {
16 | var xmlFragment = "";
17 | try {
18 | var n = 0, ranges = selection.rangeCount;
19 | // console.log('ranges='+ranges);
20 | while ( n != ranges ) {
21 | var range = selection.getRangeAt( n++ );
22 | // console.log('here',range);
23 | var content = range.cloneContents( );
24 | var serializer = new XMLSerializer( );
25 | xmlFragment += serializer.serializeToString( content );
26 | }
27 | }
28 | catch( msg ) { }
29 | return xmlFragment;
30 | }
31 |
32 | content = getContent();
33 |
34 | // console.log('content',content);
35 |
36 |
37 | var pageInfo = {
38 | "title": document.title,
39 | "url": window.location.href,
40 | "text": window.getSelection().toString(),
41 | "html": content
42 | };
43 |
44 | // console.log('sending page',pageInfo)
45 | // Send the information back to the extension
46 |
47 | chrome.extension.sendRequest(pageInfo);
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/notes.txt:
--------------------------------------------------------------------------------
1 | Two beautiful programs
2 |
3 | For a long time one of my favorite Erlang program was this:
4 |
5 | loop() ->
6 | receive
7 | F -> F(),
8 | loop()
9 | end.
10 |
11 | It's nice because it does very little, but what is does is universal. It
12 | enables mobile code.
13 |
14 | Well now I can do this in Javascript.
15 |
16 | The Javascript equivalent is:
17 |
18 | function onMessage(evt) {
19 | eval(evt.data);
20 | }
21 |
22 | Where the data comes from a websocket.
23 |
24 | Websockets are controlled with a simple API:
25 |
26 | websocket = new WebSocket(wsUri);
27 | websocket.onopen = function(evt) { onOpen(evt) };
28 | websocket.onclose = function(evt) { onClose(evt) };
29 | websocket.onmessage = function(evt) { onMessage(evt) };
30 |
31 | Linking a websocket and Erlang is pretty easy. So now I can write code like this in
32 | erlang
33 |
34 | -module(demo1).
35 | -export([start/1]).
36 |
37 | start(Pid) ->
38 | Pid ! {eval, "document.body.innerHTML='';"},
39 | Pid ! {eval, "document.body.style.backgroundColor='#eeffaa';"},
40 | Pid ! {eval, "document.body.innerHTML+='
Test of svg library. 49 |
Q: How should we interact with the svg canvas?
58 |
A: By sending JSON messages.
59 |
Q: What objects are clickabkle?
60 |
A: Only buttons. If we can make anthing clickabkle then it
61 | will not be obvious.
62 |
Q: What objects are draggable?
63 |
A: Only drag squares. Again it should be obvious what objects are draggable.
64 |
Q: What are the drag methods?
65 |
A: on_drag_start/on_moved/on_dropped
66 |
67 |
68 |
\";", 76 | X,"= document.getElementById('",X,"').getContext('2d');"]. 77 | 78 | 79 | add_txt_to_div(X, Y) -> 80 | ["document.getElementById('",X,"').innerHTML = '",Y,"';"]. 81 | 82 | button(Txt, Msg) -> 83 | ["document.body.innerHTML+=", 84 | "\"\";"]. 85 | 86 | i2s(X) -> 87 | integer_to_list(X). 88 | 89 | rgb(R,G,B) -> 90 | ["'rgb(",i2s(R),",",i2s(G),",",i2s(B),")';"]. 91 | 92 | fillRect(X,Y,W,H) -> 93 | ["fillRect(",i2s(X),",",i2s(Y),",",i2s(W),",",i2s(H),")"]. 94 | 95 | add_random_rectangle(Name) -> 96 | %% "c.fillStyle = 'rgb(255,0,0)';c.fillRect(30, 30, 50, 50);". 97 | R=random:uniform(255), 98 | G=random:uniform(255), 99 | B=random:uniform(255), 100 | X=100+random:uniform(100), 101 | Y=100+random:uniform(100), 102 | W=random:uniform(150), 103 | H=random:uniform(150), 104 | %% "c.fillStyle = 'rgb(255,0,0)';c.fillRect(30, 30, 50, 50);". 105 | [Name,".fillStyle=",rgb(R,G,B),Name,".",fillRect(X,Y,W,H),";"]. 106 | 107 | random_seed() -> 108 | {_,_,X} = erlang:now(), 109 | {H,M,S} = time(), 110 | H1 = H * X rem 32767, 111 | M1 = M * X rem 32767, 112 | S1 = S * X rem 32767, 113 | put(random_seed, {H1,M1,S1}). 114 | 115 | -------------------------------------------------------------------------------- /svg_lib.js: -------------------------------------------------------------------------------- 1 | svg_ns = 'http://www.w3.org/2000/svg'; 2 | 3 | document.onkeydown = function(e){update(e);}; 4 | 5 | 6 | var globstr = "Hello World"; 7 | var text; 8 | var svg; 9 | 10 | D = function(x, y){ 11 | return x == undefined ? y : x; 12 | }; 13 | 14 | update = function(e) { 15 | cc = e.keyCode; 16 | // can browse this in the log window 17 | console.log("pressed",e); 18 | console.log("pressed="+e.keyCode+" shift="+e.shiftKey); 19 | globstr += String.fromCharCode(cc); 20 | change_text(globstr); 21 | return false; 22 | } 23 | 24 | 25 | mk_canvas = function(o) { 26 | var x = document.createElementNS(svg_ns, 'svg'); 27 | x.setAttribute("width", D(o.width,700)); 28 | x.setAttribute("height", D(o.height, 200)); 29 | x.setAttribute("style","background-color:" + D(o.color,"red")); 30 | x.setAttribute("id", o.id); 31 | x.addEventListener("mousemove", drag, false); 32 | 33 | // x.addEventListener("mousedown", mouse_down, false); 34 | // x.addEventListener("mouseup", mouse_up, false); 35 | return x; 36 | }; 37 | 38 | mk_rect = function(o) { 39 | var obj = document.createElementNS(svg_ns, 'rect'); 40 | obj.setAttribute('x', D(o.x, 10)); 41 | obj.setAttribute('y', D(o.y,20)); 42 | obj.setAttribute('width', D(o.width,120)); 43 | obj.setAttribute('height', D(o.ht, 40)); 44 | obj.setAttribute('fill', D(o.color,'#aabb11')); 45 | obj.setAttribute('stroke','black'); 46 | obj.setAttribute('stroke-width',3); 47 | obj.setAttribute("rx", D(o.rx, 5)); 48 | obj.setAttribute("ry", D(o.ry, 5)); 49 | obj.addEventListener("click", buttonClicked, false); 50 | obj.addEventListener("mouseover", buttonOver, false); 51 | obj.addEventListener("mouseout", buttonOut, false); 52 | obj.addEventListener("onkeydown", key1, false); 53 | return obj; 54 | }; 55 | 56 | key1 = function(evt){ 57 | console.log("key1"); 58 | } 59 | 60 | buttonClicked = function(evt) { 61 | var x =evt.target; 62 | x.setAttribute("fill", "orange"); 63 | send("YES YOU CLICKED ME"); 64 | }; 65 | 66 | buttonOver = function(evt) { 67 | var x =evt.target; 68 | x.setAttribute("fill", "red"); 69 | }; 70 | 71 | buttonOut = function(evt) { 72 | var x =evt.target; 73 | x.setAttribute("fill", "#aabb11"); 74 | }; 75 | 76 | keypress = function(evt) { 77 | // var x = evt.target; 78 | console.log("pressed"); 79 | } 80 | 81 | mk_text = function(o) { 82 | var text = document.createElementNS(svg_ns, "text"); 83 | text.setAttribute("fill", D(o.fill,"green")); 84 | var size = D(o.size, 1); 85 | text.setAttribute("font-size", size+"px"); 86 | var font = D(o.font,"Courier"); 87 | text.setAttribute("font-family", font); 88 | text.setAttribute("x", D(o.x,10)); 89 | text.setAttribute("y", D(o.y,10)); 90 | text.setAttribute("text-anchor", D(o.anchor,"start")); 91 | var str = D(o.str,"** missing str in text **"); 92 | var data = document.createTextNode(globstr); 93 | text.setAttribute("id", o.id); 94 | text.appendChild(data); 95 | return text; 96 | }; 97 | 98 | change_text = function(x) { 99 | text.textContent=x; 100 | } 101 | 102 | 103 | window.onload = function() { 104 | svg = mk_canvas({width:800, height:200, color:'#ffeecc',id:1}), 105 | c = document.body.appendChild(svg); 106 | c.appendChild(mk_rect({})); 107 | text = mk_text({x:250,y:50, size:24, str:"Hello Joe"}) 108 | c.appendChild(text); 109 | } 110 | 111 | 112 | function drag(evt) 113 | { 114 | var t = evt.target; 115 | // console.log("drag="+t); 116 | } 117 | -------------------------------------------------------------------------------- /work/dragdiv.html: -------------------------------------------------------------------------------- 1 | 35 | 94 | 95 |
\n",quote(lists:flatten(io_lib:format("~p",[X]))), ""].
277 |
278 | quote("<" ++ T) -> "<" ++ quote(T);
279 | quote("&" ++ T) -> "&" ++ quote(T);
280 | quote([H|T]) -> [H|quote(T)];
281 | quote([]) -> [].
282 |
283 | test() ->
284 | K1 = "3e6b263 4 17 80",
285 | K2 = "17 9 G`ZD9 2 2b 7X 3 /r90",
286 | K3 = <<"WjN}|M(6">>,
287 | <<"n`9eBk9z$R8pOtVb">> = build_challenge(K1, K2, K3),
288 | checksum_ok.
289 |
290 | %% A typical URI looks
291 | %% like
292 | %% URI = "/a/b/c?password=aaa&invisible=Ahidden+value"+
293 |
294 | parse_uri(URI) ->
295 | case string:tokens(URI, "?") of
296 | [Root] ->
297 | {Root, []};
298 | [Root, Args] ->
299 | {Root, parse_uri_args(Args)}
300 | end.
301 |
302 | parse_uri_args(Args) ->
303 | Args1 = string:tokens(Args, "&;"),
304 | map(fun(KeyVal) ->
305 | case string:tokens(KeyVal, "=") of
306 | [Key, Val] ->
307 | {urlencoded2str(Key), urlencoded2str(Val)};
308 | [Key] ->
309 | {urlencoded2str(Key), ""};
310 | _ ->
311 | io:format("Invalid str:~p~n",[KeyVal]),
312 | {"error", "error"}
313 | end
314 | end, Args1).
315 |
316 |
317 | urlencoded2str([$%,$u,A,B,C,D|T]) -> [decode_hex(A,B,C,D)|urlencoded2str(T)];
318 | urlencoded2str([$%,Hi,Lo|T]) -> [decode_hex(Hi, Lo)|urlencoded2str(T)];
319 | urlencoded2str([$+|T]) -> [$ |urlencoded2str(T)];
320 | urlencoded2str([H|T]) -> [H|urlencoded2str(T)];
321 | urlencoded2str([]) -> [].
322 |
323 | %% decode_hex ...
324 |
325 | decode_hex(Hex1, Hex2) -> hex2dec(Hex1)*16 + hex2dec(Hex2).
326 |
327 | decode_hex(Hex1, Hex2, Hex3, Hex4) ->
328 | hex2dec(Hex1)*4096 + hex2dec(Hex2)*256 + hex2dec(Hex3)*16 + hex2dec(Hex4).
329 |
330 | hex2dec(X) when X >=$0, X =<$9 -> X-$0;
331 | hex2dec($A) -> 10;
332 | hex2dec($B) -> 11;
333 | hex2dec($C) -> 12;
334 | hex2dec($D) -> 13;
335 | hex2dec($E) -> 14;
336 | hex2dec($F) -> 15;
337 | hex2dec($a) -> 10;
338 | hex2dec($b) -> 11;
339 | hex2dec($c) -> 12;
340 | hex2dec($d) -> 13;
341 | hex2dec($e) -> 14;
342 | hex2dec($f) -> 15.
343 |
344 |
345 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 | This code is changing rapidly. Always get the latest verions from 14 | https://github.com/joearms/SEBG 15 | 16 |
This is a minimal code example. First you need an Erlang handler, for example, 24 | demo1.erl which is as follows: 25 | 26 |
27 | -module(demo1).
28 | -export([start/1]).
29 |
30 | start(Pid) ->
31 | Pid ! {eval, "document.body.innerHTML='';"},
32 | Pid ! {eval, "document.body.style.backgroundColor='#eeffaa';"},
33 | Pid ! {eval, "document.body.innerHTML+='<h1>Hello World</h1>'"},
34 | event_loop(Pid).
35 |
36 | event_loop(Pid) ->
37 | receive
38 | Any ->
39 | io:format("??event loop:~p~n",[Any]),
40 | event_loop(Pid)
41 | end.
42 |
43 |
44 |
45 | Then you need to some browser code to launch the example. 46 | A minimal code fragment to launch the example is: 47 |
48 |
49 | <script type="text/javascript" src="./generic.js"></script>
50 |
51 | <a href="#"
52 | onclick="start_session('ws://127.0.0.1:1234/connect/demo1')">demo1</a>
53 |
54 |
55 |
56 | run the above code
58 |
59 | Type make 74 | 75 |
Once you have a server running, click on any of the links below: 80 | 81 |
Writing a handler is trivial. Use the following pattern: 124 |
125 |
126 | -module(myhandler).
127 | -export([start/1]).
128 |
129 | start(Pid) ->
130 | init(Pid),
131 | loop(Pid).
132 |
133 | loop(Pid) ->
134 | receive
135 | {browser, N, Str} ->
136 | ...
137 | loop(Pid)
138 | end.
139 |
140 | 141 | Then launch the handler by from the browser by evaluating the 142 | javascript command start_session('ws://127.0.0.1:1234/connect/myhandler') 143 | The start_session command is defined in generic.js 144 | 145 | 146 |
In the above code Pid is a process that represents the 147 | brower. 148 | 149 |
Pid ! {send, Msg} sends Msg to the 150 | browser. Msg must contain a valid Javascript expression which 151 | is evaluated in the browser. 152 | 153 |
Javascript code executing in the browser can evaluate the Javascript 154 | function send(Str) this will emerge as a {browser,N,Str} 155 | message in the handler. N is a sequence number that increases 156 | by one for every message sent. 157 | 158 |
Using web sockets and SVG and a bit of Javascript it is pretty easy 163 | to make an impressive GUI in a a few hundred lines of code. 164 | 165 |
This example contains a complete web server, which supports 166 | web-sockets and shows how to interface web-sockets with jquery and 167 | the Raphael SVG javascript library. Using this we make a simple 168 | interactive graphics demo program. 169 | 170 |
Note: This is known to work with Google Chrome and Safari 171 | browsers on Mac OS-X and with Chromium on Ubuntu. It does not work with 172 | Firefox. Other browsers are untested
173 | 174 |The code here is also intended an an intermediate example for new 175 | Erlang programmers to study and extend. The code is entirely 176 | self-contained, there are no dependence on any other modules, so you 177 | don't need to know about OTP applications or generic servers or 178 | anything. As such it is suitable for a beginner and for self-study. 179 | 180 |
I have also included a number of suggestions for small projects 181 | based on this code. We often get questions asking for suggestions for 182 | projects that are suitable for learning Erlang. I have added a list of possible projects based on this code at 184 | the end of this page. 185 | 186 |
If you are running on mac os-x then all you have to do is type 187 | make. On other platforms start erlang, give the command 188 | segb:start() in the shell. Then view the page 189 | http://localhost:1234/index.html 190 | in your browser. 191 | 192 |
For a minimal example see http://localhost:1234/generic.html 195 | 196 | 197 |
Actually what gets pushed is a fragment of Javascript. The Javascript 198 | in the browser just waits for a Javascript fragment, evaluates it, 199 | then waits for the next fragment. It's very easy to understated. 200 | 201 |
The server itself is a small 300 line program that communicates 204 | with a web socket. Once a web socket has been opened the web server 205 | spawns a handler process to communicate with the web socket and turns 206 | itself into a web socket middleman. 207 | 208 |
The handler process sends javascript expression to the web 209 | socket. At the client end of the web socket (ie in the server) all we 210 | do is a javascript eval of the data received from the web 211 | socket. The server acts as a middle man taking cars or the framing 212 | protocol between the browser and the server. 213 | 214 |
The client page in the server in in a sense "universal." All it has 215 | is the ability to execute code, which comes in the form of javascript 216 | messages. 217 | 218 |
In practise this is somewhat inconvenient. So instead of being 219 | entire universal and only opening a socket and eval'ing data it 220 | receives. The client does a few more thing. It turns the web page red 221 | if the socket connection is broken, and it preloads the jquery. jquery 222 | is so useful that I consider it to almost be a part of javascriopt. 223 | 224 |
The handler process svg_demo.erl shows how to communicate with 225 | the browser. The first thing svg_demo.erl does, is to send a 226 | request to load the Javascript library raphael-min.js. Raphael 227 | is a well 228 | documented javascript SVG library. 229 | 230 |
Having loaded Raphael. I create a draggable circle, and a clickable 231 | blue box. Nothing fancy. I deliberately don't want to make the 232 | example too complicated. You should at this stage be able to read and 233 | understand svg_demo.erl and sebg.erl in their 234 | entirely.
235 | 236 |Using websockets, we can create a direct message passing channel 237 | between Erlang and a web server. Once the channel is established we 238 | can asynchronous messages between the an Erlang process and a browser 239 | window. Sending these message has a tidy overhead of 2 bytes per 240 | message. 241 | 242 |
This demo has only two Erlang modules. sebg.erl and svg_demo.erl. The example is completely 245 | self-contain, so makes no use of any Erlang libaries at all. 246 | 247 |
The work flow is:
248 | 249 |
271 |
272 | function start_session(mod){
273 | websocket = new WebSocket(wsUri);
274 | websocket.onopen = function(evt) { onOpen(evt, mod) };
275 | websocket.onclose = function(evt) { onClose(evt) };
276 | websocket.onmessage = function(evt) { onMessage(evt) };
277 | websocket.onerror = function(evt) { onError(evt) };
278 | return(false);
279 | }
280 |
281 | function onOpen(evt, mod) {
282 | websocket.send("start:" + mod + ":");
283 | }
284 |
285 | function onClose(evt) {
286 | $("body").css({'background-color':'red'});
287 | }
288 |
289 | function onMessage(evt) {
290 | {
291 | eval(evt.data);
292 | }
293 |
294 |
295 |
296 |
297 |
298 | The directory clipper contains a plugin for goole chrome that 300 | talks to the web server. To install this see Readme. 301 | 302 | 303 |
Once installed you can select any region of the current web page you are browsing 304 | by dragging the mouse over the interesting region. Then click on the extension icon. 305 | A popup menu appears. You can add a few comments, click on save 306 | and a message is sent to a little erlang server sebg.erl the server. 307 | The date extracted from the webpage is put into a snippet file 308 | for future analysis. 309 | 310 |
Next steps: 311 | 312 |
We need better javascript to extract information on the web page 315 | make a pluging for firefox etc.
316 | 317 |328 | A large amount of assistance is needed here. 329 | 330 | 331 | 332 | 333 |
Here are few suggestions for improvements and exercises. 336 | 337 |
Stare at the code for the function etop_collect/2 in the 356 | module observer_backend.erl which you will find in the standard 357 | distribution.
Note: cut pates and minimise the code you need from 358 | the library, don't just call the library cut and paste when you need 359 | and understand every line.
360 | 361 |1){x=y.sqrt(x);c=x*c;d=x*d}var z=c*c,A=d*d,C=(f==g?-1:1)*y.sqrt(B((z*A-z*u*u-A*t*t)/(z*u*u+A*t*t))),E=C*c*u/d+(a+h)/2,F=C*-d*t/c+(b+i)/2,G=y.asin(((b-F)/d).toFixed(9)),H=y.asin(((i-F)/d).toFixed(9));G=a e){if(c&&!l.start){m=cq(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n);k+=["C",m.start.x,m.start.y,m.m.x,m.m.y,m.x,m.y];if(f)return k;l.start=k;k=["M",m.x,m.y+"C",m.n.x,m.n.y,m.end.x,m.end.y,i[5],i[6]][v]();n+=j;g=+i[5];h=+i[6];continue}if(!b&&!c){m=cq(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n);return{x:m.x,y:m.y,alpha:m.alpha}}}n+=j;g=+i[5];h=+i[6]}k+=i}l.end=k;m=b?n:c?l:a.findDotsAtSegment(g,h,i[1],i[2],i[3],i[4],i[5],i[6],1);m.alpha&&(m={x:m.x,y:m.y,alpha:m.alpha});return m}},cs=cr(1),ct=cr(),cu=cr(0,1);bO.getTotalLength=function(){if(this.type!="path")return;if(this.node.getTotalLength)return this.node.getTotalLength();return cs(this.attrs.path)};bO.getPointAtLength=function(a){if(this.type!="path")return;return ct(this.attrs.path,a)};bO.getSubpath=function(a,b){if(this.type!="path")return;if(B(this.getTotalLength()-b)<"1e-6")return cu(this.attrs.path,a).end;var c=cu(this.attrs.path,b,1);return a?cu(c,a).end:c};a.easing_formulas={linear:function(a){return a},"<":function(a){return C(a,3)},">":function(a){return C(a-1,3)+1},"<>":function(a){a=a*2;if(a<1)return C(a,3)/2;a-=2;return(C(a,3)+2)/2},backIn:function(a){var b=1.70158;return a*a*((b+1)*a-b)},backOut:function(a){a=a-1;var b=1.70158;return a*a*((b+1)*a+b)+1},elastic:function(a){if(a==0||a==1)return a;var b=0.3,c=b/4;return C(2,-10*a)*y.sin((a-c)*(2*D)/b)+1},bounce:function(a){var b=7.5625,c=2.75,d;if(a<1/c)d=b*a*a;else if(a<2/c){a-=1.5/c;d=b*a*a+0.75}else if(a<2.5/c){a-=2.25/c;d=b*a*a+0.9375}else{a-=2.625/c;d=b*a*a+0.984375}return d}};var cv=[],cw=function(){var b=+(new Date);for(var c=0;ca";var e=b.getElementsByTagName("*"),f=b.getElementsByTagName("a")[0],g=c.createElement("select"),h=g.appendChild(c.createElement("option"));if(e&&e.length&&f){d.support={leadingWhitespace:b.firstChild.nodeType===3,tbody:!b.getElementsByTagName("tbody").length,htmlSerialize:!!b.getElementsByTagName("link").length,style:/red/.test(f.getAttribute("style")),hrefNormalized:f.getAttribute("href")==="/a",opacity:/^0.55$/.test(f.style.opacity),cssFloat:!!f.style.cssFloat,checkOn:b.getElementsByTagName("input")[0].value==="on",optSelected:h.selected,deleteExpando:!0,optDisabled:!1,checkClone:!1,_scriptEval:null,noCloneEvent:!0,boxModel:null,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableHiddenOffsets:!0},g.disabled=!0,d.support.optDisabled=!h.disabled,d.support.scriptEval=function(){if(d.support._scriptEval===null){var b=c.documentElement,e=c.createElement("script"),f="script"+d.now();e.type="text/javascript";try{e.appendChild(c.createTextNode("window."+f+"=1;"))}catch(g){}b.insertBefore(e,b.firstChild),a[f]?(d.support._scriptEval=!0,delete a[f]):d.support._scriptEval=!1,b.removeChild(e),b=e=f=null}return d.support._scriptEval};try{delete b.test}catch(i){d.support.deleteExpando=!1}b.attachEvent&&b.fireEvent&&(b.attachEvent("onclick",function j(){d.support.noCloneEvent=!1,b.detachEvent("onclick",j)}),b.cloneNode(!0).fireEvent("onclick")),b=c.createElement("div"),b.innerHTML="";var k=c.createDocumentFragment();k.appendChild(b.firstChild),d.support.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,d(function(){var a=c.createElement("div"),b=c.getElementsByTagName("body")[0];if(b){a.style.width=a.style.paddingLeft="1px",b.appendChild(a),d.boxModel=d.support.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,d.support.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="",d.support.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
";var e=a.getElementsByTagName("td");d.support.reliableHiddenOffsets=e[0].offsetHeight===0,e[0].style.display="",e[1].style.display="none",d.support.reliableHiddenOffsets=d.support.reliableHiddenOffsets&&e[0].offsetHeight===0,a.innerHTML="",b.removeChild(a).style.display="none",a=e=null}});var l=function(a){var b=c.createElement("div");a="on"+a;if(!b.attachEvent)return!0;var d=a in b;d||(b.setAttribute(a,"return;"),d=typeof b[a]==="function"),b=null;return d};d.support.submitBubbles=l("submit"),d.support.changeBubbles=l("change"),b=e=f=null}})();var e=/^(?:\{.*\}|\[.*\])$/;d.extend({cache:{},uuid:0,expando:"jQuery"+(d.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?d.cache[a[d.expando]]:a[d.expando];return!!a&&!d.isEmptyObject(a)},data:function(a,c,e,f){if(d.acceptData(a)){var g=d.expando,h=typeof c==="string",i,j=a.nodeType,k=j?d.cache:a,l=j?a[d.expando]:a[d.expando]&&d.expando;if((!l||f&&l&&!k[l][g])&&h&&e===b)return;l||(j?a[d.expando]=l=++d.uuid:l=d.expando),k[l]||(k[l]={}),typeof c==="object"&&(f?k[l][g]=d.extend(k[l][g],c):k[l]=d.extend(k[l],c)),i=k[l],f&&(i[g]||(i[g]={}),i=i[g]),e!==b&&(i[c]=e);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,e){if(d.acceptData(b)){var f=d.expando,g=b.nodeType,h=g?d.cache:b,i=g?b[d.expando]:d.expando;if(!h[i])return;if(c){var j=e?h[i][f]:h[i];if(j){delete j[c];if(!d.isEmptyObject(j))return}}if(e){delete h[i][f];if(!d.isEmptyObject(h[i]))return}var k=h[i][f];d.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},h[i][f]=k):g&&(d.support.deleteExpando?delete b[d.expando]:b.removeAttribute?b.removeAttribute(d.expando):b[d.expando]=null)}},_data:function(a,b,c){return d.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=d.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),d.fn.extend({data:function(a,c){var e=null;if(typeof a==="undefined"){if(this.length){e=d.data(this[0]);if(this[0].nodeType===1){var g=this[0].attributes,h;for(var i=0,j=g.length;it ","
"],tr:[2,"","
"],td:[3,"
"],col:[2,"","
"],area:[1,""],_default:[0,"",""]};X.optgroup=X.option,X.tbody=X.tfoot=X.colgroup=X.caption=X.thead,X.th=X.td,d.support.htmlSerialize||(X._default=[1,"div"&&!n?m.childNodes:[];for(var p=o.length-1;p>=0;--p)d.nodeName(o[p],"tbody")&&!o[p].childNodes.length&&o[p].parentNode.removeChild(o[p])}!d.support.leadingWhitespace&&Q.test(i)&&m.insertBefore(b.createTextNode(Q.exec(i)[0]),m.firstChild),i=m.childNodes}}else i=b.createTextNode(i);i.nodeType?g.push(i):g=d.merge(g,i)}if(e)for(h=0;g[h];h++)!f||!d.nodeName(g[h],"script")||g[h].type&&g[h].type.toLowerCase()!=="text/javascript"?(g[h].nodeType===1&&g.splice.apply(g,[h+1,0].concat(d.makeArray(g[h].getElementsByTagName("script")))),e.appendChild(g[h])):f.push(g[h].parentNode?g[h].parentNode.removeChild(g[h]):g[h]);return g},cleanData:function(a){var b,c,e=d.cache,f=d.expando,g=d.event.special,h=d.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&d.noData[j.nodeName.toLowerCase()])continue;c=j[d.expando];if(c){b=e[c]&&e[c][f];if(b&&b.events){for(var k in b.events)g[k]?d.event.remove(j,k):d.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[d.expando]:j.removeAttribute&&j.removeAttribute(d.expando),delete e[c]}}}});var ba=/alpha\([^)]*\)/i,bb=/opacity=([^)]*)/,bc=/-([a-z])/ig,bd=/([A-Z])/g,be=/^-?\d+(?:px)?$/i,bf=/^-?\d/,bg={position:"absolute",visibility:"hidden",display:"block"},bh=["Left","Right"],bi=["Top","Bottom"],bj,bk,bl,bm=function(a,b){return b.toUpperCase()};d.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return d.access(this,a,c,!0,function(a,c,e){return e!==b?d.style(a,c,e):d.css(a,c)})},d.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bj(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{zIndex:!0,fontWeight:!0,opacity:!0,zoom:!0,lineHeight:!0},cssProps:{"float":d.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,e,f){if(a&&a.nodeType!==3&&a.nodeType!==8&&a.style){var g,h=d.camelCase(c),i=a.style,j=d.cssHooks[h];c=d.cssProps[h]||h;if(e===b){if(j&&"get"in j&&(g=j.get(a,!1,f))!==b)return g;return i[c]}if(typeof e==="number"&&isNaN(e)||e==null)return;typeof e==="number"&&!d.cssNumber[h]&&(e+="px");if(!j||!("set"in j)||(e=j.set(a,e))!==b)try{i[c]=e}catch(k){}}},css:function(a,c,e){var f,g=d.camelCase(c),h=d.cssHooks[g];c=d.cssProps[g]||g;if(h&&"get"in h&&(f=h.get(a,!0,e))!==b)return f;if(bj)return bj(a,c,g)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]},camelCase:function(a){return a.replace(bc,bm)}}),d.curCSS=d.css,d.each(["height","width"],function(a,b){d.cssHooks[b]={get:function(a,c,e){var f;if(c){a.offsetWidth!==0?f=bn(a,b,e):d.swap(a,bg,function(){f=bn(a,b,e)});if(f<=0){f=bj(a,b,b),f==="0px"&&bl&&(f=bl(a,b,b));if(f!=null)return f===""||f==="auto"?"0px":f}if(f<0||f==null){f=a.style[b];return f===""||f==="auto"?"0px":f}return typeof f==="string"?f:f+"px"}},set:function(a,b){if(!be.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),d.support.opacity||(d.cssHooks.opacity={get:function(a,b){return bb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style;c.zoom=1;var e=d.isNaN(b)?"":"alpha(opacity="+b*100+")",f=c.filter||"";c.filter=ba.test(f)?f.replace(ba,e):c.filter+" "+e}}),c.defaultView&&c.defaultView.getComputedStyle&&(bk=function(a,c,e){var f,g,h;e=e.replace(bd,"-$1").toLowerCase();if(!(g=a.ownerDocument.defaultView))return b;if(h=g.getComputedStyle(a,null))f=h.getPropertyValue(e),f===""&&!d.contains(a.ownerDocument.documentElement,a)&&(f=d.style(a,e));return f}),c.documentElement.currentStyle&&(bl=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!be.test(d)&&bf.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bj=bk||bl,d.expr&&d.expr.filters&&(d.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!d.support.reliableHiddenOffsets&&(a.style.display||d.css(a,"display"))==="none"},d.expr.filters.visible=function(a){return!d.expr.filters.hidden(a)});var bo=/%20/g,bp=/\[\]$/,bq=/\r?\n/g,br=/#.*$/,bs=/^(.*?):\s*(.*?)\r?$/mg,bt=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bu=/^(?:GET|HEAD)$/,bv=/^\/\//,bw=/\?/,bx=/