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,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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