Shapes

Overview

The Curl® language Shapes package is designed to help you create diagrams, charts, graphs and other 2D graphics. It offers an approach to layout of objects in a container that is better suited to building a graph or diagram than the GUI toolkit graphical layout approach.
When you put Graphic objects into a graphic container, the actual appearance of the result on a display device, such as a monitor or printer, is controlled by the Curl layout system. This system does the best job it can of fitting all the specified contents into the container, considering any preferred sizes supplied for the objects, and the display space available. See Elastics and Page Layout for more information on how the Curl layout system positions objects.
This approach is very useful in situations where your layout needs to adapt to unpredictable display conditions, such as fitting components of a GUI into a browser window. It is less useful when you need precise control over the size and placement of objects, such as positioning data points in a line graph.
The Shapes package gives you the precision and control without the programming effort and complexity involved in using 2D rendering. Shapes also supports GUI toolkit event handling, which means you can create diagrams that use animation and respond to mouse events.
The following table lists some major differences between Graphic objects and Shape objects.
Graphic ObjectsShapes
Specify size preferences for an object. The object can stretch or shrink to fit.
Specify object size exactly. The object is clipped if it does not fit.
Position objects by a process of layout negotiation.
Position objects by applying transformations.
Move an object's outside origin with Graphic.horigin and Graphic.vorigin.
You cannot move an object's origin with respect to its dimensions.
No rotation or scaling.
Supports rotation and scaling.
The Curl language also provides the Charts package, which is built on the Shapes package, and provides specialized features for creating charts and graphs of data in a RecordSet.

Using Shapes

The Shapes package organizes shapes within shape containers. The base class for all shapes is Shape, and the base class for any container of Shape objects is ShapeContainerBase. Shape inherits ShapeContainerBase, which means that any Shape can also contain other Shapes.
The package provides a number of basic shapes, such as rectangle and ellipse, and an arrow shape to provide various types of specialized lines between shapes. The classes PathShape and RegionShape enable you to create custom graphic Shapes. You can also create subclasses of Shape to provide customized behavior or appearance.
When you create a Shape, you specify its dimensions. Sometimes, you specify these dimensions as a GRect. Other times, you specify two end points of a line, or perhaps a Region that contains a complex polygon. Whatever form you use to describe the shape's dimensions, the dimensions are interpreted relative to the Shape's origin.
For instance, a GRect can describe the dimensions of a RectangleShape, and the shape's offset from its own origin. You can then move the Shape, including its origin, to whatever position you want by modifying the Shape's transformation. You can modify the transformation by providing arguments to the Shape's constructor or by using one of the methods provided by the Shape class.

Adding a Shape to a Graphical Hierarchy

Shapes must be placed in a Graphic object in order to be visible. In practice, ShapeBox and Canvas are both suitable graphic containers for Shapes. Both objects inherit from ShapeRoot. ShapeRoot inherits from ShapeContainerBase, and is an abstract base class for any Graphical object that can anchor shape objects in a graphical hierarchy. Note that Canvas and ShapeRoot are not themselves shape objects, and do not inherit from Shape.
Canvas has a very simple layout. You must specify a size for the Canvas, and it does not change its size in response to the size of its contents, or change the size of its contents. Its contents are always exactly where you place them, and exactly the size you specify them to be.
ShapeBox uses the desired size of its contents to determine its own size preferences for GUI layout, and it notifies its contents of the size allocated to the ShapeBox during layout. For Charts, this notification results in the Chart occupying the allocated size, which produces GUI-like layout; other shapes ignore it.
You can create a ShapeBox explicitly, but often you do not have to. If you attempt to insert a shape in a graphical hierarchy, it is automatically wrapped in a ShapeBox.

Example: RectangleShape automatically wrapped in ShapeBox
{import * from CURL.GUI.SHAPES}

{value 
    {RectangleShape
        {GRect 3.5cm, 3.5cm, 2.5cm, 2.5cm},
        color = "wheat"
    }
}

