Add support for faded connectors

This commit is contained in:
David Evans 2018-05-12 00:34:11 +01:00
parent b25c5dafb4
commit e85890563c
16 changed files with 479 additions and 71 deletions

View File

@ -81,9 +81,12 @@ function findSamples(content) {
* code: ( * code: (
* 'theme chunky\n' + * 'theme chunky\n' +
* 'define ABC as A, DEF as B\n' + * 'define ABC as A, DEF as B\n' +
* 'A is red\n' +
* 'B is blue\n' +
* 'A -> B\n' + * 'A -> B\n' +
* 'B -> ]\n' + * 'B -~ ]\n' +
* '] -> B\n' + * 'divider space with height 0\n' +
* '] ~-> B\n' +
* 'B -> A\n' + * 'B -> A\n' +
* 'terminators fade' * 'terminators fade'
* ), * ),

View File

@ -598,6 +598,10 @@
'stroke-linejoin': 'miter', 'stroke-linejoin': 'miter',
}, },
}, },
'fade': {
short: 2,
size: 16,
},
'cross': { 'cross': {
short: 7, short: 7,
radius: 3, radius: 3,
@ -985,6 +989,10 @@
'stroke-linecap': 'round', 'stroke-linecap': 'round',
}, },
}, },
'fade': {
short: 3,
size: 12,
},
'cross': { 'cross': {
short: 10, short: 10,
radius: 5, radius: 5,
@ -2960,6 +2968,10 @@
'stroke-linejoin': 'miter', 'stroke-linejoin': 'miter',
}, },
}, },
'fade': {
short: 2,
size: 10,
},
'cross': { 'cross': {
short: 8, short: 8,
radius: 4, radius: 4,
@ -4424,6 +4436,7 @@
{tok: '', type: 0}, {tok: '', type: 0},
{tok: '<', type: 1}, {tok: '<', type: 1},
{tok: '<<', type: 2}, {tok: '<<', type: 2},
{tok: '~', type: 3},
]; ];
const mTypes = [ const mTypes = [
{tok: '-', type: 'solid'}, {tok: '-', type: 'solid'},
@ -4434,19 +4447,27 @@
{tok: '', type: 0}, {tok: '', type: 0},
{tok: '>', type: 1}, {tok: '>', type: 1},
{tok: '>>', type: 2}, {tok: '>>', type: 2},
{tok: 'x', type: 3}, {tok: '~', type: 3},
{tok: 'x', type: 4},
]; ];
const arrows = (combine([lTypes, mTypes, rTypes])
.filter((arrow) => (arrow[0].type !== 0 || arrow[2].type !== 0))
);
const types = new Map(); const types = new Map();
arrows.forEach((arrow) => { combine([lTypes, mTypes, rTypes]).forEach((arrow) => {
const [left, line, right] = arrow;
if(left.type === 0 && right.type === 0) {
// A line without arrows cannot be a connector
return;
}
if(left.type === 3 && line.type === 'wave' && right.type === 0) {
// ~~ could be fade-wave-none or none-wave-fade
// We allow only none-wave-fade to resolve this
return;
}
types.set(arrow.map((part) => part.tok).join(''), { types.set(arrow.map((part) => part.tok).join(''), {
left: arrow[0].type, left: left.type,
line: arrow[1].type, line: line.type,
right: arrow[2].type, right: right.type,
}); });
}); });
@ -5776,6 +5797,18 @@
'fill': 'transparent', 'fill': 'transparent',
}; };
const MASK_PAD = 5;
function applyMask(shape, maskShapes, env, bounds) {
if(!maskShapes.length) {
return;
}
const mask = env.svg.el('mask')
.attr('maskUnits', 'userSpaceOnUse')
.add(env.svg.box({'fill': '#FFFFFF'}, bounds), ...maskShapes);
shape.attr('mask', 'url(#' + env.addDef(mask) + ')');
}
class Arrowhead { class Arrowhead {
constructor(propName) { constructor(propName) {
this.propName = propName; this.propName = propName;
@ -5800,9 +5833,9 @@
} }
} }
render(layer, theme, pt, dir) { render({layer}, env, pt, dir) {
const config = this.getConfig(theme); const config = this.getConfig(env.theme);
const short = this.short(theme); const short = this.short(env.theme);
layer.add(config.render(config.attrs, { layer.add(config.render(config.attrs, {
dir, dir,
height: config.height, height: config.height,
@ -5834,13 +5867,58 @@
} }
} }
class Arrowfade {
getConfig(theme) {
return theme.connect.arrow.fade;
}
render({lineMask}, env, pt, dir) {
const config = this.getConfig(env.theme);
const {short, size} = config;
let fadeID = null;
const delta = MASK_PAD / (size + MASK_PAD * 2);
if(dir.dx >= 0) {
fadeID = env.addDef('arrowFadeL', () => env.svg.linearGradient({}, [
{'offset': delta * 100 + '%', 'stop-color': '#000000'},
{'offset': (100 - delta * 100) + '%', 'stop-color': '#FFFFFF'},
]));
} else {
fadeID = env.addDef('arrowFadeR', () => env.svg.linearGradient({}, [
{'offset': delta * 100 + '%', 'stop-color': '#FFFFFF'},
{'offset': (100 - delta * 100) + '%', 'stop-color': '#000000'},
]));
}
const p1 = {x: pt.x + dir.dx * short, y: pt.y + dir.dy * short};
const p2 = {x: p1.x + dir.dx * size, y: p1.y + dir.dy * size};
const box = env.svg.box({'fill': 'url(#' + fadeID + ')'}, {
height: Math.abs(p1.y - p2.y) + MASK_PAD * 2,
width: size + MASK_PAD * 2,
x: Math.min(p1.x, p2.x) - MASK_PAD,
y: Math.min(p1.y, p2.y) - MASK_PAD,
});
lineMask.push(box);
}
width(theme) {
return this.getConfig(theme).short;
}
height() {
return 0;
}
lineGap(theme) {
return this.getConfig(theme).short;
}
}
class Arrowcross { class Arrowcross {
getConfig(theme) { getConfig(theme) {
return theme.connect.arrow.cross; return theme.connect.arrow.cross;
} }
render(layer, theme, pt, dir) { render({layer}, env, pt, dir) {
const config = this.getConfig(theme); const config = this.getConfig(env.theme);
layer.add(config.render({ layer.add(config.render({
radius: config.radius, radius: config.radius,
x: pt.x + config.short * dir.dx, x: pt.x + config.short * dir.dx,
@ -5871,6 +5949,7 @@
}, },
new Arrowhead('single'), new Arrowhead('single'),
new Arrowhead('double'), new Arrowhead('double'),
new Arrowfade(),
new Arrowcross(), new Arrowcross(),
]; ];
@ -5950,8 +6029,9 @@
const dx1 = lArrow.lineGap(env.theme, line); const dx1 = lArrow.lineGap(env.theme, line);
const dx2 = rArrow.lineGap(env.theme, line); const dx2 = rArrow.lineGap(env.theme, line);
const rad = env.theme.connect.loopbackRadius;
const rendered = line.renderRev({ const rendered = line.renderRev({
rad: env.theme.connect.loopbackRadius, rad,
x1: x1 + dx1, x1: x1 + dx1,
x2: x2 + dx2, x2: x2 + dx2,
xR, xR,
@ -5960,15 +6040,24 @@
}); });
clickable.add(rendered.shape); clickable.add(rendered.shape);
lArrow.render(clickable, env.theme, { const lineMask = [];
lArrow.render({layer: clickable, lineMask}, env, {
x: rendered.p1.x - dx1, x: rendered.p1.x - dx1,
y: rendered.p1.y, y: rendered.p1.y,
}, {dx: 1, dy: 0}); }, {dx: 1, dy: 0});
rArrow.render(clickable, env.theme, { rArrow.render({layer: clickable, lineMask}, env, {
x: rendered.p2.x - dx2, x: rendered.p2.x - dx2,
y: rendered.p2.y, y: rendered.p2.y,
}, {dx: 1, dy: 0}); }, {dx: 1, dy: 0});
applyMask(rendered.shape, lineMask, env, {
height: y2 - y1 + MASK_PAD * 2,
width: xR + rad - Math.min(x1, x2) + MASK_PAD * 2,
x: Math.min(x1, x2) - MASK_PAD,
y: y1 - MASK_PAD,
});
} }
renderSelfConnect({label, agentIDs, options}, env, from, yBegin) { renderSelfConnect({label, agentIDs, options}, env, from, yBegin) {
@ -6066,8 +6155,20 @@
const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy}; const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy};
const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy}; const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy};
lArrow.render(clickable, env.theme, p1, {dx, dy}); const lineMask = [];
rArrow.render(clickable, env.theme, p2, {dx: -dx, dy: -dy});
lArrow.render({layer: clickable, lineMask}, env, p1, {dx, dy});
rArrow.render({layer: clickable, lineMask}, env, p2, {
dx: -dx,
dy: -dy,
});
applyMask(rendered.shape, lineMask, env, {
height: Math.abs(y2 - y1) + MASK_PAD * 2,
width: Math.abs(x2 - x1) + MASK_PAD * 2,
x: Math.min(x1, x2) - MASK_PAD,
y: Math.min(y1, y2) - MASK_PAD,
});
return { return {
lArrow, lArrow,
@ -9044,6 +9145,10 @@
}, PENCIL.normal), }, PENCIL.normal),
render: this.renderArrowHead.bind(this), render: this.renderArrowHead.bind(this),
}, },
'fade': {
short: 0,
size: 12,
},
'cross': { 'cross': {
short: 5, short: 5,
radius: 3, radius: 3,

File diff suppressed because one or more lines are too long

View File

@ -598,6 +598,10 @@
'stroke-linejoin': 'miter', 'stroke-linejoin': 'miter',
}, },
}, },
'fade': {
short: 2,
size: 16,
},
'cross': { 'cross': {
short: 7, short: 7,
radius: 3, radius: 3,
@ -985,6 +989,10 @@
'stroke-linecap': 'round', 'stroke-linecap': 'round',
}, },
}, },
'fade': {
short: 3,
size: 12,
},
'cross': { 'cross': {
short: 10, short: 10,
radius: 5, radius: 5,
@ -2960,6 +2968,10 @@
'stroke-linejoin': 'miter', 'stroke-linejoin': 'miter',
}, },
}, },
'fade': {
short: 2,
size: 10,
},
'cross': { 'cross': {
short: 8, short: 8,
radius: 4, radius: 4,
@ -4424,6 +4436,7 @@
{tok: '', type: 0}, {tok: '', type: 0},
{tok: '<', type: 1}, {tok: '<', type: 1},
{tok: '<<', type: 2}, {tok: '<<', type: 2},
{tok: '~', type: 3},
]; ];
const mTypes = [ const mTypes = [
{tok: '-', type: 'solid'}, {tok: '-', type: 'solid'},
@ -4434,19 +4447,27 @@
{tok: '', type: 0}, {tok: '', type: 0},
{tok: '>', type: 1}, {tok: '>', type: 1},
{tok: '>>', type: 2}, {tok: '>>', type: 2},
{tok: 'x', type: 3}, {tok: '~', type: 3},
{tok: 'x', type: 4},
]; ];
const arrows = (combine([lTypes, mTypes, rTypes])
.filter((arrow) => (arrow[0].type !== 0 || arrow[2].type !== 0))
);
const types = new Map(); const types = new Map();
arrows.forEach((arrow) => { combine([lTypes, mTypes, rTypes]).forEach((arrow) => {
const [left, line, right] = arrow;
if(left.type === 0 && right.type === 0) {
// A line without arrows cannot be a connector
return;
}
if(left.type === 3 && line.type === 'wave' && right.type === 0) {
// ~~ could be fade-wave-none or none-wave-fade
// We allow only none-wave-fade to resolve this
return;
}
types.set(arrow.map((part) => part.tok).join(''), { types.set(arrow.map((part) => part.tok).join(''), {
left: arrow[0].type, left: left.type,
line: arrow[1].type, line: line.type,
right: arrow[2].type, right: right.type,
}); });
}); });
@ -5776,6 +5797,18 @@
'fill': 'transparent', 'fill': 'transparent',
}; };
const MASK_PAD = 5;
function applyMask(shape, maskShapes, env, bounds) {
if(!maskShapes.length) {
return;
}
const mask = env.svg.el('mask')
.attr('maskUnits', 'userSpaceOnUse')
.add(env.svg.box({'fill': '#FFFFFF'}, bounds), ...maskShapes);
shape.attr('mask', 'url(#' + env.addDef(mask) + ')');
}
class Arrowhead { class Arrowhead {
constructor(propName) { constructor(propName) {
this.propName = propName; this.propName = propName;
@ -5800,9 +5833,9 @@
} }
} }
render(layer, theme, pt, dir) { render({layer}, env, pt, dir) {
const config = this.getConfig(theme); const config = this.getConfig(env.theme);
const short = this.short(theme); const short = this.short(env.theme);
layer.add(config.render(config.attrs, { layer.add(config.render(config.attrs, {
dir, dir,
height: config.height, height: config.height,
@ -5834,13 +5867,58 @@
} }
} }
class Arrowfade {
getConfig(theme) {
return theme.connect.arrow.fade;
}
render({lineMask}, env, pt, dir) {
const config = this.getConfig(env.theme);
const {short, size} = config;
let fadeID = null;
const delta = MASK_PAD / (size + MASK_PAD * 2);
if(dir.dx >= 0) {
fadeID = env.addDef('arrowFadeL', () => env.svg.linearGradient({}, [
{'offset': delta * 100 + '%', 'stop-color': '#000000'},
{'offset': (100 - delta * 100) + '%', 'stop-color': '#FFFFFF'},
]));
} else {
fadeID = env.addDef('arrowFadeR', () => env.svg.linearGradient({}, [
{'offset': delta * 100 + '%', 'stop-color': '#FFFFFF'},
{'offset': (100 - delta * 100) + '%', 'stop-color': '#000000'},
]));
}
const p1 = {x: pt.x + dir.dx * short, y: pt.y + dir.dy * short};
const p2 = {x: p1.x + dir.dx * size, y: p1.y + dir.dy * size};
const box = env.svg.box({'fill': 'url(#' + fadeID + ')'}, {
height: Math.abs(p1.y - p2.y) + MASK_PAD * 2,
width: size + MASK_PAD * 2,
x: Math.min(p1.x, p2.x) - MASK_PAD,
y: Math.min(p1.y, p2.y) - MASK_PAD,
});
lineMask.push(box);
}
width(theme) {
return this.getConfig(theme).short;
}
height() {
return 0;
}
lineGap(theme) {
return this.getConfig(theme).short;
}
}
class Arrowcross { class Arrowcross {
getConfig(theme) { getConfig(theme) {
return theme.connect.arrow.cross; return theme.connect.arrow.cross;
} }
render(layer, theme, pt, dir) { render({layer}, env, pt, dir) {
const config = this.getConfig(theme); const config = this.getConfig(env.theme);
layer.add(config.render({ layer.add(config.render({
radius: config.radius, radius: config.radius,
x: pt.x + config.short * dir.dx, x: pt.x + config.short * dir.dx,
@ -5871,6 +5949,7 @@
}, },
new Arrowhead('single'), new Arrowhead('single'),
new Arrowhead('double'), new Arrowhead('double'),
new Arrowfade(),
new Arrowcross(), new Arrowcross(),
]; ];
@ -5950,8 +6029,9 @@
const dx1 = lArrow.lineGap(env.theme, line); const dx1 = lArrow.lineGap(env.theme, line);
const dx2 = rArrow.lineGap(env.theme, line); const dx2 = rArrow.lineGap(env.theme, line);
const rad = env.theme.connect.loopbackRadius;
const rendered = line.renderRev({ const rendered = line.renderRev({
rad: env.theme.connect.loopbackRadius, rad,
x1: x1 + dx1, x1: x1 + dx1,
x2: x2 + dx2, x2: x2 + dx2,
xR, xR,
@ -5960,15 +6040,24 @@
}); });
clickable.add(rendered.shape); clickable.add(rendered.shape);
lArrow.render(clickable, env.theme, { const lineMask = [];
lArrow.render({layer: clickable, lineMask}, env, {
x: rendered.p1.x - dx1, x: rendered.p1.x - dx1,
y: rendered.p1.y, y: rendered.p1.y,
}, {dx: 1, dy: 0}); }, {dx: 1, dy: 0});
rArrow.render(clickable, env.theme, { rArrow.render({layer: clickable, lineMask}, env, {
x: rendered.p2.x - dx2, x: rendered.p2.x - dx2,
y: rendered.p2.y, y: rendered.p2.y,
}, {dx: 1, dy: 0}); }, {dx: 1, dy: 0});
applyMask(rendered.shape, lineMask, env, {
height: y2 - y1 + MASK_PAD * 2,
width: xR + rad - Math.min(x1, x2) + MASK_PAD * 2,
x: Math.min(x1, x2) - MASK_PAD,
y: y1 - MASK_PAD,
});
} }
renderSelfConnect({label, agentIDs, options}, env, from, yBegin) { renderSelfConnect({label, agentIDs, options}, env, from, yBegin) {
@ -6066,8 +6155,20 @@
const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy}; const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy};
const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy}; const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy};
lArrow.render(clickable, env.theme, p1, {dx, dy}); const lineMask = [];
rArrow.render(clickable, env.theme, p2, {dx: -dx, dy: -dy});
lArrow.render({layer: clickable, lineMask}, env, p1, {dx, dy});
rArrow.render({layer: clickable, lineMask}, env, p2, {
dx: -dx,
dy: -dy,
});
applyMask(rendered.shape, lineMask, env, {
height: Math.abs(y2 - y1) + MASK_PAD * 2,
width: Math.abs(x2 - x1) + MASK_PAD * 2,
x: Math.min(x1, x2) - MASK_PAD,
y: Math.min(y1, y2) - MASK_PAD,
});
return { return {
lArrow, lArrow,
@ -9044,6 +9145,10 @@
}, PENCIL.normal), }, PENCIL.normal),
render: this.renderArrowHead.bind(this), render: this.renderArrowHead.bind(this),
}, },
'fade': {
short: 0,
size: 12,
},
'cross': { 'cross': {
short: 5, short: 5,
radius: 3, radius: 3,

View File

@ -42,6 +42,7 @@ const CONNECT = {
{tok: '', type: 0}, {tok: '', type: 0},
{tok: '<', type: 1}, {tok: '<', type: 1},
{tok: '<<', type: 2}, {tok: '<<', type: 2},
{tok: '~', type: 3},
]; ];
const mTypes = [ const mTypes = [
{tok: '-', type: 'solid'}, {tok: '-', type: 'solid'},
@ -52,19 +53,27 @@ const CONNECT = {
{tok: '', type: 0}, {tok: '', type: 0},
{tok: '>', type: 1}, {tok: '>', type: 1},
{tok: '>>', type: 2}, {tok: '>>', type: 2},
{tok: 'x', type: 3}, {tok: '~', type: 3},
{tok: 'x', type: 4},
]; ];
const arrows = (combine([lTypes, mTypes, rTypes])
.filter((arrow) => (arrow[0].type !== 0 || arrow[2].type !== 0))
);
const types = new Map(); const types = new Map();
arrows.forEach((arrow) => { combine([lTypes, mTypes, rTypes]).forEach((arrow) => {
const [left, line, right] = arrow;
if(left.type === 0 && right.type === 0) {
// A line without arrows cannot be a connector
return;
}
if(left.type === 3 && line.type === 'wave' && right.type === 0) {
// ~~ could be fade-wave-none or none-wave-fade
// We allow only none-wave-fade to resolve this
return;
}
types.set(arrow.map((part) => part.tok).join(''), { types.set(arrow.map((part) => part.tok).join(''), {
left: arrow[0].type, left: left.type,
line: arrow[1].type, line: line.type,
right: arrow[2].type, right: right.type,
}); });
}); });

View File

@ -453,31 +453,62 @@ describe('Sequence Parser', () => {
const parsed = parser.parse( const parsed = parser.parse(
'A->B\n' + 'A->B\n' +
'A->>B\n' + 'A->>B\n' +
'A-~B\n' +
'A-xB\n' +
'A<-B\n' + 'A<-B\n' +
'A<->B\n' + 'A<->B\n' +
'A<->>B\n' + 'A<->>B\n' +
'A<-~B\n' +
'A<-xB\n' +
'A<<-B\n' + 'A<<-B\n' +
'A<<->B\n' + 'A<<->B\n' +
'A<<->>B\n' + 'A<<->>B\n' +
'A-xB\n' + 'A<<-~B\n' +
'A<<-xB\n' +
'A~-B\n' +
'A~->B\n' +
'A~->>B\n' +
'A~-~B\n' +
'A~-xB\n' +
'A-->B\n' + 'A-->B\n' +
'A-->>B\n' + 'A-->>B\n' +
'A--~B\n' +
'A--xB\n' +
'A<--B\n' + 'A<--B\n' +
'A<-->B\n' + 'A<-->B\n' +
'A<-->>B\n' + 'A<-->>B\n' +
'A<--~B\n' +
'A<--xB\n' +
'A<<--B\n' + 'A<<--B\n' +
'A<<-->B\n' + 'A<<-->B\n' +
'A<<-->>B\n' + 'A<<-->>B\n' +
'A--xB\n' + 'A<<--~B\n' +
'A<<--xB\n' +
'A~--B\n' +
'A~-->B\n' +
'A~-->>B\n' +
'A~--~B\n' +
'A~--xB\n' +
'A~>B\n' + 'A~>B\n' +
'A~>>B\n' + 'A~>>B\n' +
'A~~B\n' +
'A~xB\n' +
'A<~B\n' + 'A<~B\n' +
'A<~>B\n' + 'A<~>B\n' +
'A<~>>B\n' + 'A<~>>B\n' +
'A<~~B\n' +
'A<~xB\n' +
'A<<~B\n' + 'A<<~B\n' +
'A<<~>B\n' + 'A<<~>B\n' +
'A<<~>>B\n' + 'A<<~>>B\n' +
'A~xB\n' 'A<<~~B\n' +
'A<<~xB\n' +
'A~~>B\n' +
'A~~>>B\n' +
'A~~~B\n' +
'A~~xB\n'
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
@ -488,31 +519,62 @@ describe('Sequence Parser', () => {
right: 1, right: 1,
}), }),
PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 2}), PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 2}),
PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 3}),
PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 4}),
PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 0}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 0}),
PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 1}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 1}),
PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 2}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 2}),
PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 3}),
PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 4}),
PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 0}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 0}),
PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 1}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 1}),
PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 2}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 2}),
PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 3}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 3}),
PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 4}),
PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 0}),
PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 1}),
PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 2}),
PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 3}),
PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 4}),
PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 1}), PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 1}),
PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 2}), PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 2}),
PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 3}),
PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 4}),
PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 0}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 0}),
PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 1}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 1}),
PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 2}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 2}),
PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 3}),
PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 4}),
PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 0}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 0}),
PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 1}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 1}),
PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 2}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 2}),
PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 3}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 3}),
PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 4}),
PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 0}),
PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 1}),
PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 2}),
PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 3}),
PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 4}),
PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 1}), PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 1}),
PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 2}), PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 2}),
PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 3}),
PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 4}),
PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 0}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 0}),
PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 1}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 1}),
PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 2}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 2}),
PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 3}),
PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 4}),
PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 0}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 0}),
PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 1}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 1}),
PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 2}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 2}),
PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 3}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 3}),
PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 4}),
PARSED.connect(['A', 'B'], {left: 3, line: 'wave', right: 1}),
PARSED.connect(['A', 'B'], {left: 3, line: 'wave', right: 2}),
PARSED.connect(['A', 'B'], {left: 3, line: 'wave', right: 3}),
PARSED.connect(['A', 'B'], {left: 3, line: 'wave', right: 4}),
]); ]);
}); });

