home back

Transforming Objects

This chapter covers how to position, scale, rotate, and arrange objects in your animations. All classes inheriting from MathObject support these transformations, and most methods return the object itself, enabling method chaining.

Table of Contents


Positioning Objects

Shift

The shift() method moves an object by a specified vector, adding the vector to the object's current position.

Basic Usage

// Shift using a Vec object
def circle = Shape.circle().shift(Vec.to(1, 1))  // Center at (1, 1)

// Shift using x, y coordinates (simpler)
def square = Shape.square().shift(-3, 0)  // Lower-left at (-3, 0)

// Shift using a Point
def pentagon = Shape.regularPolygon(5).shift(Point.at(2, -1))

Method Chaining: Since shift() returns the object, you can chain operations:

def shape = Shape.circle()
    .shift(1, 0)      // Move right
    .shift(0, 1)      // Then move up
    .scale(2)         // Then scale

MoveTo

The moveTo() method positions an object so its center aligns with specific coordinates. Unlike shift(), which is relative, moveTo() sets an absolute position.

// Move center to (3, 3)
def pentagon = Shape.regularPolygon(5).moveTo(3, 3)

// Alternative with Point
def circle = Shape.circle().moveTo(Point.at(-1, 2))

Understanding Centers: - moveTo() uses the bounding box center - For regular polygons, this may differ from the geometric center - Use .getCentroid() for true geometric center if needed

Equivalence: moveTo(p) is equivalent to .stack().toPoint(p)


Stack

The stack() method provides powerful relative positioning, allowing you to place objects relative to other objects with precise control over anchors and gaps.

Basic Stacking

def circle1 = Shape.circle().fillColor("orange").fillAlpha(0.3)
def circle2 = circle1.copy().fillColor("violet").fillAlpha(0.6)
def circle3 = circle1.copy().fillColor("darkgreen").fillAlpha(0.5)
def circle4 = circle1.copy().fillColor("darkblue").fillAlpha(0.5)
def square = Shape.square().fillColor("darkred").fillAlpha(0.3)

// Stack circle1 to the LEFT of square
circle1.stack()
    .withGaps(0.1)
    .withDestinyAnchor(AnchorType.LEFT)
    .toObject(square)

// Stack circle2 to the RIGHT of square
circle2.stack()
    .withGaps(0.1)
    .withDestinyAnchor(AnchorType.RIGHT)
    .toObject(square)

// Stack circle3 ABOVE square
circle3.stack()
    .withDestinyAnchor(AnchorType.UPPER)
    .toObject(square)

// Stack circle4 at CENTER of square (default)
circle4.stack().toObject(square)

scene.add(circle1, circle2, circle3, circle4, square)
camera.adjustToAllObjects()

Stack to example

Stack Method Chain

The stack() method returns a StackUtils object that allows you to configure positioning:

object.stack()
    // Configuration methods (optional)
    .withOriginAnchor(AnchorType anchor)
    .withDestinyAnchor(AnchorType anchor)
    .withGaps(double gap)
    .withRelativeGaps(double gap)

    // Finalization method (required, choose one)
    .toPoint(coordinates)
    .toObject(object)
    .toScreen(screenAnchor)

Configuration Methods

Anchor Methods:

// Specify where on the destination to attach (optional, default CENTER)
.withDestinyAnchor(AnchorType.RIGHT)

// Specify which part of the object to attach (optional)
.withOriginAnchor(AnchorType.LEFT)

If origin anchor is not specified, it defaults to the opposite of destiny anchor, for example:

  • RIGHT destiny → LEFT origin
  • UPPER destiny → LOWER origin
  • CENTER destiny → CENTER origin

Available Anchor Types: - Cardinal: LEFT, RIGHT, UPPER, LOWER, CENTER - Corners: UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT - Aligned: LEFT_AND_ALIGNED_UPPER, RIGHT_AND_ALIGNED_LOWER, etc.

Gap Methods:

// Absolute gaps in math units
.withGaps(0.1, 0.2)  // horizontal: 0.1, vertical: 0.2
.withGaps(0.1)       // both: 0.1

// Relative gaps (proportional to object size)
.withRelativeGaps(0.1, 0.2)  // 10% of width, 20% of height
.withRelativeGaps(0.1)       // 10% of both

Finalization Methods

Stack to Point:

// Position relative to specific coordinates
object.stack()
    .withGaps(0.1)
    .toPoint(Vec.to(1, 1))