ShapeBox uses the concept of layout bounds to interact with its contents. Layout bounds are different from ordinary bounds, as returned by Shape.get-own-bounds, in that they are not an absolute constraint. Shape.get-own-bounds must return a rectangle that includes all areas that may be drawn by the Shape, which often means that it is somewhat larger than the specified area of the Shape. Layout bounds, on the other hand, are intended to reflect the specified dimensions of the Shape; they do not need to contain all drawn areas. ShapeBox uses layout bounds instead of bounds to determine its size because using bounds would result in too much space around the borders of the Shape.
ShapeBox also notifies its contents of the available space using Shape.constrain-shape-layout-bounds, which calls Shape.constrain-own-layout-bounds. The only standard Shape class that implements this meaningfully is Chart. Chart resizes itself to fit within the constraints, unless its size has been directly specified previously. This means that it can be stretchy in the context of GUI toolkit layout, in the same manner as Graphics; you need only specify stretchy dimensions for its container. Your Shape subclasses can be made stretchy as well. They need only implement Shape.get-own-layout-bounds and Shape.constrain-own-layout-bounds.

Working with GRect

Understanding the GRect object is crucial to working with shapes. Some shapes, such as RectangleShape and EllipseShape, take a GRect as an argument to the constructor. All shapes return a GRect from get-own-bounds. A GRect is defined by four distances that specify extents from the origin:
You need to understand how these properties of GRect define how objects are drawn. Consider a GRect specified as follows:
{GRect 3.5cm, 3.5cm, 2.5cm, 2.5cm}
The origin is at its center, and the following figure shows how these distances relate to the origin.
Figure: Properties of GRect
Note that while all the values specified to the GRect constructor are positive, some of the points in 2D space covered by the resulting object have negative values for their x- or y-coordinate. The following example shows a RectangleShape based on this GRect, placed in a Canvas.

Example: Origin of GRect is at the Center
{import * from CURL.GUI.SHAPES}

{value 
    {Canvas width = 8cm, height = 6cm,  
        {RectangleShape
            {GRect 3.5cm, 3.5cm, 2.5cm, 2.5cm},
            color = "wheat",
||--        translation = {Distance2d 3.5cm, 2.5cm},
            border-color = "black",
            border-width = 1px
        }
    }
}

The origin of the Canvas is at its upper-left corner. The origin of the GRect is at its center. As a result, you see only the lower-right quadrant of the RectangleShape. Uncomment this line and execute the example.
translation = {Distance2d 3.5cm, 2.5cm}
The rectangle is now completely visible because the translation has moved its origin to the right and down with respect to the origin of the Canvas.
Now consider a RectangleShape based on this GRect.
{GRect 0cm, 7cm, 0cm, 5cm}
As the following example illustrates, the rectangle is completely visible, because the entire GRect is drawn to the right and down from its origin.

Example: Origin of GRect is at the Upper-left Corner
{import * from CURL.GUI.SHAPES}

{value 
    {Canvas width = 8cm, height = 6cm,  
        {RectangleShape
            {GRect 0cm, 7cm, 0cm, 5cm},
            color = "wheat",
            border-color = "black",
            border-width = 1px
        }
    }
}

Finally, consider this example. No part of the RectangleShape is visible, because the entire GRect is drawn to the left and above from its origin. The line of code that specifies a translation moves the rectangle onto the Canvas. Uncomment that line and execute the example. Note that the entire rectangle is visible.

Example: Origin of GRect is at the Lower-right Corner
{import * from CURL.GUI.SHAPES}

{value 
    {Canvas width = 8cm, height = 6cm,  
        {RectangleShape
            {GRect 7cm, 0cm, 5cm, 0cm},
            color = "wheat",
||--        translation = {Distance2d 7cm, 5cm},
            border-color = "black",
            border-width = 1px
        }
    }
}

