Skip to content
shaehn edited this page Dec 20, 2019 · 1 revision

Available Shapes

There are a variety of shapes available for drawing purposes. The generic shapes are:

Arc

Here, arc refers to the portion of the circumference of a circle. The (x,y) coordinates used in the interface represent the center point of the circle on which the arc is produced. The radius is the distance from that point to any point on that arc. The final information needed to produce the arc is the angle (in degrees) at which the arc is to start and the angle at which the arc is to stop. Negative angle number specifications represent traveling counter clockwise. Positive angle numbers represent traveling clockwise. (This was done to be consistent with the rotation mechanism in the library).

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'arcs.pdf');
let txtOps = {color:'#000000', size: 10};

let x = 100;
let y = 100;
let r = 50;

recipe
    .createPage('letter')
    .circle(x, y, 2, {color:'red', width:.5})  // indicate arc center point

    .arc(x, y, r, 0, 90)
    .text('0°', x+r+5, y,txtOps)   // label start and ... 
    .text('90°', x, y+r+5,txtOps)  // ... ending angles of arc

    .arc(x, y, r, -90, -180)
    .text('-90°', x-15, y-r-13,txtOps)
    .text('-180°', x-r-30, y-10,txtOps)
    .endPage()
    .endPDF();

Results in the following image:

Arc to Sector

The -sector:true option of the interface instructs arc to render the arc sector lines, which are the radial lines from each end of the arc to the arc center point. The following sample code produces the Sector of Circles picture seen below.

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'arcs.pdf');
let txtOps = {color:'#000000', size: 10};

let x = 100;
let y = 100;
let r = 50;

recipe
    .text('Sectors of Circles', 220, y-r-30, {size:20, bold:true})

    .arc(x, y, r, 0, 90, {rotation:0, sector:true})
    .arc(x, y, r, -90, -180, {rotation:0, fill:'red', sector:true})
    .text('showing sector & fill', x-r,y+r+21,txtOps)

    .arc(x+120, y, r, 90, -90, {rotation:0})
    .arc(x+120, y, r, 90, 270, {rotation:0, fill:'green', stroke:'default'})
    .text('fill and nofill', x+120-r/2,y+r+21,txtOps)

    .arc(x+240, y, r, -140, 120, {sector:true})
    .text('sector: true\ncloses arc\nto centerpoint',x+215,y+r+21,txtOps)

    .arc(x+360, y, r, 45, -270, {rotation:0, fill:'def', sector:true})
    .arc(x+363, y+7, r, 45, 90, {rotation:0, fill:'red', sector:true})
    .text('PIZZA anyone?', x+360-20, y+r+21,txtOps)
    .endPage()
    .endPDF();

Note that when using the fill option to color in the arc area, a different outcome will occur when the sector option is not used, or set to false. Instead of forming a sector, the fill will occur between the imaginary chord line that connects the two endpoints and the arc itself. See below.

Arrow

Periodically, there is a need to advertise, or to point out something in PDF files that are produced. Thus, the reason for the arrow call. In its simplest form, all it needs is (x,y) positioning parameters associated with the center of the arrow. That simple call would produce the following arrow shape.

In the following discussion, use the Anatomy of an Arrow diagram as a reference. The arrow uses the standard polygon interface to produce its final results.

An arrow consists of 2 components which derive its overall shape; the arrow head and arrow shaft. The options which control those 2 components are named head and shaft, respectively. Of the two options, the head option is the more complictated because it is used to control the shape of the arrow head, which is a special quadrilateral shape known as a kite. The general form of the head option is an array that consists of the arrow head length (x-axis), arrow head width (y-axis), and the base offset value (x-axis). Both the width and base offset values are optional. The alternate form is a simple number which is assigned to the arrow length. Whenever the width or base offset values are missing, the width is twice the arrow head length and base offset is set to zero.

    general form  ->    head: [length, width, baseOffset]   (defaults -> [10, 20, 0])
    simple  form  ->    head: length

