What would you want from a high-level SVG wrapper package?

I’m currently working on a 2D drawing package that will use SVG under the hood but offer a (hopefully) nicer and more type-safe interface. I’d love to get some examples of things people are currently doing with SVG that you’d be interested in being able to do using a higher-level package! For example, I have already implemented support for

  • Drawing primitives like lines, triangles, polygons, circles, ellipses, arcs, text, and images
  • Setting common attributes like stroke width/color/line join/line cap, fill color, font family, font size, text alignment
  • Using linear or radial gradients for stroke and fill
  • Transforming drawing nodes (rotating, translating, mirroring etc.)
  • Adding drop shadows

and there are several more features I’d like to add for 1.0:

  • Composite paths and (filled) regions (pretty mandatory!)
  • Dashed strokes

but that certainly doesn’t cover all of what SVG can do. What else would you find most useful? Curving text along a path? Fill patterns? Clipping/masking? Fancy filter effects?

5 Likes

pan and zoom with mouse/touchpad/touchscreen, similar to what these libraries do:



1 Like

Good point! I actually put in a ton of time recently to be able to handle mouse and touch events in a clean, useful and performant way; not quite a full shrink-wrapped pan/zoom system, but hopefully should let you implement that behavior pretty easily.

I’m not sure how much further I could go without turning a drawing into something that managed its own state and had its own set of messages/subscriptions, which I’d prefer to avoid. But it would be good to look at some of those packages and make sure all the necessary tools are in place to implement their behaviors (maybe even implement a couple of them as examples to follow).

I also do animations and transitions, both with css and animateTransform. I don’t exactly what you can or can’t do in each case but maybe css animations cover all that animateTransform can do.

In my mind SVG offers the following areas:

  • Drawing geometry with fills/strokes/patterns/gradients
  • Text (yes including on path)
  • Masks and clipping
  • Animation
  • User interaction
  • Scaling and resolution independence
  • Filters

Out of these I think the most reasonable to drop for an Elm-centric API os probably “Scaling and resolution independence”, since it is pretty tricky to do without imperative APIs.

Here’s a somewhat random laundry list what I would like from an Elm SVG wrapper:

  • strongly typed - prevent errors like missing required attributes or adding attributes that don’t belong on an element, like font-size on a line
  • better interaction helpers, like having onClick give me local coordinates rather than page coordinates
  • text layout helpers, rather than manually messing with tspan and dy
  • somehow escape SVGs tendency to require string IDs for things (like patterns and animations) and thus breaking composition
  • tooling to import SVG code
  • somewhat concise API (perhaps organized according to the bullet points above), where one doesn’t need to import 4 modules just to get a very basic graphic
  • escape hatch to interop with “legacy” elm/svg code

@Dave_Doty you may be in luck soon: https://github.com/gampleman/elm-visualization/pull/42

1 Like

I agree, especially this:

String ids pops up with filters too. Very annoying and unElmlike.

Also: great news that zoom is on its way to elm-visualization! Hopefully it makes my package for zoomable plots totally obsolete!

Thanks for the detailed reply @gampleman! To address some of your points:

Missing required arguments shouldn’t be an issue; a lot of this just happens by default when you pass a Circle2d as the argument to Drawing2d.circle instead of having to remember to pass cx, cy, and r separately. Attributes that don’t belong are harder - I did experiment with some fancy types to disallow stuff like that, but I still wanted to support things like putting attributes on a group element that would then affect all children - it was hard to find an API that made sense. What I have done, though, is do things like ignore fill style when rendering curves, so you can safely set a fill style on a group object and it will only apply to child regions, not child curves. (If you add a fill style directly to a curve, it will simply be ignored.)

Done! onClick, onTouchStart etc. give you drawing coordinates (the same coordinates in which you specify your top-level view box), regardless of where on the page your drawing is. There’s even functionality included so that once a mouse button is pressed down, you can subscribe to mouse move events anywhere on the page (even outside the drawing) and still get drawing coordinates from those events.

Haven’t looked at that yet, but good point - I’ll file that as an issue.

Done! Gradients and shadows (which require using IDs and references under the hood) have IDs automatically generated from their contents. So for example adding a shadow is as simple as throwing in an

Attributes.dropShadow
    { radius = pixels 8
    , offset = Vector2d.pixels 4 -4
    , color = Color.darkGrey
    }

and all the necessary extra elements and cross-referencing will be done automatically.

Hadn’t thought of that! Seems like something that could probably be implemented separately if necessary, though.

Right now the design roughly parallels SVG/HTML where I have a Drawing2d module and a Drawing2d.Attributes module, but I’d like to see how well it would work to just combine both of those into a single Drawing2d module which would let you do quite a bit by itself. (Although you’ll likely also have to import Point2d, Arc2d, Circle2d etc. from elm-geometry…)

Haven’t added that in yet but would be pretty trivial to do - my only reservation is I’d love for it to be possible to also generate SVG strings using this package (which would allow you to save SVG files). Allowing the inclusion of raw Svg msg values would get in the way of that.

@RalfNorthman having to generate separate gradient elements/string IDs when working with SVG directly was one of the main reasons I started working on this package =)

1 Like

One thing that took some figuring out when I started with SVG is how to get drawings looking crisp on a screen.

The way I found worked best is to set the SVG drawing area so that its coordinate system is 1:1 with the pixels on the display. It gets tricky when drawing a line 1 pixel wide though (or 3 or 5), as if you draw say a horizontal line and you give it a whole number y coordinate, it will come out blury, because the whole number coordinates are actually at the boundary between 2 sets of pixels; one above and one below the line - the line will be drawn by setting these 2 sets of pixels to 50% black. To draw a crisp one pixel wide black line, you actually have to offset the y coordinate by 0.5, in this case.