Stack to Object:

// Position relative to another object
object.stack()
    .withDestinyAnchor(AnchorType.RIGHT)
    .toObject(targetObject)

Stack to Screen:

// Position relative to camera view
object.stack()
    .withGaps(0.2)
    .toScreen(ScreenAnchor.UPPER_LEFT)

Practical Example: Aligned Sequence

def previousPol = Shape.regularPolygon(3)
    .fillColor("random")
    .thickness(20)

scene.add(previousPol)

// Create a sequence of aligned polygons
for (int n = 4; n < 10; n++) {
    def pol = Shape.regularPolygon(n)
        .fillColor("random")
        .thickness(20)
        .stack()
        .withDestinyAnchor(AnchorType.RIGHT)
        .toObject(previousPol)

    scene.add(pol)
    previousPol = pol  // Next iteration uses this as reference
}

camera.adjustToAllObjects()

Stack example sequence

Tip: For complex layouts, consider using MathObjectGroup with layouts (covered later in this chapter).

Advanced Stacking Example

// Stack with explicit origin and destiny anchors
def square = Shape.square()
    .scale(0.5)
    .thickness(8)
    .fillColor("orange")
    .fillAlpha(0.5)

def circle = Shape.circle()
    .scale(0.5)
    .thickness(8)
    .fillColor("firebrick")
    .fillAlpha(0.5)

scene.add(circle, square)

// Square's CENTER goes to circle's RIGHT
square.stack()
    .withOriginAnchor(AnchorType.CENTER)
    .withDestinyAnchor(AnchorType.RIGHT)
    .toObject(circle)

Stack with explicit anchors


Stack to Screen

Position objects relative to the current camera view using toScreen().

def square1 = Shape.square().scale(0.5).style("solidblue")
def square2 = Shape.square().scale(0.5).style("solidgreen")
def circle1 = Shape.circle().style("solidred").scale(0.25)
def circle2 = Shape.circle().style("solidorange").scale(0.25)

// Square touching left edge
square1.stack()
    .toScreen(ScreenAnchor.LEFT)

// Square at right edge with gap
square2.stack()
    .withGaps(0.3)
    .toScreen(ScreenAnchor.RIGHT)

// Circle's center at upper-left corner
circle1.stack()
    .withOriginAnchor(AnchorType.CENTER)
    .toScreen(ScreenAnchor.UPPER_LEFT)

// Circle's bottom at lower-right corner
circle2.stack()
    .withOriginAnchor(AnchorType.LOWER)
    .toScreen(ScreenAnchor.LOWER_RIGHT)

scene.add(square1, square2, circle1, circle2)

Stack to screen example

Available Screen Anchors: - Edges: LEFT, RIGHT, UPPER, LOWER - Corners: UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT - Center: CENTER

Shortcut for Centering:

// These are equivalent:
object.center()
object.stack().toScreen(ScreenAnchor.CENTER)

Aligning Objects

The align() method aligns one object with another using a specific alignment type.

def floor = Line.XAxis()
scene.add(floor)

// Create random polygons and align them to the floor
for (n in 4..9) {
    def pol = Shape.regularPolygon(n)
        .moveTo(Point.random())
        .scale(Math.random() * 0.5)

    def pol2 = pol.copy()
        .fillColor("random")
        .thickness(6)
        .align(floor, AlignType.LOWER)  // Align bottom to floor

    scene.add(pol, pol2)
}

camera.adjustToAllObjects()

Align example

Available Alignment Types: - LEFT - Align left edges - RIGHT - Align right edges - UPPER - Align top edges - LOWER - Align bottom edges - HCENTER - Align horizontal centers - VCENTER - Align vertical centers

Comparison: Stack vs Align

Method Use Case
.stack() Position with gaps, multiple anchor options, flexible
.align() Simple edge/center alignment, no gaps

Scaling Objects

All MathObject instances can be scaled uniformly or non-uniformly. Scaling can be performed around a specific center or the object's bounding box center.

Basic Scaling

// Uniform scaling (all dimensions by same factor)
def circle = Shape.circle().scale(2)  // 2× larger

// Non-uniform scaling (different x and y factors)
def ellipse = Shape.circle().scale(0.5, 1)  // Ellipse: 50% width, 100% height

// Scaling around a specific point
def pentagon = Shape.regularPolygon(5)
    .shift(0, 1)
    .scale(Point.at(0, 0), 1.3, 0.2)  // Scale around origin

Detailed Example

// Circle scaled to ellipse
def s1 = Shape.circle()
    .style("solidorange")
    .shift(-1, 0)
    .scale(0.5, 1)  // 50% width, 100% height

// Pentagon scaled around origin
def s2 = Shape.regularPolygon(5)
    .style("solidred")
    .shift(0, 1)
    .scale(Point.at(0, 0), 1.3, 0.2)

// Square uniformly scaled
def s3 = Shape.square()
    .style("solidblue")
    .shift(1, 0)
    .scale(0.3)  // 30% of original size

scene.add(s1, s2, s3)

Scale example

Scale Center Behavior

Default: If no center is specified, objects scale around their bounding box center.

// These are equivalent when object is at origin:
shape.scale(2)
shape.scale(shape.getCenter(), 2)

Custom center: Specify a point to create different scaling effects:

// Scale away from origin
shape.scale(Point.origin(), 2, 1)

// Scale around top-right corner
def corner = shape.getBoundingBox().getUpperRight()
shape.scale(Point.at(corner), 1.5)

Rotating Objects

The rotate() method rotates objects by a specified angle. Rotation can be around a custom center or the object's bounding box center.

Basic Rotation

// Rotate around object's center
def square = Shape.square().rotate(45 * DEGREES)

// Rotate around a specific point
def ellipse = Shape.circle()
    .scale(0.5, 1)
    .rotate(Vec.to(0.5, 0), 30 * DEGREES)

Note: Always use * DEGREES to convert degree values to radians, as all angle methods expect radians.

Spirograph Example

def ellipse = Shape.circle()
    .scale(0.5, 1)
    .fillColor("violet")
    .fillAlpha(0.25)

def rotationCenter = Vec.to(0.5, 0)

// Create spirograph pattern
for (int n = 0; n < 180; n += 20) {
    scene.add(
        ellipse.copy()
            .rotate(rotationCenter, n * DEGREES)
    )
}

Rotation spirograph

Rotation Behavior

Default center: If not specified, rotation occurs around the bounding box center:

// These are equivalent:
shape.rotate(45 * DEGREES)
shape.rotate(shape.getCenter(), 45 * DEGREES)

Rotation direction: - Positive angles → counter-clockwise - Negative angles → clockwise

Common angles:

90 * DEGREES   // Quarter turn
180 * DEGREES  // Half turn
PI             // Half turn (radians)
2 * PI         // Full turn

Affine Transforms

Affine transformations are the mathematical foundation for shift, rotate, and scale operations. The AffineJTransform class provides advanced transformation capabilities for complex positioning and animation.

Understanding Affine Transforms

An affine transform is a geometric transformation that: - Preserves parallel lines - Preserves ratios of distances along lines - Can combine translation, rotation, scaling, shearing, and reflection

Transform Application Methods

// Modify the original object
transform.applyTransform(object)  // Returns void

// Create a transformed copy
def newObject = transform.getTransformedObject(object)  // Original unchanged

Basic Transform Constructors

// Translation
def translateTransform = AffineJTransform.createTranslationTransform(Vec.to(1, 2))
// Equivalent to: object.shift(1, 2)

// Rotation
def rotateTransform = AffineJTransform.create2DRotationTransform(
    Point.origin(),
    45 * DEGREES
)
// Equivalent to: object.rotate(Point.origin(), 45 * DEGREES)

// Scaling
def scaleTransform = AffineJTransform.createScaleTransform(
    Point.origin(),
    2.0,      // x scale
    1.5,      // y scale
    1.0       // z scale (for future 3D support)
)
// Equivalent to: object.scale(Point.origin(), 2, 1.5)

Isomorphic Transformations

Isomorphic transforms preserve shape and proportions but may change size, position, and orientation. They combine translation, rotation, and uniform scaling.

Direct Isomorphic Transform

Maps two points to two other points using the unique direct (orientation-preserving) isomorphic transform.

def square = Shape.square()
    .shift(-1.5, -1)
    .fillColor("darkgreen")
    .fillAlpha(0.3)

// Define source points
def A = square.getPoint(0).drawColor("darkblue")  // Lower-left
def B = square.getPoint(1).drawColor("darkblue")  // Lower-right

// Define destination points
def C = Point.at(1.5, -1).drawColor("darkred")
def D = Point.at(1.7, 0.5).drawColor("darkred")

scene.add(A, B, C, D)

