BOXES v0.6

In our previous lesson, we created a fully justified layout of varying-width boxes spread across multiple pages. But we cheated.

To achieve full justification, we spread the "slack" evenly in the space between all boxes in the row. If we were trying to layout text, that is not the proper way.

You see, text comes separated in words. And usually, in western languages, the words have characters called spaces between them. So what we do, when laying out text, is to make the special space boxes slightly larger and keep the separation between boxes constant (in fact, we also tweak separations between letters, but let's ignore that for now. Or for ever.

How about we choose some boxes and decide they, and only they, are stretchy?

That way, our strategy to fully justify the text will be: stretch the stretchy bits on each row just enough so that the row is exactly the width we need.

For the first time in a few lessons, we need to change our Box class:

 1class Box():
 2
 3    def __init__(self, x=0, y=0, w=1, h=1, stretchy=False):
 4        """Accept arguments to define our box, and store them."""
 5        self.x = x
 6        self.y = y
 7        self.w = w
 8        self.h = h
 9        self.stretchy = stretchy
10
11    def __repr__(self):
12        return "Box(%s, %s, %s, %s)" % (self.x, self.y, self.w, self.h)
13
14
15# Many boxes with varying widths, and about 1 in 10 will be stretchy
16from random import randint
17
18many_boxes = [
19    Box(w=1 + randint(-5, 5) / 10, stretchy=(randint(0, 5) == 4))
20    for i in range(5000)
21]
22# A few pages all the same size
23pages = [Box(i * 35, 0, 30, 50) for i in range(10)]

The changes in the layout function are not so big.

25# We add a "separation" constant so you can see the boxes individually
26separation = .2
27
28
29def layout(_boxes):
30    # Because we modify the box list, we will work on a copy
31    boxes = _boxes[:]
32    # We start at page 0
33    page = 0
34    # The 1st box should be placed in the correct page
35    previous = boxes.pop(0)
36    previous.x = pages[page].x
37    previous.y = pages[page].y
38    row = []
39    while boxes:
40        # We take the new 1st box
41        box = boxes.pop(0)
42        # And put it next to the other
43        box.x = previous.x + previous.w + separation
44        # At the same vertical location
45        box.y = previous.y
46        # But if it's too far to the right...
47        if (box.x + box.w) > (pages[page].x + pages[page].w):
48            # We adjust the row
49            slack = (pages[page].x + pages[page].w) - (
50                row[-1].x + row[-1].w
51            )

When finishing a row, see if it has stretchy boxes in it.

If it doesn't, bump each box a little to the right like we did before.

52            # Get a list of all the ones that are stretchy
53            stretchies = [b for b in row if b.stretchy]
54            if not stretchies:  # Nothing stretches do as before.
55                bump = slack / len(row)
56                # The 1st box gets 0 bumps, the 2nd gets 1 and so on
57                for i, b in enumerate(row):
58                    b.x += bump * i

If we do have stretchy boxes in the row, make each one wider.

59            else:
60                bump = slack / len(stretchies)
61                # Each stretchy gets wider
62                for b in stretchies:
63                    b.w += bump
64                # And we put each thing next to the previous one
65                for j, b in enumerate(row[1:], 1):
66                    b.x = row[j - 1].x + row[j - 1].w + separation

And continue like we did before.

68            # We start a new row
69            row = []
70            # We go all the way left and a little down
71            box.x = pages[page].x
72            box.y = previous.y + previous.h + separation
73
74        # But if we go too far down
75        if box.y + box.h > pages[page].y + pages[page].h:
76            # We go to the next page
77            page += 1
78            # And put the box at the top-left
79            box.x = pages[page].x
80            box.y = pages[page].y
81
82        # Put the box in the row
83        row.append(box)
84        previous = box
85
86
87layout(many_boxes)

The drawing code needs a change so we can see the "stretchy" boxes in a different color.

90import svgwrite
91
92
93def draw_boxes(boxes, fname, size):
94    dwg = svgwrite.Drawing(fname, profile="full", size=size)
95    # Draw the pages
96    for page in pages:
97        dwg.add(
98            dwg.rect(
99                insert=(f"{page.x}cm", f"{page.y}cm"),
100                size=(f"{page.w}cm", f"{page.h}cm"),
101                fill="lightblue",
102            )
103        )
104    # Draw all the boxes
105    for box in boxes:
106        # The box color depends on its features
107        color = "green" if box.stretchy else "red"
108        dwg.add(
109            dwg.rect(
110                insert=(f"{box.x}cm", f"{box.y}cm"),
111                size=(f"{box.w}cm", f"{box.h}cm"),
112                fill=color,
113            )
114        )
115    dwg.save()
116
117
118draw_boxes(many_boxes, "lesson6.svg", ("100cm", "50cm"))

lesson6.svg

This layout strategy works:

  • With multiple pages of arbitrary sizes and positions
  • With many boxes of different widths and stretch capabilities
  • Even if nothing can stretch

But the next lesson will start taking things to the next level.


Further references:

results matching ""

    No results matching ""