If you draw a 2 pixel wide line, you don’t need the 0.5 offset, as 2 pixels will map exactly onto the screen pixels above and below the coordinates of the line.

Kind of hard to explain, but hopefully you know what I am talking about.

If I could have a drawing system in SVG that gives me a simple coordinate system that takes care of all of this and helps me to make the clearest, sharpest, pixel perfect drawings, that would be a great system to use.

I’ll dump some of my code in a Gist tomorrow and link it here.

Yeah, I know exactly what you’re talking about! I’ve certainly used the “offset by 0.5 pixels” trick myself to get sharp lines. It’s a really good point - I’ll have to think about it a bit more, but there may be things that elm-2d-drawing can do to help there.

There could potentially be something like a snapToNearestPixel attribute that would be result in a ‘best effort’ attempt to get pixel-aligned lines (taking current stroke width into account). For a lot of common cases (horizontal or vertical lines, axis-aligned rectangles) that should work fine, but there would be some caveats:

  • Wouldn’t work with angled/curved lines (maybe could work with 45 degree lines as a special case?)
  • Wouldn’t work if you transformed the geometry after creating it, unless the transformation was something like translation by integer numbers of pixels. I guess that if the attribute was set, then translation transformations could themselves be snapped to integer numbers of pixels to ensure that they didn’t mess up sharp lines…and maybe rotations that happen to be multiples of 90 degrees could have the rotation center point snapped to a pixel boundary as well.

Alternatively, there could simply be some special draw functions like snappedVerticalLine, snappedHorizontalLine and snappedRectangle that could be restricted to accept axis-aligned stuff and document that the arguments will be snapped to create sharp lines.

@gampleman (and others) if you have any thoughts on what a nice text API would look like I would love your thoughts on this issue! Not sure whether the best way is to have some sort of actual FormattedText type with functions to create bold, italic, superscript etc. text, or just support a subset of Markdown or something.

You might draw some inspiration from elm-collage. That package probably doesn’t have all your features but I think it has a very nice and approachable API.

Michael

So I had a dig through some code I haven’t touched in a while. Since then I moved from a 24 inch 1080p screen to a 34 inch 4k screen - which by itself really helps to arrive at crisper looking drawings.

There is not much particularly useful in the code I was using, but I’ll mention a few bits that made a difference.

I am setting up the svg element like this:

<svg preserveAspectRatio="xMidYMid meet" viewBox="-112 233 1268 726" shape-rendering="geometricPrecision">

With this Elm code:

svg
    ([ preserveAspectRatio (Align ScaleMid ScaleMid) Meet
     , viewBox (round current.x |> toFloat)
        (round current.y |> toFloat)
        (round current.w |> toFloat)
        (round current.h |> toFloat)
     , svgNamespace
     , shapeRendering RenderGeometricPrecision
     ])

The current view area was obtained at application start through Browser.Dom.getViewport. After that it can be scrolled with the mouse, and all I am doing is rounding it to integers to ensure the SVG viewport remains pixel aligned.

geometricPrecision looks best to me after trying out the alternatives - even though crispEdges sounds like it might be a better choice, it usually isn’t.

Interestingly on my 4k screen the browser gets a window.devicePixelRatio of 1.5. So the 2 pixel wide lines that looked perfect on my old screen are now being drawn at 3 physical pixels wide, and suffer from being non-aligned to pixels as a result. Doesn’t really look too bad on a 4k screen, but I know that if I get it bang on the pixels the image will have more ‘pop’. Magnified image of what I currently get:

aliasing

===

Its hard to see how a higher level drawing library can automatically solve this kind of thing. I think the suggestions you made above could all be useful and it is really horizontal and vertical lines where it is best applied.

Some help with setting up the SVG viewport to match pixels, taking into account the on screen size in browser pixels and also the device pixel ratio could be useful too. I think if I multiply the SVG viewport size by 1.5, I should get back to 1:1 with physical pixels. Of course I then need to scale up my drawings by 1.5 so they come out roughly the same size on the higher density display.

So just to confirm that scaling up the SVG viewport by 1.5 had the desired effect:

svg
    ([ preserveAspectRatio (Align ScaleMid ScaleMid) Meet
     , viewBox (round (current.x * 1.5) |> toFloat)
        (round (current.y * 1.5) |> toFloat)
        (round (current.w * 1.5) |> toFloat)
        (round (current.h * 1.5) |> toFloat)
        ...

aligned

So perhaps some function to help create pixel aligned SVGs:

pixelAlignedSvg : 
    Browser.Dom.Viewport -> 
    Float -> 
    List (Attribute msg) -> 
    List (Svg msg) -> 
    Html msg

Another interesting thing one might consider for a higher level wrapper is automatic optimization. What I mean is that SVG gives you nice primitives for geometry, but these are DOM elements and as such quite expensive (I made a relatively simply graphic the other day that produced 8000 DOM nodes (it wasn’t all that complicated either, just lines arranged in a table like structure) and even scrolling the webpage was super sluggish). However, a sufficiently smart library could condense all geometry into a single path element as long as they all shared the same attributes - since diffing and setting the strings in the d attribute is much faster. Or automatically replace stuff with use declarations.

Of course careful benchmarking would be required since you wouldn’t want to spend more time optimizing than the browser rendering.

(Also I suppose one would loose some debug-abillity since the DOM would corespond less to the code one has written. That might be less of concern depending on how high level the library is anyway).

3 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.