// Create interpolated transforms (alpha from 0 to 1)
for (double alpha = 0; alpha <= 1; alpha += 0.2) {
    def transform = AffineJTransform.createDirect2DIsomorphic(
        A, B,     // Source points
        C, D,     // Destination points
        alpha     // Interpolation factor
    )
    scene.add(transform.getTransformedObject(square))
}

Direct isomorphic transform

Understanding Alpha Parameter: - alpha = 0 → Identity transform (no change) - alpha = 1 → Full transform (A→C, B→D) - 0 < alpha < 1 → Intermediate transforms (interpolated)

Use cases: - Smooth morphing animations - Coordinated object movements - Preserving proportions while repositioning

Inverse Isomorphic Transform

Creates an orientation-reversing transform (includes reflection).

// Create large R from LaTeX
def bigR = LatexMathObject.make("R").get(0)
    .scale(8)
    .center()
    .fillColor("steelblue")
    .fillAlpha(0.3)

def bbox = bigR.getBoundingBox()

// Source points
def A = Point.at(bbox.getLowerLeft()).drawColor("darkblue")
def B = Point.at(bbox.getUpperLeft()).drawColor("darkblue")

// Destination points
def C = Point.at(3.5, -1).drawColor("darkred")
def D = Point.at(3.7, 0.5).drawColor("darkred")

scene.add(A, B, C, D)

// Create inverse isomorphic transform sequence
for (double alpha = 0; alpha <= 1; alpha += 0.2) {
    def transform = AffineJTransform.createInverse2DIsomorphic(
        A, B, C, D, alpha
    )
    scene.add(transform.getTransformedObject(bigR))
}

camera.adjustToAllObjects()

Inverse isomorphic transform

Key difference: Inverse transforms include reflection, flipping the orientation of objects.


Reflections

Create mirror reflections across lines defined by two methods:

Reflection by Point Mapping

Reflects so that point A maps to point B (reflection axis is the perpendicular bisector).

def pentagon = Shape.regularPolygon(5)
    .fillColor("violet")
    .fillAlpha(0.3)

def A = pentagon.getPoint(0).copy().drawColor("darkblue")
def B = A.copy().shift(0.5, -0.2).drawColor("darkred")

scene.add(A, B)

// Create reflection sequence
for (double alpha = 0; alpha <= 1; alpha += 0.2) {
    def transform = AffineJTransform.createReflection(A, B, alpha)
    scene.add(transform.getTransformedObject(pentagon))
}

camera.adjustToAllObjects()

Reflection example

Reflection by Axis

Reflects across a line defined by two points.

def square = Shape.square().fillColor("orange").fillAlpha(0.3)

// Define reflection axis
def E1 = Point.at(-1, -1).drawColor("blue")
def E2 = Point.at(1, 1).drawColor("blue")

scene.add(square, E1, E2)

// Add visual axis line
scene.add(Line.make(E1, E2).drawColor("blue").dashStyle(DashStyle.DASHED))

// Create reflection
def transform = AffineJTransform.createReflectionByAxis(E1, E2, 1.0)
scene.add(transform.getTransformedObject(square))

camera.adjustToAllObjects()

Choosing the right method: - Use createReflection(A, B, alpha) when you know where a point should map to - Use createReflectionByAxis(E1, E2, alpha) when you know the reflection line


General Affine Transforms

The most general affine transform maps three non-collinear points to three other points.

def square = Shape.square()
    .drawColor("brown")
    .thickness(4)

def circle = Shape.circle()
    .scale(0.5)
    .shift(0.5, 0.5)
    .fillColor("orange")
    .fillAlpha(0.1)

// Source triangle (vertices of square)
def A = Point.at(0, 0).drawColor("darkblue").layer(1)
def B = Point.at(1, 0).drawColor("darkblue").layer(1)
def C = Point.at(0, 1).drawColor("darkblue").layer(1)

// Destination triangle
def D = Point.at(1.5, -0.5)
    .dotStyle(DotStyle.PLUS)
    .thickness(6)
    .drawColor("darkgreen")
def E = Point.at(2, 0)
    .dotStyle(DotStyle.PLUS)
    .thickness(6)
    .drawColor("darkgreen")
def F = Point.at(1.75, 0.75)
    .dotStyle(DotStyle.PLUS)
    .thickness(6)
    .drawColor("darkgreen")

scene.add(square, circle, A, B, C, D, E, F)

