Skip to main content

Dynamic color and opacity implementation

Β· 6 min read
Kirill Vasin

This is going to be our first dev-log post here πŸ¦„

State and stateless​

Let me give you an example

{RGBColor[1,0,0], Disk[{0,0}, 1]}

What you do see here?

A stateless RGBColor symbol modifies the local scope of a List substituting a new color value read later by Disk. Is the last one is also stateless? No

The beauty and simplicity of dynamics implemented in WLJS Interpreter

{RGBColor[1,0,0], Disk[{0,0}, Offload[radius]]}

is that each instance of a Disk has its own state - DOM element, a couple of properties such as position, radius, color, opacity and etc. It is important to note, that radius here is also an instance with its own state, determined by a symbol radius defined on Wolfram Kernel. Two nested instances can see each other form a couple. When a child changes, a parent is reevaluated with a new data

radius = 1.0;
Graphics[{RGBColor[1,0,0], Disk[{0,0}, Offload[radius]]}]

EventHandler[InputRange[0,1,0.1], Function[value, radius = value]]

What about color?..

Virtual containers​

The name is a bit weird, however, the idea is that if an interpreter sees an attribute of a symbol

g2d.Disk = async (args, env) => {
//... normal evaluation
}

g2d.Disk.update = async () => {
//... when child mutates
}

g2d.Disk.virtual = true

It alters the interpretation and creates a sort of container for this symbol to be evaluated inside it. This container has a local memory, identity and can see other such containers.

We can do in the same way and add this attribute to RGBColor

g2d.RGBColor.virtual = true

Then the construction

{RGBColor[color // Offload], Disk[{0,0}, 1]}

Will work for sure. However, now we coupled the following symbols

  1. RGBColor
    1. color

How to bind Disk to RGBColor instance, which cannot be directly seen?

New update methods and coupling schemes​

What we can do is to provide sort of a reference to env variable to a list of potentially coupled objects, i.e.

g2d.RGBColor = async () => {
//... create references list
const refs = [];
env.exposed.colorRefs = refs;
}

g2d.RGBColor.update = async () => {
//... execute one by one using new data
env.exposed.colorRefs.forEach((instance) => {
instance.execute({method: 'updateColor', color: newColor});
})
}

g2d.RGBColor.virtual = true;

This scheme will update all connected instances. To connect we need to add a couple of line to Disk and other primitives

g2d.Disk = async () => {
//...
if (env.colorRefs) {
//append this instance to a list of references
env.colorRefs.push(env.root);
}
}

g2d.Disk.update = async () => {}
//... regular update method
//for nested expressions
}

g2d.Disk.updateColor = (args, env) => {
//new method just for updating color!
env.local.object.attr('fill', env.color);
}

Here we also defined a additional method for updating just a color of a primitive. The same can be done for Opacity as well.

Of course by turning RGBColor from stateless function into a sort of object comes with a additional overhead for an interpreter and memory. However later on we will check it on our performance tests.

Examples​

This opens up more possibilities for dynamics. In principle, this was the last thing, which was missing for a long time for a complete dynamic evaluating in WLJS Notebook.

Let us see it on a simple example

color = {1,0,0};
Graphics[{RGBColor[color // Offload], Disk[{0,0}, 1]}]

EventHandler[InputJoystick[], Function[xy,
color = Normalize[{xy[[1]], xy[[2]], 0.5}] // Abs;
]]

Or using Opacity and blending between two Disks

opacity = 0.5;
Graphics[{Opacity[Offload[opacity]], Red, Disk[{0,0}, 1], Blue, Opacity[Offload[1.0 - opacity]], Disk[{0,0}, 1]}]

EventHandler[InputRange[0,1,0.1], Function[value,
opacity = value;
]]

Or even cooler - combining it with a traditional dynamics as well

opacity = 0.5;
Graphics[{Opacity[Offload[opacity]], Red, Disk[{0,0}, Offload[1-opacity]], Blue, Opacity[Offload[1.0 - opacity]], Disk[{0,0}, Offload[opacity]]}, ImagePadding->None]

EventHandler[InputRange[0,1,0.1], Function[value,
opacity = value;
]]

Benchmarking​

I have created a complete suite of tests to check the performance of the system as a whole and by certain sections, i.e.

  • Wolfram Kernel
  • WLJS Interpreter
  • HTTP and WebSockets
  • 2D/3D dynamic graphics
  • Stress test with many dynamic objects

It provides stats in the end as well as comparison to others results (if you shared)

more is better, all bars a normalized

You can download this notebook by the link down below Benchmark

According to multiple tests this new feature does not actually impacts the performance that much

The most impact I expected from a test called Bubbles, which involves a lot of creation and destruction of many graphics objects. However, it seems very weak.

See you next time ✨ Kirill