Simon Danisch / Apr 17 2019

WebIO JSCall

This is a prototype to call any JS library easily in Julia with WebIO and makes it much easier to manage state that exists on the JS side.

pkg"up; add WebIO WebSockets"
using WebSockets, WebIO
using WebIO, JSExpr, Observables
mutable struct JSObject
  name::Symbol
  scope::Scope
  typ::Symbol
end
function JSObject(jso::JSObject, typ::Symbol)
  return JSObject(getfield(jso, :name), getfield(jso, :scope), typ)
end
JSObject
0.9s
Julia
Install
#TODO: use WebIO.tojs

function to_js(jso::JSObject)
  return "window.object_pool[\"$(objectid(jso))\"]"
end

to_js(obj::Union{String, Number}) = repr(obj)
to_js(obj::Symbol) = "'$obj'"

function to_js(obj)
  "JSON.parse($(repr(JSON.json(obj))))"
end

function to_js(dict::AbstractDict)
  return sprint() do io
    print(io, "{")
    for (k, v) in dict
      print(io, to_js(k), ':', to_js(v))
    end
    print(io, "}")
  end
end

function to_js(vec::AbstractVector)
  return sprint() do io
    print(io, "[")
    for elem in vec
      print(io, to_js(elem), ',')
    end
    print(io, "]")
  end
end
to_js (generic function with 6 methods)
0.5s
Julia
Install
function WebIO.evaljs(jso::JSObject, js::String)
  task = evaljs(getfield(jso, :scope), JSString(js))
end

function Base.getproperty(jso::JSObject, name::Symbol)
  scope, typ = getfield.((jso,), (:scope, :typ))
  jname = getfield(jso, :name)
  typ = if name === :new
    jsonew = JSObject(jso, :new)
    evaljs(jso, """
    $(to_js(jsonew)) = $(to_js(jso))
    """)
    return jsonew
  else
    result = JSObject(name, scope, typ)
    js = """
    var object = $(to_js(jso))
    var result = object.$(name)
    if(result.bind != undefined){
    	result = result.bind(object)
    }
    window.object_pool[\"$(objectid(result))\"] = result
    """
    evaljs(jso, js)
    return result
  end
end

function Base.setproperty!(jso::JSObject, name::Symbol, val)
  evaljs(jso, """
  $(to_js(jso)).$(name) = $(to_js(val))
  """)
  return val
end

modifier(jso::JSObject) = getfield(jso, :typ) === :new ? "new " : ""

function get_args(args, kw_args)
  if isempty(kw_args)
    return join(to_js.(args), ", ")
  elseif isempty(args)
    return to_js(Dict(kw_args))
  else
    # TODO: I'm not actually sure about this :D 
    error("Javascript only supports keyword arguments OR arguments. Found posititional arguments and keyword arguments")
  end
end

function (jso::JSObject)(args...; kw_args...)
  scope = getfield(jso, :scope); name = getfield(jso, :name)
  result = JSObject(:result, scope, :call)
  input_args = get_args(args, kw_args)
  js = """
  var func = $(to_js(jso))
  var result = $(modifier(jso))func(
		$input_args
  )
  window.object_pool[\"$(objectid(result))\"] = result
  """
  evaljs(jso, js)
  return result
end
function Module(name::Symbol, url::String)
  scope = Scope(imports=[url])
	mod = JSObject(name, scope, :module)
  document = JSObject(:document, scope, :module)
  onimport(scope, @js function (mod)
  	window.object_pool = Dict()
    window.THREE = mod
    window.object_pool[$(string(objectid(mod)))] = mod
    window.object_pool[$(string(objectid(document)))] = document
  end)
  return mod, document
end
Module (generic function with 1 method)
THREE, document = Module(
  :THREE,
  "https://cdnjs.cloudflare.com/ajax/libs/three.js/103/three.js",
)
(getfield(THREE, :scope))(dom"div#container"())
evaljs(THREE, "alert('hi')")
Task (queued) @0x00007f3366aa1390
scene = THREE.new.Scene()
width, height = 500, 500
# Create a basic perspective camera
camera = THREE.new.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.z = 4
renderer = THREE.new.WebGLRenderer(Dict(:antialias => true))
renderer.setClearColor("#ffffff")
geometry = THREE.new.BoxGeometry(1, 1, 1)
material = THREE.new.MeshBasicMaterial(color = "#433F81")
cube = THREE.new.Mesh(geometry, material);
scene.add(cube)
container = document.querySelector("#container")
container.appendChild(renderer.domElement)
for r in LinRange(0.0, 2pi, 200)
  cube.rotation.x = r
	cube.rotation.y = r
	renderer.render(scene, camera)
  sleep(0.01)
end