The base offset value is what determines the arrow shaping. Its default value of zero produces a triangular shaped arrow head. A negative value produces a kite shape, whereas a positive value produces a dart shape.

Of course, if you don't want to fuss with the base offset mechanism, there is a type option which will give the desired arrow head shapes. The option can take on the following values.

    0, 'triangle'  (default)
    1, 'dart'
    2, 'kite'

The shaft option also has two forms; a simple number representing the length of the arrow shaft, or a two element array describing the length and width of the arrow shaft. (When the width value is missing, it takes on the same value as the length). Note that the shaft length value is allowed to be zero, which simply produces an arrow head without a shaft.

Note in the diagram above, that the circumscribed circle is for illustrative purposes and only naturally occurs when the shaft length is equal to the arrow head length, which is a property of the default arrow.

To obtain double headed arrows, set the double option to 'true'. To position the arrow at either the head or the tail of the arrow instead of the middle of the shaft, use the at option with the obvious values of 'head' or 'tail'. The at option can come in handy, especially when rotating the arrows (See Anatomy of an Arrow diagram above.) The picture below illustrates the (x,y) position/rotation point with double headed arrows and when using the at option.

Circle

The circle is a pretty straightforward mechanism. An (x,y) coordinate is provided for its center point along with a radius distance.

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'circle.pdf');
let x = 100;
let y = 100;
let radius = 50;
recipe
    .createPage('letter')
    .circle(x, y, radius)
    .endPage()
    .endPDF();

Ellipse

The only difference between the circle and ellipse interface is there is an additional radius parameter for the ellipse. Note that when both radius values are identical, as expected, a circle is produced.

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'ellipse.pdf');
let x  = 200;
let y  = 100;
let rx = 40;
let ry = 97.5;
recipe
    .createPage('letter')
    .ellipse(x, y, rx, ry)
    .ellipse(x+110, y, rx, rx)   // makes a circle
    .endPage()
    .endPDF();

N_gon

The shapes produced with this interface are 'N' sided regular polygons. A regular polygon is equiangular (all angles are equal in measure) and equilateral (all sides have the same length). The minimal number of sides that can be specified with this interface is three which produces an equilateral triangle.

The interface uses the standard (x,y) coordinate point for positioning and it represents the center of the regular polygon. The next parameter is the radius which is the distance from the center to the vertices of the polygon. Following the radius is the sides parameter, which is optional and defaults to 3 when missing.

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'regularPolygons.pdf');
const nops = {stroke: "red"}
recipe
    .createPage('letter')
    .n_gon(200, 100, 30, nops)      // optional sides parameter defaults to 3
    .n_gon(275, 100, 30,  4, nops)
    .n_gon(350, 100, 30,  5, nops)
    .n_gon(425, 100, 30,  6, nops)
    //.line([[150,100],[475,100]], {width:.5})
    .endPage()
    .endPDF();

The code above produces the following regular polygons; equilateral triangle, square, pentagon, and hexagon.

Note that the bases of the polygons do not align. However, they do align on their centers as can be seen in the following picture (uncomment the line interface in the program and add debug=1 to nops above produces that set of images. The debug option makes the center and the circle which encompasses the polygon visible.)

Note, at the current radius setting, when the sides parameter reaches 50, the resulting image is effectively a circle.

Polygon

