//
// 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",
])
}