// Create transformation sequence
for (double alpha = 0; alpha <= 1; alpha += 0.2) {
    def transform = AffineJTransform.createAffineTransformation(
        A, B, C,    // Source triangle
        D, E, F,    // Destination triangle
        alpha
    )
    scene.add(transform.getTransformedObject(square))
    scene.add(transform.getTransformedObject(circle))
}

camera.adjustToAllObjects()

General affine transform

Important: The three source points (A, B, C) must not be collinear (not on the same line), and the same applies to destination points (D, E, F).

Capabilities: General affine transforms can: - Translate (move) - Rotate - Scale (uniformly or non-uniformly) - Shear (skew) - Reflect


Layouts

Layouts automatically arrange multiple objects in MathObjectGroup instances. They're essential for creating organized, visually appealing arrangements.

Basic Layout Usage

def group = MathObjectGroup.make(obj1, obj2, obj3)
group.setLayout(LayoutType.LOWER, 0.1, 0.1)  // Type, horizontal gap, vertical gap

Standard Layout Types

The LayoutType enum provides several built-in layouts:

Cardinal Directions: - CENTER - Stack centered - LEFT - Align left edges - RIGHT - Align right edges - UPPER - Align top edges - LOWER - Align bottom edges

Corner Alignments: - URIGHT, DRIGHT - Align upper/lower with right edges - ULEFT, DLEFT - Align upper/lower with left edges - LUPPER, RUPPER - Align left/right with top edges - LLOWER, RLOWER - Align left/right with bottom edges

Diagonal Arrangements: - DIAG1 - 45° diagonal (upper-right) - DIAG2 - 135° diagonal (upper-left) - DIAG3 - 225° diagonal (lower-left) - DIAG4 - 315° diagonal (lower-right)

Layout Demonstration

def group = MathObjectGroup.make()

// Create 10 squares of increasing size
double totalHeight = 0
for (int n = 0; n < 10; n++) {
    def square = Shape.square()
        .scale(0.2 + 0.1 * n)
        .thickness(6)
        .fillColor("random")
        .fillAlpha(0.5)

    totalHeight += square.getHeight()
    group.add(square)
}

// Zoom to fit all layouts
camera.scale(2 * totalHeight / camera.getMathView().getHeight())

scene.add(group)

def layoutName = LatexMathObject.make(".").scale(7)
scene.add(layoutName)

// Cycle through all layouts
for (layout in LayoutType.values()) {
    group.setLayout(layout, 0.1)

    layoutName.setLatex(layout.name())
        .stack()
        .withRelativeGaps(0.2)
        .toScreen(ScreenAnchor.LOWER)

    scene.waitSeconds(1)
}

Layout animation


The BoxLayout

Arranges objects in a grid/matrix pattern with configurable direction and dimensions.

def group = MathObjectGroup.make()
def numBoxes = 16

// Create numbered squares
for (int n = 0; n < numBoxes; n++) {
    def square = Shape.square()
        .scale(0.25)
        .fillColor("violet")
        .fillAlpha(1 - (n + 1) / numBoxes)
        .thickness(6)

    def text = LatexMathObject.make("$n")
        .stack().toObject(square)
        .layer(1)

    group.add(MathObjectGroup.make(square, text))
}

// Reference point (visible as red dot)
def refPoint = Point.origin()
    .thickness(40)
    .layer(1)
    .drawColor("red")

scene.add(refPoint)

// Create 4-column box layout
def layout = BoxLayout.make(
    refPoint,   // Reference point
    4,          // Number of columns
    0.1,        // In-row gap
    0.1         // Between-row gap
)

scene.add(group.setLayout(layout))
camera.zoomToAllObjects()

Box layout example

Box Layout Directions

The setDirection() method controls fill order:

layout.setBoxDirection(BoxDirection.RIGHT_UP)  // Fill rows left→right, then down→up
layout.setBoxDirection(BoxDirection.LEFT_DOWN) // Fill rows right→left, then up→down
layout.setBoxDirection(BoxDirection.UP_RIGHT)  // Fill columns bottom→top, then left→right

Direction naming: [FIRST]_[SECOND] - FIRST - Direction to fill the current row/column - SECOND - Direction to move to next row/column

Box layout directions

Working with Rows and Columns

// Access individual rows as groups
for (row in layout.getRowGroups(group)) {
    row.fillColor("random")  // Color each row differently
}

// Access individual columns
for (column in layout.getColumnGroups(group)) {
    column.drawColor("random")  // Color each column differently
}