Creating a Simple Diagram

The following example shows a simple diagram created using shapes, in which two rectangles are joined by an arrow. Notice that the GRect provided in the constructor sets the dimensions of each rectangle. The translation argument to the constructor positions each rectangle in the Canvas.
The head and tail of an arrow can both take any of the arrow-head styles. In this case, both are ArrowStyle.solid, which results in a double headed arrow.

Example: Simple diagram with shapes
{import * from CURL.GUI.SHAPES}
{value
    let rect-left:RectangleShape = 
        {RectangleShape
            {GRect 0cm, 1cm, 0cm, 1cm},
            color = FillPattern.wheat,
            translation = {Distance2d .5cm, .5cm}
        }
    let rect-right:RectangleShape = 
        {RectangleShape
            {GRect 0cm, 1cm, 0cm, 1cm},
            color = FillPattern.wheat,
            translation = {Distance2d 3.5cm, .5cm}
        }
    let arr:ArrowShape = 
        {ArrowShape
            {Distance2d 1.5cm, 1cm},
            {Distance2d 3.5cm, 1cm},
            arrow-body-width = 1px,
            arrow-head-width = 11px,
            arrow-tail-width = 11px,
            arrow-head-style = ArrowStyle.solid,
            arrow-tail-style = ArrowStyle.solid
        }
    {Canvas
        height = 2cm,
        rect-left,
        arr,
        rect-right
    }
}
The next example is similar, but shows some additional features of shapes. Instead of positioning the ellipses with arguments to their constructors, this example uses the method Shape.apply-translation.
Notice also that because the Canvas class inherits AntialiasMixin, the Curl runtime can use antialiasing when rendering objects in a Canvas on the screen. For a Shape contained in a ShapeBox instead of a Canvas, consider using AntialiasedFrame as you would for any Graphical hierarchy. Antialiasing can have a negative impact on performance, so you need to use the property AntialiasMixin.factor to control whether Canvas uses antialiasing, and the degree of oversampling provided. This example uses antialiasing to improve the on-screen appearance of the curved shapes.
In this example, both ends of the arrow use ArrowStyle.none and the resulting arrow looks like a line.

Example: Simple diagram with ellipses
{import * from CURL.GUI.SHAPES}

{let ellipse-left:EllipseShape = 
    {EllipseShape
        {GRect 0cm, 1cm, 0cm, 1cm},
        color = FillPattern.wheat,
        border-color = FillPattern.maroon,
        border-width = 1px
    }
}
{let ellipse-right:EllipseShape = 
    {EllipseShape
        {GRect 0cm, 1cm, 0cm, 2cm},
        color = FillPattern.wheat,
        border-color = FillPattern.maroon,
        border-width = 1px
    }
}
{let arr:ArrowShape = 
    {ArrowShape
        {Distance2d 1.5cm, 1cm},
        {Distance2d 3.5cm, 3.5cm},
        arrow-body-width = 1px,
        arrow-head-width = 10px,
        arrow-head-style = ArrowStyle.none,
        arrow-tail-style = ArrowStyle.none,
        color = FillPattern.brown
    }
}
{ellipse-left.apply-translation .5cm, .5cm}
{ellipse-right.apply-translation 3.5cm, 2.5cm}
{Canvas
    factor = AntialiasFactor.high,
    border-width = 1px,
    ellipse-left,
    arr,
    ellipse-right
}

Grouping Shapes with ShapeGroup

ShapeGroup is a shape that has no visual representation, and exists only to act as a container for other shapes. All shapes can act as containers for other shapes, but ShapeGroup is useful when you want to group shapes in a container that does not draw anything itself. Setting nonlocal options on a ShapeGroup provides a useful way to set values for all child shapes in the group.
This example uses ShapeGroup to group the bars of a bar chart. It also shows that the Shapes package uses hierarchical transformation. Hierarchical transformation means that the transformation applied to a shape when it is drawn is the composite of all of its parents' transformations with its own. In this case, the translation set on each rectangle positions the rectangles in a horizontal row. The translation applied to the parent ShapeGroup moves the row of rectangles into the proper position in the Canvas.
Try changing the y coordinate of the ShapeGroup translation from 7cm to 6cm, then execute the modified example. Note that all of the bars are repositioned by 1cm, and the bars retain their positions relative to each other.

