Skip to content

Latest commit

 

History

History
311 lines (264 loc) · 9.62 KB

01.Drawing-a-Circle.md

File metadata and controls

311 lines (264 loc) · 9.62 KB

SVG - Drawing a Circle

Dynamically creating SVG within the browser with JavaScript can be painful. With Haxe JavaScript we can use abstracts to hide the uglier parts of working with SVG. In this tutorial we will use abstracts to help make manipulation of SVG simpler. It is assumed you have explored abstracts in Haxe in depth before trying this tutorial and have a rough understanding of SVG.

Summary First

With Haxe's abstracts it's possible to create SVG with really clean code and the structural overheads largely disappear in the compiled JavaScript.

Here is an example of the Main class we will create:

import svgShapes.SvgRoot;
import svgShapes.SvgCircle;

class MainCircle {
  public static function main() {
    new MainCircle();
  }
  
  var svgRoot:SvgRoot;
    
  public function new() {
    svgRoot = new SvgRoot();
    svgRoot.width = 800;
    svgRoot.height = 800;
    createCircle();
  }

  public function createCircle(){
    var svgCircle = new SvgCircle();
    svgCircle.cx = 200;
    svgCircle.cy = 200;
    svgCircle.r = 50;
    svgCircle.setFillAndAlpha(0xff0000, 1);
    svgRoot.appendChild(svgCircle);
  }
}

Lets look at how SVG is created.

Creating SVG node and element

To create the nodes of SVG we create SVGElements using a string type and then set attributes. So a SVG circle node would be created:

var nameSpace = "http://www.w3.org/2000/svg"
var svgElement:SVGElement = cast Browser.document.createElementNS(nameSpace, 'circle');

Its attributes can be set, such as the radius:

svgElement.setAttribute( 'r','50' );

We can also get its attributes:

var radius = svgELement.getAttribute('r');

Setting up SVG

To create SVG we create a root node which we append into our HTML document.

// create SVGElement
var svgElement: SVGElement = cast Browser.document.createElementNS("http://www.w3.org/2000/svg", 'svg');
// Append to the document body
Browser.document.body.appendChild(svgElement);

With SVG or canvas we need to setup the style so that coordinates are relative to the top corner. This is done using the 'style' property of the svg node.

var style:CSSStyleDeclaration = element.style;
style.paddingLeft = "0px";
style.paddingTop = "0px";
style.left = "0px";
style.top = "0px";
style.position = "absolute";

It is important to set the width and height attributes, those can be set like this:

svgElement.setAttribute('width', '800');
svgElement.setAttribute('height', '600');

But really this is ugly, and pretty inflexible if we want to tween or animate attributes, and luckly Haxe can help.

SVGElement has width and height properties but they are of type AnimatedLength and read only, our abstract can redefine width and height.

So instead we can wrap the 'set' and 'get' attribute methods for width and height, so for width we can create a getter/setter.

    public var width(get, set):Int;
    inline public function set_width(value:Int):Int {
        // pass in a Integer width value and convert it to a String for the setAttribute method.
        this.setAttribute("width", Std.string(value));
        return value;
    }
    inline public function get_width():Int {
        // convert String value back an Integer for easier manipulation.
        return Std.parseInt(this.getAttribute("width"));
    }

if we wrap the SVGElement in an abstract we can then call these properties directly on the abstract. This allows much easier manipulation of the SVG node's width.

svgRootAbstract.width = 800;

The full abstract for our root SVG element

import js.Browser;
import js.html.svg.SVGElement;
import js.html.Element;
import js.html.CSSStyleDeclaration;
import js.html.Node;
// we can expose the SVGElement appendChild method to allow us to directly add children without casting back to SVGElement.
@:forward( appendChild, removeChild )
abstract SvgRoot(SVGElement) from SVGElement to SVGElement {
  inline public static var svgNameSpace:String = "http://www.w3.org/2000/svg" ;
  inline public function new(?e:SVGElement) {
    if( e == null) {
      this = create();
    } else {
      this = e;
    }
  }
 
  inline static public function create():SvgRoot {
    var svgElement:SVGElement = cast Browser.document.createElementNS(svgNameSpace, 'svg');
    var element:Element = cast svgElement;
    var style:CSSStyleDeclaration = element.style;
    style.paddingLeft = "0px";
    style.paddingTop = "0px";
    style.left = "0px";
    style.top = "0px";
    style.position = "absolute";
    Browser.document.body.appendChild( element );
    var svgRoot:SvgRoot = svgElement;
    return svgRoot;
  }
    
  public var width(get, set):Int;
  inline public function set_width(value:Int):Int {
  this.setAttribute("width", Std.string(value));
    return value;
  }
  inline public function get_width(): Int {
    return Std.parseInt(this.getAttribute("width"));
  }
  
  public var height(get, set):Int;
  inline public function set_height(value:Int):Int {
    this.setAttribute("height", Std.string(value));
    return value;
  }
  inline public function get_height(): Int {
    return Std.parseInt(this.getAttribute("height"));
  }
}