Colored rows example


The PathLayout

Arranges objects following a given path.

def group = MathObjectGroup.make()
def numBoxes = 20

// Create numbered squares
for (int n = 0; n < numBoxes; n++) {
    def square = Shape.square()
        .scale(0.25)
        .fillColor("violet")
        .fillAlpha(1 - (n + 1) / numBoxes)
        .thickness(6)
    def text = LatexMathObject.make("$n")
        .stack().toObject(square)
        .layer(1)
    group.add(MathObjectGroup.make(square, text))
}
s = Shape.circle()
    .scale(1, 2)
    .rotate(-45*DEGREES)
    .drawColor('olivedrab4')

def layout = PathLayout.make(s) //You can use a Shape or a JMPath here
scene.add(s, group.setLayout(layout))
camera.zoomToAllObjects()

Gives the following result:

pathlayout

By default, objects are not rotated following the path. You can set this behaviour with the .setRotationType method

flowlayout2


The SpiralLayout

Arranges objects in a spiral pattern radiating from a center point.

def group = MathObjectGroup.make()

// Create 16 numbered squares
for (int n = 0; n < 16; n++) {
    def square = Shape.square()
        .scale(0.25)
        .fillColor("violet")
        .fillAlpha(1 - (n + 1) / 16.0)
        .thickness(6)

    def text = LatexMathObject.make("$n")
        .stack().toObject(square)
        .layer(1)

    group.add(MathObjectGroup.make(square, text))
}

def refPoint = Point.origin()
    .thickness(40)
    .layer(1)
    .drawColor("red")

scene.add(refPoint)

// Create spiral layout
def layout = SpiralLayout.make(
    refPoint,
    SpiralOrientation.RIGHT_CLOCKWISE,
    0.1,  // Horizontal gap
    0.1   // Vertical gap
)

scene.add(group.setLayout(layout))
camera.zoomToAllObjects()

Spiral layout example

Spiral Orientations

  • RIGHT_CLOCKWISE - Start right, spiral clockwise
  • RIGHT_COUNTERCLOCKWISE - Start right, spiral counter-clockwise
  • LEFT_CLOCKWISE - Start left, spiral clockwise
  • LEFT_COUNTERCLOCKWISE - Start left, spiral counter-clockwise
  • UP_CLOCKWISE - Start up, spiral clockwise
  • UP_COUNTERCLOCKWISE - Start up, spiral counter-clockwise
  • DOWN_CLOCKWISE - Start down, spiral clockwise
  • DOWN_COUNTERCLOCKWISE - Start down, spiral counter-clockwise

Controlling Spiral Aperture

// Create more spaced spiral
int numSquares = 50

def layout = SpiralLayout.make(
    refPoint,
    SpiralOrientation.RIGHT_CLOCKWISE,
    0,      // No extra horizontal gap
    0       // No extra vertical gap
).setSpiralGap(1)  // Leave 1 space between spiral turns

group.setLayout(layout)

Spiral with gap

Spiral Gap Values: - 0 (default) - Tight spiral, no space between turns - 1 - One object-width space between turns - 2 - Two object-widths space between turns - etc.


The HeapLayout

Creates a triangular pile arrangement, like stacking blocks.

def layout = HeapLayout.make(
    refPoint,
    0.1,  // Horizontal gap
    0.1   // Vertical gap
)

group.setLayout(layout)

Heap layout example

Use cases: - Pyramid arrangements - Hierarchical displays - Visual emphasis on bottom items


The PascalLayout

Arranges objects in the pattern of Pascal's triangle.

def layout = PascalLayout.make(
    refPoint,
    0.1,  // Horizontal gap
    0.1   // Vertical gap
)

group.setLayout(layout)

Pascal layout example

Use cases: - Mathematical visualizations - Binomial coefficient displays - Symmetrical arrangements


The FlowLayout

Similar to BoxLayout but creates new rows automatically when a maximum width is exceeded, like text wrapping.

int numBars = 70
def bars = MathObjectGroup.make()

def col1 = JMColor.parse('burlywood')
def col2 = JMColor.parse('white')

// Create bars with random widths
for (int n = 0; n < numBars; n++) {
    def bar = Shape.square()
        .scale(Math.random() + 0.25, 0.1)
        .fillColor(col1.interpolate(col2, n / numBars))

    def text = LatexMathObject.make("$n")
        .setHeight(bar.getHeight() * 0.8)
        .stack().toObject(bar)

    bars.add(MathObjectGroup.make(bar, text))
}

