import { RoughCanvas } from 'roughjs/bin/canvas' ; import { Options } from 'roughjs/bin/core' ; import rough from 'roughjs/bin/rough' ; import { RoughSVG } from 'roughjs/bin/svg' ; import { processRoot } from './processor' ; import { RenderMode } from './RenderMode' ; import { SvgTextures } from './SvgTextures' ; import { getDefsElement , RenderContext } from './utils' ; /**
 * Svg2Roughjs parses a given SVG and draws it with Rough.js
 * in a canvas.
 */ export class Svg2Roughjs { private $svg ? : SVGSVGElement ; private width : number = 0 ; private height : number = 0 ; private canvas : HTMLCanvasElement | SVGSVGElement ; private $roughConfig : Options ; private rc : RoughCanvas | RoughSVG ; private $fontFamily : string | null ; private $randomize : boolean ; private $backgroundColor : string | null = null ; private $renderMode : RenderMode ; private ctx : CanvasRenderingContext2D | null = null ; private $pencilFilter : boolean = false ; private idElements : Record < string , SVGElement | string > = { } ; /**
     * The SVG that should be converted.
     * Changing this property triggers drawing of the SVG into
     * the canvas or container element with which Svg2Roughjs
     * was initialized.
     */ set svg ( svg ) { if ( this . $svg !== svg ) { this . $svg = svg ; if ( svg . hasAttribute ( 'width' ) ) { this . width = svg . width . baseVal . value ; } else if ( svg . hasAttribute ( 'viewBox' ) ) { this . width = svg . viewBox . baseVal . width ; } else { this . width = 300 ; } if ( svg . hasAttribute ( 'height' ) ) { this . height = svg . height . baseVal . value ; } else if ( svg . hasAttribute ( 'viewBox' ) ) { this . height = svg . viewBox . baseVal . height ; } else { this . height = 150 ; } if ( this . renderMode === RenderMode . CANVAS && this . ctx ) { const canvas = this . canvas as HTMLCanvasElement ; canvas . width = this . width ; canvas . height = this . height ; } else { const svg = this . canvas as SVGSVGElement ; svg . setAttribute ( 'width' , this . width . toString ( ) ) ; svg . setAttribute ( 'height' , this . height . toString ( ) ) ; } this . collectElementsWithID ( ) ; this . redraw ( ) ; } } get svg ( ) : SVGSVGElement { return this . $svg as SVGSVGElement ; } /**
     * Rough.js config object that is provided to Rough.js for drawing
     * any SVG element.
     * Changing this property triggers a repaint.
     */ set roughConfig ( config ) { this . $roughConfig = config ; if ( this . renderMode === RenderMode . CANVAS && this . ctx ) { this . rc = rough . canvas ( this . canvas as HTMLCanvasElement , { options : this . roughConfig } ) ; } else { this . rc = rough . svg ( this . canvas as SVGSVGElement , { options : this . roughConfig } ) ; } this . redraw ( ) ; } get roughConfig ( ) : Options { return this . $roughConfig ; } /**
     * Set a font-family for the rendering of text elements.
     * If set to `null`, then the font-family of the SVGTextElement is used.
     * By default, 'Comic Sans MS, cursive' is used.
     * Changing this property triggers a repaint.
     */ set fontFamily ( fontFamily ) { if ( this . $fontFamily !== fontFamily ) { this . $fontFamily = fontFamily ; this . redraw ( ) ; } } get fontFamily ( ) : string | null { return this . $fontFamily ; } /**
     * Whether to randomize Rough.js' fillWeight, hachureAngle and hachureGap.
     * Also randomizes the disableMultiStroke option of Rough.js.
     * By default true.
     * Changing this property triggers a repaint.
     */ set randomize ( randomize ) { this . $randomize = randomize ; this . redraw ( ) ; } get randomize ( ) : boolean { return this . $randomize ; } /**
     * Optional solid background color with which
     * the canvas should be initialized.
     * It is drawn on a transparent canvas by default.
     */ set backgroundColor ( color ) { this . $backgroundColor = color ; } get backgroundColor ( ) : string | null { return this . $backgroundColor ; } /**
     * Changes the output format of the converted SVG.
     * Changing this property will replace the current output
     * element with either a new HTML canvas or new SVG element.
     */ set renderMode ( mode ) { if ( this . $renderMode === mode ) { return ; } this . $renderMode = mode ; const parent = this . canvas ! . parentElement as HTMLElement ; parent . removeChild ( this . canvas ! ) ; let target ; if ( mode === RenderMode . CANVAS ) { target = document . createElement ( 'canvas' ) ; target . width = this . width ; target . height = this . height ; this . ctx = target . getContext ( '2d' ) ; } else { this . ctx = null ; target = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'svg' ) ; target . setAttribute ( 'xmlns' , 'http://www.w3.org/2000/svg' ) ; target . setAttribute ( 'xmlns:xlink' , 'http://www.w3.org/1999/xlink' ) ; target . setAttribute ( 'width' , this . width . toString ( ) ) ; target . setAttribute ( 'height' , this . height . toString ( ) ) ; } parent ! . appendChild ( target ) ; this . canvas = target ; if ( mode === RenderMode . CANVAS ) { this . rc = rough . canvas ( this . canvas as HTMLCanvasElement , { options : this . roughConfig } ) ; } else { this . rc = rough . svg ( this . canvas as SVGSVGElement , { options : this . roughConfig } ) ; } this . redraw ( ) ; } get renderMode ( ) : RenderMode { return this . $renderMode ; } /**
     * Whether to apply a pencil filter.
     * Only works for SVG render mode.
     */ set pencilFilter ( value ) { if ( this . $pencilFilter !== value ) { this . $pencilFilter = value ; this . redraw ( ) ; } } get pencilFilter ( ) : boolean { return this . $pencilFilter ; } returns createRenderContext ( ) { if ( ! this . $svg ) { throw new Error ( 'No source SVG set yet.' ) ; } const ctx = { rc : this . rc , roughConfig : this . roughConfig , renderMode : this . renderMode , fontFamily : this . fontFamily , pencilFilter : this . pencilFilter , randomize : this . randomize , idElements : this . idElements , sourceSvg : this . $svg , processElement : processRoot } ; if ( this . renderMode === RenderMode . CANVAS && this . ctx ) { ctx . targetCanvas = this . canvas as HTMLCanvasElement ; ctx . targetCanvasContext = this . ctx ; } else { ctx . targetSvg = this . canvas as SVGSVGElement ; } return ctx ; } param target param renderMode param roughjsOptions constructor ( target , renderMode = RenderMode . SVG , roughjsOptions = { } ) { if ( ! target ) { throw new Error ( 'No target provided' ) ; } if ( target instanceof HTMLCanvasElement || target instanceof SVGSVGElement ) { if ( target . tagName === 'canvas' || target . tagName === 'svg' ) { this . canvas = target ; this . $renderMode = target . tagName === 'canvas' ? RenderMode . CANVAS : RenderMode . SVG ; } else { throw new Error ( 'Target object is not of type HTMLCanvasElement or SVGSVGElement' ) ; } } else { const container = document . querySelector ( target ) ; if ( ! container ) { throw new Error ( `No element found with ${ target }` ) ; } if ( renderMode === RenderMode . CANVAS ) { this . canvas = document . createElement ( 'canvas' ) ; this . canvas . width = container . clientWidth ; this . canvas . height = container . clientHeight ; } else { this . canvas = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'svg' ) ; this . canvas . setAttribute ( 'xmlns' , 'http://www.w3.org/2000/svg' ) ; this . canvas . setAttribute ( 'xmlns:xlink' , 'http://www.w3.org/1999/xlink' ) ; } this . $renderMode = renderMode ; container . appendChild ( this . canvas ) ; } this . $roughConfig = roughjsOptions ; if ( this . renderMode === RenderMode . CANVAS ) { const canvas = this . canvas as HTMLCanvasElement ; this . rc = rough . canvas ( canvas , { options : this . roughConfig } ) ; this . ctx = canvas . getContext ( '2d' ) ; } else { this . rc = rough . svg ( this . canvas as SVGSVGElement , { options : this . roughConfig } ) ; } this . $fontFamily = 'Comic Sans MS, cursive' ; this . $randomize = true ; } /**
     * Triggers an entire redraw of the SVG which
     * processes the input element anew.
     */ redraw ( ) { if ( ! this . svg ) { return ; } if ( this . renderMode === RenderMode . CANVAS ) { this . initializeCanvas ( this . canvas as HTMLCanvasElement ) ; } else { this . initializeSvg ( this . canvas as SVGSVGElement ) ; } const renderContext = this . createRenderContext ( ) ; renderContext . processElement ( renderContext , this . svg , null , this . width , this . height ) ; } /**
     * Prepares the given canvas element depending on the set properties.
     */ private initializeCanvas ( canvas ) { this . ctx = canvas . getContext ( '2d' ) ; if ( this . ctx ) { this . ctx . clearRect ( 0 , 0 , this . width , this . height ) ; if ( this . backgroundColor ) { this . ctx . fillStyle = this . backgroundColor ; this . ctx . fillRect ( 0 , 0 , this . width , this . height ) ; } this . ctx . lineCap = 'round' ; } } /**
     * Prepares the given SVG element depending on the set properties.
     */ private initializeSvg ( svgElement ) { this . ctx = null ; while ( svgElement . firstChild ) { svgElement . removeChild ( svgElement . firstChild ) ; } let backgroundElement ; if ( this . backgroundColor ) { backgroundElement = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'rect' ) ; backgroundElement . width . baseVal . value = this . width ; backgroundElement . height . baseVal . value = this . height ; backgroundElement . setAttribute ( 'fill' , this . backgroundColor ) ; svgElement . appendChild ( backgroundElement ) ; } if ( this . pencilFilter ) { const defs = getDefsElement ( svgElement ) ; defs . appendChild ( SvgTextures . pencilTextureFilter ) ; } svgElement . setAttribute ( 'stroke-linecap' , 'round' ) ; } /**
     * Stores elements with IDs for later use.
     */ private collectElementsWithID ( ) { this . idElements = { } ; const elementsWithID = Array . prototype . slice . apply ( this . svg . querySelectorAll ( '*[id]' ) ) ; for ( const elt of elementsWithID ) { const id = elt . getAttribute ( 'id' ) ; if ( id ) { this . idElements [ id ] = elt ; } } } }