Extract image export functionality, add convenience page for generating README images
This commit is contained in:
parent
71437d2576
commit
f783750c0d
|
@ -0,0 +1,37 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="content-security-policy" content="
|
||||||
|
base-uri 'self';
|
||||||
|
default-src 'none';
|
||||||
|
script-src
|
||||||
|
'self'
|
||||||
|
'sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk='
|
||||||
|
;
|
||||||
|
style-src 'self';
|
||||||
|
connect-src 'self';
|
||||||
|
img-src 'self' blob:;
|
||||||
|
form-action 'none';
|
||||||
|
">
|
||||||
|
|
||||||
|
<title>Sequence Diagram</title>
|
||||||
|
<link rel="icon" href="favicon.png">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="styles/readme_images.css">
|
||||||
|
|
||||||
|
<script src="scripts/requireConfig.js"></script>
|
||||||
|
|
||||||
|
<script
|
||||||
|
data-main="scripts/readme_images"
|
||||||
|
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.min.js"
|
||||||
|
integrity="sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<noscript>This tool requires Javascript!</noscript>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
|
@ -33,16 +33,14 @@ define([
|
||||||
parser,
|
parser,
|
||||||
generator,
|
generator,
|
||||||
renderer,
|
renderer,
|
||||||
|
exporter,
|
||||||
defaultCode = '',
|
defaultCode = '',
|
||||||
localStorage = '',
|
localStorage = '',
|
||||||
}) {
|
}) {
|
||||||
window.devicePixelRatio = 1;
|
|
||||||
this.canvas = makeNode('canvas');
|
|
||||||
this.context = this.canvas.getContext('2d');
|
|
||||||
|
|
||||||
this.parser = parser;
|
this.parser = parser;
|
||||||
this.generator = generator;
|
this.generator = generator;
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
|
this.exporter = exporter;
|
||||||
this.defaultCode = defaultCode;
|
this.defaultCode = defaultCode;
|
||||||
this.localStorage = localStorage;
|
this.localStorage = localStorage;
|
||||||
this.minScale = 1.5;
|
this.minScale = 1.5;
|
||||||
|
@ -50,11 +48,8 @@ define([
|
||||||
this.debounced = null;
|
this.debounced = null;
|
||||||
this.latestSeq = null;
|
this.latestSeq = null;
|
||||||
this.renderedSeq = null;
|
this.renderedSeq = null;
|
||||||
this.canvasDirty = true;
|
this.pngDirty = true;
|
||||||
this.svgDirty = true;
|
this.updatingPNG = false;
|
||||||
this.latestPNG = null;
|
|
||||||
this.latestSVG = null;
|
|
||||||
this.updatingPNG = null;
|
|
||||||
|
|
||||||
this._downloadSVGClick = this._downloadSVGClick.bind(this);
|
this._downloadSVGClick = this._downloadSVGClick.bind(this);
|
||||||
this._downloadPNGClick = this._downloadPNGClick.bind(this);
|
this._downloadPNGClick = this._downloadPNGClick.bind(this);
|
||||||
|
@ -168,8 +163,7 @@ define([
|
||||||
redraw(sequence) {
|
redraw(sequence) {
|
||||||
clearTimeout(this.debounced);
|
clearTimeout(this.debounced);
|
||||||
this.debounced = null;
|
this.debounced = null;
|
||||||
this.canvasDirty = true;
|
this.pngDirty = true;
|
||||||
this.svgDirty = true;
|
|
||||||
this.renderedSeq = sequence;
|
this.renderedSeq = sequence;
|
||||||
this.renderer.render(sequence);
|
this.renderer.render(sequence);
|
||||||
this.updateMinSize(this.renderer.width, this.renderer.height);
|
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() {
|
updatePNGLink() {
|
||||||
const nonce = this.updatingPNG = {};
|
this.forceRender();
|
||||||
this.getPNGData((data) => {
|
if(this.updatingPNG || !this.pngDirty) {
|
||||||
if(this.updatingPNG === nonce) {
|
return false;
|
||||||
this.downloadPNG.setAttribute('href', data);
|
}
|
||||||
this.updatingPNG = null;
|
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() {
|
_downloadPNGFocus() {
|
||||||
this.forceRender();
|
this.updatePNGLink();
|
||||||
if(this.canvasDirty) {
|
|
||||||
this.updatePNGLink();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadPNGClick(e) {
|
_downloadPNGClick(e) {
|
||||||
if(this.updatingPNG) {
|
if(this.updatingPNG) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if(this.canvasDirty) {
|
} else if(this.updatePNGLink()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.updatePNGLink();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadSVGClick() {
|
_downloadSVGClick() {
|
||||||
this.downloadSVG.setAttribute('href', this.getSVGData());
|
this.forceRender();
|
||||||
|
const url = this.exporter.getSVGURL(this.renderer);
|
||||||
|
this.downloadSVG.setAttribute('href', url);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
|
||||||
let parser = null;
|
let parser = null;
|
||||||
let generator = null;
|
let generator = null;
|
||||||
let renderer = null;
|
let renderer = null;
|
||||||
|
let exporter = null;
|
||||||
let container = null;
|
let container = null;
|
||||||
let ui = null;
|
let ui = null;
|
||||||
|
|
||||||
|
@ -26,10 +27,13 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
|
||||||
renderer = jasmine.createSpyObj('renderer', ['render', 'svg']);
|
renderer = jasmine.createSpyObj('renderer', ['render', 'svg']);
|
||||||
renderer.svg.and.returnValue(document.createElement('svg'));
|
renderer.svg.and.returnValue(document.createElement('svg'));
|
||||||
container = jasmine.createSpyObj('container', ['appendChild']);
|
container = jasmine.createSpyObj('container', ['appendChild']);
|
||||||
|
exporter = jasmine.createSpyObj('exporter', ['getSVGURL']);
|
||||||
|
|
||||||
ui = new Interface({
|
ui = new Interface({
|
||||||
parser,
|
parser,
|
||||||
generator,
|
generator,
|
||||||
renderer,
|
renderer,
|
||||||
|
exporter,
|
||||||
defaultCode: 'my default code',
|
defaultCode: 'my default code',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -49,10 +53,12 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
|
||||||
|
|
||||||
describe('download SVG', () => {
|
describe('download SVG', () => {
|
||||||
it('triggers a download of the current image in SVG format', () => {
|
it('triggers a download of the current image in SVG format', () => {
|
||||||
|
exporter.getSVGURL.and.returnValue('mySVGURL');
|
||||||
ui.build(container);
|
ui.build(container);
|
||||||
|
|
||||||
expect(ui.downloadSVG.getAttribute('href')).toEqual('#');
|
expect(ui.downloadSVG.getAttribute('href')).toEqual('#');
|
||||||
ui.downloadSVG.dispatchEvent(new Event('click'));
|
ui.downloadSVG.dispatchEvent(new Event('click'));
|
||||||
expect(ui.downloadSVG.getAttribute('href')).not.toEqual('#');
|
expect(ui.downloadSVG.getAttribute('href')).toEqual('mySVGURL');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,12 +6,14 @@
|
||||||
/* jshint -W072 */ // Allow several required modules
|
/* jshint -W072 */ // Allow several required modules
|
||||||
requirejs([
|
requirejs([
|
||||||
'interface/Interface',
|
'interface/Interface',
|
||||||
|
'interface/Exporter',
|
||||||
'sequence/Parser',
|
'sequence/Parser',
|
||||||
'sequence/Generator',
|
'sequence/Generator',
|
||||||
'sequence/Renderer',
|
'sequence/Renderer',
|
||||||
'sequence/themes/Basic',
|
'sequence/themes/Basic',
|
||||||
], (
|
], (
|
||||||
Interface,
|
Interface,
|
||||||
|
Exporter,
|
||||||
Parser,
|
Parser,
|
||||||
Generator,
|
Generator,
|
||||||
Renderer,
|
Renderer,
|
||||||
|
@ -40,6 +42,7 @@
|
||||||
parser: new Parser(),
|
parser: new Parser(),
|
||||||
generator: new Generator(),
|
generator: new Generator(),
|
||||||
renderer: new Renderer(new Theme()),
|
renderer: new Renderer(new Theme()),
|
||||||
|
exporter: new Exporter(),
|
||||||
localStorage: 'src',
|
localStorage: 'src',
|
||||||
});
|
});
|
||||||
ui.build(document.body);
|
ui.build(document.body);
|
||||||
|
|
|
@ -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(
|
||||||
|
/<img src="screenshots\/([^"]*)"[^>]*>[\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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})());
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue