diff --git a/readme_images.htm b/readme_images.htm
new file mode 100644
index 0000000..421275b
--- /dev/null
+++ b/readme_images.htm
@@ -0,0 +1,37 @@
+
+
+
+
+
+Sequence Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/interface/Exporter.js b/scripts/interface/Exporter.js
new file mode 100644
index 0000000..a70d0a3
--- /dev/null
+++ b/scripts/interface/Exporter.js
@@ -0,0 +1,76 @@
+define(() => {
+ 'use strict';
+
+ return class Exporter {
+ constructor() {
+ this.latestSVG = null;
+ this.canvas = null;
+ this.context = null;
+ this.indexPNG = 0;
+ this.latestPNGIndex = 0;
+ this.latestPNG = null;
+ }
+
+ getSVGContent(renderer) {
+ return renderer.svg().outerHTML;
+ }
+
+ getSVGBlob(renderer) {
+ return new Blob(
+ [this.getSVGContent(renderer)],
+ {type: 'image/svg+xml'}
+ );
+ }
+
+ getSVGURL(renderer) {
+ const blob = this.getSVGBlob(renderer);
+ if(this.latestSVG) {
+ URL.revokeObjectURL(this.latestSVG);
+ }
+ this.latestSVG = URL.createObjectURL(blob);
+ return this.latestSVG;
+ }
+
+ getPNGBlob(renderer, resolution, callback) {
+ if(!this.canvas) {
+ window.devicePixelRatio = 1;
+ this.canvas = document.createElement('canvas');
+ this.context = this.canvas.getContext('2d');
+ }
+
+ const width = renderer.width * resolution;
+ const height = renderer.height * resolution;
+ const img = new Image(width, height);
+
+ img.addEventListener('load', () => {
+ this.canvas.width = width;
+ this.canvas.height = height;
+ this.context.drawImage(img, 0, 0);
+ this.canvas.toBlob(callback, 'image/png');
+ }, {once: true});
+
+ img.src = this.getSVGURL(renderer);
+ }
+
+ getPNGURL(renderer, resolution, callback) {
+ ++ this.indexPNG;
+ const index = this.indexPNG;
+
+ this.getPNGBlob(renderer, resolution, (blob) => {
+ const url = URL.createObjectURL(blob);
+ const isLatest = index >= this.latestPNGIndex;
+ if(isLatest) {
+ if(this.latestPNG) {
+ URL.revokeObjectURL(this.latestPNG);
+ }
+ this.latestPNG = url;
+ this.latestPNGIndex = index;
+ callback(url, true);
+ } else {
+ callback(url, false);
+ URL.revokeObjectURL(url);
+ }
+ });
+ }
+ };
+});
diff --git a/scripts/interface/Interface.js b/scripts/interface/Interface.js
index 6a757e2..18e39df 100644
--- a/scripts/interface/Interface.js
+++ b/scripts/interface/Interface.js
@@ -33,16 +33,14 @@ define([
parser,
generator,
renderer,
+ exporter,
defaultCode = '',
localStorage = '',
}) {
- window.devicePixelRatio = 1;
- this.canvas = makeNode('canvas');
- this.context = this.canvas.getContext('2d');
-
this.parser = parser;
this.generator = generator;
this.renderer = renderer;
+ this.exporter = exporter;
this.defaultCode = defaultCode;
this.localStorage = localStorage;
this.minScale = 1.5;
@@ -50,11 +48,8 @@ define([
this.debounced = null;
this.latestSeq = null;
this.renderedSeq = null;
- this.canvasDirty = true;
- this.svgDirty = true;
- this.latestPNG = null;
- this.latestSVG = null;
- this.updatingPNG = null;
+ this.pngDirty = true;
+ this.updatingPNG = false;
this._downloadSVGClick = this._downloadSVGClick.bind(this);
this._downloadPNGClick = this._downloadPNGClick.bind(this);
@@ -168,8 +163,7 @@ define([
redraw(sequence) {
clearTimeout(this.debounced);
this.debounced = null;
- this.canvasDirty = true;
- this.svgDirty = true;
+ this.pngDirty = true;
this.renderedSeq = sequence;
this.renderer.render(sequence);
this.updateMinSize(this.renderer.width, this.renderer.height);
@@ -250,76 +244,42 @@ define([
}
}
- getSVGData() {
- this.forceRender();
- if(!this.svgDirty) {
- return this.latestSVG;
- }
- this.svgDirty = false;
- const src = this.renderer.svg().outerHTML;
- const blob = new Blob([src], {type: 'image/svg+xml'});
- if(this.latestSVG) {
- URL.revokeObjectURL(this.latestSVG);
- }
- this.latestSVG = URL.createObjectURL(blob);
- return this.latestSVG;
- }
-
- getPNGData(callback) {
- this.forceRender();
- if(!this.canvasDirty) {
- // TODO: this could cause issues if getPNGData is called
- // while another update is ongoing. Needs a more robust fix
- callback(this.latestPNG);
- return;
- }
- this.canvasDirty = false;
- const width = this.renderer.width * PNG_RESOLUTION;
- const height = this.renderer.height * PNG_RESOLUTION;
- this.canvas.width = width;
- this.canvas.height = height;
- const img = new Image(width, height);
- img.addEventListener('load', () => {
- this.context.drawImage(img, 0, 0);
- this.canvas.toBlob((blob) => {
- if(this.latestPNG) {
- URL.revokeObjectURL(this.latestPNG);
- }
- this.latestPNG = URL.createObjectURL(blob);
- callback(this.latestPNG);
- }, 'image/png');
- }, {once: true});
- img.src = this.getSVGData();
- }
-
updatePNGLink() {
- const nonce = this.updatingPNG = {};
- this.getPNGData((data) => {
- if(this.updatingPNG === nonce) {
- this.downloadPNG.setAttribute('href', data);
- this.updatingPNG = null;
+ this.forceRender();
+ if(this.updatingPNG || !this.pngDirty) {
+ return false;
+ }
+ this.pngDirty = false;
+ this.updatingPNG = true;
+ this.exporter.getPNGURL(
+ this.renderer,
+ PNG_RESOLUTION,
+ (url, latest) => {
+ if(latest) {
+ this.downloadPNG.setAttribute('href', url);
+ this.updatingPNG = false;
+ }
}
- });
+ );
+ return true;
}
_downloadPNGFocus() {
- this.forceRender();
- if(this.canvasDirty) {
- this.updatePNGLink();
- }
+ this.updatePNGLink();
}
_downloadPNGClick(e) {
if(this.updatingPNG) {
e.preventDefault();
- } else if(this.canvasDirty) {
+ } else if(this.updatePNGLink()) {
e.preventDefault();
- this.updatePNGLink();
}
}
_downloadSVGClick() {
- this.downloadSVG.setAttribute('href', this.getSVGData());
+ this.forceRender();
+ const url = this.exporter.getSVGURL(this.renderer);
+ this.downloadSVG.setAttribute('href', url);
}
};
});
diff --git a/scripts/interface/Interface_spec.js b/scripts/interface/Interface_spec.js
index 02f2865..909a125 100644
--- a/scripts/interface/Interface_spec.js
+++ b/scripts/interface/Interface_spec.js
@@ -4,6 +4,7 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
let parser = null;
let generator = null;
let renderer = null;
+ let exporter = null;
let container = null;
let ui = null;
@@ -26,10 +27,13 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
renderer = jasmine.createSpyObj('renderer', ['render', 'svg']);
renderer.svg.and.returnValue(document.createElement('svg'));
container = jasmine.createSpyObj('container', ['appendChild']);
+ exporter = jasmine.createSpyObj('exporter', ['getSVGURL']);
+
ui = new Interface({
parser,
generator,
renderer,
+ exporter,
defaultCode: 'my default code',
});
});
@@ -49,10 +53,12 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
describe('download SVG', () => {
it('triggers a download of the current image in SVG format', () => {
+ exporter.getSVGURL.and.returnValue('mySVGURL');
ui.build(container);
+
expect(ui.downloadSVG.getAttribute('href')).toEqual('#');
ui.downloadSVG.dispatchEvent(new Event('click'));
- expect(ui.downloadSVG.getAttribute('href')).not.toEqual('#');
+ expect(ui.downloadSVG.getAttribute('href')).toEqual('mySVGURL');
});
});
});
diff --git a/scripts/main.js b/scripts/main.js
index 0e03fab..a39a991 100644
--- a/scripts/main.js
+++ b/scripts/main.js
@@ -6,12 +6,14 @@
/* jshint -W072 */ // Allow several required modules
requirejs([
'interface/Interface',
+ 'interface/Exporter',
'sequence/Parser',
'sequence/Generator',
'sequence/Renderer',
'sequence/themes/Basic',
], (
Interface,
+ Exporter,
Parser,
Generator,
Renderer,
@@ -40,6 +42,7 @@
parser: new Parser(),
generator: new Generator(),
renderer: new Renderer(new Theme()),
+ exporter: new Exporter(),
localStorage: 'src',
});
ui.build(document.body);
diff --git a/scripts/readme_images.js b/scripts/readme_images.js
new file mode 100644
index 0000000..43379a8
--- /dev/null
+++ b/scripts/readme_images.js
@@ -0,0 +1,115 @@
+((() => {
+ 'use strict';
+
+ requirejs.config(window.getRequirejsCDN());
+
+ function makeText(text = '') {
+ return document.createTextNode(text);
+ }
+
+ function makeNode(type, attrs = {}) {
+ const o = document.createElement(type);
+ for(let k in attrs) {
+ if(attrs.hasOwnProperty(k)) {
+ o.setAttribute(k, attrs[k]);
+ }
+ }
+ return o;
+ }
+
+ const SAMPLE_REGEX = new RegExp(
+ /
]*>[\s]*```([^]+?)```/g
+ );
+
+ function findSamples(content) {
+ SAMPLE_REGEX.lastIndex = 0;
+ const results = [];
+ while(true) {
+ const match = SAMPLE_REGEX.exec(content);
+ if(!match) {
+ break;
+ }
+ results.push({
+ file: match[1],
+ code: match[2],
+ });
+ }
+ return results;
+ }
+
+ const PNG_RESOLUTION = 4;
+
+ /* jshint -W072 */ // Allow several required modules
+ requirejs([
+ 'sequence/Parser',
+ 'sequence/Generator',
+ 'sequence/Renderer',
+ 'sequence/themes/Basic',
+ 'interface/Exporter',
+ ], (
+ Parser,
+ Generator,
+ Renderer,
+ Theme,
+ Exporter
+ ) => {
+ const parser = new Parser();
+ const generator = new Generator();
+ const theme = new Theme();
+
+ 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);
+ const exporter = new Exporter();
+
+ const hold = makeNode('div', {'class': 'hold'});
+
+ hold.appendChild(renderer.svg());
+
+ const raster = makeNode('img', {
+ 'src': '',
+ 'class': 'raster',
+ 'title': 'new',
+ });
+ hold.appendChild(raster);
+
+ hold.appendChild(makeNode('img', {
+ 'src': 'screenshots/' + file,
+ 'class': 'original',
+ 'title': 'original',
+ }));
+
+ const downloadPNG = makeNode('a', {'href': '#', 'download': file});
+ downloadPNG.appendChild(makeText('Download PNG'));
+ hold.appendChild(downloadPNG);
+
+ document.body.appendChild(hold);
+
+ const parsed = parser.parse(code);
+ const sequence = generator.generate(parsed);
+ renderer.render(sequence);
+ exporter.getPNGURL(renderer, PNG_RESOLUTION, (url) => {
+ raster.setAttribute('src', url);
+ downloadPNG.setAttribute('href', url);
+ });
+ }
+
+ (fetch('README.md')
+ .then((response) => response.text())
+ .then(findSamples)
+ .then((samples) => {
+ samples.forEach(renderSample);
+ })
+ .then(() => {
+ document.body.removeChild(status);
+ })
+ .catch((e) => {
+ statusText.nodeValue = 'Error: ' + e;
+ })
+ );
+ });
+})());
diff --git a/styles/readme_images.css b/styles/readme_images.css
new file mode 100644
index 0000000..0eda0ff
--- /dev/null
+++ b/styles/readme_images.css
@@ -0,0 +1,60 @@
+html, body {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ margin-bottom: 50px;
+ background: #EEEEEE;
+}
+
+.status {
+ text-align: center;
+ font-size: 2em;
+ margin: 10px;
+}
+
+.hold {
+ display: block;
+ width: 920px;
+ background: #FFFFFF;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+ padding: 5px;
+ margin: 10px auto;
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.hold > svg {
+ width: 300px;
+ height: auto;
+ margin-right: 10px;
+}
+
+.hold > .raster {
+ width: 300px;
+ margin-right: 10px;
+}
+
+.hold > .original {
+ width: 300px;
+ background: #F8F8F8;
+}
+
+.hold a {
+ display: block;
+ text-align: center;
+ font-family: sans-serif;
+ padding: 15px;
+ margin: 5px -5px -5px;
+}
+
+.hold a:link, .hold a:visited {
+ color: #666699;
+ text-decoration: none;
+ cursor: pointer;
+}
+.hold a:active, .hold a:hover {
+ background: #EEEEEE;
+ color: #6666CC;
+}