rpd
Version:
RPD is a minimal framework for building Node-Based User Interfaces, powered by Reactive Programming
412 lines (352 loc) • 17.6 kB
JavaScript
;(function(global) {
"use strict";
var Rpd = global.Rpd;
if (typeof Rpd === "undefined" && typeof require !== "undefined") {
Rpd = require('rpd');
}
Rpd.style('ableton', 'svg', function(config) {
var ƒ = Rpd.unit;
var δ = Rpd.Render.data;
function _createSvgElement(name) {
return document.createElementNS(d3.namespaces.svg, name);
}
var lastCanvas = null;
var socketPadding = 25, // distance between inlets/outlets in SVG units
socketsMargin = 15; // distance between first/last inlet/outlet and body edge
var bodySizePadding = 30;
var headerHeight = 20; // height of a node header in SVG units
var letterWidth = 7;
var listeners = {};
var inletToConnector = {},
outletToConnector = {};
/*
var AbletonTheme = {
_palette: [
'#3AFDAA',
'#1EBEAF',
'#65FEE8',
'#FEEDA3',
'#BB6118',
'#FEEE4A',
'#FDA43A',
'#BFB96E',
'#31E7FB',
'#8BFD70',
'#A24FAB',
'#7661C3',
'#CDBCE3',
'#B9C2E2',
'#FC43B2',
'#FD95A7',
'#BACF79',
'#FDA378',
'#E2F134',
'#84FFFF'
],
'one': {
background: '#12181C',
header: '#939EA8',
headerSelection: '#DADDE2',
highlight: '#DADDE2',
highlight2: '#E6E947',
controls0: '#12181C',
controls1: '#22282F',
controls2: '#6F7783',
controls3: '#6495B4',
nodeBody: '#ABB3B8',
nodeBody2: '#999999',
selection: '#FACE3E',
border: '#808C94',
border2: '#ADADAD',
signal: '#59FD39',
error: '#F87829',
},
'two': {
background: '#767676',
header: '#727272',
headerSelection: '#31E9FD',
highlight: '#B5B2B1',
highlight2: '#FACE3E',
controls0: '#767676',
controls1: '#161616',
controls2: '#D6692C',
controls3: '#4286BA',
nodeBody: '#8D8D8D',
nodeBody2: '#999999',
selection: '#9CD1C9',
border: '#5A5A5A',
border2: '#ADADAD',
signal: '#59FD39',
error: '#FC151B',
}
};
var theme = AbletonTheme.one; */
var roundness = 6;
var defs = d3.select(_createSvgElement('defs'));
return {
edgePadding: { horizontal: 30, vertical: 20 },
boxPadding: { horizontal: 20, vertical: 30 },
createCanvas: function(patch, parent) {
var canvas = d3.select(_createSvgElement('g'))
.classed('rpd-patch', true);
canvas.append(ƒ(defs.node()));
canvas.append('rect').attr('width', '100%').attr('height', '100%')
.classed('rpd-background', true);
return { element: canvas.node() };
},
createNode: function(node, render, description, icon) {
var minContentSize = render.size ? { width: render.size.width || 40,
height: render.size.height || 40 }
: { width: 70, height: 40 };
var pivot = render.pivot || { x: 0.5, y: 0.5 };
function findBestNodeSize(numInlets, numOutlets, minContentSize) {
var requiredContentHeight = (2 * socketsMargin) + ((Math.max(numInlets, numOutlets) - 1) * socketPadding);
return { width: minContentSize.width,
height: headerHeight + Math.max(requiredContentHeight, minContentSize.height) };
}
var initialInlets = node.def.inlets,
initialOutlets = node.def.outlets;
var initialSize = findBestNodeSize(initialInlets ? Object.keys(initialInlets).length : 0,
initialOutlets ? Object.keys(initialOutlets).length : 0,
minContentSize);
var longestInletLabel = 0,
longestOutletLabel = 0;
if (initialInlets) {
Object.keys(initialInlets).forEach(function(alias) {
longestInletLabel = Math.max(longestInletLabel,
initialInlets[alias].label ? initialInlets[alias].label.length : alias.length);
});
}
if (initialOutlets) {
Object.keys(initialOutlets).forEach(function(alias) {
longestOutletLabel = Math.max(longestOutletLabel,
initialOutlets[alias].label ? initialOutlets[alias].label.length : alias.length);
});
}
var width = initialSize.width, height = initialSize.height;
var bodyWidth = width,
bodyHeight = height - headerHeight,
inletsMargin = longestInletLabel * letterWidth,
outletsMargin = longestOutletLabel * letterWidth,
fullNodeWidth = inletsMargin + bodyWidth + outletsMargin;
var nodeElm = d3.select(_createSvgElement('g')).attr('class', 'rpd-node');
// append shadow
nodeElm.append('path').attr('class', 'rpd-shadow')
.attr('fill', 'rgba(0,0,0,0.3)')
.attr('d', roundedRect(3, 3, fullNodeWidth, headerHeight + bodyHeight,
roundness, roundness, roundness, roundness));
// append node header
nodeElm.append('path').attr('class', 'rpd-header').classed('rpd-drag-handle', true)
.attr('d', roundedRect(0, 0, fullNodeWidth, headerHeight,
roundness, roundness, 0, 0));
nodeElm.append('text').attr('class', 'rpd-name').text(node.def.title || node.type)
.attr('x', 7).attr('y', 12)
.style('pointer-events', 'none');
// append node body
nodeElm.append('path').attr('class', 'rpd-content')
.attr('fill', 'rgba(150,150,150,.5)')
.attr('d', roundedRect(0, headerHeight, fullNodeWidth, bodyHeight, 0, 0,
roundness, roundness));
nodeElm.append('rect').attr('class', 'rpd-body')
.attr('fill', 'transparent').attr('stroke', '#222').attr('stroke-width', 1)
.attr('width', fullNodeWidth).attr('height', height)
.attr('rx', roundness).attr('ry', roundness)
.style('pointer-events', 'none');
// append tooltip with description
nodeElm.select('.rpd-header')
.append(ƒ(_createSvgElement('title')))
.text(description ? (description + ' (' + node.type + ')') : node.type);
// append remove button
nodeElm.append('g').attr('class', 'rpd-remove-button')
.attr('transform', 'translate(' + (fullNodeWidth-12) + ',1)')
.call(function(button) {
button.append('path').attr('d', roundedRect(-1, 3, 9, 9, 2, 2, 2, 3))
.attr('fill', 'transparent')
.attr('class', 'rpd-remove-button-handle');
button.append('text').text('x').attr('x', 2).attr('y', 8)
.style('pointer-events', 'none');
});
// append placeholders for inlets, outlets and a target element to render body into
var inletsGroup = nodeElm.append('g').attr('class', 'rpd-inlets')
.attr('transform', 'translate(' + 0 + ',' + headerHeight + ')');
var processGroup = nodeElm.append('g').attr('class', 'rpd-process')
.attr('transform', 'translate(' + (inletsMargin + (pivot.x * width)) + ','
+ (headerHeight + ((height - headerHeight) * pivot.y)) + ')');
var outletsGroup = nodeElm.append('g').attr('class', 'rpd-outlets')
.attr('transform', 'translate(' + fullNodeWidth + ',' + headerHeight + ')');
δ(inletsGroup, { position: { x: 0, y: headerHeight } });
δ(outletsGroup, { position: { x: width, y: headerHeight } });
nodeElm.classed('rpd-'+node.type.slice(0, node.type.indexOf('/'))+'-toolkit-node', true)
.classed('rpd-'+node.type.replace('/','-'), true);
var numInlets = 0, numOutlets = 0;
var inletElms = [], outletElms = [];
var lastSize = initialSize;
function checkNodeSize() {
var curSize = lastSize;
var newSize = findBestNodeSize(numInlets, numOutlets, minContentSize);
if ((newSize.width === curSize.width) && (newSize.height === curSize.height)) return;
inletsMargin = longestInletLabel * letterWidth;
outletsMargin = longestOutletLabel * letterWidth;
fullNodeWidth = inletsMargin + newSize.width + outletsMargin;
nodeElm.select('path.rpd-header').attr('d', roundedRect(0, 0, fullNodeWidth, headerHeight,
roundness, roundness, 0, 0));
nodeElm.select('g.rpd-remove-button').attr('transform', 'translate(' + (fullNodeWidth-12) + ',1)');
nodeElm.select('path.rpd-shadow').attr('d', roundedRect(0, 0, fullNodeWidth, newSize.height,
roundness, roundness, roundness, roundness));
nodeElm.select('rect.rpd-body').attr('height', newSize.height).attr('width', fullNodeWidth);
nodeElm.select('path.rpd-content').attr('d', roundedRect(0, headerHeight,
fullNodeWidth, newSize.height - headerHeight,
0, 0, roundness, roundness));
nodeElm.select('g.rpd-process').attr('transform', 'translate(' + (inletsMargin + (pivot.x * newSize.width)) + ','
+ (headerHeight + ((newSize.height - headerHeight) * pivot.y)) + ')');
nodeElm.select('g.rpd-outlets').attr('transform', 'translate(' + fullNodeWidth + ',' + headerHeight + ')');
lastSize = newSize;
}
function recalculateSockets() {
inletElms.forEach(function(inletElm, idx) {
var inletPos = findInletPos(idx);
inletElm.attr('transform', 'translate(' + inletPos.x + ',' + inletPos.y + ')');
//δ(inletElm).position = inletPos;
});
outletElms.forEach(function(outletElm, idx) {
var outletPos = findOutletPos(idx);
outletElm.attr('transform', 'translate(' + outletPos.x + ',' + outletPos.y + ')');
//δ(outletElm).position = outletPos;
});
}
function notifyNewInlet(elm, inlet) {
longestInletLabel = Math.max(longestInletLabel, inlet.label ? inlet.label.length : inlet.alias.length);
numInlets++; inletElms.push(elm); checkNodeSize();
recalculateSockets();
}
function notifyNewOutlet(elm, outlet) {
longestOutletLabel = Math.max(longestOutletLabel, outlet.label ? outlet.label.length : outlet.alias.length);
numOutlets++; outletElms.push(elm); checkNodeSize();
recalculateSockets();
}
function findInletPos(idx) { // index from top to down for Quartz mode, or left to right for PD mode
if (numInlets >= numOutlets) {
return { x: 0, y: socketsMargin + (socketPadding * idx) };
} else {
var fullSide = (2 * socketsMargin) + (numOutlets - 1) * socketPadding;
return { x: 0, y: (fullSide / 2) + (((-1 * (numInlets - 1)) / 2) + idx) * socketPadding };
}
}
function findOutletPos(idx) { // index from top to down for Quartz mode, or left to right for PD mode
if (numOutlets >= numInlets) {
return { x: 0, y: socketsMargin + (socketPadding * idx) };
} else {
var fullSide = (2 * socketsMargin) + (numInlets - 1) * socketPadding;
return { x: 0, y: (fullSide / 2) + (((-1 * (numOutlets - 1)) / 2) + idx) * socketPadding };
}
}
listeners[node.id] = {
inlet: notifyNewInlet,
outlet: notifyNewOutlet
};
return {
element: nodeElm.node(),
size: initialSize
};
},
createInlet: function(inlet, render) {
var inletElm = d3.select(_createSvgElement('g')).attr('class', 'rpd-inlet');
inletElm.call(function(group) {
//group.attr('transform', 'translate(' + inletPos.x + ',' + inletPos.y + ')')
group.append('circle').attr('class', 'rpd-connector')
.attr('fill', '#999').attr('stroke', '#333').attr('stroke-width', 1)
.attr('cx', 0).attr('cy', 0).attr('r', 4);
group.append('g').attr('class', 'rpd-value-holder')
.attr('transform', 'translate(-8,0)')
.attr('text-anchor', 'end')
.append('text').attr('class', 'rpd-value').attr('fill', '#999');
group.append('text').attr('class', 'rpd-name').text(inlet.def.label || inlet.alias)
.attr('x', 8).attr('y', 0);
});
listeners[inlet.node.id].inlet(inletElm, inlet);
inletToConnector[inlet.id] = inletElm.select('.rpd-connector');
return { element: inletElm.node() };
},
createOutlet: function(outlet, render) {
var outletElm = d3.select(_createSvgElement('g')).attr('class', 'rpd-outlet');
outletElm.call(function(group) {
//group.attr('transform', 'translate(' + outletPos.x + ',' + outletPos.y + ')')
group.append('circle').attr('class', 'rpd-connector')
.attr('fill', '#999').attr('stroke', '#333').attr('stroke-width', 1)
.attr('cx', 0).attr('cy', 0).attr('r', 4);
group.append('g').attr('class', 'rpd-value-holder')
.append('text').attr('class', 'rpd-value').attr('fill', '#999')
.attr('x', 10).attr('y', 0)
.style('pointer-events', 'none');
group.append('text').attr('class', 'rpd-name').text(outlet.def.label || outlet.alias)
.attr('x', -8).attr('y', 0);
});
listeners[outlet.node.id].outlet(outletElm, outlet);
outletToConnector[outlet.id] = outletElm.select('.rpd-connector');
return { element: outletElm.node() };
},
createLink: function(link) {
var linkElm = d3.select(_createSvgElement(
(config.linkForm && (config.linkForm == 'curve')) ? 'path' : 'line'
)).attr('class', 'rpd-link')
.attr('fill', 'transparent')
.attr('stroke', 'lightgray')
.attr('stroke-width', 2);
return { element: linkElm.node(),
rotate: function(x0, y0, x1, y1) {
if (config.linkForm && (config.linkForm == 'curve')) {
linkElm.attr('d', bezierByH(x0, y0, x1, y1));
} else {
linkElm.attr('x1', x0).attr('y1', y0)
.attr('x2', x1).attr('y2', y1);
}
},
noPointerEvents: function() {
linkElm.style('pointer-events', 'none');
} };
},
getInletPos: function(inlet) {
var connectorPos = getPos(inletToConnector[inlet.id].node());
return { x: connectorPos.x + 5, y: connectorPos.y + 5 };
},
getOutletPos: function(outlet) {
var connectorPos = getPos(outletToConnector[outlet.id].node());
return { x: connectorPos.x + 5, y: connectorPos.y + 5 };
},
getLocalPos: function(pos) {
if (!lastCanvas) return pos;
// calculate once on patch switch?
var canvasPos = getPos(lastCanvas.node());
return { x: pos.x - canvasPos.x, y: pos.y - canvasPos.y };
},
onPatchSwitch: function(patch, canvas) {
lastCanvas = d3.select(canvas);
},
onNodeRemove: function(node) {
listeners[node.id] = null;
}
};
function bezierByH(x0, y0, x1, y1) {
var mx = x0 + (x1 - x0) / 2;
return 'M' + x0 + ' ' + y0 + ' '
+ 'C' + mx + ' ' + y0 + ' '
+ mx + ' ' + y1 + ' '
+ x1 + ' ' + y1;
}
function roundedRect(x, y, width, height, rtl, rtr, rbr, rbl) {
return "M" + x + "," + y
+ (rtl ? ("v" + rtl
+ "a" + rtl + "," + rtl + " 0 0 1 " + rtl + "," + -rtl) : "")
+ "h" + (width - (rtl ? rtl : 0) - (rtr ? rtr : 0))
+ (rtr ? ("a" + rtr + "," + rtr + " 0 0 1 " + rtr + "," + rtr) : "")
+ "v" + (height - (rtr ? rtr : 0) - (rbr ? rbr : 0))
+ (rbr ? ("a" + rbr + "," + rbr + " 0 0 1 " + -rbr + "," + rbr) : "")
+ "h" + ((rbr ? rbr : 0) + (rbl ? rbl : 0) - width)
+ (rbl ? ("a" + rbl + "," + rbl + " 0 0 1 " + -rbl + "," + -rbl) : "")
+ "v" + ((rbl ? rbl : 0) + (rtl ? rtl : 0) - height)
+ "z";
}
function getPos(elm) { var bounds = elm.getBoundingClientRect();
return { x: bounds.left, y: bounds.top } };
});
})(this);