// Set maximum width
double maxWidth = 4
def corner = Point.relAt(0.1, 0.9)

// Visual width indicators
scene.add(corner.thickness(40).drawColor("red").layer(1))
scene.add(Line.YAxis().shift(corner.v.add(Vec.to(maxWidth, 0))))
scene.add(Line.YAxis().shift(corner.v))

// Create flow layout
def flowLayout = FlowLayout.make(
    corner,
    maxWidth,
    0.1,  // Horizontal gap
    0.1   // Vertical gap
).setDirection(BoxDirection.RIGHT_DOWN)

bars.setLayout(flowLayout)
scene.add(bars)
camera.adjustToObjects(bars)

Flow layout example

Key features: - Automatically wraps to new row when exceeding width - Useful for tag clouds, word arrangements - Natural reading flow


Composing Layouts

The ComposeLayout combines two layouts for complex hierarchical arrangements.

def composedLayout = ComposeLayout.make(
    outerLayout,  // Layout for positioning groups
    innerLayout,  // Layout within each group
    groupSize     // Number of elements per group
)

Composition Example

def group = MathObjectGroup.make()
int numSquares = 54

// Create 54 numbered squares
for (int n = 0; n < numSquares; n++) {
    def square = Shape.square()
        .scale(0.25)
        .fillColor("violet")
        .fillAlpha(1 - (n + 1) / numSquares)
        .thickness(6)

    def text = LatexMathObject.make("$n")
        .stack().toObject(square)
        .layer(1)

    group.add(MathObjectGroup.make(square, text))
}

def refPoint = Point.origin()
    .thickness(40)
    .drawColor("red")
    .layer(1)

scene.add(refPoint)

// Inner layout: 3×3 boxes
def innerLayout = BoxLayout.make(refPoint, 3, 0, 0)
    .setDirection(BoxDirection.RIGHT_DOWN)

// Outer layout: Pascal triangle of boxes
def outerLayout = PascalLayout.make(refPoint, 0.1, 0.1)

// Compose: groups of 9, arranged in Pascal triangle
def composedLayout = ComposeLayout.make(
    outerLayout,
    innerLayout,
    9  // 9 elements per group
)

scene.add(group.setLayout(composedLayout))
camera.adjustToAllObjects()

Composed layout example

How it works: 1. Divides 54 elements into 6 groups of 9 2. Arranges each group of 9 using innerLayout (3×3 box) 3. Positions the 6 groups using outerLayout (Pascal triangle)

Use cases: - Multi-level hierarchies - Complex grid systems - Nested structures


Custom Layouts

You can create custom layouts by extending the GroupLayout abstract class:

class CircularLayout extends GroupLayout {
    @Override
    void applyLayout(MathObjectGroup group) {
        int n = group.size()
        double angleStep = 2 * PI / n

        for (int i = 0; i < n; i++) {
            double angle = i * angleStep
            def position = Vec.to(
                Math.cos(angle),
                Math.sin(angle)
            )
            group.get(i).moveTo(position)
        }
    }
}

Best Practices

Positioning Strategy

  1. For simple positioning:
  2. Use .shift() for relative movement
  3. Use .moveTo() for absolute placement
  4. Use .center() for screen centering

  5. For relative positioning:

  6. Use .stack() for precise control
  7. Use .align() for simple edge alignment

  8. For multiple objects:

  9. Use MathObjectGroup with layouts
  10. Consider composed layouts for complexity

Transform Selection

Task Method
Move object .shift() or .moveTo()
Rotate .rotate()
Scale .scale()
Preserve shape Isomorphic transforms
Mirror/flip Reflection transforms
Complex mapping General affine transform

Layout Selection

Use Case Layout
Grid/table BoxLayout
Circular pattern SpiralLayout (with high gap)
Stacking HeapLayout, PascalLayout
Text-like flow FlowLayout
Multiple levels ComposeLayout
Custom arrangement Extend GroupLayout

Performance Tips

  • Layouts work best when all group elements have similar dimensions
  • Cache transforms if applying the same transform multiple times
  • Use method chaining to reduce code verbosity
  • Group before animating for coordinated movements

Next Steps

Now that you understand transformations, explore: - Basic Shapes - Objects to transform - Styling - Visual appearance - Basic Flow - Animation techniques - Animations - Animated transformations

home back