A Max/node.js based server/client system for drawing to web-browsers. Created at the Hamburg University of Music and Drama within the Stage_2.0 project.
Note: Drawsocket has been used in large scale production, but as with all technology, the system must be tested before use in prodution. Users are advised to thoroughly test your production network and display devices before any live performance.
Requires Max version >= 8.1.0, and is designed for use with CNMAT's Odot library and MaxScore.
The drawsocket
abstraction is a wrapper around the Node For Max's node.script
object, and relies on a set of scripts found in the package's code
folder. Once the package is installed in the Packages folder, the main server and client scripts will be automatically handled by the abstraction.
To install:
- Place the
drawsocket
repository in the/Documents/Max 8/Packages
folder. - When running the
drawsocket
server for the first time: click on thescript npm install
message to download the required packages and libraries (note that you will need to be connected to the internet for the download to work).
See the drawsocket.maxhelp
file for examples.
- Start the server by sending the
script start
message. - On successful startup, an IP address and port number will be printed to the Max console, and the same information will be sent out of the right-most outlet of the abstraction.
- Open a browser and type in the IP address and port specified in the Max patch, followed by a URL of your choosing. This address will be how you address the client browser from the Max server patch.
- For example, if the IP:Port is
192.168.1.1:3002
, and you wanted to use the address/foo
for your OSC style messaging to the browser, you could type the following address into your browser:192.168.1.1:3002/foo
. Note that if you are testing on the same computer, you can also uselocalhost
instead of the IP address.
- For example, if the IP:Port is
For usage with MaxScore, please see Drawsocket
folder in the Max Extras menu, example number 8, render2browser.maxscore
.
If you wish to serve file assets to your client browsers (e.g. images, pdfs, sound files, html files, etc.), the files must be in a known folder to the server, commonly referred to as a root "public" folder. The public folder path is set relative to the location of the Max patch containing the drawsocket
abstraction, and therefore you need to save your patch before any assets can be found (so that the patch has a folder location).
By default, the public folder is set to be the same folder that contains the Max patch.
someFolder
|-- yourPatch.maxpatch
|-- image.png
|-- score.pdf
Alternatively, to keep the folders a little neater, the drawsocket
abstraction can be initialized with an argument of the folder path relative to the Max patch location. For example, if you initialize drawsocket
with the relative path public
, it will expect the folder public
to be at the same folder level:
someFolder
|-- yourPatch.maxpatch
|-- public
|-- image.png
|-- score.pdf
By default the drawsocket
server responds to all URL requests with the template HTML page, drawsocket-page.html
which loads the required Javascript files, sets up the basic HTML objects, layers, and imports the drawsocket-default.css
which sets up some default display properties.
If desired, a different template HTML page may be used by sending the drawsocket
object the html_template
message followed by a relative path to the template HTML file to use.
All messages in the drawsocket
API are formatted as an object, enclosed by curly braces { }
. Messages can be encoded as Odot nested sub-bundles, or nested JSON objects. In the examples below we will be using OSC (odot) formatting, however you may also use a Max Dictionary, which will be exactly the same, except that address names will not have the leading /
character.
Odot:
/bundle : {
/subbundle : {
/foo : 1
}
}
Max Dictionary (JSON):
"bundle" : {
"subbundle" : {
"foo" : 1
}
}
The URL used by the client to log into the server IP address and port is used by the Max patch as an OSC address to route messages to all clients logged into a given URL. For example, any users logged into the example above, 192.168.1.1:3002/foo
, will receive OSC messages with the address /foo
.
Messages to the client are formatted as objects with key
and val
addresses
- The
key
value is a switch key which tells the client how it should interpret the messages in theval
field. For example, validkey
values includesvg
,html
,tween
, and so on. See below for more details on these options. - The
val
value, stores one or more objects to be handled by the client.
For example, with a svg
key, the val
object might create a new SVG object. In this example, we ask all clients logged into /foo
to create a new SVG rect
, using the drawsocket-SVG new
keyword:
/foo : {
/key : "svg",
/val : {
/new : "rect",
/id : "rectangular",
/x : 100,
/y : 100,
/width : 25,
/height : 25
}
}
The wildcard *
will match all URL clients, so for example if you replace /foo
above with the address /*
the above example would be sent to all clients.
Each drawn object needs to have a unique name to identify the object. The name can be any combination of numbers and letters, but needs to be unique. This id can be used to identify the object in situations where you want to change the color, position or other attributes.
For example, in the above example, we we set the id
to be the name "rectangular". If we have already created the object (in this case using the drawsocket-SVG new
keyword), we can alter attributes of the rectangle, by referring to the id
. Here we change the width of the rectangle:
/foo : {
/key : "svg",
/val : {
/id : "rectangular",
/width : 50
}
}
Each key
type has a slightly different API based on the needs of the objects created in the browser.
The svg
key specifies that the val
objects will be used to create or modify SVG elements, which will be placed by default in the webpage's main SVG element.
The object(s) set to the client via the val
field predominantly consist of attributes that configure the created object, as specified by the SVG specification, available online in many places. See the Mozilla SVG documentation for information about the basic SVG object types.
Object attributes may be set as members of the val
object, as demonstrated above.
In addition there are several keywords used by drawsocket
to handle special cases.
new
: tells the SVG creation routine to create a new object of a given SVG type. See the SVG documentation for information on object types and their associated attribute options.parent
: SVG nodes may be inserted as child objects of another element, (most often an SVG groupg
element) -- aliascontainer
child
: SVG nodes may also be created as dependents of a newly created group node. Mainly this is useful for cases where you don't want to have anid
for a child node, for example when setting objects in the special SVGdefs
group (described below).text
: sets the inner text of atext
node.href
: sets the address for linked assets, used by theimage
anduse
SVG elements.
HTML objects may be used inside SVG using a foreignObject element to wrap the HTML content. To create HTML elements within a SVG parent element, the drawsocket
new
keyword recognizes the identifier prefix html:
as a flag to create a HTML node instead of an SVG element. For example:
/* : {
/key : "svg",
/val : {
/new : "foreignObject",
/x : 100,
/y : 100,
/width : "100%",
/height : "100%",
/children : {
/new : "html:div",
/text : "foo"
}
}
}
A sub-bundle labeled style
may optionally be included which will set inline CSS style properties for the created node, which will be applied by the browser, depending on the SVG specification, and the browser's implementation.
For example:
/* : {
/key : "svg",
/val : {
/new : "path",
/id : "bar",
/d : "M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80",
/style : {
/stroke : "red",
/stroke-width : 2
}
}
}
There are three levels of inheritance with SVG CSS styling:
- presentation attributes, set within the element, as demonstrated above in the context of SVG object attributes.
- stylesheet definitions, set in an attached CSS stylesheet, or within an
<style>
element in the HTML page (settable via the "css" key detailed below). - inline styling, a set of CSS rules included as part of an elements's
style
attribute. Indrawsocket
we set these values via the/style
sub-bundle.
Each is overridden by the next: stylesheets override presentation attributes, and inline styles override all the others.
See the online CSS documentation for more informaiton.
SVG elements are drawn in the order they are created via the new
keyword (and remain in layer order if updated after creation).
Sometimes it is useful to separate groups of objects in specific drawing layer order to maintain background, mid, and foreground layers. The can be achived using the SVG group element.
For example, we can first create three layers that we will name "back", "main", and "overlay", using an array of objects.
/* : {
/key : "svg",
/val : [{
/new : "g",
/id : "back"
}, {
/new : "g",
/id : "main"
}, {
/new : "g",
/id : "overlay"
}]
}
Note that the array specifies the order of the objects in the message, so "back" will be drawn first, then "main", then "overlay".
When objects are added to any of these groups, they will be appended to the end of the stack of child nodes within the group, however, all objects in the "back" group will appear behind the "main" and "overlay" layers; while objects added to the "overlay" layer will always be above the other layers, and so on.
Preexisting object maybe reused with the use
SVG element, which uses the id
of another SVG object by reference, set by the href
attribute.
There are two approaches recommended for reusing SVG elements:
The SVG specification provides a special storage element within the SVG object called defs
(see the online documentation for more details). Objects stored in the defs
are not drawn to the screen, but are stored as prototypes of objects that can be drawn with the use
tag. The defs
can be used in this way for storing a library of objects. To add an object to the defs
group, you can use the parent
keyword to tell the client to add a new object (or group of objects) to the defs
, for example:
/* : {
/key : "svg",
/val : {
/parent : "defs",
/new : "rect",
/id : "squarePrototype",
/x : 50,
/y : 50,
/width : 20,
/height : 20
}
}
To use our new defs
object, we refer to it by id
in the use
element's href
attribute, prefixed by the id selector #
:
/* : {
/key : "svg",
/val : {
/new : "use",
/id : "sq",
/href : "#squarePrototype",
/x : 0,
/y : 0
}
}
Note that the x
y
coordinates of the use
object set the top left corner of the original object. That means that in this case, the top left corder of the drawn rectangle will be at {50,50}
not {0,0}
. So, for best results, set defs
object positions relative to {0,0}
.
The use
element's href
attribute can also be used to import an object by reference to an id
present in an SVG file asset. For example, if there is a file called "demo.svg" stored in the public folder, you could reference an element with the id foo
by using the #
id selector:
/* : {
/key : "svg",
/val : {
/new : "use",
/id : "importedObject",
/href : "demo.svg#foo",
/x : 0,
/y : 0
}
}
An additional utility provided by drawsocket
provides an offset to the imported object, offsetting its origin (top-left corner) to be {0,0}
, helpful for placing objects intuitively in a new context. To enable this offsetting, send an extra 1
value for the href
attribute. For example:
/* : {
/key : "svg",
/val : {
/new : "use",
/id : "importedObject",
/href : ["demo.svg#foo", 1],
/x : 0,
/y : 0
}
}
The clear
and remove
keys have similar, but slightly different behaviours.
remove
: removes the objects matching each of theids
set in theval
field.clear
: removes the children of the object, or key type, specified in theval
field.
For example, to clear all SVG elements, you can send the messages:
/* : {
/key : "clear",
/val : "svg"
}
To clear the elements of g
layer, use the clear
message. For example, using the layers we created above, we might want to clear the "main" and "overlay" layers while leaving the "back" layer:
/* : {
/key : "clear",
/val : ["main", "overlay"]
}
To remove a specific object, use the remove
message. For example:
/* : {
/key : "remove",
/val : "main"
}
Note that removing a g
object, will remove all of its children and the g
object. This means that you will not be able to add objects to the g
group "main" until you recreate it with the new
keyword.
The css
key allows the user to to add new CSS rules to the webpage's <style>
tag. See the online documentation for more information.
There are two keywords used by the drawsocket
API: selector
, and props
.
The selector
sets the CSS rule's selector, which could be one of three types:
- type selector, which applies to all objects of a given type (e.g.
line
) - class selector, prefixed by the
.
character (e.g..example
), which applies the rule to all objects with the matchingclass
attribute. - id selector, prefixed by the
#
character (e.g.#example
), which is a unique id, and only applies the rule to one object.
The props
nested object, contains the properties for the CSS rule.
For example, here we create two rules, one for line
objects, and one specifically for objects with the id
"bar":
/* : {
    /key : "css",
    /val : [{
        /selector : "line",
        /props : {
            /stroke : "red",
            /stroke-width : 5
        }
    }, {
        /selector : "#bar",
        /props : {
            /stroke : "black",
            /stroke-width : 10
        }
    }]
}
The html
key uses the same keywords as the svg
key type:
new
: tells the HTML creation routine to create a new object of a given HTML type. See the HTML documentation for information on object types and their associated attribute options.parent
: HTML nodes may be inserted as child objects of another element (most often an HTMLdiv
element). -- aliascontainer
child
: HTML nodes may also be created as dependents of a newly created group node.text
: sets the inner text of an HTML node (e.g.div
orp
).href
: sets the address for linked assets.
Some HTML objects also can be manipulated with JS object methods, via the call
keyword:
call
: an object or array of objects contains calls to HTML objectmethod
with optionalargs
, for example HTMLMediaElement methods, see online documentation for more information.method
: element method to call.args
: optional arguments to method call (see note below for methods requiring separated arguments).then
: optional next method to call on the returned value from the previous call.
Note: if a method requires separated arguments, rather than a single object, you can use an array of objects for args
, each containing the key val
to set the value of that argument.
This is still in beta, but hypothetically the example below should work for a video player, you may need to click on the browser window first before the browser will let you call this though due to privacy restrictions.
/* : {
/key : "html",
/val : {
/id : "foovideo",
/call : {
/method : "play"
}
}
}
By default HTML objects will be added to an HTML div
object, one layer below the SVG content (see the drawsocket-page.html
file for details). However, HTML content requiring user interaction, for example input
forms, or media players, will not be clickable if located behind the SVG layer. Therefore a special-purpose, top-layer div
is set in the HTML file, called forms
which will should always be reachable by user interaction. Set objects in the forms
layer just as you would for other parent objects, using the parent
keyword.
Additionaly, HTML layers may be created with a new div
tag, just as discussed above via the g
SVG tag.
drawsocket
provides access to the GreenSock Animation Platform TweenMax and TimelineMax libraries via the tween
key. The object types are identified by the keywords used.
TweenMax objects use the keywords:
id
: a unique identifier for the TweenMax object.target
: a CSS selector to choose which objects are animated (e.g.#foo
).dur
: the duration of the animation in seconds.vars
: an object containing the variables to animate, and their destination values, and any other TweenMax special properties (see the TweenMax vars documentation for more information).cmd
: an action command to a new or preexistingtween
. Understood commands are:start
: start playing thetween
, from the beginning, using the message's timestamp to determine an offset start time to be synchronized with the server clock. If the duration is already past, sets the object to its final position.play
: start playing thetween
from whatever its current position is, synchronised to the server clock.playfrom
: if atime
parameter is also found in the command object, start playing thetween
from the time specified by thetime
parameter.time
(requried), sets the time to start from in seconds.
stop
: stops thetween
at the current position.pause
: stops thetween
at the current position.- optionally, if the parameter
time
is found, pause, and move the playhead to that time.
- optionally, if the parameter
reset
: resets the object to its original position.reverse
: reversestween
direction.kill
: kills thetween
but doesn't delete it (not sure how useful this is).
TimelineMax objects use the keywords:
id
: a unique identifier for the TimelineMax object.init
: an object of TimelineMax initialization variables.tweens
: antween
object, or sequential array oftween
objects containing the keywords:target
: a CSS selector to choose which objects are animated (e.g.#foo
).dur
: the duration of the tween animation in seconds, note that within the timeline, the tweens are sequential, so eachdur
is the duration for that section within the timeline.vars
: an object containing the variables to animate, and their destination values, and any other TweenMax special properties (see the TweenMax vars documentation for more information).
cmd
: same as thetween
commands, but applied to the timeline.
A special function
keyword has been added to handle the tween
var
callbacks. For example:
/* : {
/key : "tween",
/val : {
/id : "clock-tween",
/target : "#clock",
/dur : 3600,
/vars : {
/x : "+=0",
/ease : "linear",
/paused : "true",
/onUpdate : {
/function : "
let text = document.getElementById('clock');
let seconds = this.time() % 60;
if( seconds < 10 ) seconds = '0'+seconds;
let minutes = Math.floor( this.time() / 60);
if( minutes < 10 ) minutes = '0'+minutes;
text.innerHTML = minutes+':'+seconds;
"
}
}
}
}
See the TweenMax vars documentation, for more information about callbacks.
PDF files may be imported into drawsocket
by reference, the settable attributes are as follows:
href
: the path location of the .pdf file, relative to the Max patch.page
: the page to open in the .pdf file.- size and position attributes:
x
,y
,width
, andheight
/* : {
/key : "pdf",
/val : {
/id : "newpdf",
/href : "/media/flint_piccolo_excerpt.pdf",
/width : 600,
/x : 100,
/page : 2
}
}
drawsocket
includes the tone.js library, note: currently in beta.
Keywords:
new
: creates a new instace of a Tone type (e.g Tone.Oscillator, would be/new : "Oscillator"
).id
: unique identifier to use for this sound object.vars
: an object of variables used on initialization of a Tone.js object, refer to the Tone.js API for for informaiton.call
: an object or array of objects contains calls to HTML objectmethod
with optionalargs
, see the Tone.js API for more informaiton on object methods.method
: element method to call.args
: optional arguments to method call (see note below for methods requiring separated arguments).then
: optional next method to call on the returned value from the previous call.
Note: if a method requires separated arguments, rather than a single object, you can use an array of objects for args
, each containing the key val
to set the value of that argument.
Some examples:
/* : {
/key : "sound",
/val : {
/new : "Player",
/id : "kick",
/vars : {
/url : "/media/808_mp3/kick1.mp3",
/autostart : "false",
/loop : "false"
},
/call : {
/method : "toMaster"
}
}
}
Replay a sound via the restart
object method:
/* : {
/key : "sound",
/val : {
/id : "kick",
/call : {
/method : "restart"
}
}
}
Play a chord on a PolySynth via the triggerAttackRelease
function:
/* : {
/key : "sound",
/val : {
/id : "psynth",
/call : {
/method : "triggerAttackRelease",
/args : [{
/val : ["Eb3", "G4", "C4"]
}, {
/val : 0.1
}]
}
}
}
To set the value of a Tone.js Signal object, for example the frequency of an Oscillator, we need to use a the set
operator, which functions a bit like the call
operator, but sets a member value rather than calling a function.
The set
keyword should contain an object or array of objects, with member keywords:
member
: the name of the property to set.value
: the value to assign to the property.
For example:
/* : {
/key : "sound",
/val : {
/id : "sine",
/set : {
/member : "frequency.value",
/value : 1000
}
}
}
In some cases we need to refer to an object from a library or one that we have created, which needs to be passed to a function that makes connections between things.
For this (currently only in the Tone.js interface) you can use the obj
and get
message to get an element from an object.
For example, the Tone.js PolySynth needs a member of the Tone libarary to set as the voice
type. To do this we can use an object argument with the obj
and get
keywords, here to to ge the Synth member of the Tone library.
/* : {
/key : "sound",
/val : {
/new : "PolySynth",
/id : "psynth",
/vars : {
/polyphony : 4,
/volume : 0,
/detune : 0,
/voice : {
/obj : "Tone",
/get : "Synth"
}
},
/call : {
/method : "toMaster"
}
}
}
The drawsocket object itself maybe referred to in JS scripts, for example in an object event watcher. The drawsocket
object, exposes the following methods for general usage:
drawsocket.input
: the main entry point to the client scriptdrawsocket.send
: the WebSocket interface to send a JS object back to the server.drawsocket.url
(aliasdrawsocket.oscprefix
): the URL/OSC-prefix of the client, useful for self identifying when sending messages back to the server.drawsocket.syncOffset()
: function which returns timesync offset.
For example, the following message creates a new HTML input field, where users can type. When the user hits the Enter key, the script will send the value of the HTML form to the server prefixed by the client's OSC-prefix, and the id
of this object:
/* : {
/key : "html",
/val : {
/parent : "forms",
/new : "input",
/type : "text",
/id : "userinput",
/name : "userinput",
/size : 10,
/onkeydown : "if( event.key == 'Enter' ){
let obj = {};
obj[ drawsocket.oscprefix+'/'+this.id+'/input' ] = this.value;
drawsocket.send( obj );
}"
}
}
drawsocket.setInputListener(cb_fn)
: sets additional callback handlers forkey
values.drawsocket.setConnectionCallback(cb_fn)
: sets callback function for notificaiton of socket connection.
Clients can load JSON of stored messages, formatted in the drawsocket
API detailed here.
Keywords:
fetch
: (required) URL of file to fetch relative to thedrawsocket
root html folder (by default this is the same as the folder that the patch containg thedrawsocket
is saved in.,prefix
: (optional) the URL prefix to load into the page. If no prefix is specifiedfetch
will load only the prefix matching the client URL.
For example, here we tell clients logged into the URL /foo
to load the messages for URL /bar
from the file "savedState.json".
/foo : {
/key : "file",
/val : {
/fetch : "savedState.json",
/prefix : "/bar"
}
}
The event
keyword provides a mechanism for scheduling future object sent to the drawsocket.input
function.
id
: the id of the eventdel
: the delay time in millisecondsschedule
: the delay in milliseconds, which will be offset by the clock sync offset.obj
: an object to be sent todrawsocket.input
, containingkey
andval
values.
For example, the following example, an event is created and set with a delay (del
) of 1000ms (1s). After this delay, the object stored at the the obj
address, is sent to drawsocket.input
and is executed, resulting in a short diagonal line, with the id
"foo1".
/* : {
/key : "event",
/val : {
/id : "event1",
/del : 1000,
/obj : {
/key : "svg",
/val : {
/new : "line",
/id : "foo2",
/x1 : 10,
/x2 : 20,
/y1 : 30,
/y2 : 10
}
}
}
}
The do_sync
keyword triggers the client clock time sychronization routine.
/foo : {
/key : "do_sync",
/val : 1
}
The writeSVG
keyword requests the SVG element from a client browser. The result is saved to disk in the local folder of the patch containing the drawsocket
object.
For example:
/URLtoWrite : {
/key : "writeSVG",
/val : 1
}
will output the file: path/to/patch/downloaded-URLtoWrite.svg
.
The drawsocket
object in Max accepts the writecache
message,to write the current cached messages to a file on disk.
The folder path is relative to the folder path of the patch in which the drawsocket
object is in.
Message syntax:
writecache <relative folder path>/<filename>.json
or, to write only one URL prefix:
writecache <relative folder path>/<filename>.json /myURLPrefix
The drawsocket
object in Max accepts the importcache
message, to read a file from disk and import one or all prefix
objects in the file.
The folder path is relative to the folder path of the patch in which the drawsocket
object is in.
Message syntax:
importcache <relative folder path>/<filename>.json
or, to read only one URL prefix:
importcache <relative folder path>/<filename>.json /myURLPrefix
A stored server/client state, saved in JSON format, may also be for online viewing, without the realtime WebSocket system, by serving the drawsocket-default.html
file (with the associated scripts, and CSS files), and specifying a file name and prefix to load as discussed above via the file
key.
For example, on a website called www.foo.com
and a stored JSON file named stored-cache.json
, we could load the /1
OSC-URL prefix by using the following URL arguments (using the standard ?
,&
, =
special characters):
http://www.foo.com/drawsocket-default.html?fetch=stored-cache.json&prefix=/1
(Of course you could also save the HTML file under a differnt name of your choosing for your server)
The drawsocket
object accepts the ping
Max message to query the connection status of one or more clients.
For example, the message ping /*
pings all clients.
The drawsocket
object accepts the statereq
Max message to trigger a client update request for one or more clients.
For example, the message statereq /*
triggers a state request for all clients.
The drawsocket
object accepts the port
Max message to set the server port number. Takes effect on start up.
The drawsocket
object accepts the html_root
Max message to add a public asset folder to the server search path. Takes effect on start up.
Clients can send messages to other URLs on the server by using the signalPeer
key sent to the server.
url
: URL OSC address to send the message to,*
will send to everyone (including the sender)val
: message to send to the peer client(s)
For example, here is a button that sends a message to another client on being clicked:
/foo : {
/key : "html",
/val : {
/parent : "forms",
/id : "button-foo",
/new : "button",
/text : "click me!",
/style : {
/position : "absolute",
/top : "100px",
/left : "100px",
/width : "70px"
},
/onclick : "
drawsocket.send({
key: 'signalPeer',
url: '/bar',
val: {
key: 'svg',
val: {
new: 'text',
text: 'hello bar!',
x: 200,
y: 200
}
}
})
"
}
}
function
: create and call user defined funcitons from JSON format.body
,args
{
/key : "funciton",
/val : {
/args : ["a", "b"],
/body : " console.log( a + b ); "
}
}
-
in some object type parsers, the parameters passed an object including the address
/funciton
can be used to create an anonymous function:/id : "foo", /onload : { /function : "drawsocket.send({ loaded : "foo" }) }
-
get
- ... needs some testing -
selector
object with one of the following:attr
: attribute to getcoord
: coord to get from bbox, valid options are bbox parameters fromgetBoundingClientRect
, plus addedcx
, andcy
values for the center of the bbox.
{ /new : "line", /id : "connector", /x1 : { /selector : "#hello > .notehead", /coord : "cx" }, /y1 : { /selector : "#hello > .notehead", /coord : "cy" }, /x2 : { /selector : "#hello ~ .note > .notehead", /coord : "cx" }, /y2 : { /selector : "#hello ~ .note > .notehead", /coord : "cy" }, /stroke : "green" }
-
relativeTo
make an element relative to another, note: the reference element must exist already- takes an argument of a string CSS style selector
/* : { /key : "svg", /val : [{ /new : "circle", /id : "foo", /cx : 100, /cy : 100, /r : 5, /fill : "blue" }, { /new : "circle", /relativeTo : "#foo", /id : "bar", /cx : 100, /cy : 100, /r : 3, /fill : "red" }] }
- optionally
relativeTo
can ben an object with aselector
and bbox anchor points, plus cx and cy for center values.
{ /new : "circle", /relativeTo : { /selector : "#hello2 > .notehead", /anchor_x : "right", /anchor_y : "cy" }, /id : "bar", /cx : 0, /cy : 0, /r : 1, /fill : "red" }