"use strict"; import XMLWriter from 'xml-writer'; import Rectangle from './Rectangle.mjs'; import Vector2 from './Vector2.mjs'; /* * Simplifies the process for creating an SVG dynamically. * Originally written for MusicBoxConverter, but lifted, reused, and extended for FloatingIslands. * Ported from C# to Javascript for AirQualityWeb. * @license MPL-2.0 */ class SvgWriter { /** * @param {String} [widthspec="100%"] * @param {String} [heightspec="100%"] * @param {Retangle} [viewBox=null] */ constructor(widthspec = "100%", heightspec = "100%", viewBox = null, pretty_print = false) { this.unitSuffix = ""; // ---------------------------------- this.xml = new XMLWriter(pretty_print); this.xml.startDocument(); this.xml.writeDocType("svg", "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd", null); this.xml.writeComment("Generated by SVGWriter.js, which was written by Starbeamrainbowlabs & ported from SVGWriter.cs"); this.xml.startElement("svg", "http://www.w3.org/2000/svg"); this.xml.writeAttribute("version", "1.1"); this.xml.writeAttribute("width", widthspec); this.xml.writeAttribute("height", heightspec); if (viewBox != null) { this.xml.writeAttribute( "viewBox", `${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}` ); } } /** * Completes the SVG image currently being generated and closes the underlying file stream. * @return {this} */ complete() { this.xml.endElement(); this.xml.endDocument(); return this; } toString() { return this.xml.toString(); } /** * Adds a line to the image. * @param {Vector2} start The start position of the line. * @param {Vector2} end The end position of the line. * @param {String} [strokeStyle="darkgreen"] The colour to draw the line. * @param {Number} [strokeWidth=3] The width to draw the line. */ addLine(start, end, strokeStyle = "darkgreen", strokeWidth = 3) { this.xml.startElement("line"); this.xml.writeAttribute("x1", `${start.X}${UnitSuffix}`); this.xml.writeAttribute("y1", `${start.Y}${UnitSuffix}`); this.xml.writeAttribute("x2", `${end.X}${UnitSuffix}`); this.xml.writeAttribute("y2", `${end.Y}${UnitSuffix}`); this.xml.writeAttribute("stroke", strokeStyle); this.xml.writeAttribute("stroke-width", strokeWidth.toString()); this.xml.endElement(); return this; } /** * Opens a new SVG group. * @param {string} [classes=null] The class(es) to apply to the new group. * @param {string} [transform=null] The transform(s) to apply to the new group. * @return {this} */ startGroup(classes = null, transform = null) { this.xml.startElement("g"); if(classes != null) this.xml.writeAttribute("class", classes); if (transform != null) this.xml.writeAttribute("transform", transform); return this; } /** * Ends the most recently created unclosed group. * @return {this} */ endGroup() { this.xml.endElement(); return this; } /// /// /// /// /** * Starts a scale transform. * @param {number} scale The scale to enlarge (or shrink!) the next items by. * @return {this} */ startScaleTransform(scale) { this.xml.startElement("g"); this.xml.writeAttribute("transform", `scale(${scale})`); return this; } /** * Ends the most recently created scale transform. * @return {this} */ endTransform() { this.xml.endElement(); return this; } /** * Adds a hollow rectangle to the image. * @param {Vector2} position The position of the rectangle. * @param {Vector2} size The size of the rectangle. * @param {string} strokeStyle The colour to use when drawing. * @param {float} strokeWidth The line width to use when drawing. * @return {this} */ addRectangle(position, size, strokeStyle = "red", strokeWidth = 3) { this.xml.startElement("rect"); this.xml.writeAttribute("x", `${position.x}${this.unitSuffix}`); this.xml.writeAttribute("y", `${position.y}${this.unitSuffix}`); this.xml.writeAttribute("width", `${size.X}${this.unitSuffix}`); this.xml.writeAttribute("height", `${size.Y}${this.unitSuffix}`); this.xml.writeAttribute("fill", "none"); this.xml.writeAttribute("stroke", strokeStyle); this.xml.writeAttribute("stroke-width", strokeWidth.toString()); this.xml.endElement(); return this; } /** * Adds a circle to the image. * @param {Vector2} centre The position of the centre of the circle. * @param {Number} radius The radius of the circle. * @param {String} [fillStyle="blue"] The colour to fill the circle with. * @return {this} */ addCircle(centre, radius, fillStyle = "blue") { this.xml.startElement("circle"); this.xml.writeAttribute("cx", `${centre.x}${this.unitSuffix}`); this.xml.writeAttribute("cy", `${centre.y}${this.unitSuffix}`); this.xml.writeAttribute("r", `${radius}${this.unitSuffix}`); this.xml.writeAttribute("fill", fillStyle); this.xml.endElement(); return this; } /** * Adds a solid n-sided polygon to the image. * @param {string} fillStyle The colour to fill the polygon with. * @param {Vector2[]} points The co-ordinates that make up the polygon. * @return {this} */ addPolygon(fillStyle, points) { this.xml.startElement("polygon"); this.xml.writeAttribute("fill", fillStyle); this.xml.writeAttribute( "points", points.map((point) => `${point.x},${point.y}`).join(" ") ); this.xml.endElement(); return this; } /** * Adds an isosceles / equilateral triangle to the image. * This is a shorthand method that calls AddPolygon() under-the-hood. * @param {Vector2} position The position to draw the triangle at. * @param {Number} baseWidth The width of the triangle's base. * @param {Number} height The height of the triangle. * @param {Boolean} upsideDown If set to true, then the triangle will point downwards instead of upwards. * @param {string} fillStyle The colour to fill the triangle with. * @return {this} */ addTriangleRegular(position, baseWidth, height, upsideDown, fillStyle) { this.addPolygon( fillStyle, position.subtract(new Vector2(baseWidth / 2, 0)), position.add(new Vector2(baseWidth / 2, 0)), position.subtract(new Vector2(0, upsideDown ? -height : height)) ); return this; } } SvgWriter.string2element = (svg_string) => { let temp = document.createElement("div"); temp.innerHTML = svg_string; return temp.querySelector("svg"); } export default SvgWriter;