import { RoughCanvas } from 'roughjs/bin/canvas' ; import { Drawable , Options } from 'roughjs/bin/core' ; import { RoughSVG } from 'roughjs/bin/svg' ; import { Point } from './geom/point' ; import { RenderMode } from './RenderMode' ; const tinycolor = require ( 'tinycolor2' ) ; const units = require ( 'units-css' ) ; type Color = any ; /**
 * Regexp that detects curved commands in path data.
 */ export const PATH_CURVES_REGEX = /[acsqt]/i ; /**
 * A simple regexp which is used to test whether a given string value
 * contains unit identifiers, e.g. "1px", "1em", "1%", ...
 */ export const CONTAINS_UNIT_REGEXP = /[a-z%]/ ; returns export function averageColor ( colorArray ) { const count = colorArray . length ; let r = 0 ; let g = 0 ; let b = 0 ; let a = 0 ; colorArray . forEach ( tinycolor => { const color = tinycolor . toRgb ( ) ; r += color . r * color . r ; g += color . g * color . g ; b += color . b * color . b ; a += color . a ; } ) ; return tinycolor ( { r : Math . sqrt ( r / count ) , g : Math . sqrt ( g / count ) , b : Math . sqrt ( b / count ) , a : a / count } ) ; } /**
 * Returns the Node's children, since Node.prototype.children is not available on all browsers.
 * https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/children
 */ export function getNodeChildren ( element ) { if ( typeof element . children !== 'undefined' ) { return element . children as unknown as Element [ ] ; } let i = 0 ; let node ; const nodes = element . childNodes ; const children = [ ] ; while ( ( node = nodes [ i ++ ] ) ) { if ( node . nodeType === 1 ) { children . push ( node ) ; } } return children as Element [ ] ; } return export function getLengthInPx ( svgLengthList ) { if ( svgLengthList && svgLengthList . baseVal . numberOfItems > 0 ) { return svgLengthList . baseVal . getItem ( 0 ) . value ; } return 0 ; } returns export function isIdentityTransform ( svgTransform ) { if ( ! svgTransform ) { return true ; } const matrix = svgTransform . matrix ; return ( ! matrix || ( matrix . a === 1 && matrix . b === 0 && matrix . c === 0 && matrix . d === 1 && matrix . e === 0 && matrix . f === 0 ) ) ; } returns export function isTranslationTransform ( svgTransform ) { if ( ! svgTransform ) { return true ; } const matrix = svgTransform . matrix ; return ! matrix || ( matrix . a === 1 && matrix . b === 0 && matrix . c === 0 && matrix . d === 1 ) ; } /**
 * Applies a given `SVGTransform` to the point.
 *
 * [a c e] [x] = (a*x + c*y + e)
 * [b d f] [y] = (b*x + d*y + f)
 * [0 0 1] [1] = (0 + 0 + 1)
 */ export function applyMatrix ( point , svgTransform ) { if ( ! svgTransform ) { return point ; } const matrix = svgTransform . matrix ; const x = matrix . a * point . x + matrix . c * point . y + matrix . e ; const y = matrix . b * point . x + matrix . d * point . y + matrix . f ; return new Point ( x , y ) ; } /**
 * Returns a random number in the given range.
 */ export function getRandomNumber ( min , max ) { return Math . random ( ) * ( max - min ) + min ; } return export function getStopOffset ( stop ) { const offset = stop . getAttribute ( 'offset' ) ; if ( ! offset ) { return 0 ; } if ( offset . indexOf ( '%' ) ) { return parseFloat ( offset . substring ( 0 , offset . length - 1 ) ) ; } else { return parseFloat ( offset ) * 100 ; } } /**
 * Returns the `stop-color` of an `SVGStopElement`.
 */ export function getStopColor ( stop ) { let stopColorStr = stop . getAttribute ( 'stop-color' ) ; if ( ! stopColorStr ) { const style = stop . getAttribute ( 'style' ) ?? '' ; const match = /stop-color:\s?(.*);?/ . exec ( style ) ; if ( match && match . length > 1 ) { stopColorStr = match [ 1 ] ; } } return stopColorStr ? tinycolor ( stopColorStr ) : tinycolor ( 'white' ) ; } /**
 * Converts an SVG gradient to a color by mixing all stop colors
 * with `tinycolor.mix`.
 */ export function gradientToColor ( gradient , opacity ) { const stops = Array . prototype . slice . apply ( gradient . querySelectorAll ( 'stop' ) ) ; if ( stops . length === 0 ) { return 'transparent' ; } else if ( stops . length === 1 ) { const color = getStopColor ( stops [ 0 ] ) ; color . setAlpha ( opacity ) ; return color . toString ( ) ; } else { const resolution = 10 ; const discreteColors = [ ] ; let lastColor = null ; for ( let i = 0 ; i < stops . length ; i ++ ) { const currentColor = getStopColor ( stops [ i ] ) ; const currentOffset = getStopOffset ( stops [ i ] ) ; const combinedColor = lastColor ? averageColor ( [ lastColor , currentColor ] ) : currentColor ; let entries = Math . max ( 1 , ( currentOffset / resolution ) | 0 ) ; while ( entries > 0 ) { discreteColors . push ( combinedColor ) ; entries -- ; } lastColor = currentColor ; } const mixedColor = averageColor ( discreteColors ) ; mixedColor . setAlpha ( opacity ) ; return mixedColor . toString ( ) ; } } /**
 * Returns the id from the url string
 */ export function getIdFromUrl ( url ) { if ( url === null ) { return null ; } const result = /url\('#?(.*?)'\)/ . exec ( url ) || /url\("#?(.*?)"\)/ . exec ( url ) || /url\(#?(.*?)\)/ . exec ( url ) ; if ( result && result . length > 1 ) { return result [ 1 ] ; } return null ; } /**
 * Converts SVG opacity attributes to a [0, 1] range.
 */ export function getOpacity ( element , attribute ) { const attr = ( getComputedStyle ( element ) as any ) [ attribute ] || element . getAttribute ( attribute ) ; if ( attr ) { if ( attr . indexOf ( '%' ) !== - 1 ) { return Math . min ( 1 , Math . max ( 0 , parseFloat ( attr . substring ( 0 , attr . length - 1 ) ) / 100 ) ) ; } return Math . min ( 1 , Math . max ( 0 , parseFloat ( attr ) ) ) ; } return 1 ; } /**
 * Returns the consolidated transform of the given element.
 */ export function getSvgTransform ( element ) { if ( element . transform && element . transform . baseVal . numberOfItems > 0 ) { return element . transform . baseVal . consolidate ( ) ; } return null ; } export function getDefsElement ( svgElement ) { let outputDefs = svgElement . querySelector ( 'defs' ) ; if ( ! outputDefs ) { outputDefs = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'defs' ) ; if ( svgElement . childElementCount > 0 ) { svgElement . insertBefore ( outputDefs , svgElement . firstElementChild ) ; } else { svgElement . appendChild ( outputDefs ) ; } } return outputDefs ; } export function isHidden ( element ) { const style = element . style ; if ( ! style ) { return false ; } return style . display === 'none' || style . visibility === 'hidden' ; } /**
 * The angle in degree of the line defined by the given points.
 */ export function getAngle ( p0 , p1 ) { return Math . atan2 ( p1 . y - p0 . y , p1 . x - p0 . x ) * ( 180 / Math . PI ) ; } export function getPointsArray ( element ) { const pointsAttr = element . getAttribute ( 'points' ) ; if ( ! pointsAttr ) { return [ ] ; } let coordinateRegexp ; if ( pointsAttr . indexOf ( ' ' ) > 0 ) { coordinateRegexp = /\s+/g ; } else { coordinateRegexp = /,/g ; } const pointList = pointsAttr . split ( coordinateRegexp ) ; const points = [ ] ; for ( let i = 0 ; i < pointList . length ; i ++ ) { const currentEntry = pointList [ i ] ; const coordinates = currentEntry . split ( ',' ) ; if ( coordinates . length === 2 ) { points . push ( new Point ( parseFloat ( coordinates [ 0 ] ) , parseFloat ( coordinates [ 1 ] ) ) ) ; } else { const next = i + 1 ; if ( next < pointList . length ) { points . push ( new Point ( parseFloat ( currentEntry ) , parseFloat ( pointList [ next ] ) ) ) ; i = next ; } } } return points ; } param currentUseCtx export function getEffectiveElementOpacity ( context , element , currentOpacity , currentUseCtx ? ) { let attr ; if ( ! currentUseCtx ) { attr = getComputedStyle ( element ) [ 'opacity' ] || element . getAttribute ( 'opacity' ) ; } else { attr = element . getAttribute ( 'opacity' ) ; } if ( attr ) { let elementOpacity = 1 ; if ( attr . indexOf ( '%' ) !== - 1 ) { elementOpacity = Math . min ( 1 , Math . max ( 0 , parseFloat ( attr . substring ( 0 , attr . length - 1 ) ) / 100 ) ) ; } else { elementOpacity = Math . min ( 1 , Math . max ( 0 , parseFloat ( attr ) ) ) ; } currentOpacity *= elementOpacity ; } let parent = element . parentElement ; const useCtx = currentUseCtx ; let nextUseCtx = useCtx ; if ( useCtx && useCtx . referenced === element ) { parent = useCtx . root ; nextUseCtx = useCtx . parentContext ; } if ( ! parent || parent === context . sourceSvg ) { return currentOpacity ; } return getEffectiveElementOpacity ( context , parent as SVGElement , currentOpacity , nextUseCtx ) ; } param attributeName param currentUseCtx return export function getEffectiveAttribute ( context , element , attributeName , currentUseCtx ? ) { let attr ; if ( ! currentUseCtx ) { attr = ( getComputedStyle ( element ) as any ) [ attributeName ] || element . getAttribute ( attributeName ) ; } else { attr = element . getAttribute ( attributeName ) ; } if ( ! attr ) { let parent = element . parentElement ; const useCtx = currentUseCtx ; let nextCtx = useCtx ; if ( useCtx && useCtx . referenced === element ) { parent = useCtx . root ; nextCtx = useCtx . parentContext ; } if ( ! parent || parent === context . sourceSvg ) { return null ; } return getEffectiveAttribute ( context , parent as SVGElement , attributeName , nextCtx ) ; } return attr ; } returns export function parseFillUrl ( context , url , opacity ) { const id = getIdFromUrl ( url ) ; if ( ! id ) { return 'transparent' ; } const fill = context . idElements [ id ] ; if ( fill ) { if ( typeof fill === 'string' ) { return fill ; } else { if ( fill instanceof SVGLinearGradientElement || fill instanceof SVGRadialGradientElement ) { const color = gradientToColor ( fill , opacity ) ; context . idElements [ id ] = color ; return color ; } } } } returns export function convertToPixelUnit ( context , value ) { if ( value . match ( CONTAINS_UNIT_REGEXP ) !== null ) { return units . convert ( 'px' , value , context . sourceSvg ) ; } return parseFloat ( value ) ; } return export function parseStyleConfig ( context , element , svgTransform ) { const config = Object . assign ( { } , context . roughConfig ) ; let scaleFactor = 1 ; if ( ! isIdentityTransform ( svgTransform ) ) { const m = svgTransform ! . matrix ; const det = m . a * m . d - m . c * m . b ; scaleFactor = Math . sqrt ( det ) ; } const elementOpacity = getEffectiveElementOpacity ( context , element , 1 , context . useElementContext ) ; const fill = getEffectiveAttribute ( context , element , 'fill' , context . useElementContext ) || 'black' ; const fillOpacity = elementOpacity * getOpacity ( element , 'fill-opacity' ) ; if ( fill ) { if ( fill . indexOf ( 'url' ) !== - 1 ) { config . fill = parseFillUrl ( context , fill , fillOpacity ) ; } else if ( fill === 'none' ) { delete config . fill ; } else { const color = tinycolor ( fill ) ; color . setAlpha ( fillOpacity ) ; config . fill = color . toString ( ) ; } } const stroke = getEffectiveAttribute ( context , element , 'stroke' , context . useElementContext ) ; const strokeOpacity = elementOpacity * getOpacity ( element , 'stroke-opacity' ) ; if ( stroke ) { if ( stroke . indexOf ( 'url' ) !== - 1 ) { config . stroke = parseFillUrl ( context , fill , strokeOpacity ) ; } else if ( stroke === 'none' ) { config . stroke = 'none' ; } else { const color = tinycolor ( stroke ) ; color . setAlpha ( strokeOpacity ) ; config . stroke = color . toString ( ) ; } } else { config . stroke = 'none' ; } const strokeWidth = getEffectiveAttribute ( context , element , 'stroke-width' , context . useElementContext ) ; if ( strokeWidth ) { config . strokeWidth = convertToPixelUnit ( context , strokeWidth ) * scaleFactor ; } else { config . strokeWidth = 0 ; } const strokeDashArray = getEffectiveAttribute ( context , element , 'stroke-dasharray' , context . useElementContext ) ; if ( strokeDashArray && strokeDashArray !== 'none' ) { config . strokeLineDash = strokeDashArray . split ( /[\s,]+/ ) . filter ( entry => entry . length > 0 ) . map ( dash => Math . max ( 0.5 , convertToPixelUnit ( context , dash ) * scaleFactor ) ) ; } const strokeDashOffset = getEffectiveAttribute ( context , element , 'stroke-dashoffset' , context . useElementContext ) ; if ( strokeDashOffset ) { config . strokeLineDashOffset = convertToPixelUnit ( context , strokeDashOffset ) * scaleFactor ; } if ( config . fill && config . stroke === 'none' ) { config . stroke = config . fill ; config . strokeWidth = 1 ; } if ( typeof config . combineNestedSvgPaths === 'undefined' ) { config . combineNestedSvgPaths = true ; } if ( context . randomize ) { config . fillWeight = getRandomNumber ( 0.5 , 3 ) ; config . hachureAngle = getRandomNumber ( - 30 , - 50 ) ; config . hachureGap = getRandomNumber ( 3 , 5 ) ; if ( typeof config . disableMultiStroke === 'undefined' ) { config . disableMultiStroke = Math . random ( ) > 0.3 ; } } return config ; } /**
 * A context that represents the current state of the rendering,
 * which is used in the rendering functions.
 */ export type RenderContext = { rc : RoughCanvas | RoughSVG ; roughConfig : Options ; renderMode : RenderMode ; fontFamily : string | null ; pencilFilter : boolean ; randomize : boolean ; idElements : Record < string , SVGElement | string > ; sourceSvg : SVGSVGElement ; targetCanvas ? : HTMLCanvasElement ; targetCanvasContext ? : CanvasRenderingContext2D ; targetSvg ? : SVGSVGElement ; useElementContext ? : UseContext | null ; processElement : ( context : RenderContext , root : SVGSVGElement | SVGGElement | SVGSymbolElement | SVGMarkerElement | SVGElement , svgTransform : SVGTransform | null , width ? : number , height ? : number ) => void ; } ; /**
 * The context for rendering use elements.
 */ export type UseContext = { referenced : SVGElement ; root : Element | null ; parentContext : UseContext | null ; } ; /**
 * Helper method to append the returned `SVGGElement` from
 * Rough.js when drawing in SVG mode.
 */ export function postProcessElement ( context , element , sketchElement ? ) { if ( context . renderMode === RenderMode . SVG && context . targetSvg && sketchElement ) { let sketch = sketchElement as SVGElement ; const sketchClipPathId = element . getAttribute ( 'data-sketchy-clip-path' ) ; const applyPencilFilter = context . pencilFilter && element . tagName !== 'text' ; const isPlainContainer = sketch . tagName === 'g' && sketch . attributes . length === 0 ; if ( ! isPlainContainer && ( sketchClipPathId || applyPencilFilter ) ) { const g = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'g' ) ; g . appendChild ( sketch ) ; sketch = g ; } if ( sketchClipPathId ) { sketch . setAttribute ( 'clip-path' , `url(#${ sketchClipPathId })` ) ; } if ( applyPencilFilter ) { sketch . setAttribute ( 'filter' , 'url(#pencilTextureFilter)' ) ; } context . targetSvg . appendChild ( sketch ) ; } } returns export function sketchPath ( context , path , options ? ) { if ( PATH_CURVES_REGEX . test ( path ) ) { options = options ? { ... options , preserveVertices : true } : { preserveVertices : true } ; } return context . rc . path ( path , options ) ; } /**
 * Combines the given transform with the element's transform.
 * If no transform is given, it returns the SVGTransform of the element.
 */ export function getCombinedTransform ( context , element , transform ) { if ( ! transform ) { return getSvgTransform ( element ) ; } const elementTransform = getSvgTransform ( element ) ; if ( elementTransform ) { const elementTransformMatrix = elementTransform . matrix ; const combinedMatrix = transform . matrix . multiply ( elementTransformMatrix ) ; return context . sourceSvg . createSVGTransformFromMatrix ( combinedMatrix ) ; } return transform ; } param element export function applyGlobalTransform ( context , svgTransform , element ? ) { if ( svgTransform && svgTransform . matrix ) { const matrix = svgTransform . matrix ; if ( context . renderMode === RenderMode . CANVAS && context . targetCanvasContext ) { context . targetCanvasContext . setTransform ( matrix . a , matrix . b , matrix . c , matrix . d , matrix . e , matrix . f ) ; } else if ( context . renderMode === RenderMode . SVG && element ) { if ( element . transform . baseVal . numberOfItems > 0 ) { element . transform . baseVal . getItem ( 0 ) . setMatrix ( matrix ) ; } else { element . transform . baseVal . appendItem ( svgTransform ) ; } } } }