663 lines
14 KiB
JavaScript
663 lines
14 KiB
JavaScript
define([
|
|
'./BaseTheme',
|
|
'svg/SVGUtilities',
|
|
'svg/SVGShapes',
|
|
'./HandleeFontData',
|
|
], (
|
|
BaseTheme,
|
|
svg,
|
|
SVGShapes,
|
|
Handlee
|
|
) => {
|
|
'use strict';
|
|
|
|
const FONT = Handlee.name;
|
|
const FONT_FAMILY = '\'' + FONT + '\',cursive';
|
|
const LINE_HEIGHT = 1.5;
|
|
const MAX_CHAOS = 5;
|
|
|
|
const PENCIL = {
|
|
'stroke': 'rgba(0,0,0,0.7)',
|
|
'stroke-width': 0.8,
|
|
'stroke-linejoin': 'round',
|
|
'stroke-linecap': 'round',
|
|
};
|
|
|
|
const THICK_PENCIL = {
|
|
'stroke': 'rgba(0,0,0,0.8)',
|
|
'stroke-width': 1.2,
|
|
'stroke-linejoin': 'round',
|
|
'stroke-linecap': 'round',
|
|
};
|
|
|
|
const SETTINGS = {
|
|
titleMargin: 10,
|
|
outerMargin: 5,
|
|
agentMargin: 10,
|
|
actionMargin: 10,
|
|
minActionMargin: 3,
|
|
agentLineHighlightRadius: 4,
|
|
|
|
agentCap: {
|
|
box: {
|
|
padding: {
|
|
top: 5,
|
|
left: 10,
|
|
right: 10,
|
|
bottom: 5,
|
|
},
|
|
arrowBottom: 5 + 12 * 1.3 / 2,
|
|
labelAttrs: {
|
|
'font-family': FONT_FAMILY,
|
|
'font-size': 12,
|
|
'line-height': LINE_HEIGHT,
|
|
'text-anchor': 'middle',
|
|
},
|
|
boxRenderer: null,
|
|
},
|
|
cross: {
|
|
size: 15,
|
|
render: null,
|
|
},
|
|
bar: {
|
|
height: 6,
|
|
render: null,
|
|
},
|
|
fade: {
|
|
width: Math.ceil(MAX_CHAOS * 2 + 2),
|
|
height: 6,
|
|
extend: Math.ceil(MAX_CHAOS * 0.3 + 1),
|
|
},
|
|
none: {
|
|
height: 10,
|
|
},
|
|
},
|
|
|
|
connect: {
|
|
loopbackRadius: 6,
|
|
line: {
|
|
'solid': {
|
|
attrs: Object.assign({
|
|
'fill': 'none',
|
|
}, PENCIL),
|
|
render: null,
|
|
renderRev: null,
|
|
},
|
|
'dash': {
|
|
attrs: Object.assign({
|
|
'fill': 'none',
|
|
'stroke-dasharray': '4, 2',
|
|
}, PENCIL),
|
|
render: null,
|
|
renderRev: null,
|
|
},
|
|
'wave': {
|
|
attrs: Object.assign({
|
|
'fill': 'none',
|
|
'stroke-linejoin': 'round',
|
|
'stroke-linecap': 'round',
|
|
'wave-width': 6,
|
|
'wave-height': 0.5,
|
|
}, PENCIL),
|
|
render: null,
|
|
renderRev: null,
|
|
},
|
|
},
|
|
arrow: {
|
|
'single': {
|
|
width: 5,
|
|
height: 6,
|
|
attrs: Object.assign({
|
|
'fill': 'rgba(0,0,0,0.9)',
|
|
}, PENCIL),
|
|
render: null,
|
|
},
|
|
'double': {
|
|
width: 4,
|
|
height: 8,
|
|
attrs: Object.assign({
|
|
'fill': 'none',
|
|
}, PENCIL),
|
|
render: null,
|
|
},
|
|
},
|
|
label: {
|
|
padding: 6,
|
|
margin: {top: 2, bottom: 1},
|
|
attrs: {
|
|
'font-family': FONT_FAMILY,
|
|
'font-size': 8,
|
|
'line-height': LINE_HEIGHT,
|
|
'text-anchor': 'middle',
|
|
},
|
|
loopbackAttrs: {
|
|
'font-family': FONT_FAMILY,
|
|
'font-size': 8,
|
|
'line-height': LINE_HEIGHT,
|
|
},
|
|
},
|
|
mask: {
|
|
padding: {
|
|
top: 0,
|
|
left: 3,
|
|
right: 3,
|
|
bottom: 1,
|
|
},
|
|
},
|
|
},
|
|
|
|
titleAttrs: {
|
|
'font-family': FONT_FAMILY,
|
|
'font-size': 20,
|
|
'line-height': LINE_HEIGHT,
|
|
'text-anchor': 'middle',
|
|
'class': 'title',
|
|
},
|
|
|
|
agentLineAttrs: {
|
|
'fill': 'none',
|
|
'stroke': '#000000',
|
|
'stroke-width': 1,
|
|
},
|
|
};
|
|
|
|
const SHARED_BLOCK_SECTION = {
|
|
padding: {
|
|
top: 3,
|
|
bottom: 2,
|
|
},
|
|
mode: {
|
|
padding: {
|
|
top: 2,
|
|
left: 3,
|
|
right: 5,
|
|
bottom: 0,
|
|
},
|
|
boxRenderer: null,
|
|
labelAttrs: {
|
|
'font-family': FONT_FAMILY,
|
|
'font-weight': 'bold',
|
|
'font-size': 9,
|
|
'line-height': LINE_HEIGHT,
|
|
'text-anchor': 'left',
|
|
},
|
|
},
|
|
label: {
|
|
padding: {
|
|
top: 1,
|
|
left: 5,
|
|
right: 3,
|
|
bottom: 0,
|
|
},
|
|
labelAttrs: {
|
|
'font-family': FONT_FAMILY,
|
|
'font-size': 8,
|
|
'line-height': LINE_HEIGHT,
|
|
'text-anchor': 'left',
|
|
},
|
|
},
|
|
};
|
|
|
|
const BLOCKS = {
|
|
'ref': {
|
|
margin: {
|
|
top: 0,
|
|
bottom: 0,
|
|
},
|
|
boxRenderer: null,
|
|
section: SHARED_BLOCK_SECTION,
|
|
},
|
|
'': {
|
|
margin: {
|
|
top: 0,
|
|
bottom: 0,
|
|
},
|
|
boxRenderer: null,
|
|
section: SHARED_BLOCK_SECTION,
|
|
sepRenderer: null,
|
|
},
|
|
};
|
|
|
|
const NOTE_ATTRS = {
|
|
'font-family': FONT_FAMILY,
|
|
'font-size': 8,
|
|
'line-height': LINE_HEIGHT,
|
|
};
|
|
|
|
const NOTES = {
|
|
'text': {
|
|
margin: {top: 0, left: 2, right: 2, bottom: 0},
|
|
padding: {top: 2, left: 2, right: 2, bottom: 2},
|
|
overlap: {left: 10, right: 10},
|
|
boxRenderer: SVGShapes.renderBox.bind(null, {
|
|
'fill': '#FFFFFF',
|
|
}),
|
|
labelAttrs: NOTE_ATTRS,
|
|
},
|
|
'note': {
|
|
margin: {top: 0, left: 5, right: 5, bottom: 0},
|
|
padding: {top: 5, left: 5, right: 10, bottom: 5},
|
|
overlap: {left: 10, right: 10},
|
|
boxRenderer: null,
|
|
labelAttrs: NOTE_ATTRS,
|
|
},
|
|
'state': {
|
|
margin: {top: 0, left: 5, right: 5, bottom: 0},
|
|
padding: {top: 7, left: 7, right: 7, bottom: 7},
|
|
overlap: {left: 10, right: 10},
|
|
boxRenderer: null,
|
|
labelAttrs: NOTE_ATTRS,
|
|
},
|
|
};
|
|
|
|
class Random {
|
|
// xorshift+ 64-bit random generator
|
|
// https://en.wikipedia.org/wiki/Xorshift
|
|
|
|
constructor() {
|
|
this.s = new Uint32Array(4);
|
|
}
|
|
|
|
reset() {
|
|
// Arbitrary random seed with roughly balanced 1s / 0s
|
|
// (taken from running Math.random a few times)
|
|
this.s[0] = 0x177E9C74;
|
|
this.s[1] = 0xAE6FFDCE;
|
|
this.s[2] = 0x3CF4F32B;
|
|
this.s[3] = 0x46449F88;
|
|
}
|
|
|
|
nextFloat() {
|
|
/* jshint -W016 */ // bit-operations are part of the algorithm
|
|
const range = 0x100000000;
|
|
let x0 = this.s[0];
|
|
let x1 = this.s[1];
|
|
const y0 = this.s[2];
|
|
const y1 = this.s[3];
|
|
this.s[0] = y0;
|
|
this.s[1] = y1;
|
|
x0 ^= (x0 << 23) | (x1 >>> 9);
|
|
x1 ^= (x1 << 23);
|
|
this.s[2] = x0 ^ y0 ^ (x0 >>> 17) ^ (y0 >>> 26);
|
|
this.s[3] = (
|
|
x1 ^ y1 ^
|
|
(x0 << 15 | x1 >>> 17) ^
|
|
(y0 << 6 | y1 >>> 26)
|
|
);
|
|
return (((this.s[3] + y1) >>> 0) % range) / range;
|
|
}
|
|
}
|
|
|
|
const RIGHT = {};
|
|
const LEFT = {};
|
|
|
|
class SketchTheme extends BaseTheme {
|
|
constructor(handedness = RIGHT) {
|
|
super({
|
|
name: '',
|
|
settings: SETTINGS,
|
|
blocks: BLOCKS,
|
|
notes: NOTES,
|
|
});
|
|
|
|
if(handedness === RIGHT) {
|
|
this.name = 'sketch';
|
|
this.handedness = 1;
|
|
} else {
|
|
this.name = 'sketch left handed';
|
|
this.handedness = -1;
|
|
}
|
|
this.random = new Random();
|
|
|
|
this._assignFunctions();
|
|
}
|
|
|
|
_assignFunctions() {
|
|
this.renderBar = this.renderBar.bind(this);
|
|
this.renderBox = this.renderBox.bind(this);
|
|
this.renderArrowHead = this.renderArrowHead.bind(this);
|
|
this.renderConnect = this.renderConnect.bind(this);
|
|
this.renderTag = this.renderTag.bind(this);
|
|
|
|
this.agentCap.cross.render = this.renderCross.bind(this);
|
|
this.agentCap.bar.render = this.renderBar;
|
|
this.agentCap.box.boxRenderer = this.renderBox;
|
|
this.connect.arrow.single.render = this.renderArrowHead;
|
|
this.connect.arrow.double.render = this.renderArrowHead;
|
|
this.connect.line.solid.render = this.renderConnect;
|
|
this.connect.line.dash.render = this.renderConnect;
|
|
this.notes.note.boxRenderer = this.renderNote.bind(this);
|
|
this.notes.state.boxRenderer = this.renderState.bind(this);
|
|
this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this);
|
|
this.blocks[''].boxRenderer = this.renderBlock.bind(this);
|
|
this.blocks.ref.section.mode.boxRenderer = this.renderTag;
|
|
this.blocks[''].section.mode.boxRenderer = this.renderTag;
|
|
this.blocks[''].sepRenderer = this.renderSeparator.bind(this);
|
|
}
|
|
|
|
reset() {
|
|
this.random.reset();
|
|
}
|
|
|
|
addDefs(builder) {
|
|
builder('sketch_font', () => {
|
|
const style = document.createElement('style');
|
|
// For some uses, it is fine to load this font externally,
|
|
// but this fails when exporting as SVG / PNG (svg tags must
|
|
// have no external dependencies).
|
|
// const url = 'https://fonts.googleapis.com/css?family=' + FONT;
|
|
// style.innerText = '@import url("' + url + '")';
|
|
style.innerText = (
|
|
'@font-face{' +
|
|
'font-family:"' + Handlee.name + '";' +
|
|
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
|
|
'}'
|
|
);
|
|
return style;
|
|
});
|
|
}
|
|
|
|
vary(range) {
|
|
if(!range) {
|
|
return 0;
|
|
}
|
|
// cosine distribution [-pi/2 pi/2] scaled to [-range range]
|
|
// int(cos(x))dx = sin(x)
|
|
// from -pi/2: sin(x) - sin(-pi/2) = sin(x) + 1
|
|
// total: sin(pi/2) + 1 = 2
|
|
// normalise to area 1: /2
|
|
// (sin(x) + 1) / 2
|
|
// inverse: p = (sin(x) + 1) / 2
|
|
// asin(2p - 1) = x
|
|
// normalise range
|
|
// x = asin(2p - 1) * range / (pi/2)
|
|
const rand = this.random.nextFloat(); // [0 1)
|
|
return Math.asin(rand * 2 - 1) * 2 * range / Math.PI;
|
|
}
|
|
|
|
lineNodes(p1, p2, {
|
|
var1 = 1,
|
|
var2 = 1,
|
|
varX = 1,
|
|
varY = 1,
|
|
move = true,
|
|
}) {
|
|
const length = Math.sqrt(
|
|
(p2.x - p1.x) * (p2.x - p1.x) +
|
|
(p2.y - p1.y) * (p2.y - p1.y)
|
|
);
|
|
const rough = Math.min(Math.sqrt(length) * 0.2, MAX_CHAOS);
|
|
const x1 = p1.x + this.vary(var1 * varX * rough);
|
|
const y1 = p1.y + this.vary(var1 * varY * rough);
|
|
const x2 = p2.x + this.vary(var2 * varX * rough);
|
|
const y2 = p2.y + this.vary(var2 * varY * rough);
|
|
|
|
// -1 = p1 higher, 1 = p2 higher
|
|
const upper = Math.max(-1, Math.min(1,
|
|
(y1 - y2) / (Math.abs(x1 - x2) + 0.001)
|
|
));
|
|
const frac = upper / 6 + 0.5;
|
|
|
|
// Line curve is to top / left (simulates right-handed drawing)
|
|
// or top / right (left-handed)
|
|
const curveX = (0.5 + this.vary(0.5)) * rough;
|
|
const curveY = (0.5 + this.vary(0.5)) * rough;
|
|
const xc = x1 * (1 - frac) + x2 * frac - curveX * this.handedness;
|
|
const yc = y1 * (1 - frac) + y2 * frac - curveY;
|
|
const nodes = (
|
|
(move ? ('M' + x1 + ' ' + y1) : '') +
|
|
'C' + xc + ' ' + yc +
|
|
',' + x2 + ' ' + y2 +
|
|
',' + x2 + ' ' + y2
|
|
);
|
|
return {
|
|
nodes,
|
|
p1: {x: x1, y: y1},
|
|
p2: {x: x2, y: y2},
|
|
};
|
|
}
|
|
|
|
renderLine(p1, p2, lineOptions) {
|
|
const line = this.lineNodes(p1, p2, lineOptions);
|
|
const shape = svg.make('path', Object.assign({
|
|
'd': line.nodes,
|
|
'fill': 'none',
|
|
'stroke-dasharray': lineOptions.dash ? '6, 5' : 'none',
|
|
}, lineOptions.thick ? THICK_PENCIL : PENCIL));
|
|
return shape;
|
|
}
|
|
|
|
renderBox({x, y, width, height}, {fill = null, thick = false} = {}) {
|
|
const lT = this.lineNodes(
|
|
{x, y},
|
|
{x: x + width, y},
|
|
{}
|
|
);
|
|
const lB = this.lineNodes(
|
|
{x: x + width, y: y + height},
|
|
{x, y: y + height},
|
|
{move: false}
|
|
);
|
|
const lR = this.lineNodes(
|
|
lT.p2,
|
|
lB.p1,
|
|
{var1: 0, var2: 0, move: false}
|
|
);
|
|
const lL = this.lineNodes(
|
|
lB.p2,
|
|
lT.p1,
|
|
{var1: 0, var2: 0.3, move: false}
|
|
);
|
|
|
|
const shape = svg.make('path', Object.assign({
|
|
'd': lT.nodes + lR.nodes + lB.nodes + lL.nodes,
|
|
'fill': fill || '#FFFFFF',
|
|
}, thick ? THICK_PENCIL : PENCIL));
|
|
|
|
return shape;
|
|
}
|
|
|
|
renderNote({x, y, width, height}) {
|
|
const flickSize = 5;
|
|
const lT = this.lineNodes(
|
|
{x, y},
|
|
{x: x + width - flickSize, y},
|
|
{}
|
|
);
|
|
const lF = this.lineNodes(
|
|
lT.p2,
|
|
{x: x + width, y: y + flickSize},
|
|
{move: false, var1: 0}
|
|
);
|
|
const lB = this.lineNodes(
|
|
{x: x + width, y: y + height},
|
|
{x, y: y + height},
|
|
{move: false}
|
|
);
|
|
const lR = this.lineNodes(
|
|
lF.p2,
|
|
lB.p1,
|
|
{var1: 0, var2: 0, move: false}
|
|
);
|
|
const lL = this.lineNodes(
|
|
lB.p2,
|
|
lT.p1,
|
|
{var1: 0, var2: 0.3, move: false}
|
|
);
|
|
const lF1 = this.lineNodes(
|
|
lF.p1,
|
|
{x: x + width - flickSize, y: y + flickSize},
|
|
{var1: 0.3}
|
|
);
|
|
const lF2 = this.lineNodes(
|
|
lF1.p2,
|
|
lF.p2,
|
|
{var1: 0, move: false}
|
|
);
|
|
|
|
const g = svg.make('g');
|
|
g.appendChild(svg.make('path', Object.assign({
|
|
'd': (
|
|
lT.nodes +
|
|
lF.nodes +
|
|
lR.nodes +
|
|
lB.nodes +
|
|
lL.nodes
|
|
),
|
|
'fill': '#FFFFFF',
|
|
}, PENCIL)));
|
|
g.appendChild(svg.make('path', Object.assign({
|
|
'd': lF1.nodes + lF2.nodes,
|
|
'fill': 'none',
|
|
}, PENCIL)));
|
|
|
|
return g;
|
|
}
|
|
|
|
renderConnect({x1, dx1, x2, dx2, y}, attrs) {
|
|
const ln = this.lineNodes(
|
|
{x: x1 + dx1, y},
|
|
{x: x2 + dx2, y},
|
|
{varX: 0.3}
|
|
);
|
|
return {
|
|
shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)),
|
|
p1: {x: ln.p1.x - dx1, y: ln.p2.y},
|
|
p2: {x: ln.p2.x - dx2, y: ln.p2.y},
|
|
};
|
|
}
|
|
|
|
renderArrowHead(attrs, {x, y, dx, dy}) {
|
|
const w = dx * (1 + this.vary(0.2));
|
|
const h = dy * (1 + this.vary(0.3));
|
|
const l1 = this.lineNodes(
|
|
{x: x + w, y: y - h},
|
|
{x, y},
|
|
{var1: 2.0, var2: 0.2}
|
|
);
|
|
const l2 = this.lineNodes(
|
|
l1.p2,
|
|
{x: x + w, y: y + h},
|
|
{var1: 0, var2: 2.0, move: false}
|
|
);
|
|
const l3 = (attrs.fill === 'none') ? {nodes: ''} : this.lineNodes(
|
|
l2.p2,
|
|
l1.p1,
|
|
{var1: 0, var2: 0, move: false}
|
|
);
|
|
return svg.make('path', Object.assign({
|
|
'd': l1.nodes + l2.nodes + l3.nodes,
|
|
}, attrs));
|
|
}
|
|
|
|
renderState({x, y, width, height}) {
|
|
// TODO: rounded corners
|
|
return this.renderBox({x, y, width, height});
|
|
}
|
|
|
|
renderRefBlock({x, y, width, height}) {
|
|
return this.renderBox(
|
|
{x, y, width, height},
|
|
{fill: '#FFFFFF', thick: true}
|
|
);
|
|
}
|
|
|
|
renderBlock({x, y, width, height}) {
|
|
return this.renderBox(
|
|
{x, y, width, height},
|
|
{fill: 'none', thick: true}
|
|
);
|
|
}
|
|
|
|
renderTag({x, y, width, height}) {
|
|
const x2 = x + width;
|
|
const y2 = y + height;
|
|
|
|
const l1 = this.lineNodes(
|
|
{x: x2 + 3, y},
|
|
{x: x2 - 2, y: y2},
|
|
{}
|
|
);
|
|
const l2 = this.lineNodes(
|
|
l1.p2,
|
|
{x, y: y2 + 1},
|
|
{var1: 0, move: false}
|
|
);
|
|
|
|
const line = l1.nodes + l2.nodes;
|
|
|
|
const g = svg.make('g');
|
|
|
|
g.appendChild(svg.make('path', {
|
|
'd': line + 'L' + x + ' ' + y,
|
|
'fill': '#FFFFFF',
|
|
}));
|
|
|
|
g.appendChild(svg.make('path', Object.assign({
|
|
'd': line,
|
|
'fill': '#FFFFFF',
|
|
}, PENCIL)));
|
|
|
|
return g;
|
|
}
|
|
|
|
renderSeparator({x1, y1, x2, y2}) {
|
|
return this.renderLine(
|
|
{x: x1, y: y1},
|
|
{x: x2, y: y2},
|
|
{thick: true, dash: true}
|
|
);
|
|
}
|
|
|
|
renderBar({x, y, width, height}) {
|
|
return this.renderBox({x, y, width, height}, {fill: '#000000'});
|
|
}
|
|
|
|
renderCross({x, y, radius}) {
|
|
const r1 = (this.vary(0.2) + 1) * radius;
|
|
const l1 = this.lineNodes(
|
|
{x: x - r1, y: y - r1},
|
|
{x: x + r1, y: y + r1},
|
|
{}
|
|
);
|
|
const r2 = (this.vary(0.2) + 1) * radius;
|
|
const l2 = this.lineNodes(
|
|
{x: x + r2, y: y - r2},
|
|
{x: x - r2, y: y + r2},
|
|
{}
|
|
);
|
|
|
|
return svg.make('path', Object.assign({
|
|
'd': l1.nodes + l2.nodes,
|
|
'fill': 'none',
|
|
}, PENCIL));
|
|
}
|
|
|
|
renderAgentLine({x, y0, y1, width, className}) {
|
|
if(width > 0) {
|
|
const shape = this.renderBox({
|
|
x: x - width / 2,
|
|
y: y0,
|
|
width,
|
|
height: y1 - y0,
|
|
}, {fill: 'none'});
|
|
shape.setAttribute('class', className);
|
|
return shape;
|
|
} else {
|
|
const shape = this.renderLine(
|
|
{x, y: y0},
|
|
{x, y: y1},
|
|
{varY: 0.3}
|
|
);
|
|
shape.setAttribute('class', className);
|
|
return shape;
|
|
}
|
|
}
|
|
}
|
|
|
|
SketchTheme.RIGHT = RIGHT;
|
|
SketchTheme.LEFT = LEFT;
|
|
|
|
return SketchTheme;
|
|
});
|