diff --git a/scripts/main.js b/scripts/main.js index a39a991..28797cd 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -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', }); diff --git a/scripts/readme_images.js b/scripts/readme_images.js index 6ab372e..85fee0a 100644 --- a/scripts/readme_images.js +++ b/scripts/readme_images.js @@ -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( - /]*>[\s]*```(?!shell).*\n([^]+?)```/g + /]*>[\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); }); diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js index 1b6f227..e897d9a 100644 --- a/scripts/sequence/CodeMirrorMode.js +++ b/scripts/sequence/CodeMirrorMode.js @@ -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: {}}, diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js index b1bea09..fa4c524 100644 --- a/scripts/sequence/Generator.js +++ b/scripts/sequence/Generator.js @@ -539,6 +539,7 @@ define(['core/ArrayUtilities'], (array) => { return { meta: { title: meta.title, + theme: meta.theme, }, agents: this.agents.slice(), stages: globals.stages, diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js index b5701f4..38275a2 100644 --- a/scripts/sequence/Generator_spec.js +++ b/scripts/sequence/Generator_spec.js @@ -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', () => { diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js index 5334aa0..2de9327 100644 --- a/scripts/sequence/Parser.js +++ b/scripts/sequence/Parser.js @@ -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: [], diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js index fdc6171..c7254de 100644 --- a/scripts/sequence/Parser_spec.js +++ b/scripts/sequence/Parser_spec.js @@ -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'); diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js index 11ca9ee..ba24a34 100644 --- a/scripts/sequence/Renderer.js +++ b/scripts/sequence/Renderer.js @@ -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) { diff --git a/scripts/sequence/Renderer_spec.js b/scripts/sequence/Renderer_spec.js index 9e88687..7b8ff0e 100644 --- a/scripts/sequence/Renderer_spec.js +++ b/scripts/sequence/Renderer_spec.js @@ -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()); }); diff --git a/scripts/sequence/components/AgentCap.js b/scripts/sequence/components/AgentCap.js index b6efea9..067579d 100644 --- a/scripts/sequence/components/AgentCap.js +++ b/scripts/sequence/components/AgentCap.js @@ -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%', diff --git a/scripts/sequence/sequence_integration_spec.js b/scripts/sequence/sequence_integration_spec.js index 87d5a17..25aea72 100644 --- a/scripts/sequence/sequence_integration_spec.js +++ b/scripts/sequence/sequence_integration_spec.js @@ -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( '' + '' + - '' + + '' + '' + '' + '' + '' + - '' + + '' + '' ); }); @@ -63,12 +67,12 @@ defineDescribe('Sequence Integration', [ expect(getSimplifiedContent(renderer)).toEqual( '' + '' + - '' + + '' + '' + '' + '' + '' + - '' + + '' + ' { }, }; - return class Theme { + return class BasicTheme { constructor() { + this.name = 'basic'; Object.assign(this, SETTINGS); } }; diff --git a/scripts/sequence/themes/Basic_spec.js b/scripts/sequence/themes/Basic_spec.js index 02a7ed3..7ca74fe 100644 --- a/scripts/sequence/themes/Basic_spec.js +++ b/scripts/sequence/themes/Basic_spec.js @@ -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); }); }); diff --git a/scripts/sequence/themes/Chunky.js b/scripts/sequence/themes/Chunky.js new file mode 100644 index 0000000..af6154a --- /dev/null +++ b/scripts/sequence/themes/Chunky.js @@ -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); + } + }; +}); diff --git a/scripts/sequence/themes/Chunky_spec.js b/scripts/sequence/themes/Chunky_spec.js new file mode 100644 index 0000000..fe5a524 --- /dev/null +++ b/scripts/sequence/themes/Chunky_spec.js @@ -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); + }); +}); diff --git a/scripts/specs.js b/scripts/specs.js index 1a9ca45..2aa5688 100644 --- a/scripts/specs.js +++ b/scripts/specs.js @@ -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',