Making an SVG using Observables
This app mimics this JSServe app but with interactivity provided by Julia code compiled to WebAssembly.
Making the app
Observables are used widely by Julia packages to provide interactivity. For static compilation, they are problematic, though. The Observable
type is not strongly typed, and it's challenging to compile statically.
What we can do is define some Observables and some interactivity and compile the results.
First, we'll create the basics for the updating SVG.
const colors = ["black", "gray", "silver", "maroon", "red", "olive", "yellow", "green", "lime", "teal", "aqua", "navy", "blue", "purple", "fuchsia"]
function circ(cx, cy, r, icol)
["<circle cx='", cx, "' cy='", cy, "' r='", r, "' fill='", colors[icol % length(colors) + 1], "'></circle>"]
end
function set_svg(nsamples, sample_step, phase, radii)
width, height = 900.0, 300.0
cxs_unscaled = [i*sample_step + phase for i in 1:nsamples]
cys = [sin(cxs_unscaled[i]) * height/3 + height/2 for i in 1:nsamples]
cxs = [cxs_unscaled[i] * width/4pi for i in 1:nsamples]
rr = radii
# make an array of strings and numbers to join in JavaScript
geom = Any["<svg width=", width, " height=", height, " ><g>"]
for i in 1:nsamples
append!(geom, circ(cxs[i], cys[i], rr, i))
end
push!(geom, "</g></svg>")
obj = JS.object(geom)
geom = JS.join(obj)
JS.sethtml("plot", geom)
end
The method set_svg
is what we'll ultimately want to compile.
To get there, we need the code that'll compile the Observables. The fix!
methods take a set of Observables, walk their connections, and return a set of methods that will update the Observables provided. These methods unroll calls to the listeners of each Observable to make it easier to statically compile.
using Observables
fix!(os::AbstractObservable...) = fix!(Set{AbstractObservable}(), os...)
function fix!(ctx::Set{AbstractObservable}, x...)
end
function fix!(ctx::Set{AbstractObservable}, observables::Observable...)
setfuns = []
notifies = []
for observable in observables
observable in ctx && continue
push!(ctx, observable)
push!(setfuns, (makeset(ctx, observable), typeof(observable.val)))
end
return setfuns
end
function makeset(ctx::Set{AbstractObservable}, o::Observable)
listeners = tuple((fix!(ctx, l[2]) for l in o.listeners)...)
return val -> begin
o.val = Observables.to_value(val)
nnotify(o, listeners...)
end
end
function fix!(ctx::Set{AbstractObservable}, oa::Observables.OnAny)
return OnAnyHolder(oa.f, tuple(oa.args...))
return val -> begin
f(valargs(args...)...)
return Consume(false)
end
end
mutable struct OnAnyHolder{F,A}
f::F
args::A
end
function (x::OnAnyHolder)(val)
x.f(valargs(x.args...)...)
return Consume(false)
end
function fix!(ctx::Set{AbstractObservable}, mc::Observables.MapCallback)
set! = makeset(ctx, mc.result)
result = mc.result
resultlisteners = tuple((fix!(ctx, l[2]) for l in result.listeners)...)
return MapCallbackHolder(mc.result, mc.f, tuple(mc.args...), resultlisteners)
end
mutable struct MapCallbackHolder{O,F,A,L}
result::O
f::F
args::A
listeners::L
end
function (x::MapCallbackHolder)(val)
x.result.val = x.f(valargs(x.args...)...)
nnotify(x.result, x.listeners...)
return Consume(false)
end
@inline valargs() = ()
@inline valargs(x) = (Observables.to_value(x),)
@inline valargs(x, xs...) = (Observables.to_value(x), valargs(xs...)...)
@inline nnotify(o::Observable) = nothing
@inline nnotify(::Nothing) = nothing
@inline function nnotify(o::Observable, f)
result = f(o.val)
result.x && return true
return false
end
@inline function nnotify(o::Observable, f, fs...)
nnotify(o, f)
nnotify(o, fs...)
return false
end
We also need to patch up some internals in WebAssemblyCompiler. WebAssembly doesn't handle circular references. As a kludge, we assign defaults to circular references. We're okay with that here because these circular references are never used.
W.default(o::Observable{T}) where T = Observable(o.val)
The last part is pretty simple. We'll call onany
to connect our set of input Observables os
to the set_svg
method. fix!
returns a named tuple with a setfuns
component that can be passed to compile
. Each setfuns
method is passed a value appropriate for that Observable. This triggers propagation of updates to the listeners.
onany(set_svg, os...)
setfuns! = fix!(os...)
compile(setfuns!...; names = names, filepath = "observables/observables.wasm")
With this interactivity provided by Observables, we've eliminated almost all of the JavaScript. JS.h
methods are used to define the inputs, including an onupdate
trigger that calls the appropriate WebAssembly function. These also define Observables and collect names for the WebAssembly functions. See examples/observables/observables.jl
for the complete source for this example.