No matter, have you compiled your own customized version of RPD, or have you downloaded the version with default options, you are ready to build a Patch network. Just ensure that you've included all the required Styles, Toolkits and Modules into your page, this process is also covered in the Setup section.
NB: All the updates inside the Network are based on purely functional code, so there are no actual data modifications performed, only signals are sent. Among other useful things, it allows to easily record and restore things. Your code could be imperative, if you decide, but you should not modify the Network inner structure if you plan to share the code with others.
Setup Rendering
You have to have some target DOM element to render your Patch into. So, first,
there should be the one accessible to the RPD code. It could be a div
:
<body>
<div id="patch-target"></div>
</body>
Anywhere below (or when body.onload
or document.onDocumentReady
event was fired), you may start writing the code to build the RPD network:
<body>
<div id="patch-target"></div>
<script>
var targetElement = document.getElementById('patch-target');
// use SVG Renderer with Quartz Style to render into div#patch-target,
// also configure Renderer to allow multiple connections to inlets
// (only single connection is allowed by default)
Rpd.renderNext('svg', targetElement, { style: 'quartz',
inletAcceptsMultipleLinks: true });
// Network creation code
</script>
</body>
The renderNext
method assumes that everything later on, unless it meets another renderNext
definition, will be rendered to the specified target and following specified options. You also may want to render a particular patch to a particular target with particular renderer, there's a patch.render
method existing specially for that, and it accepts exactly the same arguments, but you need to create a Patch to use it, and we haven't covered it yet.
Rendering Configuration
Options passed to renderNext
or patch.render
could belong to one particular Renderer, but Renderers supplied with RPD tend to use a generalized set of options:
style
— the only required option, sets the style used to visualize nodes; you need to ensure to include this Style code in your RPD version or else this option will fail;fullPage
— (false
) iftrue
, network takes the full page, so the target element will be resized to match browser window size when it was resized by user;nodeMovingAllowed
— (true
) are nodes allowed to be dragged or should they be immovable;inletAcceptsMultipleLinks
— (false
) could several outlets connect to the same inlet;linkForm
— ('line'
) connect inlets to outlets with straight lines or nice curves (set to'curve'
), curves could not be rendered with HTML renderer, also only certain styles support them (quartz
,compact
,compact-v
);black-white
is the only style that supportspipe
form of the link;valuesOnHover
— (false
) show inlet/outlet value only when user hovers over its connector (values are always shown, by default);closeParent
— (false
) when user opens a projected sub-patch, automatically close its parent patch;effectTime
— (1000
) milliseconds to show the value update effect on the connectorshowTypes
— (false
) show inlet/outlet types for debugging purposes;showBoxes
— (false
) show node bounding for debugging purposes;logErrors
— (true
) write all the happening system-wide errors to the console, if it exists;
Further on let's assume that you write subsequent code below those two lines:
var targetElement = document.getElementById('patch-target');
Rpd.renderNext('svg', targetElement, { style: 'quartz',
inletAcceptsMultipleLinks: true });
// Creating a Patch, Adding Nodes, ...
NB: When
fullPage
is set tofalse
, Renderer is usually unaware of the required canvas height, so you need to callpatch.resizeCanvas
for every new Patch, as soon as possible after the correspondingRpd.renderNext
orpatch.render
call.
Creating a Patch
NB: If you what to know what exactly all the terms like Patch, Node, Inlet, Outlet, Link mean in context of RPD, see Terminology sub-section on the Main page.
Patch is a collection and topology of connected Nodes.
And creating a Patch is super-easy:
var patch = Rpd.addPatch();
or, a named Patch:
var patch = Rpd.addPatch('My Patch');
Adding Nodes
Adding nodes is also super-easy in a default form:
var untitledUtilNode = patch.addNode('util/random');
var untitledCustomNode = patch.addNode('my-toolkit/my-node-type');
var titledCustomNode = patch.addNode('my-toolkit/my-node-type', 'My Node');
At least, you need to specify a type of the Node you want to create. Type determines how many inlets/outlets new Node will have, their names and types, and the way (algorithm) it will use to process incoming data before sending it to other nodes. Or, type may determine that this Node won't change the data and just pass it through, but visualize it in some way.
For example, all the Nodes with type util/random
always have two inlets, min
and max
(both accept only numbers), and one outlet named random
. You are free to add other inlets or outlets to any instance of any type, though. When one of the inlets gets new value, Node with util/random
type generates new random number laying between the requested bounds and immediately sends it to the random
outlet. Renderer of the util/random
type ensures that last four generated numbers are also shown in the body of every such Node.
New nodes are positioned in the free space automatically, though the placing algorithm is intentionally not perfect, to keep it simple, so you have the ability to force-move the created node to the desired place if you don't like what machine suggested for you:
patch.addNode('my-toolkit/my-node-type')
.move(100 /* x */, 150 /* y */); // in pixels for HTML, or SVG units
Actually, you may define a new type for a Node anywhere above the place it is created. Having a Renderer for a Node type is absolutely optional:
var patch = Rpd.addPatch('My Patch');
Rpd.nodetype('custom/type', {
inlets: { 'in': { type: 'core/any' } },
outlets: { 'out': { type: 'core/any' } }
});
patch.addNode('custom/type').move(20, 20);
NB: I really know how could it be tempting to name your Outlet
out
, just like in this example, but please avoid that in any case, may be only excluding the cases when your Inlet is calledin
. Always try to find a better alias for an Outlet, especially when it's just a single Outlet of the Node. If there's no documentation for a Toolkit (and there's none for the moment), user should be able to imagine the alias without inspecting code or variable. At least to try. Usually output inlets are named similarly to the type, or if they produce something processed, alias represents how exactly values are transformed, likerandom
Outlet of typeutil/number
inutil/random
Node.
For a complete list of properties could be used to define a Node, see Node Definition in API section. Also see Defining Node Type and Writing a Renderer for a Node chapters in Toolkits section if you want to create bundles of your Node types, called Toolkits.
Connecting Nodes
Nodes process data that came inside through Inlets and then send processed data through their Outlets to Inlets of the other nodes. The connection between Outlet of one node and Inlet of another node is called Link. User may connect one Outlet to several Inlets, and also, if it's allowed by configuration (usually not), connect several Outlets to one Inlet. But this rules are controlled only by Renderer, in the code you may connect outlets to inlets as many times as you want, so you should control existing connections by yourself.
Some nodes have no Inlets, and that's ok, since they probably have some initial state and/or default values for Inlets and/or hidden inlets (a bit later about it). Some nodes may have no outlets if they send nothing, and that's also completely ok!
Nodes may have any number of Inlets and any number of Outlets defined in their type and also may have any number of Inlets and any number of Outlets attached by you to the instance of the Node.
Type-defined Inlets and Outlets are accessible through node.inlets
and node.outlets
properties:
var knob = patch.addNode('util/knob', 'Number');
var color = patch.addNode('util/color', 'Color');
knob.outlets['number'].connect(color.inlets['r']);
Disconnecting is also very easy:
var link = knob.outlets['number'].connect(color.inlets['r']);
...
link.disconnect();
Also, you may temporary disable the link and then enable it later, when you don't want to remove the connection completely. When link is disabled, it skips all the updates coming through, but still exists. So this way link will be shown to the user, but will be greyed out:
var link = knob.outlets['number'].connect(color.inlets['r']);
...
link.disable();
...
link.enable();
There is also a possibility to create custom Nodes just in place by adding Inlets and Outlets of any type to a core/basic
Node:
// through redefining Node instance:
var myNode = patch.addNode('core/basic', 'My Node', {
inlets: {
foo: { type: 'core/any', 'default': 0 },
bar: { type: 'core/any' }
},
outlets: {
out: { type: 'core/any' }
}
process: function(inlets) {
return { out: (inlets.foo || 0) + (inlets.bar || 0) }
}
});
knob.outlets['number'].connect(myNode.inlets['foo']);
// through adding inlets/outlets to the instance
var myNode = patch.addNode('core/basic', 'My Node', {
process: function(inlets) {
return { out: (inlets.foo || 0) + (inlets.bar || 0) }
}
});
var fooInlet = myNode.addInlet('core/any', 'foo', { 'default': 0 });
var barInlet = myNode.addInlet('core/any', 'bar');
var outlet = myNode.addOutlet('core/any', 'out');
knob.outlets['number'].connect(fooInlet);
Actually, you also may add Inlets and Outlets to any instance of the Node of any type using node.addInlet
/node.addOutlet
, but it probably has no sense, since such Nodes usually have inner processing logic hardly bound to what is inside.
In UI, user commonly starts creating a Link from the Outlet and finishes it on the Inlet. That's same for your code. You get the Outlet instance (defined by type or added by you) and connect it to the Inlet instance (defined by type or added by you).
You may notice that Inlets and Outlets also have their own types. Their type determines which data they may accept, connections of which type they allow, how they present the data to the user or how they transform it before sending it to the Node.
Same way as for the Nodes, Channel (Inlets and Outlets together are called Channels in RPD) type may be defined just before Channel usage.
var customNode = patch.addNode('custom/node-type', 'Foo');
Rpd.channeltype('custom/channel-type', {
accept: function(value) {
return (value >= 0) && (value <= 255);
}
});
customNode.addInlet('custom/channel-type', 'foo');
customNode.addOutlet('custom/channel-type', 'bar');
Rpd.nodetype('custom/another-node-type', {
inlets: {
'in': { type: 'custom/channel-type' }
}
});
Node types and Channel Types may intersect, for example there is util/color
Channel which operates with objects in a form of { r: 255, g: 255, b: 255 }
and util/color
Node, which has Inlets r
, g
and b
of type util/wholenumber
and outputs color in the Outlet of type util/color
. Also, the Node includes rectangle filled with current color in its body.
For a complete list of properties could be used to define an Inlet or Outlet, see Channel Definition in API section. As well as Writing a Renderer for a Channel. Also see Defining Channel Type and Writing a Renderer for a Channel chapters in Toolkits section if you want to create bundles of your Node and/or Channel types, called Toolkits.
Sending Data
To send your own data to an Inlet, you may use its receive
method. There's no requirement for this inlet to be connected to anything, but if it is indeed connected, you'll just insert your update in it's established data flow.
var randomNode = patch.addNode('util/random');
randomNode.inlets['max'].receive(256);
var colorNode = patch.addNode('util/color', 'Color');
randomNode.outlets['random'].connect(colorNode.inlets['g']);
randomNode.inlets['bang'].receive({}); // trigger generating random number
setTimeout(function() {
// trigger generating random number once again after a second
randomNode.inlets['bang'].receive({});
}, 1000);
To send some data from an outlet, use it's send
method. You might want it to be connected to something before. The data then will flow through all the connections until the end of the wire or until some Node on the way will interrupt it.
// override the output and send 1000 from Random Generator Node
randomNode.outlets['random'].send(1000);
NB: When you send data to some Inlet, data is first transformed according to Inlet type, if there was such transformation requested. So the Node may receive and may want to receive a bit different data than you've sent to the Inlet. On the other hand, Outlets usually do not transform data and just send them out. So, the law is, always prefer sending data to the Inlets of the Node, since Inlets insert data into the Node processing flow, as it expects it to be received. Or, even better in the cases you do same thing several times, find or define a transformation Node type which will send the data you want and will be re-usable.
Of course you may use inlet.receive
or outlet.send
in any moment after the corresponding Inlet or Outlet was created. If you want, use setTimeout
to postpone the update or setInterval
to send value each period of time. But I have a better suggestion for you.
You may schedule the updates using the Stream approach. Streams are sequences of data distributed over time, and they are the major part of Reactive Programming, so you may find more details on this topic in any documentation covering FRP. Using Streams provides truly a lot of possibilities and combinations, since data flows may be combined and transformed in a lot of ways independently of time when a data itself was produced.
But let me give just the basic example here, so you may either realise the full potential if you already know something about them, or head to FRP docs if you ever plan to use Streams and need complex workflows.
Out of the box, RPD uses Kefir library for Streams, since it's very tiny and neat at the same time. Both Inlets and Outlets have stream
method, which plugs given Kefir Stream into the flow:
randomNode.inlets['bang'].stream(Kefir.interval(1000, {}));
randomNode.outlets['random'].stream(Kefir.sequentially(1000, [1, 2, 3]));
Streams allow you to do really powerful things:
// set red component of the color using the mouse X position
colorNode.inlets['r'].stream(
Kefir.fromEvents(document.body, 'mousemove').throttle(10)
.map(function(event) {
return { x: event.clientX, y: event.clientY };
})
.map(function(position) {
return position.x % 255;
});
);
Adding Sub-patches
Adding Import/Export
Currently, there are only JSON and Plain Modules included with RPD distribution. Both may save Networks to *.json
and *.txt
(Plain Text) files correspondingly and restore them back from these files.
To add JSON export, just ensure to include the Module into your version of RPD, then call this:
var finalize = Rpd.export.json('Name of the Patch');
var patch = Rpd.addPatch(...);
patch.addNode(...);
...
...
console.log(finalize());
And you'll get the full restorable log of actions in JSON format in the console. Flush it to the file and read it back with:
var jsonContent = JSON.parse(readFile('my-file.json'));
Rpd.import.json(jsonContent);
To do the same, but with Plain Text format, just change Rpd.export.json
and Rpd.import.json
to Rpd.export.plain
and Rpd.import.plain
, and you're done!