This abstract allows us to create the root SVG node very cleanly hiding the ugly parts of JavaScript SVG.

MainCircle - main class.

We can create a main class and setup very simply to initialize this abstract, notice the createCircle is for the moment removed.

// import svgShapes.SvgCircle; // not yet implemented

class MainCircle {
  public static function main() {
    new MainCircle();
  }
  
  var svgRoot:SvgRoot;
    
  public function new(){
    svgRoot = new SvgRoot();
    svgRoot.width = 800;
    svgRoot.height = 800;
    createCircle();
  }
    
  public function createCircle() {
    // not yet implemented
  }
}

SvgCircle - Abstracting a SVG circle.

We have already seen how to create a SVGElement circle. So we can follow exactly the same approach and wrap all the set and get attributes in an abstract to allow us easy control of properties.

import js.Browser;
import js.html.svg.SVGElement;

abstract SvgCircle(SVGElement) from SVGElement to SVGElement {
  inline public static var svgNameSpace:String = "http://www.w3.org/2000/svg";
  
  public inline function new(e:SVGElement = null) {
    if (e == null) {
      this = create();
    } else {
      this = e;
    }
  }

  inline static public function create():SvgCircle {
    return cast Browser.document.createElementNS(svgNameSpace, 'circle');
  }

  public var cx(get, set):Float;
  inline function set_cx(cx:Float):Float {
    this.setAttribute("cx", Std.string(cx));
    return ( cx_ );
  }
  inline function get_cx():Float {
    return Std.parseFloat(this.getAttribute("cx"));
  }
  
  public var cy(get, set):Float;
  inline function set_cy(value:Float):Float {
    this.setAttribute("cy", Std.string(value));
    return value;
  }
  inline function get_cy():Float {
    return Std.parseFloat(this.getAttribute("cy"));
  }
  
  public var r(get, set):Float;
  inline function set_r(value:Float):Float {
    this.setAttribute("r", Std.string(value));
    return value;
  }
  inline function get_r():Float{
    return Std.parseFloat(this.getAttribute("r"));
  }
  
  public var strokeWidth(get, set):Float;
  inline function set_strokeWidth(value:Float):Float {
    this.setAttribute("stroke-width", Std.string(value));
    return value;
  }
  inline function get_strokeWidth():Float {
    return Std.parseFloat(this.getAttribute("stroke-width"));
  }

  public var fill(get, set):String;
  inline function set_fill(value:String):String {
    this.setAttribute("fill", value);
    return value;
  }
  inline function get_fill():String {
    return this.getAttribute("fill");
  }

  inline public function setFillAndAlpha(col:Int, ?alpha:Float):Void {
    fill = getColor(col, alpha);
  }
  
  public var strokeFill(get, set):String;
  inline function set_strokeFill(value:String):String {
    this.setAttribute("stroke", value);
    return value;
  }
  inline function get_strokeFill():String {
    return this.getAttribute("stroke");
  }

  inline public function setStrokeFillAndAlpha(col:Int, ?alpha:Float):Void {
    strokeFill = getColor(col, alpha);
  }

  public static inline function getColor(col:Int, ?alpha:Float):String {
    if (alpha != null && alpha != 1.0) {
      var r = (col >> 16) & 0xFF;
      var g = (col >> 8) & 0xFF;
      var b = (col) & 0xFF;
      return 'rgba($r,$g,$b,$alpha)';
    } else {
      return '#' + StringTools.hex( col, 6 );
    }
  }
}

The getColor function is a bit complex but it's to help us set fill and stroke colors of the circle both as solid colors or alpha.

##Creating a Circle using our SvgCircle abstract.

In our Main class we have a 'createCircle' method we can create a red SVG circle here.

public function createCircle() {
  var svgCircle = new SvgCircle();
  svgCircle.cx = 200;
  svgCircle.cy = 200;
  svgCircle.r = 50;
  svgCircle.setFillAndAlpha(0xff0000, 1);    
  svgRoot.appendChild(svgCircle);
}

So the extra effort of the abstract hides all the ugly parts of creating a circle and it also makes it much easier for you to extend this tutorial to animate and create lots of circles. Obviously you can explore the different SVGElements and perhaps wrap them in abstracts for easy use. If you look at the generated source you will see that the all the abstract code has disappeared, so the approach really has very little overhead and allows us to build and manipulate complex SVG structures in code fairly easily.

git source

< https://github.com/nanjizal/CookingHaxe/tree/master/Graphics/Create-SVG/01.Drawing-a-Circle>

view result

https://cdn.rawgit.com/nanjizal/CookingHaxe/master/Graphics/Create-SVG/01.Drawing-a-Circle/index.html