diff --git a/CHANGELOG.md b/CHANGELOG.md index 9705f1a..bf2299a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG.md +## v1.1.0 + +Added SVG and PNG downloads + ## v1.0.2 * Fixed bug which was stopping slanted grids diff --git a/src/components/download.js b/src/components/download.js new file mode 100644 index 0000000..9089c03 --- /dev/null +++ b/src/components/download.js @@ -0,0 +1,85 @@ + +import * as resvg from 'npm:@resvg/resvg-wasm'; + + +const xmlns = "http://www.w3.org/2000/xmlns/"; +const xlinkns = "http://www.w3.org/1999/xlink"; +const svgns = "http://www.w3.org/2000/svg"; + +// adapted from the DOM.download method as per this issue: + +// https://github.com/observablehq/framework/issues/906 + + +export function download(value, name = "untitled", label = "Save") { + const a = document.createElement("a"); + const b = a.appendChild(document.createElement("button")); + b.textContent = label; + a.download = name; + + async function reset() { + await new Promise(requestAnimationFrame); + URL.revokeObjectURL(a.href); + a.removeAttribute("href"); + b.textContent = label; + b.disabled = false; + } + + a.onclick = async event => { + console.log("clicked download"); + b.disabled = true; + if (a.href) { + console.log(`already saved: ${a.href}`); + return reset(); // Already saved. + } + b.textContent = "Saving…"; + try { + console.log("awaiting value function"); + const object = await (typeof value === "function" ? value() : value); + console.log(object); + b.textContent = "Download"; + a.href = URL.createObjectURL(object); // eslint-disable-line require-atomic-updates + console.log(`url = ${a.href}`); + } catch (ignore) { + b.textContent = label; + } + if (event.eventPhase) return reset(); // Already downloaded. + b.disabled = false; + }; + + return a; +} + + +export function svg_to_string(svg) { + svg = svg.cloneNode(true); + svg.setAttributeNS(xmlns, "xmlns", svgns); + svg.setAttributeNS(xmlns, "xmlns:xlink", xlinkns); + const serializer = new window.XMLSerializer; + return serializer.serializeToString(svg); +} + + +export function download_as_svg(svg) { + console.log("HEY download_as_svg"); + const str = svg_to_string(svg); + return new Blob([str], {type: "image/svg+xml"}) +} + +export async function download_as_png (svg) { + // The Wasm must be initialized first + + const svgstr = svg_to_string(svg); + + const opts = { + fitTo: { + mode: 'width', // If you need to change the size + value: 400, + } + }; + const resvgJS = new resvg.Resvg(svgstr, opts) + const pngData = resvgJS.render(svgstr, opts) + const pngBuffer = pngData.asPng(); + return new Blob([pngBuffer], { type: 'image/png' }); +} + diff --git a/src/index.md b/src/index.md index bf32c35..d63cead 100644 --- a/src/index.md +++ b/src/index.md @@ -2,9 +2,11 @@ toc: false --- + +

poptimal

-

v1.0.2 | by mike lynch | @mikelynch@aus.social | source

+

v1.1.0 | by mike lynch | @mikelynch@aus.social | source

@@ -14,8 +16,13 @@ toc: false import {RADIUS_OPTS, DotMaker} from './components/dots.js'; import {PALETTES, DotControls} from './components/controls.js'; +import {download, download_as_svg, download_as_png} from './components/download.js'; import random from "npm:random"; +import * as resvg from 'npm:@resvg/resvg-wasm'; + +await resvg.initWasm(fetch('https://unpkg.com/@resvg/resvg-wasm/index_bg.wasm')); + const CELL = 10; const MAG = 2; const WIDTH = 20; @@ -149,10 +156,27 @@ dots_g2.selectAll("circle") display(svg.node()); - ``` -
+```js + +display(download(() => { + const thing = download_as_svg(svg.node()) + return thing; +}, "poptimal.svg", "Save as SVG")); + +display(download(() => { + console.log("PNG value"); + const thing = download_as_png(svg.node()) + return thing; +}, "poptimal.png", "Save as PNG")); + + +``` +(PNGs made with resvg-wasm in-browser) + + +