This is to demonstrate that you can write high-level
graphics-intensive code that runs entirely in the browser. The
objective is maximally convenient portability.
Your can write code that is "mostly" compatible with conventional
visual python. That's a high-level language with a high-level
graphics library. Your code then gets compiled and executed, all in
the browser.
I'm calling this approach "glorpy" ... which is a portmanteau,
alluding to glowscript (the runtime graphics library), visual python
(the top-level programming language) and rapydscript (the compiler
that runs in the browser).
Rapydscript compiles your python code into javascript, which is then
executed by the browser. The compiler and the glowscript graphics
routines exist as .js libraries. The "interesting" part of this
document is the driver code, which takes care of all the details
necessary to call those libraries in a reasonable way.
The source to the libraries can be found at:
https://github.com/BruceSherwood/glowscript
Some of the example programs on the glowscript site were written using
the glorpy approach; look for the ones marked "rapydscript":
http://www.glowscript.org/#/user/GlowScriptDemos/folder/Examples/
The interface to this demo program uses the URL Query-String:
In all cases, error messages will be reported in a special area near bottom of this document.
'P' to toggle seeing python source;
'J' to toggle seeing how it compiles into javascript,
'C' to clear the print area
right-drag to rotate the scene.
// ********************************************
// The javascript driver code.
// Invoke the compiler, then run the compiled code.
// First, some auxiliary functions needed at compile time,
// or otherwise needed by the driver:
var get_trace_win = function() {
var trace_win = document.getElementById('trace-win');
if (!trace_win) {
trace_win = document.createElement("pre");
document.documentElement.appendChild(trace_win)
}
return trace_win
}
var add_trace = function(trace_win, msgline, lineno, codewin,
code_line_start, color) {
var txtnode = document.createTextNode(msgline);
var span = document.createElement("span");
span.appendChild(txtnode);
trace_win.appendChild(span);
// This is an example of an HTML closure:
// Function within a parent function, has permanent access to parent's variables:
if (lineno >= 0) {
span.style.color = color
span.ondblclick = (function() {
var LN = lineno
return function() {
if (codewin.style.height == '0px') codewin.style.height = '300px'
codewin.setSelectionRange(code_line_start[LN-1],
code_line_start[LN])
codewin.focus()
}
})()
}
}
var dump_doc = function(printarea){
var all = document.getElementsByTagName("*");
for (var ii=0; ii < all.length; ii++) {
var xx = all[ii];
printarea.textContent += "t: " + xx.type
+ " c: " + xx.getAttribute("class")
+ " id: " + xx.id + "\n";
}
}
// insert A into B:
var insert = function(inner, outer, where) {
return [outer.slice(0, where), inner, outer.slice(where)].join('')
}
// The main driver:
var driver = function(){
var synerr = args['synerr']
var compiled_code = "<-->"
var errmsg = "---"
var source_line_start = {}
var run_line_start = {}
var add_pyline = function(pyline){
if (pyline > 0 && pyline <= comp.__original.text.length) {
var txt = comp.__original.text[pyline-1]
if (1||txt) {
var msgline = "<<< " + (txt) + "\n"
add_trace(trace_win, msgline, pyline, py_win, source_line_start, "red")
}
}
}
// Beware:
// For a wide range of simple syntax errors, the compiler errors out.
// It replaces the *entire* root document with a terse error message
// which is quite nasty.
//
// Also, under firefox (but not chrome), the compiler clears the
// javascript console and leaves it in an unusable state,
// which is amazingly nasty.
var wrapper = document.getElementById('wrapper');
// Grab the text of the vpython program to compile:
var py_win = document.getElementById('py_win');
var raw_win = document.getElementById('raw_win');
var support_win = document.getElementById('support_win');
var print_win = document.getElementById('print');
var extra_win = document.getElementById('extra_win');
var js_win = document.getElementById('js_win');
if (synerr) py_win.textContent = '# 1\n====ShouldFail==== # 2\n# 3\n'
else {
var prog = raw_win.textContent
if (prog.charAt(0) === "\n") py_win.textContent = prog.substr(1)
else py_win.textContent = prog
}
var py_code = py_win.textContent;
if (verbosity) py_win.style.height = '300px'
var comp = {} // common block for compiler shared data
var errmsg // save for later
try {
var compiled_code = glowscript_compile(py_code,
{lang: "vpython", msg_handler: compiler_msg, shared: comp})
}
catch(err) {
errmsg = err.message // will be used later
var emsg = "Compiler bombed out;"
+ " thrown errmsg: (" + err.message + ")"
+ " file: (" + err.fileName + ")"
+ " line: (" + err.lineNumber + ")"
console.log(emsg)
console.log(err.stack)
var pos = 0;
// N+1 entries labeled 0 through N *inclusive*
source_line_start[0] = pos
for (var ii = 0; ii < comp.__original.text.length; ii++){
pos += 1 + comp.__original.text[ii].length
source_line_start[1+ii] = pos
}
var trace_win = get_trace_win()
add_trace(trace_win, err.message + "\n", -1)
// Kludge: thrown lineNumber is often one unit too big:
add_pyline(err.lineNumber - 1)
add_pyline(err.lineNumber)
if (!verbosity) verbosity = 1
}
if (verbosity > 1) {
// Dump the line-number translation table so we can look at it:
for (var ii in comp.__linenumbers) {
extra_win.textContent += ii + " --> " + comp.__linenumbers[ii] + "\n"
}
} else {
// Don't need to look at the XMP blocks holding the code:
wrapper.removeChild(raw_win)
wrapper.removeChild(support_win)
}
if (verbosity || len(compiler_msg.str)) extra_win.textContent += ""
+ "Compiler message was: '" + compiler_msg.str + "'\n"
var compiled_split = compiled_code.split("\n")
if (verbosity) extra_win.textContent += "compiled code has "
+ compiled_split.length + " lines\n"
if (compiled_code === '' || compiled_code === '<-->'){
console.log('Evaluation skipped')
} else {
// Here with some possibly-usable compiled code.
if (typeof comp.__original !== 'undefined'){
extra_win.textContent += "program has "
+ comp.__original.text.length + " lines\n"
var pos = 0;
// N+1 entries labeled 0 through N *inclusive*
source_line_start[0] = pos
for (var ii = 0; ii < comp.__original.text.length; ii++){
pos += 1 + comp.__original.text[ii].length
source_line_start[1+ii] = pos
}
}
js_win = document.getElementById('js_win');
// The printarea is the /active/ print area.
// In principle, it could be switched from print_win to some other window.
var printarea = print_win
var run_code = "";
if (verbosity) run_code += "console.log('start of code', printarea);";
// The glowscript library always requires the following incantation;
// don't ask me why:
run_code += 'window.__context = '
+ '{ glowscript_container: $("#glowscript") };'
run_code += support_win.textContent
run_code += compiled_code
if (verbosity) run_code += "console.log('end of code');main"
var where = run_code.match("function main.wait. {") //("}")
if (where && where.length) {
var begin = where.index // not where[0].index (don't ask me why)
var size = where[0].length
if (0) run_code = insert(
"console.log('top of main()'"
+ ", arguments.length"
+ ", typeof arguments[0]"
+ ")",
run_code, begin+size)
}
// Show the js code, in case the user is curious:
js_win.textContent = run_code;
if (verbosity) js_win.style.height = '300px'
var run_split = run_code.split("\n")
var pos = 0;
// N+1 entries labeled 0 through N *inclusive*
run_line_start[0] = pos
for (var ii = 0; ii < run_split.length; ii++){
pos += 1 + run_split[ii].length
run_line_start[1+ii] = pos
}
// This eval apparently defines main() but doesn't run it:
var mainprog;
try {
mainprog = eval(run_code);
}
catch(err) {
console.log("Unable to load compiled code...."
+ " thrown errmsg: (" + err.message + ")"
+ " file: (" + err.fileName + ")"
+ " line: (" + err.lineNumber + ")"
)
console.log(err.stack)
return
}
if (verbosity) console.log("after eval", typeof mainprog);
// Actually run the program:
// BEWARE: You must call main() with an argument;
// otherwise bizarre and undocumented things will happen.
// This includes error messages getting blackholed.
// This includes parts of the compiled javascript
// getting executed twice.
try {
mainprog(_hand_basket);
}
catch(err){
console.log("main() bombed out;"
+ " thrown errmsg: (" + err.message + ")"
+ " file: (" + err.fileName + ")"
+ " line: (" + err.lineNumber + ")"
)
console.log(err.stack)
return
}
if (verbosity) console.log("after main()");
}
// Dump all the elements in the root document.
// Among other things, this allows us to see what elements
// the runtime libraries have created.
if (0 && verbosity) dump_doc(extra_win);
}
driver
// end of javascript "driver" code.
// ******************************
"use strict";
////// :::::::: Support functions needed at runtime ::::::::: //////////
var protectHTML = function(rawtext) {
return rawtext
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
if (0) { // optional, for testing keyboard events
var fmt_kbd_event = function(evt){
var msg = ""
// war-and-peace mode:
if (0) {
for (var item in evt) {
msg += item + ' --> ' + evt[item] + "; "
}
return msg
}
// relatively concise mode:
return "type: " + evt.type
+ " key: '" + evt.key + "'"
+ " keyCode: " + evt.keyCode
+ " charCode: " + evt.charCode
+ " which: " + evt.which
+ (evt.shiftKey ? " shift" : "")
+ (evt.ctrlKey ? " ctrl" : "")
+ (evt.altKey ? " alt" : "")
}
var test = document.getElementById('char-test');
test.onkeydown = function(evt){
console.log("char_test onkeydown: " + fmt_kbd_event(evt))
}
test.onkeypress = function(evt){
console.log("char_test onkeypress: " + fmt_kbd_event(evt))
}
}
var biteme = function(){
console.log("bite me")
throw new Error("bite me &")
}
var estack_parse = function(line){
var rslt = {}
var stuff = line.split(/>/);
rslt.depth = stuff.length
rslt.func = stuff[0].split('@')[0]
if (rslt.depth > 1) {
var spec = stuff[stuff.length-1].split(":")
rslt.line = Number(spec[1])
rslt.char = Number(spec[2])
var foo = stuff[stuff.length-2].split("line")
rslt.parent = Number(foo[foo.length-1])
} else {
rslt.parent = -1
rslt.line = -1
rslt.char = -1
}
return rslt
}
var sanitize_lineno = function(cmpline){
var bad = Number(comp.__linenumbers[cmpline-1])
//console.log("???????", cmpline, bad)
for (var ii = cmpline; ii < compiled_code.length; ii++) {
if (comp.__linenumbers[ii-1]) {
var val = Number(comp.__linenumbers[ii-1])
//console.log("checking ln[" + (ii-1) + "] = " + val)
if (val != bad) return
//console.log("sanitizing ln[" + (ii-1) + "] = " + val)
comp.__linenumbers[ii-1] = -2
}
}
}
var trimline = function(txt, where) {
if (where > 35) {
var chomp = where - 30
txt = txt.substr(chomp)
where -= chomp
txt = "⊕" + txt // compare "…"
where++
}
if (txt.length > 70) {
txt = txt.substr(0,70) + "⊕"
}
return insert("▼", txt, where) // compare "☠"
}
// Object used to decode traceback stack:
var egger = function(){
this.line0 = 0
// The arg is supposed to be an Error object:
this.traceback = function(arg) {
var trace_win = get_trace_win()
if (typeof arg.stack !== 'undefined'){
console.log("Runtime error: '" + arg.message + "'\n"
+ " on line: [" + arg.lineNumber + "]"
+ " in file: '" + arg.fileName + "'"
)
var msg = ""
var stack = arg.stack.split("\n")
for (var ii = 0; ii < stack.length; ii++){
var line = stack[ii]
var x = estack_parse(line)
var cmpline = 0
var pyline = -1
var runline = x.line
var fr = ""
fr += "From " + (x.func || '()')
+ " parent: " + x.parent
+ " runline: " + runline
+ " char: " + x.char
// If it was in the compiled code (as opposed to auxilliary code)
// locate the line-number within the python source:
if (runline > this.line0 && x.parent == this.parent0) {
cmpline = runline - this.line0
if (x.func === 'main' || x.func === '__$main') sanitize_lineno(cmpline)
pyline = comp.__linenumbers[cmpline-1]
if (pyline) pyline--
fr += " cmpline: " + cmpline
fr += " pyline: " + pyline
}
fr += "\n"
msg += fr;
add_trace(trace_win, fr, -1)
// Show text of line from python source:
if (pyline > 0) {
var txt = comp.__original.text[pyline-1]
if (1||txt) {
var msgline = "<<< " + (txt) + "\n"
msg += msgline
add_trace(trace_win, msgline, pyline, py_win, source_line_start, "red")
}
}
// Show text of line from compiled code;
// not needed, given that the run-code is a superset,
// but useful for debugging.......
if (0 && cmpline > 0) {
var txt = compiled_split[cmpline-1]
var where = x.char-1
if (1||txt) {
var msgline = ">>> " + (trimline(txt, where)) + "\n"
msg += msgline
add_trace(trace_win, msgline, -1)
}
}
// Show text of line from run code:
if (runline > 0 && x.parent == this.parent0) {
var txt = run_split[runline-1]
var where = x.char-1
if (1||txt) {
var msgline = ">>> " + (trimline(txt, where)) + "\n"
msg += msgline
add_trace(trace_win, msgline, runline, js_win, run_line_start, "blue")
}
}
}
console.log(msg)
if (verbosity > 1) console.log(arg.stack)
throw ''
}
// Arg is not what we were expecting
// Whatever it is, (re)throw it, and hope for the best.
throw arg;
}
// Calibrate the "line0" value:
this.setup = function(arg){
var line = arg.stack.split("\n")
var x = estack_parse(line[0])
//xxx console.log("---- setup:", x.func, x.parent, x.line, x.char)
this.line0 = x.line
this.parent0 = x.parent
}
}
var eggs = new egger()
// This function is passed as the argument to main()
// Implemented as wrapper around egger object.
window._hand_basket = function(arg) {
eggs.traceback(arg)
}
// Very rough, approximate, emergency workarounds for chr() and ord()
var ord = function(ch){
if (len(ch) > 1) return -1
return ch.charCodeAt(0)
}
var chr = function(code){
return String.fromCharCode(code)
}
/////// this needs to be the last thing before the compiled code /////////
try{throw new Error("whatever")}catch(e){eggs.setup(e)}
args = parseQueryString()
#import random
#import visual
if args['import']:
print("hi there")
#---# import causes bizarre failures,
#---# even if the statement is never executed:
#---import coals_to_newcastle
print("still here")
def chompchomp():
if args['rterr']:
biteme()
def chomp():
chompchomp()
# show the args (as specified via URL query string)
if 0:
for arg in dir(args):
val = args[arg]
print(" arg: ", arg, "-->", val, val ? 'yes' : 'no')
num = 123; print(num)
chomp()
print(4*atan(1), verbosity)
print(456, Math.random())
#--# The rapydscript compiler gets confused by try/except blocks:
#--try:
#-- print(chr(38))
#--except:
#-- print("chr failed")
if 1:
print("above ord/chr")
print('&', "-->", ord('&'))
print('38', "-->", chr('38'))
# zz = unichr(38)
print ("below ord/chr")
scene = canvas()
bx = box(length=1.5, height=.3, color=vector(1, 0, 1))
bx.rotate(angle=0.4, axis=vector(1, 0, 0))
sp = sphere(radius=0.5)
def keyDown(ev):
ctrl = ev.ctrl ? ' ctrl' : ''
shift = ev.shift ? ' shift' : ''
alt = ev.alt ? ' alt' : ''
print("Event type: " + ev.type,
" key: '" + ev.key + "'",
" ord: " + ord(ev.key),
" keyCode: " + ev.keyCode,
" charCode: " + ev.charCode,
ctrl, alt, shift)
def pr(x) : print(x, '-->', ev[x])
###dir(ev).map(pr) # for debugging
if ev.key == 'P':
if py_win.style.height == '0px':
py_win.style.height = '300px'
else:
py_win.style.height = '0px'
if ev.key == 'J':
if js_win.style.height == '0px':
js_win.style.height = '300px'
else:
js_win.style.height = '0px'
if ev.key == 'C':
print_options(clear=True)
if printarea.style.height == '0px': printarea.style.height = '300px'
0-0
def mouse(ev):
print('mouse at: ', ev.pos)
scene.bind('keydown', keyDown)
scene.bind('click', mouse)
while 1:
rate(1)
### print(scene.mouse.pos)
#########################
Traceback area. To see the problematic code in context,
double-click on any code-line here. Lines starting with "<<<" are
python source code-lines, while lines starting with ">>>" are the
resulting assembly-language (javascript) code-lines.
The "▼"
symbol indicates where on the line the javascript evaluation machine
thinks the problem is.
Bottom of document.