This is the generic interface which all the other polygon generation interfaces use to produce their final output. Its main parameter is an array, of (x,y) coordinates specifying the points to which lines will be drawn. The list of points does not have to resolve into closing the polygon to the first point in the list, the polygon interface will do that for you. (I happened to find it vary tedious to specify points to get simple geometric constructs. That is why I introduced polygon generator functions). The following generates a stylized letter 'M'. Points purposely absconded from the web.

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'polygon.pdf');
recipe
    .createPage('letter')
    .polygon([[118, 253],[137, 193],[4, 254]], {fill: "#a32a60",})
    .polygon([[37, 154],[137, 193],[4, 254]], {fill: "#e6385d"})
    .polygon([[37, 154],[137, 193],[113, 117]], {fill: "#f04883"})
    .polygon([[37, 154],[80, 24],[113, 117]], {fill: "#f286b7"})

    .polygon([[231, 80],[89, 6],[209, 5]], {fill: "#83d0e2"})
    .polygon([[231, 80],[89, 6],[139, 147]], {fill: "#00b1cf"})
    .polygon([[231, 80],[176, 250],[139, 147]], {fill: "#4aa4bf"})
    .polygon([[231, 80],[176, 250],[276, 210]], {fill: "#2d7aa6"})
    .polygon([[289, 250],[176, 250],[276, 210]], {fill: "#223a7a"})

    .polygon([[289, 250],[276, 210],[311, 196]], {fill: "#398940"})
    .polygon([[248, 132],[276, 210],[311, 196]], {fill: "#5eaa46"})
    .polygon([[252, 21],[231, 80],[251, 140],[311, 196]], {fill: "#9ac43c"})

    .polygon([[408, 93],[378, 5],[261, 6]], {fill: "#ffd83f"})
    .polygon([[408, 93],[313, 149],[261, 6]], {fill: "#fbb146"})
    .polygon([[408, 93],[313, 149],[440, 187]], {fill: "#f99233"})
    .polygon([[348, 251],[313, 149],[440, 187]], {fill: "#ef493d"})
    .polygon([[348, 251],[462, 251],[440, 187]], {fill: "#d12c2a"})
    .endPage()
    .endPDF();

Rectangle

Ok, now we get to the boring box interface, also known as the rectangle. It requires an (x,y) position point which is the top-left corner of the rectangle. Then the width followed by the height parameters is all that is needed to finish defining the rectangle. (It may be boring, but a very necessary component in background highlighting of text boxes)

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'box.pdf');
recipe
    .createPage('letter')
    .rectangle(100,200,100,50)
    .circle(100,200,2,{color:'red', width:.5})  // marks top-left corner of rectangle
    .endPage()
    .endPDF();

Star

The star interface also makes a regular polygon, except in the form of star shapes. It too uses the generic polygon interface to actually produce the final generated image. Being a regular polygon, it requires the (x,y) center point along with a radius parameter giving the distance to each star vertex/point. The next parameter is the number of points that are desired on the star. The minimum number of points allowed is 5. (This is the default value when the parameter is missing)

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'star.pdf');
recipe
    .createPage('letter')
    .text("Look at\nthem STARS!", 130,320, {size:20,color:'red'})
    .star(300, 340, 50,     {fill: "#0000ff", })  // default 5 points used here
    .star(400, 340, 50,  6, {fill: "#0000ff",  opacity:.3, rotation:30})
    .star(200, 450, 50,  7)
    .star(300, 450, 50,  8, {fill: "#0000ff",  opacity:.3})
    .star(400, 450, 50,  9, {fill: "#0000ff",  opacity:.3})
    .star(200, 560, 50, 10, {fill: "#0000ff",  opacity:.3})
    .star(300, 560, 50, 11, {fill: "#0000ff",  opacity:.3})
    .star(400, 560, 50, 40)
    .endPage()
    .endPDF();

Triangle

The triangle interface is another polygon generator. The required parameters are the (x,y) position and a 3 element traits array with data used to define the triangle. The call allows the user to define a triangle using three side lengths (SSS), two sides and an inclusive angle (SAS), or two angles and an inclusive side (ASA). The parameter which defines what input traits are being given is called traitID. Its value can be 'sss' (side-side-side), 'sas' (side-angle-side where side and angle are sideA, <C, sideB), 'asa' (angle-side-angle where angles and side are <B, sideC, <)), or 'vtx' with 3 (x,y) vertex points. When the traitsID is not given, it defaults to 'sss'. Refer to diagram below. It shows the labelled triangle as envisioned by the underlying code. (debug:true labels triangle). The default positioning for a triangle is at vertex B, with side AB set as the base of the triangle and vertex C at its apex.

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'triangle.pdf');
let x = 50;
let y = 100;
recipe
    .createPage('letter')
    .triangle(x, y, [90,60,80], {width:.5, color:'blue', debug:true})
    .text('"SSS", (a,b,c)', x+15, y+21, {color:'#000000', size: 10})
    .endPage()
    .endPDF();