View File

@ -6,6 +6,18 @@ const OUTLINE_ATTRS = {
'fill': 'transparent', 'fill': 'transparent',
}; };
const MASK_PAD = 5;
function applyMask(shape, maskShapes, env, bounds) {
if(!maskShapes.length) {
return;
}
const mask = env.svg.el('mask')
.attr('maskUnits', 'userSpaceOnUse')
.add(env.svg.box({'fill': '#FFFFFF'}, bounds), ...maskShapes);
shape.attr('mask', 'url(#' + env.addDef(mask) + ')');
}
class Arrowhead { class Arrowhead {
constructor(propName) { constructor(propName) {
this.propName = propName; this.propName = propName;
@ -30,9 +42,9 @@ class Arrowhead {
} }
} }
render(layer, theme, pt, dir) { render({layer}, env, pt, dir) {
const config = this.getConfig(theme); const config = this.getConfig(env.theme);
const short = this.short(theme); const short = this.short(env.theme);
layer.add(config.render(config.attrs, { layer.add(config.render(config.attrs, {
dir, dir,
height: config.height, height: config.height,
@ -64,13 +76,58 @@ class Arrowhead {
} }
} }
class Arrowfade {
getConfig(theme) {
return theme.connect.arrow.fade;
}
render({lineMask}, env, pt, dir) {
const config = this.getConfig(env.theme);
const {short, size} = config;
let fadeID = null;
const delta = MASK_PAD / (size + MASK_PAD * 2);
if(dir.dx >= 0) {
fadeID = env.addDef('arrowFadeL', () => env.svg.linearGradient({}, [
{'offset': delta * 100 + '%', 'stop-color': '#000000'},
{'offset': (100 - delta * 100) + '%', 'stop-color': '#FFFFFF'},
]));
} else {
fadeID = env.addDef('arrowFadeR', () => env.svg.linearGradient({}, [
{'offset': delta * 100 + '%', 'stop-color': '#FFFFFF'},
{'offset': (100 - delta * 100) + '%', 'stop-color': '#000000'},
]));
}
const p1 = {x: pt.x + dir.dx * short, y: pt.y + dir.dy * short};
const p2 = {x: p1.x + dir.dx * size, y: p1.y + dir.dy * size};
const box = env.svg.box({'fill': 'url(#' + fadeID + ')'}, {
height: Math.abs(p1.y - p2.y) + MASK_PAD * 2,
width: size + MASK_PAD * 2,
x: Math.min(p1.x, p2.x) - MASK_PAD,
y: Math.min(p1.y, p2.y) - MASK_PAD,
});
lineMask.push(box);
}
width(theme) {
return this.getConfig(theme).short;
}
height() {
return 0;
}
lineGap(theme) {
return this.getConfig(theme).short;
}
}
class Arrowcross { class Arrowcross {
getConfig(theme) { getConfig(theme) {
return theme.connect.arrow.cross; return theme.connect.arrow.cross;
} }
render(layer, theme, pt, dir) { render({layer}, env, pt, dir) {
const config = this.getConfig(theme); const config = this.getConfig(env.theme);
layer.add(config.render({ layer.add(config.render({
radius: config.radius, radius: config.radius,
x: pt.x + config.short * dir.dx, x: pt.x + config.short * dir.dx,
@ -101,6 +158,7 @@ const ARROWHEADS = [
}, },
new Arrowhead('single'), new Arrowhead('single'),
new Arrowhead('double'), new Arrowhead('double'),
new Arrowfade(),
new Arrowcross(), new Arrowcross(),
]; ];
@ -180,8 +238,9 @@ export class Connect extends BaseComponent {
const dx1 = lArrow.lineGap(env.theme, line); const dx1 = lArrow.lineGap(env.theme, line);
const dx2 = rArrow.lineGap(env.theme, line); const dx2 = rArrow.lineGap(env.theme, line);
const rad = env.theme.connect.loopbackRadius;
const rendered = line.renderRev({ const rendered = line.renderRev({
rad: env.theme.connect.loopbackRadius, rad,
x1: x1 + dx1, x1: x1 + dx1,
x2: x2 + dx2, x2: x2 + dx2,
xR, xR,
@ -190,15 +249,24 @@ export class Connect extends BaseComponent {
}); });
clickable.add(rendered.shape); clickable.add(rendered.shape);
lArrow.render(clickable, env.theme, { const lineMask = [];
lArrow.render({layer: clickable, lineMask}, env, {
x: rendered.p1.x - dx1, x: rendered.p1.x - dx1,
y: rendered.p1.y, y: rendered.p1.y,
}, {dx: 1, dy: 0}); }, {dx: 1, dy: 0});
rArrow.render(clickable, env.theme, { rArrow.render({layer: clickable, lineMask}, env, {
x: rendered.p2.x - dx2, x: rendered.p2.x - dx2,
y: rendered.p2.y, y: rendered.p2.y,
}, {dx: 1, dy: 0}); }, {dx: 1, dy: 0});
applyMask(rendered.shape, lineMask, env, {
height: y2 - y1 + MASK_PAD * 2,
width: xR + rad - Math.min(x1, x2) + MASK_PAD * 2,
x: Math.min(x1, x2) - MASK_PAD,
y: y1 - MASK_PAD,
});
} }
renderSelfConnect({label, agentIDs, options}, env, from, yBegin) { renderSelfConnect({label, agentIDs, options}, env, from, yBegin) {
@ -296,8 +364,20 @@ export class Connect extends BaseComponent {
const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy}; const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy};
const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy}; const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy};
lArrow.render(clickable, env.theme, p1, {dx, dy}); const lineMask = [];
rArrow.render(clickable, env.theme, p2, {dx: -dx, dy: -dy});
lArrow.render({layer: clickable, lineMask}, env, p1, {dx, dy});
rArrow.render({layer: clickable, lineMask}, env, p2, {
dx: -dx,
dy: -dy,
});
applyMask(rendered.shape, lineMask, env, {
height: Math.abs(y2 - y1) + MASK_PAD * 2,
width: Math.abs(x2 - x1) + MASK_PAD * 2,
x: Math.min(x1, x2) - MASK_PAD,
y: Math.min(y1, y2) - MASK_PAD,
});
return { return {
lArrow, lArrow,

View File

@ -170,6 +170,10 @@ export default class BasicTheme extends BaseTheme {
'stroke-linejoin': 'miter', 'stroke-linejoin': 'miter',
}, },
}, },
'fade': {
short: 2,
size: 16,
},
'cross': { 'cross': {
short: 7, short: 7,
radius: 3, radius: 3,

View File

@ -179,6 +179,10 @@ export default class ChunkyTheme extends BaseTheme {
'stroke-linecap': 'round', 'stroke-linecap': 'round',
}, },
}, },
'fade': {
short: 3,
size: 12,
},
'cross': { 'cross': {
short: 10, short: 10,
radius: 5, radius: 5,

View File

@ -170,6 +170,10 @@ export default class MonospaceTheme extends BaseTheme {
'stroke-linejoin': 'miter', 'stroke-linejoin': 'miter',
}, },
}, },
'fade': {
short: 2,
size: 10,
},
'cross': { 'cross': {
short: 8, short: 8,
radius: 4, radius: 4,

View File

@ -201,6 +201,10 @@ export default class SketchTheme extends BaseTheme {
}, PENCIL.normal), }, PENCIL.normal),
render: this.renderArrowHead.bind(this), render: this.renderArrowHead.bind(this),
}, },
'fade': {
short: 0,
size: 12,
},
'cross': { 'cross': {
short: 5, short: 5,
radius: 3, radius: 3,

19
spec/images/Fade.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,14 +1,15 @@
export default [ export default [
'Connect.svg', 'AgentOptions.svg',
'SelfConnect.svg',
'Divider.svg',
'DividerMasking.svg',
'Asynchronous.svg', 'Asynchronous.svg',
'Block.svg', 'Block.svg',
'CollapsedBlocks.svg', 'CollapsedBlocks.svg',
'Connect.svg',
'Divider.svg',
'DividerMasking.svg',
'Fade.svg',
'Markdown.svg',
'Parallel.svg',
'Reference.svg', 'Reference.svg',
'ReferenceLayering.svg', 'ReferenceLayering.svg',
'Markdown.svg', 'SelfConnect.svg',
'AgentOptions.svg',
'Parallel.svg',
]; ];

View File

@ -200,6 +200,10 @@
code: '[ -> {Agent1}: {Message1}\n{Agent1} -> ]: {Message2}', code: '[ -> {Agent1}: {Message1}\n{Agent1} -> ]: {Message2}',
title: 'Arrows to/from the sides', title: 'Arrows to/from the sides',
}, },
{
code: '{Agent1} -~ ]: {Message1}\n{Agent1} <-~ ]: {Message2}',
title: 'Fading arrows',
},
{ {
code: 'text right: {Message}', code: 'text right: {Message}',
preview: ( preview: (

File diff suppressed because one or more lines are too long

View File

@ -197,6 +197,10 @@ export default [
code: '[ -> {Agent1}: {Message1}\n{Agent1} -> ]: {Message2}', code: '[ -> {Agent1}: {Message1}\n{Agent1} -> ]: {Message2}',
title: 'Arrows to/from the sides', title: 'Arrows to/from the sides',
}, },
{
code: '{Agent1} -~ ]: {Message1}\n{Agent1} <-~ ]: {Message2}',
title: 'Fading arrows',
},
{ {
code: 'text right: {Message}', code: 'text right: {Message}',
preview: ( preview: (