// // Copyright (c) 2017, Brian Frank and Andy Frank // Licensed under the Academic Free License version 3.0 // // History: // 17 May 2017 Brian Frank Creation // using concurrent using graphics ** ** SVG (Scalar Vector Graphics) utilities ** @Js final const class Svg { ** Cannot create/subclass private new make() {} ** SVG XML namesapce static const Uri ns := `http://www.w3.org/2000/svg` ** XLink XML namespace static const Uri nsXLink := `http://www.w3.org/1999/xlink` ** Create element with proper namespace static Elem elem(Str tagName) { Elem(tagName, ns) } ** Convenience to create 'line' element static Elem line(Num x1, Num y1, Num x2, Num y2) { elem("line") { it->x1 = x1; it->y1 = y1; it->x2 = x2; it->y2 = y2 } } ** Convenience to create 'rect' element static Elem rect(Num x, Num y, Num w, Num h) { elem("rect") { it->x = x; it->y = y; it->width = w; it->height = h } } ** Convenience to create 'text' element static Elem text(Str text, Num x, Num y) { elem("text") { it.text = text; it->x = x; it->y = y } } ** Convenience to create a 'image' element static Elem image(Uri href, Float x, Float y, Float w, Float h) { elem("image") { it->x = x it->y = y it->width = w it->height = h it.setAttr("href", href.encode, nsXLink) } } ** Auto-generate an id for the def element and mount it into ** the svg document's defs section. This method will automatically ** generate a '<defs>' child in the svg document as needed. ** If defs already has an id or is already mounted, then no ** action is taken. static Str def(Elem svgElem, Elem defElem) { // sanity check document element if (svgElem.tagName != "svg") throw Err("Document not <svg> element: $svgElem.tagName") // check for <defs> defsElem := svgElem.children.find |kid| { kid.tagName == "defs" } // create it if needed if (defsElem == null) { defsElem = elem("defs") if (svgElem.hasChildren) svgElem.insertBefore(defsElem, svgElem.children.first) else svgElem.add(defsElem) } // auto-generate if needed if (defElem.id == null) defElem.id = "def-" + genId.incrementAndGet.toHex // mount if needed if (defElem.parent == null) defsElem.add(defElem) // return id return defElem.id } private static const AtomicInt genId := AtomicInt() ** Mount a definition element using `def` and return a CSS URL ** to the fragment identifier such as "url(#def-d)". This is used ** to reference gradient and clip definitions. static Str defUrl(Elem svgElem, Elem defElem) { "url(#" + def(svgElem, defElem) + ")" } ** Internal hook to customize Elem.trap behavoir. internal static Obj? doTrap(Elem svgElem, Str name, Obj?[]? args := null) { if (args == null || args.isEmpty) { // get return svgElem.attr(name)?.toStr } else { // set val := args.first // TODO: should we be using trap for text? if (name == "text") { svgElem.text = val.toStr; return null } // convenience to explode font attrs if (name == "font") { if (val is Str) val = Font.fromStr(val) f := (Font)val f.toProps.each |v,n| { svgElem.setAttr(n, v.toStr) } return null } // convert to hyphens if needed and route to setAttr if (camelMap.containsKey(name)) name = fromCamel(name) svgElem.setAttr(name, val.toStr) return null } } ** Convert camel case to hyphen notation. private static Str fromCamel(Str s) { h := StrBuf(s.size + 2) for (i:=0; i<s.size; ++i) { ch := s[i] if (ch.isUpper) h.addChar('-').addChar(ch.lower) else h.addChar(ch) } return h.toStr } // TODO: just keep a big whitelist here?? private static const [Str:Str] camelMap := Str:Str[:].setList([ "accentHeight", "alignmentBaseline", "baselineShift", "capHeight", "clipPath", "clipRule", "colorInterpolation", "colorInterpolationFilters", "colorProfile", "colorRendering", "dominantBaseline", "enableBackground", "fillOpacity", "fillRule", "floodColor", "floodOpacity", "fontFamily", "fontSize", "fontSizeAdjust", "fontStretch", "fontStyle", "fontVariant", "fontWeight", "glyphName", "glyphOrientationHorizontal", "glyphOrientationVertical", "horizAdvX", "horizOriginX", "imageRendering", "letterSpacing", "lightingColor", "markerEnd", "markerMid", "markerStart", "overlinePosition", "overlineThickness", "panose1", "paintOrder", "renderingIntent", "shapeRendering", "stopColor", "stopOpacity", "strikethroughPosition", "strikethroughThickness", "strokeDasharray", "strokeDashoffset", "strokeLinecap", "strokeLinejoin", "strokeMiterlimit", "strokeOpacity", "strokeWidth", "textAnchor", "textDecoration", "textRendering", "underlinePosition", "underlineThickness", "unicode", "unicodeBidi", "unicodeRange", "unitsPerEm", "vAlphabetic", "vHanging", "vIdeographic", "vMathematical", "values", "version", "vertAdvY", "vertOriginX", "vertOriginY", "wordSpacing", "xHeight", ]) }