Example: Bar chart using ShapeGroup
{import * from CURL.GUI.SHAPES}
{Canvas
    width = 9cm,
    height = 8cm,
    {ShapeGroup
        translation = {Distance2d 0cm, 7cm},
        {RectangleShape
            {GRect 0cm, 1cm, 1cm, 0cm},
            color = "#006968",
            translation = {Distance2d (1.1cm * 1), 0cm}
        },
        {RectangleShape
            {GRect 0cm, 1cm, 5cm, 0cm},
            color = "#006968",
            translation = {Distance2d (1.1cm * 3), 0cm}
        },
        {RectangleShape
            {GRect 0cm, 1cm, 3cm, 0cm},
            color = "#006968",
            translation = {Distance2d (1.1cm * 5), 0cm}
        },
        {RectangleShape
            {GRect 0cm, 1cm, 2cm, 0cm},
            color = "#2462a2",
            translation = {Distance2d (1.1cm * 2), 0cm}
        },
        {RectangleShape
            {GRect 0cm, 1cm, 4cm, 0cm},
            color = "#2462a2",
            translation = {Distance2d (1.1cm * 4), 0cm}
        },
        {RectangleShape
            {GRect 0cm, 1cm, 5cm, 0cm},
            color = "#2462a2",
            translation = {Distance2d (1.1cm * 6), 0cm}
        }
    }
}

Using Graphic Objects with Shapes

GraphicShape enables you to embed Graphic objects into a Shape hierarchy. Graphic objects include the containers, controls, and graphics of the GUI toolkit. You should note that because graphic objects do not support the rotation and scaling transformations that Shapes supports, a graphic ignores any rotation or scaling applied to a shape that contains it. This approach is sometimes called "billboarding." You can construct a GraphicShape explicitly, but you do not need to, because any graphic you add to a Shape is implicitly cast to GraphicShape.
By default, ImageShape also uses billboarding rather than rotation. See Working with ImageShape
The following example embeds a TextFlowBox in an EllipseShape. Because the TextFlowBox is a child of the ellipse, the translation applied to the ellipse is also applied to the text. However, the rotation applied to the ellipse is not.
A Graphic in a shape also picks up values of nonlocal options from its parent. In this example, if you comment the line:
color = FillPattern.black
The text in the TextFlowBox takes on the color set in the EllipseShape, which makes the text invisible.
The result displays the text in a way that is not possible using either the GUI toolkit or the Shapes package alone. The TextFlowBox provides a way to include rich text, which is not possible with TextShape, and and the rotated ellipse adds visual interest that the GUI toolkit cannot otherwise provide.

Example: Adding a Graphic to a Shape
{import * from CURL.GUI.SHAPES}
{let txt:TextFlowBox = 
    {TextFlowBox
        width = 2.7cm,
        horigin = "center",
        vorigin = "center",
        {paragraph
            paragraph-justify = "center", 
            This text is contained in a 
            {monospace TextFlowBox}, which is a 
            {monospace Graphic} object.
        },
        color = FillPattern.black
    }
}
{let ell:EllipseShape = 
    {EllipseShape
        {GRect 2cm, 2cm, 2.5cm, 2.5cm},
        color = "#ddffff",
        font-size = 11pt
    }
}
{value 
    {ell.add txt}
    {ell.apply-translation 2.5cm, 2.5cm}
    {ell.apply-rotation 45deg}
    {Canvas
        width = 5cm, height = 5cm,
        ell
    }
}