The following picture shows 2 triangles produced using traitID's of 'sas' and 'asa'.

The data to the triangle call is supplied by the traits parameter, which is a three element array. Side value units are in points and angles are in degrees. As mentioned previously, the default position of the triangle is the vertex B set at the given (x,y) coordinates. This can be changed using the position option. The available position values are:

    'A' - vertex A (right vertex of triangle base), 
    'B' - vertex B (left vertex of triangle base),  (default)
    'C' - vertex C (apex of triangle), 
    'centroid', 
    'circumcenter', or 
    'incenter' of the triangle.

The following code shows using the position option. It is important to note that the position point is also the rotation point. When the position is not explicitly set, the rotation point is the center of the triangle's imaginary bounding box.

const HummusRecipe = require('hummus-recipe');
const recipe = new HummusRecipe('new', 'trianglePosition.pdf');
let x = 50;
let y = 100;
let txtOps = {color:'#000000', size: 10};

recipe
    .createPage('letter')
    .line([[x,50],[x,400]],{color:'#aa',width:.5, opacity:.5})
    .line([[50,y],[550,y]],{color:'#aa',width:.5, opacity:.5})
    .triangle(x, y, [40,40,40], {traitID: 'sas', position: 'a', width:.5, color:'blue', debug:true})
    .text('position: "A"', x+20, y-63, txtOps)
    .arrow(x+34, y-48, {head:[10,5], shaft: [40,.5], fill: '#000000', rotation: 125, at:'tail'})
    .triangle(x+100, y, [40,60,40], {traitID: 'sas', position: 'centroid', width:.5, color:'blue', debug:true})
    .text('position:\n"centroid"', x+80, y+35, txtOps)
    .triangle(x+200, y, [70,60,50], {traitID: 'sas', position: 'circumcenter', width:.5, color:'blue', debug:true})
    .text('position:\n"circumcenter"', x+175, y+45, txtOps)
    .triangle(x+320, y, [70,60,50], {traitID: 'sas', position: 'incenter', width:.5, color:'blue', debug:true})
    .text('position:\n"incenter"', x+295, y+45, txtOps)
    .triangle(x+420, y, [70,60,50], {traitID: 'sas', position: 'incenter', rotation:30, width:.5, color:'blue', debug:true})
    .text('position:\n"incenter"\nrotation:30', x+395, y+45, txtOps);

y = 250;
recipe
    .line([[50,y],[550,y]],{color:'#aa',width:.5, opacity:.5})
    .rectangle(x, y-59, 51,59, {width: 1, rotation:15, color:'green', dash:[3], rotationOrigin:[x+24.5,y-29.5]})
    .circle(x+24.5,y-29.5, 2, {fill: 'green',width:1})
    .circle(x, y, 2, {color: 'red', width:.5})
    .triangle(x, y, [70,60,50], {traitID: 'sss', rotation:15, width:.5, color:'blue'})
    .text('NOTE: When no position point specified\nrotation occurs around bounding box center,\nnot at the given x,y coordinate.', x+80, y-50, {color:'red'})
    .text('rotation: 15', x, y+21, txtOps)
    .endPage()
    .endPDF();

There are two more options, flipX and flipY, which allow the triangle to change its orientation through its positioning point. FlipX has the triangle provide a mirror image of itself around the x-axis positioning coordinate, whereas flipY does the same around the y-axis positioning coordinate.