p5.js-svg
Version:
The main goal of p5.SVG is to provide a SVG runtime for p5.js, so that we can draw using p5's powerful API in \<svg\>, save things to svg file and manipulating existing SVG file without rasterization.
313 lines (285 loc) • 9.7 kB
text/typescript
import { P5SVG } from './types'
export default function (p5: P5SVG) {
/**
* Convert SVG Element to jpeg / png data url
*
* @private
* @param {SVGElement} svg SVG Element
* @param {String} mine Mine
* @param {Function} callback
*/
const svg2img = function (svg: SVGElement, mine: string, callback: (err: Error | null, dataURL: string) => void) {
let svgText = (new XMLSerializer()).serializeToString(svg)
svgText = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgText)
if (mine == 'image/svg+xml') {
callback(null, svgText)
return
}
const img = new Image()
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
img.onload = function () {
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
const dataURL = canvas.toDataURL(mine)
callback(null, dataURL)
}
img.src = svgText
}
/**
* Get SVG frame, and convert to target type
*
* @private
* @param {Object} options
* @param {SVGElement} options.svg SVG Element, defaults to current svg element
* @param {String} options.filename
* @param {String} options.ext Extension: 'svg' or 'jpg' or 'jpeg' or 'png'
* @param {Function} options.callback
*/
p5.prototype._makeSVGFrame = function (options: {
svg: SVGElement
filename?: string
extension?: string
callback: (err: Error | null, frame: {
imageData: string,
filename: string,
ext: string
}) => void
}) {
let filename = options.filename || 'untitled'
let ext = options.extension
ext = ext || this._checkFileExtension(filename, ext)[1]
const regexp = new RegExp('\\.' + ext + '$')
filename = filename.replace(regexp, '')
if (ext === '') {
ext = 'svg'
}
const mine = {
png: 'image/png',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
svg: 'image/svg+xml'
}[ext]
if (!mine) {
throw new Error('Fail to getFrame, invalid extension: ' + ext + ', please use png | jpeg | jpg | svg.')
}
const svg = options.svg || this._renderer.svg
svg2img(svg, mine, function (err, dataURL) {
const downloadMime = 'image/octet-stream'
dataURL = dataURL.replace(mine, downloadMime)
options.callback(err, {
imageData: dataURL,
filename: filename,
ext: ext
})
})
}
/**
* Save the current SVG as an image. In Safari, will open the
* image in the window and the user must provide their own
* filename on save-as. Other browsers will either save the
* file immediately, or prompt the user with a dialogue window.
*
* @function saveSVG
* @memberof p5.prototype
* @param {Graphics|Element|SVGElement} [svg] Source to save
* @param {String} [filename]
* @param {String} [extension] Extension: 'svg' or 'jpg' or 'jpeg' or 'png'
*/
p5.prototype.saveSVG = function (...args: any[]) {
args = [args[0], args[1], args[2]]
let svg
if (args[0] instanceof p5.Graphics) {
svg = args[0]._renderer.svg
args.shift()
}
if (args[0] && args[0].elt) {
svg = args[0].elt
args.shift()
}
if (typeof args[0] == 'object') {
svg = args[0]
args.shift()
}
const filename = args[0]
const ext = args[1]
this._makeSVGFrame({
svg: svg,
filename: filename,
extension: ext,
callback: (err: Error | null, frame: {
imageData: string,
filename: string,
ext: string
}) => {
if (err) {
throw err
}
this.downloadFile(frame.imageData, frame.filename, frame.ext)
}
})
}
/**
* Extends p5's saveFrames with SVG support
*
* @function saveFrames
* @memberof p5.prototype
* @param {String} filename filename
* @param {String} extension Extension: 'svg' or 'jpg' or 'jpeg' or 'png'
* @param {Number} duration duration
* @param {Number} fps fps
* @param {Function} callback callback
*/
const _saveFrames = p5.prototype.saveFrames
p5.prototype.saveFrames = function (...args: any) {
const filename: string = args[0]
const extension: string = args[1]
let duration: number = args[2]
let fps: number = args[3]
const callback: any = args[4]
if (!this._renderer.svg) {
return _saveFrames.apply(this, args)
}
duration = duration || 3
duration = p5.prototype.constrain(duration, 0, 15)
duration = duration * 1000
fps = fps || 15
fps = p5.prototype.constrain(fps, 0, 22)
let count = 0
const frames: any = []
let pending = 0
const frameFactory = setInterval(() => {
((count) => {
pending++
this._makeSVGFrame({
filename: filename + count,
extension: extension,
callback: function (err: Error | null, frame: any) {
if (err) {
throw err
}
frames[count] = frame
pending--
}
})
})(count)
count++
}, 1000 / fps)
const done = () => {
if (pending > 0) {
setTimeout(function () {
done()
}, 10)
return
}
if (callback) {
callback(frames)
} else {
frames.forEach((f: any) => {
this.downloadFile(f.imageData, f.filename, f.ext)
})
}
}
setTimeout(function () {
clearInterval(frameFactory)
done()
}, duration + 0.01)
}
/**
* Extends p5's save method with SVG support
*
* @function save
* @memberof p5.prototype
* @param {Graphics|Element|SVGElement} [source] Source to save
* @param {String} [filename] filename
*/
const _save = p5.prototype.save
p5.prototype.save = function (...args: any[]) {
let svg
if (args[0] instanceof p5.Graphics) {
const svgcanvas = args[0].elt
svg = svgcanvas.svg
args.shift()
}
if (args[0] && args[0].elt) {
svg = args[0].elt
args.shift()
}
if (typeof args[0] == 'object') {
svg = args[0]
args.shift()
}
svg = svg || (this._renderer && this._renderer.svg)
const filename = args[0]
const supportedExtensions = ['jpeg', 'png', 'jpg', 'svg', '']
const ext = this._checkFileExtension(filename, '')[1]
const useSVG = svg && svg.nodeName && svg.nodeName.toLowerCase() === 'svg' && supportedExtensions.indexOf(ext) > -1
if (useSVG) {
this.saveSVG(svg, filename)
} else {
return _save.apply(this, args)
}
}
/**
* Custom get in p5.svg (handles http and dataurl)
* @private
*/
p5.prototype._svg_get = function (path: string, successCallback: any, failureCallback: any) {
if (path.indexOf('data:') === 0) {
if (path.indexOf(',') === -1) {
failureCallback(new Error('Fail to parse dataurl: ' + path))
return
}
let svg = path.split(',').pop()
// force request to dataurl to be async
// so that it won't make preload mess
setTimeout(function () {
if (path.indexOf(';base64,') > -1) {
svg = atob(svg)
} else {
svg = decodeURIComponent(svg)
}
successCallback(svg)
}, 1)
return svg
} else {
this.httpGet(path, successCallback)
return null
}
}
/**
* loadSVG (like loadImage, but will return SVGElement)
*
* @function loadSVG
* @memberof p5.prototype
* @returns {p5.SVGElement}
*/
p5.prototype.loadSVG = function (path: string, successCallback: any, failureCallback: any) {
const div = document.createElement('div')
const element = new p5.SVGElement(div)
this._incrementPreload()
new Promise((resolve, reject) => {
this._svg_get(path, function (svg: string) {
div.innerHTML = svg
const svgEl = div.querySelector('svg')
if (!svgEl) {
reject('Fail to create <svg>.')
return
}
element.elt = svgEl
resolve(element)
}, reject)
}).then((v) => {
successCallback && successCallback(v)
}).catch((e) => {
failureCallback && failureCallback(e)
}).finally(() => {
this._decrementPreload()
})
return element
}
p5.prototype.getDataURL = function () {
return this._renderer.elt.toDataURL('image/svg+xml')
}
}