Beginnings of theme switching capability

This commit is contained in:
David Evans 2017-11-05 22:39:36 +00:00
parent 5b6cbd518e
commit f6f557bcd7
16 changed files with 398 additions and 47 deletions

View File

@ -11,13 +11,15 @@
'sequence/Generator',
'sequence/Renderer',
'sequence/themes/Basic',
'sequence/themes/Chunky',
], (
Interface,
Exporter,
Parser,
Generator,
Renderer,
Theme
BasicTheme,
ChunkyTheme
) => {
const defaultCode = (
'title Labyrinth\n' +
@ -41,7 +43,10 @@
defaultCode,
parser: new Parser(),
generator: new Generator(),
renderer: new Renderer(new Theme()),
renderer: new Renderer({themes: [
new BasicTheme(),
new ChunkyTheme(),
]}),
exporter: new Exporter(),
localStorage: 'src',
});

View File

@ -17,8 +17,18 @@
return o;
}
const FAVICON_SRC = (
'theme chunky\n' +
'define ABC as A, DEF as B\n' +
'A -> B\n' +
'B -> ]\n' +
'] -> B\n' +
'B -> A\n' +
'terminators fade'
);
const SAMPLE_REGEX = new RegExp(
/<img src="screenshots\/([^"]*)"[^>]*>[\s]*```(?!shell).*\n([^]+?)```/g
/<img src="([^"]*)"[^>]*>[\s]*```(?!shell).*\n([^]+?)```/g
);
function findSamples(content) {
@ -34,9 +44,23 @@
code: match[2],
});
}
results.push({
file: 'favicon.png',
code: FAVICON_SRC,
height: 64,
});
return results;
}
function filename(path) {
const p = path.lastIndexOf('/');
if(p !== -1) {
return path.substr(p + 1);
} else {
return path;
}
}
const PNG_RESOLUTION = 4;
/* jshint -W072 */ // Allow several required modules
@ -45,25 +69,30 @@
'sequence/Generator',
'sequence/Renderer',
'sequence/themes/Basic',
'sequence/themes/Chunky',
'interface/Exporter',
], (
Parser,
Generator,
Renderer,
Theme,
BasicTheme,
ChunkyTheme,
Exporter
) => {
const parser = new Parser();
const generator = new Generator();
const theme = new Theme();
const themes = [
new BasicTheme(),
new ChunkyTheme(),
];
const status = makeNode('div', {'class': 'status'});
const statusText = makeText('Loading\u2026');
status.appendChild(statusText);
document.body.appendChild(status);
function renderSample({file, code}) {
const renderer = new Renderer(theme);
function renderSample({file, code, height}) {
const renderer = new Renderer({themes});
const exporter = new Exporter();
const hold = makeNode('div', {'class': 'hold'});
@ -78,12 +107,15 @@
hold.appendChild(raster);
hold.appendChild(makeNode('img', {
'src': 'screenshots/' + file,
'src': file,
'class': 'original',
'title': 'original',
}));
const downloadPNG = makeNode('a', {'href': '#', 'download': file});
const downloadPNG = makeNode('a', {
'href': '#',
'download': filename(file),
});
downloadPNG.appendChild(makeText('Download PNG'));
hold.appendChild(downloadPNG);
@ -92,7 +124,11 @@
const parsed = parser.parse(code);
const sequence = generator.generate(parsed);
renderer.render(sequence);
exporter.getPNGURL(renderer, PNG_RESOLUTION, (url) => {
let resolution = PNG_RESOLUTION;
if(height) {
resolution = height / renderer.height;
}
exporter.getPNGURL(renderer, resolution, (url) => {
raster.setAttribute('src', url);
downloadPNG.setAttribute('href', url);
});

View File

@ -121,6 +121,9 @@ define(['core/ArrayUtilities'], (array) => {
'title': {type: 'keyword', suggest: true, then: {
'': textToEnd,
}},
'theme': {type: 'keyword', suggest: true, then: {
'': textToEnd,
}},
'terminators': {type: 'keyword', suggest: true, then: {
'none': {type: 'keyword', suggest: true, then: {}},
'cross': {type: 'keyword', suggest: true, then: {}},

View File

@ -539,6 +539,7 @@ define(['core/ArrayUtilities'], (array) => {
return {
meta: {
title: meta.title,
theme: meta.theme,
},
agents: this.agents.slice(),
stages: globals.stages,

View File

@ -148,13 +148,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
};
describe('.generate', () => {
it('propagates title metadata', () => {
it('propagates title and theme metadata', () => {
const input = {
meta: {title: 'bar'},
meta: {title: 'bar', theme: 'zig', nope: 'skip'},
stages: [],
};
const sequence = generator.generate(input);
expect(sequence.meta).toEqual({title: 'bar'});
expect(sequence.meta).toEqual({title: 'bar', theme: 'zig'});
});
it('returns an empty sequence for blank input', () => {

View File

@ -188,6 +188,15 @@ define([
return true;
},
(line, meta) => { // theme
if(tokenKeyword(line[0]) !== 'theme') {
return null;
}
meta.theme = joinLabel(line, 1);
return true;
},
(line, meta) => { // terminators
if(tokenKeyword(line[0]) !== 'terminators') {
return null;
@ -356,6 +365,7 @@ define([
const result = {
meta: {
title: '',
theme: '',
terminators: 'none',
},
stages: [],

View File

@ -33,6 +33,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed).toEqual({
meta: {
title: '',
theme: '',
terminators: 'none',
},
stages: [],
@ -44,6 +45,11 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.meta.title).toEqual('foo');
});
it('reads theme metadata', () => {
const parsed = parser.parse('theme foo');
expect(parsed.meta.theme).toEqual('foo');
});
it('reads terminators metadata', () => {
const parsed = parser.parse('terminators bar');
expect(parsed.meta.terminators).toEqual('bar');

View File

@ -63,8 +63,24 @@ define([
};
}
function makeThemes(themes) {
if(themes.length === 0) {
throw new Error('Cannot render without a theme');
}
const themeMap = new Map();
themes.forEach((theme) => {
themeMap.set(theme.name, theme);
});
themeMap.set('', themes[0]);
return themeMap;
}
let globalNamespace = 0;
return class Renderer {
constructor(theme, {
constructor({
themes = [],
namespace = null,
components = null,
SVGTextBlockClass = SVGShapes.TextBlock,
} = {}) {
@ -93,11 +109,16 @@ define([
this.state = {};
this.width = 0;
this.height = 0;
this.theme = theme;
this.themes = makeThemes(themes);
this.theme = null;
this.namespace = namespace;
if(namespace === null) {
this.namespace = 'R' + globalNamespace;
++ globalNamespace;
}
this.components = components;
this.SVGTextBlockClass = SVGTextBlockClass;
this.knownDefs = new Set();
this.currentSequence = null;
this.buildStaticElements();
this.components.forEach((component) => {
component.makeState(this.state);
@ -112,11 +133,13 @@ define([
this.defs = svg.make('defs');
this.mask = svg.make('mask', {
'id': 'lineMask',
'id': this.namespace + 'LineMask',
'maskUnits': 'userSpaceOnUse',
});
this.maskReveal = svg.make('rect', {'fill': '#FFFFFF'});
this.agentLines = svg.make('g', {'mask': 'url(#lineMask)'});
this.agentLines = svg.make('g', {
'mask': 'url(#' + this.namespace + 'LineMask)',
});
this.blocks = svg.make('g');
this.sections = svg.make('g');
this.actionShapes = svg.make('g');
@ -133,11 +156,15 @@ define([
}
addDef(name, generator) {
const namespacedName = this.namespace + name;
if(this.knownDefs.has(name)) {
return;
return namespacedName;
}
this.knownDefs.add(name);
this.defs.appendChild(generator());
const def = generator();
def.setAttribute('id', namespacedName);
this.defs.appendChild(def);
return namespacedName;
}
addSeparation(agentName1, agentName2, dist) {
@ -517,16 +544,6 @@ define([
this.height = (y1 - y0);
}
setTheme(theme) {
if(this.theme === theme) {
return;
}
this.theme = theme;
if(this.currentSequence) {
this.render(this.currentSequence);
}
}
_reset() {
this.knownDefs.clear();
svg.empty(this.defs);
@ -546,6 +563,12 @@ define([
render(sequence) {
this._reset();
const themeName = sequence.meta.theme;
this.theme = this.themes.get(themeName);
if(!this.theme) {
this.theme = this.themes.get('');
}
this.title.set({
attrs: this.theme.titleAttrs,
text: sequence.meta.title,
@ -564,7 +587,6 @@ define([
this.sizer.resetCache();
this.sizer.detach();
this.currentSequence = sequence;
}
getAgentX(name) {

View File

@ -3,14 +3,14 @@ defineDescribe('Sequence Renderer', [
'./themes/Basic',
], (
Renderer,
Theme
BasicTheme
) => {
'use strict';
let renderer = null;
beforeEach(() => {
renderer = new Renderer(new Theme());
renderer = new Renderer({themes: [new BasicTheme()]});
document.body.appendChild(renderer.svg());
});

View File

@ -155,11 +155,8 @@ define([
render(y, {x, label}, env, isBegin) {
const config = env.theme.agentCap.fade;
const gradID = isBegin ? 'fadeIn' : 'fadeOut';
env.addDef(gradID, () => {
const gradID = env.addDef(isBegin ? 'FadeIn' : 'FadeOut', () => {
const grad = svg.make('linearGradient', {
'id': gradID,
'x1': '0%',
'y1': isBegin ? '100%' : '0%',
'x2': '0%',

View File

@ -9,7 +9,7 @@ defineDescribe('Sequence Integration', [
Parser,
Generator,
Renderer,
Theme,
BasicTheme,
SVGTextBlock
) => {
'use strict';
@ -20,10 +20,14 @@ defineDescribe('Sequence Integration', [
let theme = null;
beforeEach(() => {
theme = new Theme();
theme = new BasicTheme();
parser = new Parser();
generator = new Generator();
renderer = new Renderer(theme, {SVGTextBlockClass: SVGTextBlock});
renderer = new Renderer({
themes: [new BasicTheme()],
namespace: '',
SVGTextBlockClass: SVGTextBlock,
});
document.body.appendChild(renderer.svg());
});
@ -45,12 +49,12 @@ defineDescribe('Sequence Integration', [
expect(getSimplifiedContent(renderer)).toEqual(
'<svg width="100%" height="100%" viewBox="-5 -5 10 10">' +
'<defs>' +
'<mask id="lineMask" maskUnits="userSpaceOnUse">' +
'<mask id="LineMask" maskUnits="userSpaceOnUse">' +
'<rect fill="#FFFFFF" x="-5" y="-5" width="10" height="10">' +
'</rect>' +
'</mask>' +
'</defs>' +
'<g mask="url(#lineMask)"></g>' +
'<g mask="url(#LineMask)"></g>' +
'</svg>'
);
});
@ -63,12 +67,12 @@ defineDescribe('Sequence Integration', [
expect(getSimplifiedContent(renderer)).toEqual(
'<svg width="100%" height="100%" viewBox="-11.5 -16 23 21">' +
'<defs>' +
'<mask id="lineMask" maskUnits="userSpaceOnUse">' +
'<mask id="LineMask" maskUnits="userSpaceOnUse">' +
'<rect fill="#FFFFFF" x="-11.5" y="-16" width="23" height="21">' +
'</rect>' +
'</mask>' +
'</defs>' +
'<g mask="url(#lineMask)"></g>' +
'<g mask="url(#LineMask)"></g>' +
'<text' +
' x="0"' +
' font-family="sans-serif"' +

View File

@ -235,8 +235,9 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => {
},
};
return class Theme {
return class BasicTheme {
constructor() {
this.name = 'basic';
Object.assign(this, SETTINGS);
}
};

View File

@ -1,8 +1,13 @@
defineDescribe('Basic Theme', ['./Basic'], (Theme) => {
defineDescribe('Basic Theme', ['./Basic'], (BasicTheme) => {
'use strict';
const theme = new BasicTheme();
it('has a name', () => {
expect(theme.name).toEqual('basic');
});
it('contains settings for the theme', () => {
const theme = new Theme();
expect(theme.outerMargin).toEqual(5);
});
});

View File

@ -0,0 +1,247 @@
define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => {
'use strict';
const LINE_HEIGHT = 1.3;
const SETTINGS = {
titleMargin: 10,
outerMargin: 5,
agentMargin: 8,
actionMargin: 5,
agentLineHighlightRadius: 4,
agentCap: {
box: {
padding: {
top: 1,
left: 3,
right: 3,
bottom: 1,
},
arrowBottom: 2 + 14 * 1.3 / 2,
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 3,
'rx': 4,
'ry': 4,
},
labelAttrs: {
'font-family': 'sans-serif',
'font-weight': 'bold',
'font-size': 14,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 20,
attrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
},
},
bar: {
attrs: {
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 3,
'height': 4,
},
},
fade: {
width: 5,
height: 10,
},
none: {
height: 10,
},
},
connect: {
loopbackRadius: 6,
lineAttrs: {
'solid': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
},
'dash': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
'stroke-dasharray': '4, 2',
},
},
arrow: {
width: 10,
height: 12,
attrs: {
'fill': '#000000',
'stroke-width': 0,
'stroke-linejoin': 'miter',
},
},
label: {
padding: 6,
margin: {top: 2, bottom: 1},
attrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
loopbackAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
},
},
mask: {
padding: {
top: 0,
left: 3,
right: 3,
bottom: 1,
},
},
},
block: {
margin: {
top: 0,
bottom: 0,
},
boxAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1.5,
'rx': 2,
'ry': 2,
},
section: {
padding: {
top: 3,
bottom: 2,
},
mode: {
padding: {
top: 1,
left: 3,
right: 3,
bottom: 0,
},
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 3,
'rx': 2,
'ry': 2,
},
labelAttrs: {
'font-family': 'sans-serif',
'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': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
'text-anchor': 'left',
},
},
},
separator: {
attrs: {
'stroke': '#000000',
'stroke-width': 1.5,
'stroke-dasharray': '4, 2',
},
},
},
note: {
'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: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
},
},
'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: SVGShapes.renderNote.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
}, {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
},
},
'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: SVGShapes.renderBox.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'rx': 10,
'ry': 10,
}),
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
},
},
},
titleAttrs: {
'font-family': 'sans-serif',
'font-size': 20,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
'class': 'title',
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
},
};
return class ChunkyTheme {
constructor() {
this.name = 'chunky';
Object.assign(this, SETTINGS);
}
};
});

View File

@ -0,0 +1,13 @@
defineDescribe('Chunky Theme', ['./Chunky'], (ChunkyTheme) => {
'use strict';
const theme = new ChunkyTheme();
it('has a name', () => {
expect(theme.name).toEqual('chunky');
});
it('contains settings for the theme', () => {
expect(theme.outerMargin).toEqual(5);
});
});

View File

@ -9,6 +9,7 @@ define([
'sequence/Generator_spec',
'sequence/Renderer_spec',
'sequence/themes/Basic_spec',
'sequence/themes/Chunky_spec',
'sequence/components/AgentCap_spec',
'sequence/components/AgentHighlight_spec',
'sequence/components/Connect_spec',